UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

260 lines (259 loc) 12.5 kB
import { b as NODE_WORKER_DESKTOP_STREAM_COMMAND, y as NODE_WORKER_DESKTOP_LAUNCH_COMMAND } from "./node-commands-BC8PhxqU.js"; import { t as DesktopSessionStaleOwnerError } from "./session-registry-un13apYG.js"; import { isDeepStrictEqual } from "node:util"; //#region src/gateway/worker-environments/node-desktop-carrier.ts const APP_LAUNCH_TIMEOUT_MS = 3e4; function snapshotNodeDesktopBinding(record) { if (record.state !== "ready" && record.state !== "idle" && record.state !== "attached" || record.destroyRequestedAtMs !== null || !record.leaseId || !record.nodeDeviceId || record.sshEndpoint !== null || !record.desktop) throw new Error("Worker environment node desktop owner is not active"); return { environmentId: record.environmentId, leaseId: record.leaseId, nodeDeviceId: record.nodeDeviceId, ownerEpoch: record.ownerEpoch, desktop: structuredClone(record.desktop) }; } function isBindingCurrent(store, binding) { const current = store.get(binding.environmentId); return Boolean(current && (current.state === "ready" || current.state === "idle" || current.state === "attached") && current.destroyRequestedAtMs === null && current.leaseId === binding.leaseId && current.nodeDeviceId === binding.nodeDeviceId && current.sshEndpoint === null && current.ownerEpoch === binding.ownerEpoch && current.desktop !== null && isDeepStrictEqual(current.desktop, binding.desktop)); } function invocationError(result) { const message = result.error?.message?.trim(); return new Error(message || "worker node desktop stream closed before attachment"); } function requireLaunchReady(result) { if (!result.ok) throw invocationError(result); let payload; try { payload = result.payloadJSON ? JSON.parse(result.payloadJSON) : void 0; } catch { throw new Error("Worker environment node desktop launcher returned malformed JSON"); } if (!payload || typeof payload !== "object" || Array.isArray(payload) || Object.keys(payload).length !== 1 || !("status" in payload) || payload.status !== "ready") throw new Error("Worker environment node desktop launcher returned an invalid result"); } function launchKey(binding, app) { return `${binding.environmentId}\0${app.id}`; } function signalError(signal) { return signal.reason instanceof Error ? signal.reason : /* @__PURE__ */ new Error("Worker environment node desktop operation aborted"); } function raceWithSignal(operation, signal) { if (signal.aborted) return Promise.reject(signalError(signal)); return new Promise((resolve, reject) => { const onAbort = () => { signal.removeEventListener("abort", onAbort); reject(signalError(signal)); }; signal.addEventListener("abort", onAbort, { once: true }); operation.then((value) => { signal.removeEventListener("abort", onAbort); resolve(value); }, (error) => { signal.removeEventListener("abort", onAbort); reject(error instanceof Error ? error : /* @__PURE__ */ new Error("Node desktop operation failed")); }); }); } /** Carries one durable worker environment's desktop over its private node connection. */ function createWorkerNodeDesktopCarrier(options) { let runtime; const claimedEpochs = /* @__PURE__ */ new Map(); const activeStreams = /* @__PURE__ */ new Set(); const activeLaunches = /* @__PURE__ */ new Map(); const bindingIsCurrent = (binding, capturedRuntime, node) => runtime === capturedRuntime && isBindingCurrent(options.store, binding) && capturedRuntime.transport.isCurrent(node, false); const findCurrentNode = async (binding, capturedRuntime, signal) => { signal.throwIfAborted(); const nodes = await raceWithSignal(capturedRuntime.transport.listCurrentNodes(), signal); signal.throwIfAborted(); const node = nodes.find((candidate) => candidate.nodeId === binding.nodeDeviceId); if (!node || !bindingIsCurrent(binding, capturedRuntime, node)) throw new Error("Worker environment node desktop connection is not current"); return node; }; const retireStream = (active) => { if (active.stopped) return; active.stopped = true; clearTimeout(active.unclaimedTimer); active.ticket?.cancel(); active.controller.abort(/* @__PURE__ */ new Error("Worker environment node desktop owner stopped")); if (!active.reservationTransferred) active.reservation?.release(); active.stream?.destroy(); activeStreams.delete(active); }; const stopStream = async (active) => { retireStream(active); await active.invocation?.catch(() => void 0); }; const stopLaunch = async (active) => { active.controller.abort(/* @__PURE__ */ new Error("Worker environment node desktop owner stopped")); await active.operation.catch(() => void 0); }; const stopOwnedOperations = async (environmentId, ownerEpoch) => { const streams = [...activeStreams].filter((active) => active.binding.environmentId === environmentId && (ownerEpoch === void 0 || active.binding.ownerEpoch === ownerEpoch)); const launches = [...activeLaunches.values()].filter((active) => active.binding.environmentId === environmentId && (ownerEpoch === void 0 || active.binding.ownerEpoch === ownerEpoch)); await Promise.all([...streams.map(stopStream), ...launches.map(stopLaunch)]); }; const claimOwner = async (binding) => { let advanced; try { advanced = options.desktopRegistry.claimOwnerEpoch(binding.environmentId, binding.ownerEpoch); } catch (error) { if (error instanceof DesktopSessionStaleOwnerError) throw new Error("Worker environment node desktop owner epoch is stale", { cause: error }); throw error; } const previousEpoch = claimedEpochs.get(binding.environmentId); if (previousEpoch === void 0 || binding.ownerEpoch > previousEpoch) claimedEpochs.set(binding.environmentId, binding.ownerEpoch); if (!advanced) return; const staleStreams = [...activeStreams].filter((active) => active.binding.environmentId === binding.environmentId && active.binding.ownerEpoch < binding.ownerEpoch); const staleLaunches = [...activeLaunches.values()].filter((active) => active.binding.environmentId === binding.environmentId && active.binding.ownerEpoch < binding.ownerEpoch); await options.desktopRegistry.stopSuperseded(binding.environmentId, binding.ownerEpoch); await Promise.all([...staleStreams.map(stopStream), ...staleLaunches.map(stopLaunch)]); }; const observe = async (request) => { const binding = snapshotNodeDesktopBinding(request.record); const active = { binding, controller: new AbortController(), reservationTransferred: false, stopped: false }; activeStreams.add(active); try { await claimOwner(binding); active.controller.signal.throwIfAborted(); await options.desktopRegistry.activate({ sourceKey: binding.environmentId, ownerEpoch: binding.ownerEpoch, teardown: async () => { await stopOwnedOperations(binding.environmentId, binding.ownerEpoch); } }); active.controller.signal.throwIfAborted(); const capturedRuntime = runtime; if (!capturedRuntime) throw new Error("Worker environment node desktop runtime is unavailable"); const node = await findCurrentNode(binding, capturedRuntime, active.controller.signal); active.reservation = options.desktopRegistry.reserveObserver(binding.environmentId, binding.ownerEpoch); if (!active.reservation) throw new Error("Worker environment desktop observer limit reached"); active.ticket = capturedRuntime.streamBroker.mint({ nodeId: node.nodeId, connId: node.connId, pairingGeneration: node.pairingGeneration }); active.invocation = capturedRuntime.transport.invoke({ node, command: NODE_WORKER_DESKTOP_STREAM_COMMAND, params: { ticket: active.ticket.ticket, attachPath: active.ticket.attachPath, port: binding.desktop.port, ...binding.desktop.passwordFilePath ? { passwordFilePath: binding.desktop.passwordFilePath } : {} }, timeoutMs: 0, signal: active.controller.signal, isDispatchAuthorized: () => bindingIsCurrent(binding, capturedRuntime, node) }); const invocationFinished = active.invocation.then((result) => { throw invocationError(result); }); invocationFinished.catch(() => void 0); const attached = await Promise.race([active.ticket.attached, invocationFinished]); active.stream = attached.stream; if (!bindingIsCurrent(binding, capturedRuntime, node)) throw new Error("Worker environment node desktop owner changed before attachment"); if (attached.auth !== "vnc-password" || !attached.vncPassword) throw new Error("Worker environment node desktop did not provide VNC authentication"); const { DESKTOP_OBSERVE_PATH, mintDesktopObserverToken } = await import("./observe-bridge-BoVfcTPP.js"); if (!bindingIsCurrent(binding, capturedRuntime, node)) throw new Error("Worker environment node desktop owner changed before publication"); const attachment = options.desktopRegistry.publishStream({ sourceKey: binding.environmentId, ownerEpoch: binding.ownerEpoch, stream: attached.stream, reservation: active.reservation }); if (!attachment) throw new Error("Worker environment node desktop owner changed before publication"); active.reservationTransferred = true; const issuedAtMs = Date.now(); const minted = mintDesktopObserverToken({ sourceKey: binding.environmentId, ownerEpoch: binding.ownerEpoch, control: request.control, attachment, preauth: { auth: "vnc-password", credentials: { password: attached.vncPassword } }, nowMs: issuedAtMs }); active.unclaimedTimer = setTimeout(() => { if (options.desktopRegistry.hasPendingStream(binding.environmentId, attachment)) stopStream(active); }, Math.max(0, minted.expiresAtMs - Date.now())); active.unclaimedTimer.unref?.(); active.invocation.finally(() => retireStream(active)).catch(() => void 0); return { transport: "rfb", wsPath: `${DESKTOP_OBSERVE_PATH}?token=${minted.token}`, expiresAtMs: minted.expiresAtMs, control: request.control }; } catch (error) { await stopStream(active); throw error; } }; const launchApp = (request) => { const binding = snapshotNodeDesktopBinding(request.record); const advertisedApp = binding.desktop.apps?.find((app) => app.id === request.app.id); if (!advertisedApp || !isDeepStrictEqual(advertisedApp, request.app)) return Promise.reject(/* @__PURE__ */ new Error("Worker environment node desktop app descriptor is not current")); const app = structuredClone(advertisedApp); const key = launchKey(binding, app); const previous = activeLaunches.get(key); if (previous?.binding.ownerEpoch === binding.ownerEpoch && isDeepStrictEqual(previous.app, app)) return previous.operation; const controller = new AbortController(); const operation = Promise.resolve().then(async () => { await claimOwner(binding); if (previous) { previous.controller.abort(/* @__PURE__ */ new Error("Worker environment node desktop launch owner replaced")); await previous.operation.catch(() => void 0); } controller.signal.throwIfAborted(); const capturedRuntime = runtime; if (!capturedRuntime) throw new Error("Worker environment node desktop runtime is unavailable"); const node = await findCurrentNode(binding, capturedRuntime, controller.signal); if (activeLaunches.get(key) !== entry) throw new Error("Worker environment node desktop launch owner was replaced"); requireLaunchReady(await capturedRuntime.transport.invoke({ node, command: NODE_WORKER_DESKTOP_LAUNCH_COMMAND, params: app, timeoutMs: APP_LAUNCH_TIMEOUT_MS, signal: controller.signal, isDispatchAuthorized: () => bindingIsCurrent(binding, capturedRuntime, node) })); if (!bindingIsCurrent(binding, capturedRuntime, node)) throw new Error("Worker environment node desktop launch owner changed"); }); const entry = { binding, app, controller, operation }; activeLaunches.set(key, entry); operation.finally(() => { if (activeLaunches.get(key) === entry) activeLaunches.delete(key); }).catch(() => void 0); return operation; }; const stop = async (environmentId, ownerEpoch) => { await Promise.all([options.desktopRegistry.stop(environmentId, ownerEpoch), stopOwnedOperations(environmentId, ownerEpoch)]); }; const stopAll = async () => { await Promise.all([...claimedEpochs].map(([environmentId, ownerEpoch]) => stop(environmentId, ownerEpoch))); }; return { bindRuntime(next) { runtime = next; }, launchApp, observe, stop, stopAll }; } //#endregion export { createWorkerNodeDesktopCarrier };