openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
1,171 lines (1,170 loc) • 42.3 kB
JavaScript
import { t as createDeferredCore } from "./deferred-D0La5CRk.mjs";
import { c as sleepWithAbort } from "./src-DqaLEvc_.mjs";
import { s as registerSecretValueForRedaction } from "./secret-redaction-registry-b2B9lcfy.mjs";
import { t as runCommandBuffered } from "./exec-DkOImFhW.mjs";
import { i as tryListenOnPort } from "./ports-probe-i0c0rzCC.mjs";
import { t as getProcessSupervisor } from "./supervisor-B-UzEf0M.mjs";
import { t as detectBinary } from "./detect-binary-CYArgZQe.mjs";
import { i as probeRfbServer, t as classifyRfbSecurity } from "./rfb-probe-CpoOs5tq.mjs";
import { n as HostDesktopCredentialsRequiredError } from "./host-source-errors-MaOVAU6G.mjs";
import { r as mintDesktopObserverToken } from "./observe-bridge-DULhOcz9.mjs";
import path from "node:path";
import fs from "node:fs/promises";
import os from "node:os";
import crypto from "node:crypto";
import { Readable } from "node:stream";
//#region src/gateway/desktop/host-guidance.ts
/** Platform-specific next steps for preparing a loopback-only host VNC server. */
const HOST_DESKTOP_GUIDANCE = {
darwin: "Enable System Settings -> General -> Sharing -> Screen Sharing, or run `sudo launchctl enable system/com.apple.screensharing && sudo launchctl kickstart -k system/com.apple.screensharing`.",
linux: "Install the managed desktop binaries with `apt install tigervnc-standalone-server tigervnc-tools xfce4-session dbus-daemon`, enable desktop.host.managed, or run a loopback-only VNC server yourself. For optional managed desktop audio, also install pulseaudio pulseaudio-utils; audio uses a private null sink, never the host microphone or physical system mix. gnome-remote-desktop uses unsupported VeNCrypt.",
win32: "Install TightVNC with `SET_USEVNCAUTHENTICATION=1 SET_ALLOWLOOPBACK=1 ACCEPTHTTPCONNECTIONS=0` and listen on 127.0.0.1:5900. Locked or UAC sessions may render black."
};
/** Resolves guidance for supported gateway platforms, falling back to Linux-style setup. */
function getHostDesktopGuidance(platform) {
return HOST_DESKTOP_GUIDANCE[platform] ?? HOST_DESKTOP_GUIDANCE.linux;
}
//#endregion
//#region src/gateway/desktop/managed-linux-audio.ts
const SINK_NAME = "openclaw_desktop";
const STARTUP_TIMEOUT_MS = 5e3;
const MAX_BUFFER_BYTES = 192e3;
const AUDIO_GUIDANCE = "Install pulseaudio and pulseaudio-utils for managed desktop audio.";
/** Private server/capture routing; apply to apps only after the server is ready. */
function managedLinuxAudioEnv(tempDir) {
const runtimeDir = path.join(tempDir, "pulse");
return {
PULSE_SERVER: `unix:${path.join(runtimeDir, "native")}`,
PULSE_SINK: SINK_NAME,
PULSE_SOURCE: `${SINK_NAME}.monitor`,
PULSE_RUNTIME_PATH: runtimeDir,
PULSE_STATE_PATH: path.join(runtimeDir, "state"),
PULSE_CLIENTCONFIG: path.join(runtimeDir, "client.conf")
};
}
/** One server per desktop generation; capture processes exist only after source.start(). */
function createManagedLinuxAudio(params) {
const { supervisor } = params;
const env = {
...params.env,
...managedLinuxAudioEnv(params.tempDir)
};
const daemonEnv = {
...env,
LC_ALL: "C",
PULSE_CONFIG: path.join(params.tempDir, "pulse", "daemon.conf")
};
delete daemonEnv.PULSE_SCRIPT;
delete daemonEnv.PULSE_DLPATH;
delete daemonEnv.NOTIFY_SOCKET;
delete daemonEnv.PULSE_COOKIE;
delete daemonEnv.PULSE_LOG;
delete daemonEnv.PULSE_LOG_TARGET;
const scopeKey = `managed-desktop-audio:${crypto.randomUUID()}`;
const cleanup = supervisor.acquireScopeCleanup(scopeKey, { processTree: "required-all" });
const ready = createDeferredCore();
const serverReady = createDeferredCore();
const daemonFailed = createDeferredCore();
const captures = /* @__PURE__ */ new Set();
const startupTimeoutMs = params.runtime?.startupTimeoutMs ?? STARTUP_TIMEOUT_MS;
let active = true;
let closing;
let stderr = "";
const result = { stop: () => stop() };
const assertActive = () => {
params.assertCurrent();
if (!active) throw new Error(result.unavailableReason ?? "Managed desktop audio is stopped");
};
const stop = () => {
if (closing) return closing;
active = false;
delete result.source;
serverReady.reject(/* @__PURE__ */ new Error("Managed desktop audio stopped during startup"));
const captureCleanup = Promise.allSettled([...captures].map((close) => close()));
supervisor.cancelScope(scopeKey, "manual-cancel");
closing = Promise.resolve().then(async () => {
await launch;
const outcomes = await captureCleanup;
await cleanup();
const failed = outcomes.find((outcome) => outcome.status === "rejected");
if (failed) throw failed.reason;
});
return closing;
};
const unavailable = (error) => {
const detail = error instanceof Error ? error.message : String(error);
result.unavailableReason ??= `Managed desktop audio unavailable: ${detail}. ${AUDIO_GUIDANCE}`;
stop().catch(() => {});
daemonFailed.resolve();
};
serverReady.promise.catch(() => {});
const timer = setTimeout(() => {
unavailable(/* @__PURE__ */ new Error("private PulseAudio startup timed out"));
}, startupTimeoutMs);
timer.unref?.();
const source = { async start(signal, assertAuthority) {
const assertCaptureCurrent = () => {
assertActive();
signal.throwIfAborted();
assertAuthority?.();
};
assertCaptureCurrent();
const captureScope = `${scopeKey}:capture:${crypto.randomUUID()}`;
const cleanupCapture = supervisor.acquireScopeCleanup(captureScope, { processTree: "required-all" });
const firstData = createDeferredCore();
let running = true;
let stopped;
let captureStderr = "";
let captureRun;
const stream = new Readable({
read() {},
highWaterMark: MAX_BUFFER_BYTES
});
stream.on("error", () => {});
const stopCapture = () => {
if (stopped) return stopped;
running = false;
clearTimeout(captureTimer);
signal.removeEventListener("abort", abort);
firstData.reject(/* @__PURE__ */ new Error("Desktop audio capture stopped"));
stream.destroy();
captureRun?.detachOutput?.();
supervisor.cancelScope(captureScope, "manual-cancel");
stopped = Promise.resolve().then(async () => {
await captureLaunch;
await cleanupCapture();
captures.delete(stopCapture);
});
return stopped;
};
const failCapture = (error) => {
if (!running) return;
firstData.reject(error);
stream.destroy(error);
stopCapture().catch(() => {});
};
const abort = () => failCapture(/* @__PURE__ */ new Error("Desktop audio capture aborted"));
const captureTimer = setTimeout(() => failCapture(/* @__PURE__ */ new Error("Desktop audio capture startup timed out")), startupTimeoutMs);
captureTimer.unref?.();
captures.add(stopCapture);
signal.addEventListener("abort", abort, { once: true });
stream.once("close", () => void stopCapture().catch(() => {}));
const captureLaunch = Promise.resolve().then(async () => {
try {
captureRun = await supervisor.spawn({
scopeKey: captureScope,
mode: "child",
argv: [
"parec",
`--server=${env.PULSE_SERVER}`,
`--device=${SINK_NAME}.monitor`,
"--raw",
"--format=s16le",
"--rate=48000",
"--channels=2",
"--channel-map=front-left,front-right",
"--latency-msec=40"
],
env,
exactEnv: true,
stdinMode: "pipe-closed",
captureOutput: false,
assertCurrent: () => {
assertCaptureCurrent();
if (!running) throw new Error("Desktop audio capture stopped");
},
onStderr: (chunk) => {
captureStderr = (captureStderr + chunk).slice(-4096);
},
onStdoutRaw: (chunk) => {
if (!running || chunk.length === 0) return;
if (stream.readableLength + chunk.length > MAX_BUFFER_BYTES) {
failCapture(/* @__PURE__ */ new Error("Desktop audio viewer is not consuming audio"));
return;
}
stream.push(chunk);
firstData.resolve();
}
});
captureRun.wait().then(() => failCapture(new Error(captureStderr.trim() || "Desktop audio capture exited")), (error) => failCapture(error instanceof Error ? error : new Error(String(error))));
} catch (error) {
failCapture(error instanceof Error ? error : new Error(String(error)));
}
});
try {
await firstData.promise;
await captureLaunch;
assertCaptureCurrent();
if (!running) throw new Error("Desktop audio capture stopped during startup");
return {
stream,
stop: stopCapture
};
} catch (error) {
await stopCapture();
throw error;
} finally {
clearTimeout(captureTimer);
}
} };
const launch = Promise.resolve().then(async () => {
try {
const detect = params.runtime?.detectBinary ?? detectBinary;
for (const binary of ["pulseaudio", "parec"]) {
if (!await detect(binary)) throw new Error(`${binary} is not installed`);
assertActive();
}
const runtimeDir = path.join(params.tempDir, "pulse");
await fs.mkdir(runtimeDir, {
recursive: true,
mode: 448
});
await fs.writeFile(path.join(runtimeDir, "client.conf"), "autospawn = no\n", { mode: 384 });
await fs.writeFile(path.join(runtimeDir, "daemon.conf"), "local-server-type = user\n", { mode: 384 });
const script = path.join(runtimeDir, "default.pa");
await fs.writeFile(script, [
".fail",
`load-module module-null-sink sink_name=${SINK_NAME} format=s16le rate=48000 channels=2 channel_map=front-left,front-right`,
`set-default-sink ${SINK_NAME}`,
`set-default-source ${SINK_NAME}.monitor`,
"load-module module-native-protocol-unix auth-anonymous=1 auth-cookie-enabled=0",
""
].join("\n"), { mode: 384 });
assertActive();
(await supervisor.spawn({
scopeKey,
mode: "child",
argv: [
"pulseaudio",
"--daemonize=no",
"--fail=yes",
"--use-pid-file=no",
"--exit-idle-time=-1",
"--disable-shm=yes",
"--system=no",
"--realtime=no",
"--high-priority=no",
"--disallow-exit=yes",
"--disallow-module-loading=yes",
"--log-target=stderr",
"--log-level=info",
"-n",
"--file",
script
],
env: daemonEnv,
exactEnv: true,
assertCurrent: assertActive,
stdinMode: "pipe-closed",
captureOutput: false,
onStderr: (chunk) => {
stderr = (stderr + chunk).slice(-4096);
if (stderr.includes("Daemon startup complete.")) serverReady.resolve();
}
})).wait().then(() => unavailable(new Error(stderr.trim() || "private PulseAudio exited")), unavailable);
await serverReady.promise;
assertActive();
result.source = source;
result.failed = daemonFailed.promise;
} catch (error) {
unavailable(error);
} finally {
clearTimeout(timer);
ready.resolve(result);
}
});
return {
ready: ready.promise.then(async (value) => {
if (closing) await closing;
return value;
}),
stop
};
}
//#endregion
//#region src/gateway/desktop/managed-linux.ts
const MANAGED_DISPLAY_FIRST = 99;
const MANAGED_DISPLAY_LAST = 199;
const MANAGED_RESTART_LIMIT = 3;
const MANAGED_RESTART_WINDOW_MS = 3e5;
const AUDIO_RESTART_LIMIT = 3;
const AUDIO_RESTART_WINDOW_MS = 3e5;
const MANAGED_READINESS_TIMEOUT_MS = 15e3;
const MANAGED_READINESS_POLL_MS = 100;
const STDERR_TAIL_CHARS = 4096;
function buildTigerVncArgv(resources) {
return [
"Xtigervnc",
`:${resources.display}`,
"-geometry",
"1920x1080",
"-depth",
"24",
"-localhost",
"yes",
"-rfbport",
String(resources.port),
"-SecurityTypes",
"VncAuth",
"-PasswordFile",
resources.passwordFile,
"-AlwaysShared",
"-AcceptSetDesktopSize",
"-nolisten",
"tcp",
"-ac"
];
}
function chooseDisplayNumber(socketNames) {
const occupied = new Set(socketNames.flatMap((name) => {
const match = /^X(\d+)$/u.exec(name);
return match ? [Number.parseInt(match[1] ?? "", 10)] : [];
}));
for (let display = MANAGED_DISPLAY_FIRST; display <= MANAGED_DISPLAY_LAST; display += 1) if (!occupied.has(display)) return display;
throw new Error(`managed Linux desktop could not find an unused X display between :${MANAGED_DISPLAY_FIRST} and :${MANAGED_DISPLAY_LAST}`);
}
function appendTail(current, chunk) {
const next = current + chunk;
return next.length <= STDERR_TAIL_CHARS ? next : next.slice(-4096);
}
function lastStderrLine(stderr) {
const lines = stderr.split(/\r?\n/u);
for (let index = lines.length - 1; index >= 0; index -= 1) {
const line = lines[index]?.trim();
if (line) return line;
}
}
async function readDisplaySocketNames(socketDir) {
try {
return await fs.readdir(socketDir);
} catch (error) {
if (error.code === "ENOENT") return [];
throw error;
}
}
function binaryError(binary, error) {
const reason = error instanceof Error ? error.message : String(error);
return new Error(`managed Linux desktop could not start ${binary}: ${reason}. ${getHostDesktopGuidance("linux")}`, { cause: error });
}
function createManagedLinuxDesktop(params = {}) {
const supervisor = params.supervisor ?? getProcessSupervisor();
const nowMs = params.runtime?.nowMs ?? Date.now;
const createAudio = params.runtime?.createAudio ?? createManagedLinuxAudio;
const probeRfb = params.runtime?.probeRfb ?? probeRfbServer;
const randomBytes = params.runtime?.randomBytes ?? crypto.randomBytes;
const readinessPollMs = params.runtime?.readinessPollMs ?? MANAGED_READINESS_POLL_MS;
const readinessTimeoutMs = params.runtime?.readinessTimeoutMs ?? MANAGED_READINESS_TIMEOUT_MS;
const runPasswordTool = params.runtime?.runPasswordTool ?? runCommandBuffered;
const wait = params.runtime?.sleep ?? ((ms) => sleepWithAbort(ms, void 0, { ref: false }));
const tempRoot = params.runtime?.tempRoot ?? os.tmpdir();
const pickPort = params.runtime?.tryListenOnPort ?? tryListenOnPort;
const x11SocketDir = params.runtime?.x11SocketDir ?? "/tmp/.X11-unix";
const scopeKey = `host-desktop-managed-linux:${crypto.randomUUID()}`;
let status = { state: "not-started" };
let resources;
let pair;
let startPromise;
let stopPromise;
let stopProcesses;
let audioOwner;
let epoch = 0;
let stopping = false;
let stderrTail = "";
let restartTimes = [];
let audioRestartTimes = [];
const activeWaits = /* @__PURE__ */ new Set();
const monitorTasks = /* @__PURE__ */ new Set();
const isPairCurrent = (current) => !stopping && pair === current && current.current && !current.vnc.activity.resultSettled && !current.bus.activity.resultSettled && !current.session.activity.resultSettled;
const publicResult = (active) => ({
attachment: {
kind: "tcp",
host: "127.0.0.1",
port: active.port
},
auth: "vnc-password",
vncPassword: active.password,
resolveAudio: () => {
if (resources !== active) return;
if (!pair || !isPairCurrent(pair)) throw new Error("managed Linux desktop is restarting; retry when it is ready");
return pair.audio.source;
},
get audioUnavailableReason() {
return resources === active && pair && isPairCurrent(pair) ? pair.audio.unavailableReason : void 0;
}
});
const removeResources = async () => {
const current = resources;
resources = void 0;
if (current) await fs.rm(current.tempDir, {
recursive: true,
force: true
});
};
const markFailed = (error) => {
const coordinates = resources ? {
display: resources.display,
port: resources.port
} : status.state === "starting" || status.state === "failed" ? {
display: status.display,
port: status.port
} : {};
status = {
state: "failed",
error: error.message,
...coordinates
};
params.onFailed?.(error.message);
};
const prepareResources = async () => {
const tempDir = await fs.mkdtemp(path.join(tempRoot, "openclaw-managed-desktop-"));
await fs.chmod(tempDir, 448);
const plaintextFile = path.join(tempDir, "password.txt");
const passwordFile = path.join(tempDir, "passwd");
try {
const password = randomBytes(12).toString("base64url").slice(0, 8);
registerSecretValueForRedaction(password);
await fs.writeFile(plaintextFile, password, {
mode: 384,
flag: "wx"
});
const passwordInput = await fs.readFile(plaintextFile);
const filtered = await runPasswordTool(["tigervncpasswd", "-f"], {
input: passwordInput,
maxOutputBytes: {
stdout: 64,
stderr: 4096
},
timeoutMs: 1e4
});
if (filtered.termination !== "exit" || filtered.code !== 0 || filtered.stdout.length === 0) throw binaryError("tigervncpasswd", (filtered.error?.message ?? filtered.stderr.toString("utf8").trim()) || `exit code ${filtered.code ?? "none"}`);
await fs.writeFile(passwordFile, filtered.stdout, {
mode: 384,
flag: "wx"
});
await fs.rm(plaintextFile, { force: true });
const port = await pickPort({
port: 0,
host: "127.0.0.1",
exclusive: true
});
const display = chooseDisplayNumber(await readDisplaySocketNames(x11SocketDir));
const env = {
...process.env,
DISPLAY: `:${display}`,
DBUS_SESSION_BUS_ADDRESS: `unix:path=${path.join(tempDir, "bus")}`,
XDG_SESSION_TYPE: "x11"
};
delete env.WAYLAND_DISPLAY;
delete env.SESSION_MANAGER;
delete env.AT_SPI_BUS_ADDRESS;
delete env.DBUS_SESSION_BUS_PID;
return {
tempDir,
passwordFile,
password,
display,
port,
env: Object.freeze(env)
};
} catch (error) {
await fs.rm(tempDir, {
recursive: true,
force: true
});
throw error;
}
};
const waitUntilReady = async (active, activeEpoch) => {
const deadline = nowMs() + readinessTimeoutMs;
let lastProbe = "unreachable";
for (;;) {
if (activeEpoch !== epoch || stopping) break;
const probe = await probeRfb({
host: "127.0.0.1",
port: active.port,
timeoutMs: Math.min(1e3, readinessTimeoutMs)
});
lastProbe = probe.kind;
if (probe.kind === "rfb" && probe.securityTypes.includes(2)) return;
if (nowMs() >= deadline) break;
await wait(readinessPollMs);
}
if (activeEpoch !== epoch || stopping) throw new Error("managed Linux desktop stopped during startup");
throw new Error(`managed Linux desktop did not become ready on 127.0.0.1:${active.port} within ${readinessTimeoutMs}ms (last probe: ${lastProbe})`);
};
const stopPair = (current) => {
if (current?.stopPromise) return current.stopPromise;
if (current) current.current = false;
const retiringAudio = audioOwner;
const audioStopped = retiringAudio?.stop();
audioStopped?.catch(() => void 0);
const stopped = Promise.resolve().then(async () => {
const failure = (await Promise.allSettled([...current?.computerLeases ?? []].map(async (lease) => {
await lease.onStop();
current?.computerLeases.delete(lease);
}))).find((outcome) => outcome.status === "rejected");
await audioStopped;
if (audioOwner === retiringAudio) audioOwner = void 0;
if (failure) throw failure.reason;
supervisor.cancelScope(scopeKey, "manual-cancel");
const cleanup = stopProcesses;
await cleanup?.();
await Promise.allSettled(activeWaits);
if (stopProcesses === cleanup) stopProcesses = void 0;
if (pair === current) pair = void 0;
});
if (current) {
current.stopPromise = stopped;
stopped.catch(() => {
current.stopPromise = void 0;
});
}
return stopped;
};
const waitForRun = (run) => {
const pending = run.wait().catch((error) => ({
reason: "spawn-error",
exitCode: null,
exitSignal: null,
durationMs: Math.max(0, nowMs() - run.startedAtMs),
stdout: "",
stderr: error instanceof Error ? error.message : String(error),
timedOut: false,
noOutputTimedOut: false
}));
activeWaits.add(pending);
pending.finally(() => activeWaits.delete(pending));
return pending;
};
const spawnRun = async (binary, argv, activeEpoch, env, onStdout) => {
try {
return await supervisor.spawn({
scopeKey,
mode: "child",
argv,
...env ? { env } : {},
assertCurrent: () => {
if (activeEpoch !== epoch || stopping) throw new Error("managed Linux desktop stopped during startup");
},
...onStdout ? { onStdout } : {},
stdinMode: "pipe-closed",
maxCapturedOutputChars: STDERR_TAIL_CHARS,
onStderr: (chunk) => {
stderrTail = appendTail(stderrTail, chunk);
}
});
} catch (error) {
throw binaryError(binary, error);
}
};
const describeExit = (binary, exit) => {
return lastStderrLine(exit.stderr) ?? lastStderrLine(stderrTail) ?? `${binary} exited with code ${exit.exitCode ?? "none"}`;
};
const startPair = async (active, activeEpoch) => {
status = {
state: "starting",
display: active.display,
port: active.port
};
stopProcesses = supervisor.acquireScopeCleanup(scopeKey, { processTree: "required-all" });
const vnc = await spawnRun("Xtigervnc", buildTigerVncArgv(active), activeEpoch);
const vncExit = waitForRun(vnc);
try {
await Promise.race([waitUntilReady(active, activeEpoch), vncExit.then((exit) => {
throw new Error(describeExit("Xtigervnc", exit));
})]);
if (activeEpoch !== epoch || stopping) throw new Error("managed Linux desktop stopped during startup");
let audioPair = null;
audioOwner = createAudio({
supervisor,
tempDir: active.tempDir,
env: active.env,
assertCurrent: () => {
if (activeEpoch !== epoch || stopping || audioPair && !isPairCurrent(audioPair)) throw new Error("managed Linux desktop stopped");
}
});
const audio = await audioOwner.ready;
const env = audio.source ? Object.freeze({
...active.env,
...managedLinuxAudioEnv(active.tempDir)
}) : active.env;
const busReady = createDeferredCore();
let busOutput = "";
await fs.rm(path.join(active.tempDir, "bus"), { force: true });
const bus = await spawnRun("dbus-daemon", [
"dbus-daemon",
"--session",
"--nofork",
"--nopidfile",
"--print-address=1",
`--address=${active.env.DBUS_SESSION_BUS_ADDRESS}`
], activeEpoch, env, (chunk) => {
busOutput = appendTail(busOutput, chunk);
if (busOutput.includes("\n")) busReady.resolve();
});
const busExit = waitForRun(bus);
const busTimeout = setTimeout(() => busReady.reject(/* @__PURE__ */ new Error("managed Linux desktop D-Bus session did not become ready")), readinessTimeoutMs);
try {
await Promise.race([
busReady.promise,
busExit.then((exit) => {
throw new Error(describeExit("dbus-daemon", exit));
}),
vncExit.then((exit) => {
throw new Error(describeExit("Xtigervnc", exit));
})
]);
} finally {
clearTimeout(busTimeout);
}
const session = await spawnRun("startxfce4", ["startxfce4"], activeEpoch, env);
const nextPair = {
current: true,
vnc,
bus,
session,
vncExit,
busExit,
sessionExit: waitForRun(session),
computerLeases: /* @__PURE__ */ new Set(),
audio,
env
};
audioPair = nextPair;
pair = nextPair;
status = {
state: "running",
display: active.display,
port: active.port
};
return nextPair;
} catch (error) {
await stopPair(void 0);
throw error;
}
};
const monitorPair = (current, active, activeEpoch) => {
const desktopExit = Promise.race([
current.vncExit.then((exit) => describeExit("Xtigervnc", exit)),
current.busExit.then((exit) => describeExit("dbus-daemon", exit)),
current.sessionExit.then((exit) => describeExit("startxfce4", exit))
]);
const stillOwned = () => pair === current && activeEpoch === epoch && !stopping;
const task = (async () => {
for (;;) {
const audio = current.audio;
const failure = await Promise.race([desktopExit, ...audio.failed ? [audio.failed.then(() => audio.unavailableReason ?? "private PulseAudio exited")] : []]);
if (!stillOwned()) return;
if (isPairCurrent(current) && audio.failed) {
const disableAudio = (reason) => {
const retiringAudio = audioOwner ?? audio;
current.audio = {
stop: () => retiringAudio.stop(),
unavailableReason: `Managed desktop audio unavailable: ${reason}. Healthy desktop applications remain running. Check pulseaudio and pulseaudio-utils, then restart the managed desktop if audio is needed.`
};
};
try {
await audio.stop();
if (!stillOwned()) return;
if (isPairCurrent(current)) {
const now = nowMs();
audioRestartTimes = audioRestartTimes.filter((startedAt) => now - startedAt < AUDIO_RESTART_WINDOW_MS);
if (audioRestartTimes.length >= AUDIO_RESTART_LIMIT) {
disableAudio(`${AUDIO_RESTART_LIMIT} restarts within 5 minutes: ${failure}`);
continue;
}
audioRestartTimes.push(now);
audioOwner = createAudio({
supervisor,
tempDir: active.tempDir,
env: active.env,
assertCurrent: () => {
if (!stillOwned() || !isPairCurrent(current)) throw new Error("managed Linux desktop stopped");
}
});
const recovered = await audioOwner.ready;
if (!stillOwned()) return;
current.audio = recovered;
if (isPairCurrent(current)) {
if (!recovered.source) {
await recovered.stop();
disableAudio(recovered.unavailableReason ?? "private PulseAudio recovery failed");
}
continue;
}
}
} catch (error) {
if (!stillOwned()) return;
if (isPairCurrent(current)) {
disableAudio(error instanceof Error ? error.message : String(error));
continue;
}
}
}
const now = nowMs();
restartTimes = restartTimes.filter((startedAt) => now - startedAt < MANAGED_RESTART_WINDOW_MS);
if (restartTimes.length >= MANAGED_RESTART_LIMIT) {
await stopPair(current);
if (activeEpoch === epoch && !stopping) markFailed(/* @__PURE__ */ new Error(`managed Linux desktop failed after ${MANAGED_RESTART_LIMIT} restarts within 5 minutes: ${failure}`));
return;
}
restartTimes.push(now);
status = {
state: "starting",
display: active.display,
port: active.port
};
await stopPair(current);
if (activeEpoch !== epoch || stopping) return;
const restarted = await startPair(active, activeEpoch);
monitorPair(restarted, active, activeEpoch);
return;
}
})().catch(async (error) => {
try {
await stopPair(current);
} finally {
if (activeEpoch === epoch && !stopping) markFailed(error instanceof Error ? error : new Error(String(error)));
}
});
monitorTasks.add(task);
task.finally(() => monitorTasks.delete(task)).catch(() => void 0);
};
const start = async (activeEpoch) => {
try {
resources = await prepareResources();
if (activeEpoch !== epoch || stopping) throw new Error("managed Linux desktop stopped during startup");
const started = await startPair(resources, activeEpoch);
monitorPair(started, resources, activeEpoch);
return resources;
} catch (error) {
if (activeEpoch === epoch && !stopping) markFailed(error instanceof Error ? error : new Error(String(error)));
throw error;
}
};
return {
async acquire() {
if (stopping || stopPromise) throw new Error("managed Linux desktop is stopping");
if (status.state === "failed") throw new Error(status.error);
if (status.state === "running" && resources && pair && isPairCurrent(pair)) return publicResult(resources);
if (monitorTasks.size > 0) throw new Error("managed Linux desktop is restarting; retry when it is ready");
if (!startPromise) {
stopping = false;
restartTimes = [];
audioRestartTimes = [];
stderrTail = "";
const activeEpoch = ++epoch;
startPromise = start(activeEpoch).finally(() => {
startPromise = void 0;
});
}
return publicResult(await startPromise);
},
async acquireComputer(request) {
const current = pair;
if (status.state !== "running" || !resources || !current || !isPairCurrent(current)) throw new Error("managed Linux desktop is unavailable for computer control");
const lease = { onStop: () => request.onStop() };
current.computerLeases.add(lease);
return {
env: current.env,
isCurrent: () => isPairCurrent(current) && current.computerLeases.has(lease),
release: () => {
current.computerLeases.delete(lease);
}
};
},
stop() {
if (stopPromise) return stopPromise;
stopping = true;
++epoch;
const failed = status.state === "failed" ? status : void 0;
const stopped = Promise.resolve().then(async () => {
await stopPair(pair);
await startPromise?.catch(() => void 0);
await Promise.all(monitorTasks);
await stopPair(pair);
await removeResources();
startPromise = void 0;
restartTimes = [];
audioRestartTimes = [];
status = failed ?? { state: "not-started" };
stopping = false;
});
stopPromise = stopped;
stopped.finally(() => {
stopPromise = void 0;
}).catch(() => void 0);
return stopped;
},
status() {
return { ...status };
}
};
}
//#endregion
//#region src/gateway/desktop/host-source.ts
const DEFAULT_HOST_DESKTOP_PORT = 5900;
const HOST_DESKTOP_PROBE_TIMEOUT_MS = 1500;
function nonRfbError(port) {
return `desktop.host.port ${port} is occupied by a non-VNC service; configure desktop.host.port for the loopback VNC server, then retry`;
}
function unavailableError(port, platform) {
return `gateway host desktop is unavailable at 127.0.0.1:${port}. ${getHostDesktopGuidance(platform)}`;
}
function managedPlatformError(platform) {
return `desktop.host.managed is available only on Linux; disable it on ${platform} or configure desktop.host.port for an existing loopback VNC server`;
}
function managedInspection(managedStatus) {
const status = {
enabled: true,
state: "managed",
managedState: managedStatus.state,
port: DEFAULT_HOST_DESKTOP_PORT
};
if (managedStatus.state !== "not-started" && managedStatus.state !== "unknown") {
status.port = managedStatus.port ?? DEFAULT_HOST_DESKTOP_PORT;
if (managedStatus.display !== void 0) status.display = managedStatus.display;
}
if (managedStatus.state === "running") return {
status: {
...status,
security: "VncAuth"
},
detail: `managed (running, display :${managedStatus.display}, port ${managedStatus.port}, security: VncAuth)`
};
if (managedStatus.state === "failed") return {
status: {
...status,
error: managedStatus.error
},
detail: `managed (failed: ${managedStatus.error})`,
unavailableReason: "unsupported"
};
return {
status,
detail: managedStatus.state === "unknown" ? "managed (configured; runtime state is available from the running Gateway status)" : managedStatus.state === "starting" ? "managed (starting)" : "managed (not started)"
};
}
function securityLabel(probe) {
const auth = classifyRfbSecurity(probe.securityTypes);
if (auth === "vnc-password") return "VncAuth";
if (auth === "ard-account") return "ARD";
if (auth === "none") return "None";
return probe.securityTypes.includes(19) ? "VeNCrypt" : "unsupported";
}
/** Probes the configured host desktop without reading or exposing password material. */
async function inspectHostDesktop(params) {
if (params.config?.enabled !== true) return {
status: {
enabled: false,
state: "disabled",
port: params.config?.port ?? DEFAULT_HOST_DESKTOP_PORT
},
detail: "disabled; enable the Desktop lab with desktop.host.enabled=true"
};
return inspectConfiguredHostDesktop(params);
}
/** Setup inspection discovers a source without enabling access or starting a desktop. */
async function inspectHostDesktopSetup(params) {
const inspection = await inspectConfiguredHostDesktop(params);
if (inspection.status.state === "attached") return { state: "ready" };
if (inspection.status.state === "managed") return { state: "managed" };
return {
state: inspection.unavailableReason === "not-listening" ? "needs-server" : "unsupported",
detail: inspection.detail
};
}
async function inspectConfiguredHostDesktop(params) {
const port = params.config?.port ?? DEFAULT_HOST_DESKTOP_PORT;
const platform = params.platform ?? process.platform;
const probe = await (params.probeRfb ?? probeRfbServer)({
host: "127.0.0.1",
port,
timeoutMs: HOST_DESKTOP_PROBE_TIMEOUT_MS
});
if (probe.kind === "unreachable" || probe.kind === "timeout") {
if (params.config?.port === void 0 && params.config?.managed === true) {
if (platform !== "linux") return {
status: {
enabled: true,
state: "unavailable",
port
},
detail: managedPlatformError(platform),
unavailableReason: "unsupported"
};
return managedInspection(params.managedDesktop?.status() ?? { state: "unknown" });
}
return {
status: {
enabled: true,
state: "unavailable",
port
},
detail: unavailableError(port, platform),
unavailableReason: "not-listening"
};
}
if (probe.kind === "not-rfb") return {
status: {
enabled: true,
state: "unavailable",
port
},
detail: nonRfbError(port),
unavailableReason: "not-rfb"
};
const security = securityLabel(probe);
const auth = classifyRfbSecurity(probe.securityTypes);
if (auth === "vnc-password" || auth === "ard-account") return {
status: {
enabled: true,
state: "attached",
port,
security
},
detail: `attached (127.0.0.1:${port}, security: ${security})`
};
const detail = auth === "none" ? `unavailable: unauthenticated VNC server at 127.0.0.1:${port}; require a password-protected VncAuth server, then retry` : `unavailable: ${security} security is not supported; configure a VncAuth server and desktop.host.passwordFile, then retry`;
return {
status: {
enabled: true,
state: "unavailable",
port,
security
},
detail,
unavailableReason: "unsupported"
};
}
/** Creates the host acquisition hook consumed by the source-agnostic desktop registry. */
function createHostDesktopSource(params) {
const port = params.config.port ?? DEFAULT_HOST_DESKTOP_PORT;
const platform = params.platform ?? process.platform;
const probeRfb = params.probeRfb ?? probeRfbServer;
const managedDesktop = params.managedDesktop ?? (params.config.managed === true && platform === "linux" ? createManagedLinuxDesktop() : void 0);
let selectedManagedDesktop = false;
const acquireAttached = async (probe) => {
const security = classifyRfbSecurity(probe.securityTypes);
if (security === "none") throw new Error(`refusing unauthenticated VNC server on 127.0.0.1:${port}; require a password-protected VncAuth server, then retry`);
if (security === "unsupported") {
const name = probe.securityTypes.includes(19) ? "VeNCrypt" : "the offered VNC security";
throw new Error(`${name} is not supported; configure a VncAuth server and desktop.host.passwordFile, then retry`);
}
let vncPassword;
if (params.config.passwordFile) {
try {
vncPassword = (await fs.readFile(params.config.passwordFile, "utf8")).replace(/[\r\n]+$/u, "");
} catch (error) {
const reason = error instanceof Error ? error.message : String(error);
throw new Error(`could not read desktop.host.passwordFile ${params.config.passwordFile}: ${reason}; fix the absolute path or remove desktop.host.passwordFile so the UI can prompt`, { cause: error });
}
if (!vncPassword) throw new Error("desktop.host.passwordFile is empty; write the VNC password or remove desktop.host.passwordFile so the UI can prompt");
registerSecretValueForRedaction(vncPassword);
}
return {
attachment: {
kind: "tcp",
host: "127.0.0.1",
port
},
auth: security,
...vncPassword ? { vncPassword } : {}
};
};
const acquire = async (assertCurrent) => {
assertCurrent?.();
selectedManagedDesktop = false;
const probe = await probeRfb({
host: "127.0.0.1",
port,
timeoutMs: HOST_DESKTOP_PROBE_TIMEOUT_MS
});
assertCurrent?.();
if (probe.kind === "unreachable" || probe.kind === "timeout") {
if (params.config.port === void 0 && params.config.managed === true) {
if (platform !== "linux") throw new Error(managedPlatformError(platform));
if (!managedDesktop) throw new Error("managed Linux desktop lifecycle is unavailable; retry");
const acquired = await managedDesktop.acquire();
assertCurrent?.();
selectedManagedDesktop = true;
return acquired;
}
throw new Error(unavailableError(port, platform));
}
if (probe.kind === "not-rfb") throw new Error(nonRfbError(port));
return await acquireAttached(probe);
};
return {
acquire,
acquireComputer: async (computerParams) => {
if (!selectedManagedDesktop || !managedDesktop) throw new Error("COMPUTER_HOST_UNAVAILABLE: the selected host desktop is an external VNC server; its local computer session is unknown");
return await managedDesktop.acquireComputer(computerParams);
},
teardown: managedDesktop ? () => {
selectedManagedDesktop = false;
return managedDesktop.stop();
} : void 0,
inspect: () => inspectHostDesktop({
config: params.config,
platform,
managedDesktop,
probeRfb
})
};
}
/** Combines host acquisition, registry ownership, and observer-token minting. */
function createHostDesktopService(params) {
const platform = params.platform ?? process.platform;
let current;
let nextOwnerEpoch = 0;
const isCurrent = (runtime) => {
const config = params.getConfig();
return current === runtime && !runtime.controller.signal.aborted && config?.enabled === true && config.managed === runtime.config.managed && config.port === runtime.config.port && config.passwordFile === runtime.config.passwordFile;
};
const assertCurrent = (runtime) => {
if (!isCurrent(runtime)) throw new Error("gateway host desktop configuration changed; retry");
};
const reconcileRuntimePolicy = async () => {
const runtime = current;
if (!runtime || isCurrent(runtime)) return;
runtime.controller.abort();
if (!runtime.stopping) runtime.stopping = params.registry.stop("host", runtime.ownerEpoch).then(() => {
if (current === runtime) current = void 0;
}, (error) => {
runtime.stopping = void 0;
throw error;
});
await runtime.stopping;
};
const resolveRuntime = async () => {
await reconcileRuntimePolicy();
const config = params.getConfig();
if (config?.enabled !== true) return;
if (!current) {
const ownerEpoch = nextOwnerEpoch++;
const managedDesktop = params.managedDesktop ?? (config.managed === true && platform === "linux" ? createManagedLinuxDesktop({ onFailed: () => {
params.registry.stop("host", ownerEpoch);
} }) : void 0);
const snapshot = { ...config };
current = {
config: snapshot,
ownerEpoch,
controller: new AbortController(),
source: createHostDesktopSource({
config: snapshot,
platform,
...managedDesktop ? { managedDesktop } : {}
})
};
}
return current;
};
const acquire = async () => {
const runtime = await resolveRuntime();
if (!runtime) throw new Error("gateway host desktop is disabled; enable the Desktop lab (config: desktop.host.enabled=true)");
assertCurrent(runtime);
const acquired = await params.registry.acquire({
sourceKey: "host",
ownerEpoch: runtime.ownerEpoch,
start: () => runtime.source.acquire(() => assertCurrent(runtime)),
...runtime.source.teardown ? { teardown: runtime.source.teardown } : {}
});
assertCurrent(runtime);
return {
acquired,
runtime
};
};
return {
async observe(observeParams) {
const { acquired, runtime } = await acquire();
assertCurrent(runtime);
const auth = acquired.auth;
const audio = acquired.resolveAudio?.();
if (!auth) throw new Error("gateway host desktop authentication state is unavailable; retry observe");
let preauth;
if (auth === "ard-account") {
const username = observeParams.credentials?.username?.trim() ?? "";
const password = observeParams.credentials?.password ?? "";
if (!username || !password) throw new HostDesktopCredentialsRequiredError();
registerSecretValueForRedaction(password);
preauth = {
auth: "ard-account",
credentials: {
username,
password
}
};
}
if (audio && auth === "vnc-password" && acquired.vncPassword) preauth = {
auth,
credentials: { password: acquired.vncPassword }
};
const minted = mintDesktopObserverToken({
sourceKey: "host",
ownerEpoch: runtime.ownerEpoch,
control: observeParams.control,
requester: {
...observeParams.requester,
signal: observeParams.requester?.signal ? AbortSignal.any([runtime.controller.signal, observeParams.requester.signal]) : runtime.controller.signal,
isCurrent: () => isCurrent(runtime) && observeParams.requester?.isCurrent() !== false
},
attachment: acquired.attachment,
...audio ? { audio } : {},
...preauth ? { preauth } : {}
});
return {
transport: "rfb",
wsPath: `/desktop/observe?token=${minted.token}`,
expiresAtMs: minted.expiresAtMs,
control: observeParams.control,
auth,
...minted.audio ? {
audio: minted.audio,
preauthenticated: true
} : {},
...!audio && acquired.audioUnavailableReason ? { audioUnavailableReason: "setup-unavailable" } : {},
...auth === "vnc-password" && acquired.vncPassword && !preauth ? { vncPassword: acquired.vncPassword } : {}
};
},
async acquireComputer(computerParams) {
const { runtime } = await acquire();
assertCurrent(runtime);
const activity = params.registry.retainActivity("host", runtime.ownerEpoch);
if (!activity) throw new Error("COMPUTER_HOST_UNAVAILABLE: the host desktop stopped during acquisition");
try {
const computer = await runtime.source.acquireComputer(computerParams);
if (!isCurrent(runtime) || !activity.isCurrent() || !computer.isCurrent()) {
computer.release();
throw new Error("COMPUTER_HOST_UNAVAILABLE: the host desktop stopped during acquisition");
}
return {
env: computer.env,
isCurrent: () => isCurrent(runtime) && activity.isCurrent() && computer.isCurrent(),
release() {
computer.release();
activity.release();
}
};
} catch (error) {
activity.release();
throw error;
}
},
async status() {
for (;;) {
const runtime = await resolveRuntime();
if (!runtime) return (await inspectHostDesktop({
config: params.getConfig(),
platform
})).status;
const inspection = await runtime.source.inspect();
if (isCurrent(runtime)) return inspection.status;
}
},
reconcileRuntimePolicy
};
}
//#endregion
export { inspectHostDesktopSetup as i, createHostDesktopSource as n, inspectHostDesktop as r, createHostDesktopService as t };