summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorWayne-Cole <77279425+Wacky404@users.noreply.github.com>2026-05-28 13:22:33 -0500
committerWayne-Cole <77279425+Wacky404@users.noreply.github.com>2026-05-28 13:22:33 -0500
commitbb0d5fecf8d839efa0e89c33d310c5202c6f8919 (patch)
tree05f69bc0a4121599222a63d26a2cee53244a2cf6
parente8147659b719e5a1b5ad8dff3d0bfab15d08fe8e (diff)
downloadlurchers-bb0d5fecf8d839efa0e89c33d310c5202c6f8919.tar.xz
lurchers-bb0d5fecf8d839efa0e89c33d310c5202c6f8919.zip
feat: api built
-rw-r--r--Makefile9
-rw-r--r--cmd/lurchers/main.go153
-rw-r--r--go.mod5
-rw-r--r--howlers/src/howler.jac68
-rw-r--r--internal/api/logic.go16
-rw-r--r--internal/api/lurchql.go153
-rw-r--r--internal/lcommon.go59
7 files changed, 412 insertions, 51 deletions
diff --git a/Makefile b/Makefile
index 5867a0b..865299a 100644
--- a/Makefile
+++ b/Makefile
@@ -1,8 +1,13 @@
BINARY=bin/lurchers
SRC=cmd/lurchers/main.go
+.PHONY: run clean
+
run: $(BINARY)
- ./$(BINARY)
+ sudo ./$(BINARY)
-$(BINARY): $(SRC)
+$(BINARY): $(shell find . -name '*.go')
go build -o $(BINARY) $(SRC)
+
+clean:
+ rm -f $(BINARY)
diff --git a/cmd/lurchers/main.go b/cmd/lurchers/main.go
index 8516b90..57d2019 100644
--- a/cmd/lurchers/main.go
+++ b/cmd/lurchers/main.go
@@ -1,55 +1,170 @@
package main
import (
+ "bufio"
+ "context"
+ "fmt"
"log"
"log/slog"
+ "net"
+ "os"
+ "os/signal"
"runtime"
- "context"
+ "sync"
+ "sync/atomic"
+ "syscall"
+ "time"
- "github.com/Wacky404/lurchers/internal"
- "github.com/joho/godotenv"
+ "lurchers/internal"
+ "lurchers/internal/api"
)
-// make sure I'm putting the mem file in the
-// right place
-// windows? priority 0
var (
- FILE_MEM string
- FILE_LOGS string
+ FileMem string
+ FileLogs string
)
+const SocketPath = "/tmp/lurchers.sock"
+
func init() {
switch runtime.GOOS {
case "darwin":
- FILE_MEM = "/var/run/lurchers.mem"
- FILE_LOGS = "/tmp/lurchers_logs/lurchers.log"
+ FileMem = "/var/run/lurchers.mem"
+ FileLogs = "/var/log/lurchers/lurchers.log"
case "linux":
- FILE_MEM = "/run/lurchers.mem"
- FILE_LOGS = "/tmp/lurchers_logs/lurchers.log"
+ FileMem = "/run/lurchers.mem"
+ FileLogs = "/var/log/lurchers/lurchers.log"
}
}
+type Daemon struct {
+ mem *internal.ReaderAt
+ workerWg sync.WaitGroup
+ workerCount atomic.Int64
+}
+
func main() {
- logFile, err := internal.SetupLogger(internal.WithLogName(FILE_LOGS))
+ if _, err := os.Stat(FileLogs); os.IsNotExist(err) {
+ fpath := "/var/log/lurchers/"
+ os.MkdirAll(fpath, 0o700)
+ f, err := os.Create(FileLogs)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer f.Close()
+ }
+
+ if _, err := os.Stat(FileMem); os.IsNotExist(err) {
+ m, err := os.OpenFile(FileMem, os.O_RDWR|os.O_CREATE, 0o644)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer m.Close()
+ }
+
+ logFile, err := internal.SetupLogger(internal.WithLogName(FileLogs))
if err != nil {
log.Fatal("setuplogger: error setting up logger", err)
}
defer logFile.Close()
- err = godotenv.Load()
+ mem, err := internal.Open(FileMem)
if err != nil {
- slog.Error("load: error loading .env file", slog.Any("error", err))
+ slog.Error("open: error opening mem file", "error", err)
+ os.Exit(1)
}
+ defer mem.Close()
- mem, err := internal.Open(FILE_MEM)
+ os.Remove(SocketPath)
+ ln, err := net.Listen("unix", SocketPath)
if err != nil {
- log.Fatal("open: error opening mem file", slog.Any("error", err))
+ slog.Error("failed to listen", "error", err)
+ os.Exit(1)
}
- defer mem.Close()
+ // allows non-root to connect to socket
+ os.Chmod(SocketPath, 0o666)
+ defer ln.Close()
+
+ slog.Info("daemon started", "socket", SocketPath)
+
+ ctx, cancel := context.WithCancel(context.Background())
+
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
+ go func() {
+ sig := <-sigCh
+ slog.Info("received signal, shutting down", "signal", sig)
+ cancel()
+ ln.Close()
+ }()
- ctx := context.TODO()
+ d := &Daemon{mem: mem}
+ for {
+ conn, err := ln.Accept()
+ if err != nil {
+ select {
+ case <-ctx.Done():
+ slog.Info("waiting for workers to finish...")
+ d.workerWg.Wait()
+ slog.Info("daemon stopped")
+ return
+ default:
+ slog.Error("accept error", "error", err)
+ continue
+ }
+ }
+ go d.handleConn(ctx, conn)
+ }
+}
+
+// {"method": "query","function": "status","params": {"id": 1, "user": "wcole"}}
+func (d *Daemon) handleConn(ctx context.Context, conn net.Conn) {
+ defer conn.Close()
+
+ scanner := bufio.NewScanner(conn)
+ for scanner.Scan() {
+ var msg api.LurchMsg
+
+ if err := api.Decode(&msg, scanner.Bytes()); err != nil {
+ slog.Error("decode: failed to decode", "error", err)
+ fmt.Fprintln(conn, "error: invalid json")
+ continue
+ }
+
+ slog.Info("received", "json", msg)
+
+ if err := api.Compute(ctx, &msg, conn); err != nil {
+ slog.Error("parse: failed to parse lurch logic", "error", err)
+ fmt.Fprintln(conn, "error: failed to parse")
+ continue
+ }
+ }
+
+ if err := scanner.Err(); err != nil {
+ slog.Error("scanner: something broke, fix it", "error", err)
+ }
+}
+func (d *Daemon) worker(ctx context.Context) {
+ defer d.workerWg.Done()
+ defer d.workerCount.Add(-1)
+ // replace eventually with shared memory polling
+ ticker := time.NewTicker(5 * time.Second)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-ctx.Done():
+ slog.Info("worker shutting down")
+ return
+ case <-ticker.C:
+ go doWork()
+ }
+ }
+}
+func doWork() {
+ time.Sleep(5 * time.Second)
}
diff --git a/go.mod b/go.mod
index 42f4399..7122369 100644
--- a/go.mod
+++ b/go.mod
@@ -1,9 +1,8 @@
-module github.com/Wacky404/lurchers
+module lurchers
-go 1.23.4
+go 1.26.3
require (
- github.com/gocolly/colly v1.2.0
github.com/joho/godotenv v1.5.1
)
diff --git a/howlers/src/howler.jac b/howlers/src/howler.jac
index 5789e20..6771549 100644
--- a/howlers/src/howler.jac
+++ b/howlers/src/howler.jac
@@ -1,7 +1,10 @@
import sys;
+import socket;
+import signal;
import mmap;
import struct;
-import from typing { List, Dict }
+import from types { FrameType }
+import from typing { List, Dict, Optional }
import from os { path }
glob MAX_EVENTS: int = 256;
@@ -17,12 +20,12 @@ enum EVENT {
}
glob EventName: Dict[EVENT, str] = {
- CHLD_PROC_START: "child_start",
- CHLD_PROC_DONE: "child_done",
- CHLD_PROC_FAILED: "child_failed",
- CHLD_PROC_HURT: "child_hurt",
- CHLD_PROC_HEALING: "child_healing",
- CHLD_PROC_HEALED: "child_healed"
+ EVENT.CHLD_PROC_START: "child_start",
+ EVENT.CHLD_PROC_DONE: "child_done",
+ EVENT.CHLD_PROC_FAILED: "child_failed",
+ EVENT.CHLD_PROC_HURT: "child_hurt",
+ EVENT.CHLD_PROC_HEALING: "child_healing",
+ EVENT.CHLD_PROC_HEALED: "child_healed"
};
obj SysLurchEvent_t {
@@ -68,6 +71,53 @@ walker Crawler {
def fix_scrape_script(script: str) -> str by llm();
-with entry {
- print("Hello, World!");
+def signal_handler(sig: int, frame: Optional[FrameType]) -> None {
+ print("\n\n[!] Interupt CTRL+C. Exiting...");
+ sys.exit(0);
+}
+
+with entry:__main__ {
+ SOCKET_PATH: str = "/tmp/lurchers.sock";
+
+ signal.signal(signal.SIGINT, signal_handler);
+
+ while True {
+ try {
+ user_input: str = input("\nsend_bytes> ");
+
+ if user_input.strip().lower() in ["exit()", "quit()"] {
+ print("exiting...");
+ break;
+ }
+
+ if not user_input {
+ continue;
+ }
+
+ # Ex: '{"method": "query","function": "get User","params": {"id": 1, "user": "wcole"}}'
+ payload: bytes = (user_input + "\n").encode('utf-8');
+
+ with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as client {
+ client.connect(SOCKET_PATH);
+ client.sendall(payload);
+
+ resp = client.recv(4096);
+ if resp {
+ print("\n--- Server Response ---");
+ print(resp.decode('utf-8').strip());
+ } else {
+ print("\nData sent to server success (no data returned)");
+ }
+ }
+
+ } except FileNotFoundError {
+ print(f"Error: the socket file '{SOCKET_PATH}' does not exist");
+ } except PermissionError {
+ print(f"Error: missing permissions to write to '{SOCKET_PATH}");
+ } except ConnectionRefusedError {
+ print(f"Error: conn refused");
+ } except Exception as e {
+ print(f"Error: unexpected error occurred: {e}");
+ }
+ }
}
diff --git a/internal/api/logic.go b/internal/api/logic.go
new file mode 100644
index 0000000..e60ec50
--- /dev/null
+++ b/internal/api/logic.go
@@ -0,0 +1,16 @@
+package api
+
+import "log/slog"
+
+var (
+ statusArgs lurchArgs = lurchArgs{}
+ status lurchCallback = func(args lurchArgs) error {
+ if len(args) == 0 {
+ slog.Debug("empty args map", "length", len(args))
+ }
+
+ slog.Debug("stuff is happening eventually")
+
+ return nil
+ }
+)
diff --git a/internal/api/lurchql.go b/internal/api/lurchql.go
new file mode 100644
index 0000000..2f5c125
--- /dev/null
+++ b/internal/api/lurchql.go
@@ -0,0 +1,153 @@
+// Package api: programming interface for encoding and decoding
+// of lurch messages via the Unix Socket.
+package api
+
+/* TODO: Keep any actual logic/compute out of this file */
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net"
+ "reflect"
+)
+
+// {"method": "query","function": "get User","params": {"id": 1, "user": "wcole"}}
+
+type (
+ LurchMsgBytes = []byte
+ LurchRespBytes = []byte
+
+ lurchLogic = map[string][]any
+ lurchArgs = map[string]any
+ lurchCallback func(args map[string]any) error
+)
+
+type LurchPackage interface {
+ Encode() []byte
+ Decode() error
+}
+
+type LurchMsg struct {
+ Method string `json:"method"`
+ Function string `json:"function"`
+ Params lurchArgs `json:"params"`
+}
+
+func (m *LurchMsg) Encode() ([]byte, error) {
+ return json.Marshal(&m)
+}
+
+func (m *LurchMsg) Decode(data LurchMsgBytes) error {
+ return json.Unmarshal(data, m)
+}
+
+// arb data; don't know the shape of api data response yet, tbd
+// {"data": {"id": 1, "user": "wcole"}}
+// {"method": "query","function": "get User","params": {"id": 1, "user": "wcole"}}
+
+type LurchResp struct {
+ Ok bool `json:"ok"`
+ Data map[string][]any `json:"data"`
+ Meta map[string]string `json:"meta"`
+}
+
+func (r *LurchResp) Encode() ([]byte, error) {
+ return json.Marshal(&r)
+}
+
+func (r *LurchResp) Decode(data LurchRespBytes) error {
+ return json.Unmarshal(data, r)
+}
+
+func Encode(lp interface{}) ([]byte, error) {
+ switch v := lp.(type) {
+ case LurchMsg:
+ return json.Marshal(v)
+ case *LurchMsg:
+ return json.Marshal(*v)
+ case LurchResp:
+ return json.Marshal(v)
+ case *LurchResp:
+ return json.Marshal(*v)
+ default:
+ return nil, errors.New("encode: package value is nil")
+ }
+}
+
+func Decode(lp interface{}, data []byte) error {
+ switch v := lp.(type) {
+ case LurchMsg:
+ return json.Unmarshal(data, &v)
+ case *LurchMsg:
+ return json.Unmarshal(data, v)
+ case LurchResp:
+ return json.Unmarshal(data, &v)
+ case *LurchResp:
+ return json.Unmarshal(data, v)
+ default:
+ return errors.New("decode: failed to determine type of package")
+ }
+}
+
+// {"method": "query","function": "get User","params": {"id": 1, "user": "wcole"}}
+var fns = lurchLogic{
+ // using the base rep of the type
+ "workerStart": []any{lurchArgs{"id": int(0), "user": ""}, func(args map[string]any) error { return nil }},
+ "status": []any{statusArgs, status},
+}
+
+func Compute(ctx context.Context, m *LurchMsg, conn net.Conn) error {
+ a := func(args lurchArgs, pArgs lurchArgs) (lurchArgs, bool) {
+ buf := lurchArgs{}
+ for n, v := range args {
+ if primitive, ok := pArgs[n]; ok {
+ if reflect.TypeOf(v) != reflect.TypeOf(primitive) {
+ buf[n] = v
+ }
+ }
+ }
+
+ if len(buf) != 0 {
+ return buf, false
+ }
+
+ return args, true
+ }
+ f := func(fn string) error {
+ if signature, ok := fns[fn]; ok {
+ // function signature is in lurch functions; sign is value
+ if callee, ok := signature[1].(lurchCallback); ok {
+ // args validation
+ // COULD PANIC, DON'T GOOF UP THE FNS INF
+ if params, ok := a(m.Params, signature[0].(lurchArgs)); ok {
+ err := callee(params)
+ return err
+ } else {
+ return errors.New("parse: malformed lurch message - args")
+ }
+ }
+ }
+
+ return errors.New("parse: signature not in lurch logic")
+ }
+
+ switch m.Method {
+ case "query":
+ fmt.Fprintln(conn, "query received")
+ // some query system logic here
+ return f(m.Function)
+ case "mutation":
+ // mutation logic
+ fmt.Fprintln(conn, "mutation received")
+ return f(m.Function)
+ case "subscription":
+ // subscription logic
+ fmt.Fprintln(conn, "subscription received")
+ return f(m.Function)
+ default:
+ fmt.Fprintf(conn, "unknown command: %s\n", m.Method)
+ return errors.New("parse: malformed lurch message - method")
+ }
+}
diff --git a/internal/lcommon.go b/internal/lcommon.go
index d6f53d0..49c5b28 100644
--- a/internal/lcommon.go
+++ b/internal/lcommon.go
@@ -29,6 +29,7 @@ func GetVar(key string, fallback string) string {
* Lurchers json logger; two handers for file and stdout
* =====================================================
*/
+
type MultiHandler struct {
handlers []slog.Handler
}
@@ -189,6 +190,7 @@ func (w *WatchMen) SysGetEvent() *SysLurchEvent_t {
// not safe to call Close and reading methods concurrently.
type ReaderAt struct {
data []byte
+ file *os.File
}
// implements the io.ReaderAt interface
@@ -210,40 +212,48 @@ func (r *ReaderAt) ReadAt(p []byte, offset int64) (int, error) {
func (r *ReaderAt) Close() error {
if r.data == nil {
+ if r.file != nil {
+ r.file.Close()
+ r.file = nil
+ }
return nil
} else if len(r.data) == 0 {
+ if r.file != nil {
+ r.file.Close()
+ r.file = nil
+ }
r.data = nil
return nil
}
- data := r.data
- r.data = nil
- if debug {
- var p *byte
- if len(data) != 0 {
- p = &data[0]
- }
- println("munmap", r, p)
+ err := r.file.Sync()
+ if err != nil {
+ return fmt.Errorf("failed to sync memory pages to disk: %v", err)
}
+ _ = syscall.Munmap(r.data)
+ r.data = nil
- runtime.SetFinalizer(r, nil)
+ if r.file != nil {
+ _ = r.file.Close()
+ r.file = nil
+ }
- return syscall.Munmap(data)
+ return nil
}
func Open(filename string) (*ReaderAt, error) {
// just opening a file that will store
// bytes of data that parent proc and
// and child proc(s) share
- f, err := os.Open(filename)
+ f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0o666)
if err != nil {
- return nil, err
+ return nil, fmt.Errorf("open: %v", err)
}
- defer f.Close()
fs, err := f.Stat()
if err != nil {
- return nil, err
+ f.Close()
+ return nil, fmt.Errorf("stat: %v", err)
}
// size of the memory file in question
@@ -252,26 +262,39 @@ func Open(filename string) (*ReaderAt, error) {
// Treat (size == 0) as a special case, truncating the
// file to the specified file size, in the case that the
// file is new; upon first run of program.
- f.Truncate(int64(SIZE_FILE))
+ if err := f.Truncate(int64(SIZE_FILE)); err != nil {
+ f.Close()
+ return nil, fmt.Errorf("truncate: %v", err)
+ }
+
+ size = int64(SIZE_FILE)
}
if size < 0 {
+ f.Close()
return nil, fmt.Errorf("mmap: file %q has negative size", filename)
}
if size != int64(int(size)) {
+ f.Close()
return nil, fmt.Errorf("mmap: file %q is too large", filename)
}
data, err := syscall.Mmap(int(f.Fd()), 0, int(size), syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_SHARED)
if err != nil {
- return nil, err
+ f.Close()
+ return nil, fmt.Errorf("mmap failed: %v", err)
+ }
+
+ r := &ReaderAt{
+ data: data,
+ file: f,
}
- r := &ReaderAt{data}
+
if debug {
var p *byte
if len(data) != 0 {
p = &data[0]
}
- println("mmap", r, p)
+ fmt.Printf("mmap struct address %p, underlying memory addr: %p\n", r, p)
}
runtime.SetFinalizer(r, (*ReaderAt).Close)