openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
155 lines (154 loc) • 4.54 kB
JavaScript
import { t as createDeferredCore } from "../deferred-D0La5CRk.mjs";
import { c as isRecord } from "../record-coerce-TIsquoVR.mjs";
import { r as signalProcessTree } from "../kill-tree-ecObdMeW.mjs";
import "../tailscale-route-owner-protocol-B2D1XMya.mjs";
import process from "node:process";
import { execFile, spawn } from "node:child_process";
import { promisify } from "node:util";
//#region src/infra/tailscale-route-owner.worker.ts
const READY_MARKER = "Press Ctrl+C to exit.";
const OUTPUT_LIMIT = 2e5;
const STOP_GRACE_MS = 2e3;
const execFileAsync = promisify(execFile);
function appendBounded(current, chunk) {
const next = current + chunk.toString();
return next.length <= OUTPUT_LIMIT ? next : next.slice(next.length - OUTPUT_LIMIT);
}
function parseStart(raw) {
const parsed = JSON.parse(raw ?? "null");
const argv = isRecord(parsed) ? parsed.argv : void 0;
if (!Array.isArray(argv) || !argv.every((entry) => typeof entry === "string") || argv.length === 0) throw new Error("invalid Tailscale route-owner start payload");
return { argv };
}
function send(message) {
if (!process.connected || !process.send) return;
try {
process.send(message, () => void 0);
} catch {}
}
async function signalChild(child, signal, privileged, onError) {
if (typeof child.pid !== "number" || child.pid <= 0) return;
if (process.platform !== "win32") {
if (privileged) {
try {
await execFileAsync("sudo", [
"-n",
"/bin/kill",
`-${signal.slice(3)}`,
"--",
`-${child.pid}`
], {
timeout: 5e3,
maxBuffer: 16384
});
} catch {
onError(`Could not stop the owned Tailscale process group ${child.pid} through sudo. Run \`sudo /bin/kill -TERM -- -${child.pid}\` to stop it, then \`sudo tailscale set --operator=\$USER\` to avoid privileged claims.`);
}
return;
}
signalProcessTree(child.pid, signal, { detached: true });
return;
}
child.kill(signal === "SIGKILL" ? "SIGTERM" : signal);
}
function runTailscaleRouteOwner(start, sendMessage = send) {
const command = start.argv[0];
if (!command) throw new Error("Tailscale route-owner command is empty");
const args = start.argv.slice(1);
const output = {
stdout: "",
stderr: ""
};
let ready = false;
let stopping = false;
let closed = false;
let forceTimer;
const signalOperations = [];
const exit = createDeferredCore();
const child = spawn(command, args, {
detached: process.platform !== "win32",
stdio: [
"ignore",
"pipe",
"pipe"
],
windowsHide: true
});
const stop = () => {
if (stopping || closed) return;
stopping = true;
const signal = (value) => {
signalOperations.push(signalChild(child, value, command === "sudo" && args[0] === "-n", (message) => {
if (!closed) sendMessage({
type: "stop-failed",
message
});
}));
};
signal("SIGTERM");
forceTimer = setTimeout(() => signal("SIGKILL"), STOP_GRACE_MS);
forceTimer.unref?.();
};
child.once("spawn", () => {
if (typeof child.pid === "number") sendMessage({
type: "spawned",
pid: child.pid
});
});
for (const stream of ["stdout", "stderr"]) child[stream]?.on("data", (chunk) => {
output[stream] = appendBounded(output[stream], chunk);
if (!ready && output[stream].includes(READY_MARKER)) {
ready = true;
sendMessage({ type: "ready" });
}
});
child.once("error", (error) => {
output.stderr = appendBounded(output.stderr, error instanceof Error ? error.message : String(error));
});
child.once("close", (code, signal) => {
closed = true;
if (forceTimer) clearTimeout(forceTimer);
Promise.all(signalOperations).then(() => {
if (!stopping || !ready) sendMessage({
type: "failed",
code,
signal,
...output
});
exit.resolve({
code,
signal,
stopping
});
});
});
return {
exited: exit.promise,
stop
};
}
if (process.argv[2] === "--openclaw-tailscale-route-owner") try {
const owner = runTailscaleRouteOwner(parseStart(process.argv[3]));
for (const signal of [
"SIGINT",
"SIGTERM",
"SIGHUP"
]) process.once(signal, owner.stop);
process.once("disconnect", owner.stop);
process.once("message", (message) => {
if (isRecord(message) && message.type === "stop") owner.stop();
});
if (!process.connected) owner.stop();
owner.exited.then((exit) => process.exit(exit.stopping ? 0 : 1));
} catch (error) {
send({
type: "failed",
code: null,
signal: null,
stdout: "",
stderr: error instanceof Error ? error.message : String(error)
});
process.exit(1);
}
//#endregion
export { runTailscaleRouteOwner };