Spaces:
Running
Running
| import { spawn } from "node:child_process"; | |
| import type { RuntimeEnv } from "../runtime.js"; | |
| export type SignalDaemonOpts = { | |
| cliPath: string; | |
| account?: string; | |
| httpHost: string; | |
| httpPort: number; | |
| receiveMode?: "on-start" | "manual"; | |
| ignoreAttachments?: boolean; | |
| ignoreStories?: boolean; | |
| sendReadReceipts?: boolean; | |
| runtime?: RuntimeEnv; | |
| }; | |
| export type SignalDaemonHandle = { | |
| pid?: number; | |
| stop: () => void; | |
| }; | |
| export function classifySignalCliLogLine(line: string): "log" | "error" | null { | |
| const trimmed = line.trim(); | |
| if (!trimmed) { | |
| return null; | |
| } | |
| // signal-cli commonly writes all logs to stderr; treat severity explicitly. | |
| if (/\b(ERROR|WARN|WARNING)\b/.test(trimmed)) { | |
| return "error"; | |
| } | |
| // Some signal-cli failures are not tagged with WARN/ERROR but should still be surfaced loudly. | |
| if (/\b(FAILED|SEVERE|EXCEPTION)\b/i.test(trimmed)) { | |
| return "error"; | |
| } | |
| return "log"; | |
| } | |
| function buildDaemonArgs(opts: SignalDaemonOpts): string[] { | |
| const args: string[] = []; | |
| if (opts.account) { | |
| args.push("-a", opts.account); | |
| } | |
| args.push("daemon"); | |
| args.push("--http", `${opts.httpHost}:${opts.httpPort}`); | |
| args.push("--no-receive-stdout"); | |
| if (opts.receiveMode) { | |
| args.push("--receive-mode", opts.receiveMode); | |
| } | |
| if (opts.ignoreAttachments) { | |
| args.push("--ignore-attachments"); | |
| } | |
| if (opts.ignoreStories) { | |
| args.push("--ignore-stories"); | |
| } | |
| if (opts.sendReadReceipts) { | |
| args.push("--send-read-receipts"); | |
| } | |
| return args; | |
| } | |
| export function spawnSignalDaemon(opts: SignalDaemonOpts): SignalDaemonHandle { | |
| const args = buildDaemonArgs(opts); | |
| const child = spawn(opts.cliPath, args, { | |
| stdio: ["ignore", "pipe", "pipe"], | |
| }); | |
| const log = opts.runtime?.log ?? (() => {}); | |
| const error = opts.runtime?.error ?? (() => {}); | |
| child.stdout?.on("data", (data) => { | |
| for (const line of data.toString().split(/\r?\n/)) { | |
| const kind = classifySignalCliLogLine(line); | |
| if (kind === "log") { | |
| log(`signal-cli: ${line.trim()}`); | |
| } else if (kind === "error") { | |
| error(`signal-cli: ${line.trim()}`); | |
| } | |
| } | |
| }); | |
| child.stderr?.on("data", (data) => { | |
| for (const line of data.toString().split(/\r?\n/)) { | |
| const kind = classifySignalCliLogLine(line); | |
| if (kind === "log") { | |
| log(`signal-cli: ${line.trim()}`); | |
| } else if (kind === "error") { | |
| error(`signal-cli: ${line.trim()}`); | |
| } | |
| } | |
| }); | |
| child.on("error", (err) => { | |
| error(`signal-cli spawn error: ${String(err)}`); | |
| }); | |
| return { | |
| pid: child.pid ?? undefined, | |
| stop: () => { | |
| if (!child.killed) { | |
| child.kill("SIGTERM"); | |
| } | |
| }, | |
| }; | |
| } | |