UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

312 lines (311 loc) • 9.91 kB
import { u as toErrorObject } from "./error-coercion-CvJK4lmH.mjs"; import { t as createDeferredCore } from "./deferred-D0La5CRk.mjs"; import { n as serializeBrokerError, t as SpawnBrokerError } from "./protocol-foRdUdX7.mjs"; import { AsyncResource } from "node:async_hooks"; import { Socket } from "node:net"; import { EventEmitter } from "node:events"; //#region src/process/spawn-broker/pipe.ts const readers = /* @__PURE__ */ new WeakMap(); const holdReadable = () => {}; /** Defer consumption and EOF until the transferred socket is ready for its caller. */ function holdPipe(socket) { const held = { read: socket.read.bind(socket), resumed: false, onResume: () => { held.resumed = true; } }; readers.set(socket, held); socket.on("resume", held.onResume); socket.on("readable", holdReadable); socket.read = () => null; } /** Restore native stdin's write-only direction after Node's duplex IPC handoff. */ function restoreStdinPipe(socket) { const state = Reflect.get(socket, "_readableState"); if (!state || typeof state !== "object" || [ "ended", "endEmitted", "reading" ].some((flag) => typeof Reflect.get(state, flag) !== "boolean")) throw new SpawnBrokerError("Spawn broker requires a Node readable state for stdin"); stopPipeReads(socket); Object.assign(state, { readable: false, ended: true, endEmitted: true, reading: false }); } function stopPipeReads(socket) { const handle = Reflect.get(socket, "_handle"); if (!handle || typeof handle !== "object" || !("readStop" in handle) || typeof handle.readStop !== "function" || !("reading" in handle) || typeof handle.reading !== "boolean") throw new SpawnBrokerError("Spawn broker requires a transferable Node pipe handle"); if (handle.readStop() !== 0) throw new SpawnBrokerError("Spawn broker could not stop pipe reads"); handle.reading = false; } /** Publish stream data and EOF after the caller's readiness continuation. */ function releasePipe(socket) { const held = restoreReader(socket); if (!held) return; process.nextTick(() => { if (socket.destroyed) return; socket.emit("readable"); if (held.resumed) socket.resume(); }); } function restoreReader(socket) { const held = readers.get(socket); if (!held) return; socket.read = held.read; readers.delete(socket); socket.removeListener("resume", held.onResume); socket.removeListener("readable", holdReadable); return held; } //#endregion //#region src/process/spawn-broker/child.ts /** Native pipes remain native streams; only lifecycle and IPC cross the broker. */ var BrokerChild = class extends EventEmitter { constructor(requestId, argv, transmit) { super(); this.requestId = requestId; this.transmit = transmit; this.callbackContext = new AsyncResource("OpenClawSpawnBrokerChild"); this.stdin = null; this.stdout = null; this.stderr = null; this.connected = false; this.killed = false; this.exitCode = null; this.signalCode = null; this.stdio = [ null, null, null, null, null ]; this.opened = createDeferredCore(); this.completion = createDeferredCore(); this.pipes = /* @__PURE__ */ new Set(); this.sends = /* @__PURE__ */ new Map(); this.sendSequence = 0; this.spawnReceived = false; this.eventsReleased = false; this.pendingEventOverflow = false; this.pendingEvents = []; this.exited = false; this.closed = false; this.processNotStarted = false; this.spawnfile = argv[0]; this.spawnargs = [...argv]; this.opened.promise.catch(() => {}); this.on("error", () => {}); } /** Only the admission owner can establish that no native process was started. */ markNotStarted() { this.processNotStarted = true; } get notStarted() { return this.processNotStarted; } ready() { return this.opened.promise; } emit(event, ...args) { return this.callbackContext.runInAsyncScope(() => super.emit(event, ...args)); } /** Transport bookkeeping survives disposal of the caller's event listeners. */ waitForClose() { return this.completion.promise; } attachPipe(fd, socket) { socket.emit = this.callbackContext.bind(socket.emit.bind(socket)); this.stdio[fd] = socket; if (fd === 0) this.stdin = socket; if (fd === 1) this.stdout = socket; if (fd === 2) this.stderr = socket; this.pipes.add(socket); let acknowledged = false; const acknowledge = (error) => { if (acknowledged) return; acknowledged = true; this.transmit({ type: "output-drained", id: this.requestId, fd, error: error ? serializeBrokerError(error) : void 0 }).catch(() => {}); }; socket.once(fd === 0 ? "finish" : "end", () => acknowledge()); socket.on("error", acknowledge); socket.once("close", () => { acknowledge(); this.pipes.delete(socket); this.finishClose(); }); } receive(message) { if (message.type === "spawned") { if (this.spawnReceived || this.closed) return; this.spawnReceived = true; this.pid = message.pid; this.spawnfile = message.spawnfile; this.spawnargs = message.spawnargs; this.connected = message.connected; this.stdio.splice(message.stdioLength); while (this.stdio.length < message.stdioLength) this.stdio.push(null); if (this.connected) this.channel = Object.assign(new EventEmitter(), { ref() {}, unref() {} }); this.opened.resolve(); setImmediate(() => { if (this.closed) return; this.emit("spawn"); setImmediate(() => { for (const [fd, pipe] of this.stdio.entries()) if (fd > 0 && pipe instanceof Socket && !pipe.destroyed) releasePipe(pipe); this.eventsReleased = true; for (const event of this.pendingEvents.splice(0)) event(); }); }); return; } if (!this.eventsReleased && (this.spawnReceived || message.type !== "error")) { this.deferEvent(() => this.emitMessage(message)); return; } this.emitMessage(message); } deferEvent(event) { if (this.pendingEventOverflow) return; if (this.pendingEvents.length >= 64) { this.pendingEventOverflow = true; this.pendingEvents.splice(0); this.pendingEvents.push(() => { this.kill("SIGKILL"); this.failNow(new SpawnBrokerError("Spawn broker startup event capacity exceeded")); }); return; } this.pendingEvents.push(event); } emitMessage(message) { switch (message.type) { case "error": { const error = Object.assign(new Error(message.error.message), message.error); this.opened.reject(error); this.emit("error", error); if (!this.pid) { this.exited = true; this.finishClose(); } break; } case "exit": this.exited = true; this.exitCode = message.code; this.signalCode = message.signal; this.stdin?.destroy(); this.emit("exit", message.code, message.signal); process.nextTick(() => { for (const pipe of this.pipes) if (pipe.readable) pipe.resume(); }); this.finishClose(); break; case "ipc-sent": this.finishSend(message.sequence, message.error ? Object.assign(new Error(message.error.message), message.error) : null); break; case "disconnect": this.failSends(/* @__PURE__ */ new Error("Child process IPC channel is closed")); this.connected = false; this.channel = void 0; this.emit("disconnect"); break; case "ipc": this.emit("message", message.message); break; case "closed": this.finishClose(); } } fail(error) { if (this.spawnReceived && !this.eventsReleased) { this.deferEvent(() => this.failNow(error)); return; } this.failNow(error); } failNow(error) { if (this.closed) return; this.opened.reject(error); this.failSends(error); const wasConnected = this.connected; this.connected = false; this.channel = void 0; if (wasConnected) this.emit("disconnect"); if (!this.exited) this.emit("error", error); for (const pipe of this.pipes) pipe.destroy(error); this.exited = true; this.finishClose(); } finishClose() { if (!this.exited || this.pipes.size > 0 || this.closed) return; this.closed = true; this.completion.resolve(); this.emit("close", this.exitCode, this.signalCode); } kill(signal = "SIGTERM") { if (this.exited) return false; this.killed = true; this.transmit({ type: "kill", id: this.requestId, signal }).catch((error) => this.fail(toErrorObject(error, "Spawn broker signal delivery failed"))); return true; } send(message, handleOrCallback, optionsOrCallback, callback) { const done = typeof handleOrCallback === "function" ? handleOrCallback : typeof optionsOrCallback === "function" ? optionsOrCallback : callback; if (!this.connected || this.sends.size >= 1024) { const error = /* @__PURE__ */ new Error(this.connected ? "Child process IPC capacity exceeded" : "Child process IPC channel is closed"); queueMicrotask(() => done ? done(error) : this.emit("error", error)); return false; } const sequence = ++this.sendSequence; this.sends.set(sequence, AsyncResource.bind((error) => { if (done) done(error); else if (error) this.emit("error", error); })); const handle = typeof handleOrCallback === "function" ? void 0 : handleOrCallback; this.transmit({ type: "ipc", id: this.requestId, sequence, message }, handle).catch((error) => this.finishSend(sequence, toErrorObject(error, "Spawn broker IPC delivery failed"))); return true; } finishSend(sequence, error) { const callback = this.sends.get(sequence); this.sends.delete(sequence); callback?.(error); } failSends(error) { for (const sequence of this.sends.keys()) this.finishSend(sequence, error); } disconnect() { if (!this.connected) return; this.connected = false; this.transmit({ type: "disconnect", id: this.requestId }).catch((error) => this.fail(toErrorObject(error, "Spawn broker disconnect failed"))); } ref() {} unref() {} [Symbol.dispose]() { this.kill("SIGTERM"); } }; //#endregion export { holdPipe as n, restoreStdinPipe as r, BrokerChild as t };