From bb0d5fecf8d839efa0e89c33d310c5202c6f8919 Mon Sep 17 00:00:00 2001 From: Wayne-Cole <77279425+Wacky404@users.noreply.github.com> Date: Thu, 28 May 2026 13:22:33 -0500 Subject: feat: api built --- Makefile | 9 ++- cmd/lurchers/main.go | 153 ++++++++++++++++++++++++++++++++++++++++++------ go.mod | 5 +- howlers/src/howler.jac | 68 ++++++++++++++++++--- internal/api/logic.go | 16 +++++ internal/api/lurchql.go | 153 ++++++++++++++++++++++++++++++++++++++++++++++++ internal/lcommon.go | 59 +++++++++++++------ 7 files changed, 412 insertions(+), 51 deletions(-) create mode 100644 internal/api/logic.go create mode 100644 internal/api/lurchql.go 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) -- cgit v1.3-3-g829e