// hbrtl_ext/nodebridge — fivenode 의 Node 모듈 브리지를 순수 Go 로 구현. // // 기존 C++ fivenode 는 Harbour ⇄ C 글루 ⇄ Node(npm) 구조였다(JS 는 항상 // 진짜 Node 에서 실행, C 는 JSON 을 주고받는 글루일 뿐). FIVE 는 C→Go 재구현 // 이므로 그 'C 글루'를 'Go 글루'로 바꾼다: // // Five(Go) ⇄ 이 RTL(Go, os/exec+stdio JSON) ⇄ node bridge.js ⇄ npm // // node -e 문자열 평가가 아니라, C++ 판과 동일한 '핸들 기반' 프로토콜 // (require→handle, call→메서드 호출[Promise 자동 await], end)을 쓴다. // node 프로세스는 한 번만 띄워 상주시킨다. C/CGO 한 줄 없음. // // PRG surface: // FN_REQUIRE(cModule) -> nHandle (-1 on error) // FN_CALL(nHandle, cMethod[, cArgsJson]) -> cJson {"ok","type","value","err"} // FN_END(nHandle) -> NIL // FN_LASTERROR() -> cErr package nodebridge import ( "bufio" "encoding/json" "os" "os/exec" "path/filepath" "sync" "five/hbrt" ) func init() { hbrt.HB_FUNC("FN_REQUIRE", fnRequire) hbrt.HB_FUNC("FN_CALL", fnCall) hbrt.HB_FUNC("FN_END", fnEnd) hbrt.HB_FUNC("FN_LASTERROR", fnLastError) } var ( mu sync.Mutex proc *exec.Cmd stdin *bufio.Writer stdout *bufio.Scanner lastErr string bridgeJS = ` 'use strict'; const handles = new Map(); let next = 1; function out(o){ process.stdout.write(JSON.stringify(o) + "\n"); } function enc(v){ if (v === null || v === undefined) return { t: "null" }; if (Buffer.isBuffer(v)) return { t: "buffer", v: v.toString("base64") }; const tp = typeof v; if (tp === "string") return { t: "string", v: v }; if (tp === "number") return { t: "number", v: String(v) }; if (tp === "boolean") return { t: "boolean", v: v ? "true" : "false" }; try { return { t: "json", v: JSON.stringify(v) }; } catch (e) { return { t: "string", v: String(v) }; } } async function handle(req){ try { if (req.cmd === "require") { const m = require(req.module); const h = next++; handles.set(h, m); return { ok: true, handle: h }; } if (req.cmd === "call") { const obj = handles.get(req.handle); if (obj === undefined) return { ok: false, err: "invalid handle" }; const fn = obj[req.method]; let val; if (typeof fn !== "function") { val = obj[req.method]; // property access } else { const args = req.args || []; if (/^[A-Z]/.test(req.method)) val = new fn(...args); // constructor else val = fn.apply(obj, args); } if (val && typeof val.then === "function") val = await val; // auto-await Promise return { ok: true, value: enc(val) }; } if (req.cmd === "end") { handles.delete(req.handle); return { ok: true }; } return { ok: false, err: "unknown cmd" }; } catch (e) { return { ok: false, err: String((e && e.message) || e) }; } } let buf = ""; process.stdin.on("data", async (d) => { buf += d; let i; while ((i = buf.indexOf("\n")) >= 0) { const line = buf.slice(0, i); buf = buf.slice(i + 1); if (!line.trim()) continue; let req; try { req = JSON.parse(line); } catch (e) { out({ ok: false, err: "bad json" }); continue; } out(await handle(req)); } }); console.log = (...a) => process.stderr.write(a.join(" ") + "\n"); // keep stdout clean process.stdin.resume(); ` ) func nodeBin() string { if p, err := exec.LookPath("node"); err == nil { return p } for _, p := range []string{"/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node"} { if _, err := os.Stat(p); err == nil { return p } } return "" } func workDir() string { if d := os.Getenv("SOLMADE_NODE_DIR"); d != "" { return d } return "/Users/charleskwon/solmade/node" } // ensure starts the persistent node bridge once. Caller holds mu. func ensure() bool { if proc != nil { return true } bin := nodeBin() if bin == "" { lastErr = "node not found" return false } dir := workDir() jsPath := filepath.Join(os.TempDir(), "fivenode_bridge.js") if err := os.WriteFile(jsPath, []byte(bridgeJS), 0o644); err != nil { lastErr = "write bridge: " + err.Error() return false } cmd := exec.Command(bin, jsPath) cmd.Dir = dir // require() 는 스크립트 위치 기준이라 /tmp 의 bridge.js 에선 node_modules 를 // 못 찾는다 → NODE_PATH 로 작업 디렉터리의 node_modules 를 지정. cmd.Env = append(os.Environ(), "NODE_PATH="+filepath.Join(dir, "node_modules")) cmd.Stderr = os.Stderr wp, err := cmd.StdinPipe() if err != nil { lastErr = err.Error() return false } rp, err := cmd.StdoutPipe() if err != nil { lastErr = err.Error() return false } if err := cmd.Start(); err != nil { lastErr = "start node: " + err.Error() return false } proc = cmd stdin = bufio.NewWriter(wp) stdout = bufio.NewScanner(rp) stdout.Buffer(make([]byte, 0, 64*1024), 8*1024*1024) // allow large results return true } func reset() { if proc != nil { _ = proc.Process.Kill() } proc, stdin, stdout = nil, nil, nil } // rpc sends one request and reads one response line. Caller holds mu. func rpc(req map[string]interface{}) (map[string]interface{}, bool) { if !ensure() { return nil, false } b, _ := json.Marshal(req) b = append(b, '\n') if _, err := stdin.Write(b); err != nil { lastErr = "write: " + err.Error() reset() return nil, false } if err := stdin.Flush(); err != nil { lastErr = "flush: " + err.Error() reset() return nil, false } if !stdout.Scan() { lastErr = "node bridge closed" reset() return nil, false } var resp map[string]interface{} if err := json.Unmarshal(stdout.Bytes(), &resp); err != nil { lastErr = "bad response: " + err.Error() return nil, false } return resp, true } // FN_REQUIRE(cModule) -> nHandle (-1 on error) func fnRequire(ctx *hbrt.HBContext) { if ctx.PCount() < 1 || !ctx.IsChar(1) { ctx.RetNI(-1) return } mu.Lock() defer mu.Unlock() resp, ok := rpc(map[string]interface{}{"cmd": "require", "module": ctx.ParC(1)}) if !ok || resp["ok"] != true { if ok { lastErr = str(resp["err"]) } ctx.RetNI(-1) return } if h, ok := resp["handle"].(float64); ok { ctx.RetNI(int(h)) return } ctx.RetNI(-1) } // FN_CALL(nHandle, cMethod[, cArgsJson]) -> {"ok","type","value","err"} JSON func fnCall(ctx *hbrt.HBContext) { if ctx.PCount() < 2 || !ctx.IsNum(1) || !ctx.IsChar(2) { ctx.RetC(envelope(false, "", "", "FN_CALL(nHandle, cMethod[, cArgsJson])")) return } handle := ctx.ParNI(1) method := ctx.ParC(2) var args interface{} if ctx.PCount() >= 3 && ctx.IsChar(3) { _ = json.Unmarshal([]byte(ctx.ParC(3)), &args) } mu.Lock() defer mu.Unlock() resp, ok := rpc(map[string]interface{}{"cmd": "call", "handle": handle, "method": method, "args": args}) if !ok { ctx.RetC(envelope(false, "", "", lastErr)) return } if resp["ok"] != true { ctx.RetC(envelope(false, "", "", str(resp["err"]))) return } val, _ := resp["value"].(map[string]interface{}) ctx.RetC(envelope(true, str(val["t"]), str(val["v"]), "")) } // FN_END(nHandle) func fnEnd(ctx *hbrt.HBContext) { if ctx.PCount() >= 1 && ctx.IsNum(1) { mu.Lock() _, _ = rpc(map[string]interface{}{"cmd": "end", "handle": ctx.ParNI(1)}) mu.Unlock() } ctx.RetNil() } func fnLastError(ctx *hbrt.HBContext) { mu.Lock() e := lastErr mu.Unlock() ctx.RetC(e) } func str(v interface{}) string { if s, ok := v.(string); ok { return s } return "" } func envelope(ok bool, t, v, err string) string { b, _ := json.Marshal(map[string]interface{}{"ok": ok, "type": t, "value": v, "err": err}) return string(b) }