UNPKG

relay-companion

Version:

Install Relay for Claude Code, Cowork, and Codex, then sign in from the Relay pill.

734 lines (701 loc) • 30.4 kB
import fs from "node:fs"; import os from "node:os"; import path from "node:path"; import { randomUUID } from "node:crypto"; import { spawnSync } from "node:child_process"; import { fileURLToPath } from "node:url"; import { createRequire } from "node:module"; import { compatibleNodeRuntime, persistentNodePath, stableNodePath } from "./install.js"; import { ACTIVE_UPDATE_WORKER_ENV } from "./update-agent-cleanup.js"; import { canonicalRuntimeLayout, readCanonicalRuntime, recoverCanonicalRuntime, repairCanonicalRuntime, verifyCanonicalCandidate, } from "./canonical-runtime.js"; const WINDOWS_DAEMON_TASK = "Relay Companion Daemon"; const WINDOWS_PILL_TASK = "Relay Companion Pill"; export const UPDATE_WORKER_LABEL_PREFIX = "work.relay.companion.update."; export const UPDATE_WORKER_LABEL = "work.relay.companion.update"; export const UPDATE_REQUEST_SCHEMA = 1; const require = createRequire(import.meta.url); const { stageVerifiedRuntime, releasePlatform } = require("../bootstrap/relay-setup.cjs"); const { systemdRunEnvironmentArgs } = require("../bootstrap/linux-systemd.cjs"); const { activateLinuxRuntimeServices, activateMacRuntimeServices, exactRuntimeHealth, runtimeProcessCommands: processCommands, } = require("../bootstrap/runtime-health.cjs"); export { exactRuntimeHealth }; export async function installSignedRuntimeCandidate({ stagingRoot, version, platform = process.platform } = {}) { try { const expectedPlatform = releasePlatform(platform, process.arch); await stageVerifiedRuntime({ version, platformKey: expectedPlatform, destination: stagingRoot }); return { ok: true, source: "signed-runtime-artifact" }; } catch (error) { return { ok: false, detail: error?.message || String(error) }; } } function commandOk(result) { return Boolean(result && !result.error && result.status === 0); } function defaultRun(command, args, options = {}) { return spawnSync(command, args, { encoding: "utf8", timeout: 30_000, windowsHide: true, ...options }); } export function legacyRuntimeTarget(packageRoot, version, { node = process.execPath, platform = process.platform, } = {}) { const verified = verifyCanonicalCandidate(packageRoot, version, { platform }); if (!verified.ok) return null; return { kind: "legacy", version, packageRoot, bin: verified.bin, electronPath: verified.electronPath, node, }; } export function runtimeNpmCommand(node = process.execPath, { platform = process.platform, existsSync = fs.existsSync, } = {}) { const api = platform === "win32" ? path.win32 : path.posix; const directory = api.dirname(String(node || "")); const names = platform === "win32" ? ["npm.cmd", "npm"] : ["npm"]; for (const name of names) { const candidate = api.join(directory, name); try { if (existsSync(candidate)) return candidate; } catch {} } return null; } export function smokeCanonicalCandidate(candidate, { run = defaultRun } = {}) { const result = run(candidate.node, [candidate.bin, "--help"], { env: { ...process.env, RELAY_SKIP_DESKTOP_POSTINSTALL: "1" }, }); return commandOk(result) ? { ok: true } : { ok: false, reason: "candidate-cli-smoke-failed", detail: result?.error?.message || result?.stderr || result?.stdout || "" }; } export function processReferencedCanonicalRoots({ homeDir = os.homedir(), platform = process.platform, run = defaultRun, fsImpl = fs, } = {}) { const api = platform === "win32" ? path.win32 : path.posix; const { releasesDir } = canonicalRuntimeLayout({ homeDir, platform }); let names = []; try { names = fsImpl.readdirSync(releasesDir, { withFileTypes: true }) .filter((entry) => entry.isDirectory() && !entry.isSymbolicLink?.()) .map((entry) => entry.name); } catch { return []; } const commands = processCommands(platform, run); const normalize = (value) => (platform === "win32" ? String(value).toLowerCase() : String(value)).replaceAll("\\", "/"); return names .map((name) => api.join(releasesDir, name, "node_modules", "relay-companion")) .filter((packageRoot) => commands.some((command) => normalize(command).includes(normalize(packageRoot)))); } export async function activateCanonicalRuntime(target, { homeDir = os.homedir(), platform = process.platform, run = defaultRun, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), // Exact-root health polls, 500ms apart. 30s: an Electron pill can legitimately // take >15s to be killed and >5s to come back up on a loaded machine. attempts = 60, domainPollMs = 250, bootstrapDelayMs = 500, activationDeadlineMs = 90_000, now = Date.now, repairExecutable = target, } = {}) { if (!target?.bin || !target?.node || !target?.packageRoot) return { ok: false, reason: "activation-target-invalid" }; if (!repairExecutable?.bin || !repairExecutable?.node) return { ok: false, reason: "repair-runtime-missing" }; const targetOverride = repairExecutable.bin === target.bin ? [] : ["--target-bin", target.bin, "--target-node", target.node]; const repair = run(repairExecutable.node, [ repairExecutable.bin, "repair-runtime", "--no-restart", ...targetOverride, ], { env: { ...process.env, RELAY_SKIP_DESKTOP_POSTINSTALL: "1", [ACTIVE_UPDATE_WORKER_ENV]: "1" }, }); if (!commandOk(repair)) { return { ok: false, reason: "runtime-registration-failed", detail: repair?.error?.message || repair?.stderr || repair?.stdout || "" }; } if (platform === "darwin") { return activateMacRuntimeServices(target, { homeDir, platform, run, sleep, domainPollMs, bootstrapDelayMs, activationDeadlineMs, now, }); } else if (platform === "linux") { return activateLinuxRuntimeServices(target, { homeDir, platform, run, sleep, attempts, }); } else if (platform === "win32") { for (const task of [WINDOWS_PILL_TASK, WINDOWS_DAEMON_TASK]) run("schtasks.exe", ["/End", "/TN", task]); // The tasks launch through a short-lived wscript/WshShell.Run wrapper. `/End` // can therefore succeed while the actual node/Electron child survives. Stop // every Relay service process by command-line identity before starting the new // exact-root actions; the updater worker itself does not match either pattern. const stopRelayChildren = [ "$p=Get-CimInstance Win32_Process -ErrorAction SilentlyContinue | Where-Object {", " $_.CommandLine -and ($_.CommandLine -match '[\\\\/]node_modules[\\\\/]relay-companion[\\\\/]') -and (($_.CommandLine -match '[\\\\/]relay\\.js.*\\bdaemon\\b') -or ($_.CommandLine -match '[\\\\/]overlay[\\\\/]main\\.cjs'))", "}; foreach($x in $p){ try { Invoke-CimMethod -InputObject $x -MethodName Terminate -ErrorAction Stop | Out-Null } catch {} }", ].join(" "); const stopped = run("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", stopRelayChildren]); if (!commandOk(stopped)) return { ok: false, reason: "old-service-stop-failed", detail: stopped?.stderr || stopped?.stdout || "" }; for (const task of [WINDOWS_PILL_TASK, WINDOWS_DAEMON_TASK]) { const started = run("schtasks.exe", ["/Run", "/TN", task]); if (!commandOk(started)) return { ok: false, reason: "service-start-failed", detail: `${task}: ${started?.stderr || started?.stdout || ""}` }; } } else { return { ok: false, reason: "activation-platform-unsupported" }; } for (let attempt = 0; attempt < attempts; attempt += 1) { const health = exactRuntimeHealth(target, { platform, run }); if (health.ok) return { ok: true, health }; if (attempt + 1 < attempts) await sleep(500); } return { ok: false, reason: "exact-root-health-failed", detail: target.packageRoot }; } export async function runCanonicalUpdateTransaction({ version, runningPackageRoot, runningVersion, node = process.execPath, homeDir = os.homedir(), platform = process.platform, log = () => {}, activate = activateCanonicalRuntime, repair = repairCanonicalRuntime, installCandidate = installSignedRuntimeCandidate, smoke = smokeCanonicalCandidate, npmCommand = runtimeNpmCommand(node, { platform }), getProtectedPackageRoots = processReferencedCanonicalRoots, resolveServiceNode = stableNodePath, onLockAcquired = () => {}, requestId = null, workerId = null, } = {}) { const selectedServiceNode = resolveServiceNode(node); if (!selectedServiceNode) return { ok: false, phase: "input", reason: "service-node-missing", detail: node }; let serviceNode; try { serviceNode = persistentNodePath(selectedServiceNode, { platform, homeDir }); } catch (error) { return { ok: false, phase: "input", reason: "service-node-preservation-failed", detail: error?.message || String(error), }; } const persistentTargetNode = (candidate) => { const selected = resolveServiceNode(candidate); if (!selected) return serviceNode; try { return persistentNodePath(selected, { platform, homeDir }); } catch { return serviceNode; } }; // Electron must never be baked into plists/pointers as the runtime's node, and it // cannot resolve npm either. spawnCanonicalUpdate substitutes a real node before // the worker exists; this guard is the backstop for every other caller. if (isElectronExecutable(serviceNode)) { return { ok: false, phase: "input", reason: "service-node-electron", detail: serviceNode }; } const protectedPackageRoots = getProtectedPackageRoots({ homeDir, platform }); const lockIdentity = { ...(requestId ? { requestId } : {}), ...(workerId ? { workerId } : {}) }; let admitted = false; const admit = async (owner) => { if (admitted) return; await onLockAcquired(owner); admitted = true; }; const recovered = await recoverCanonicalRuntime({ homeDir, platform, protectedPackageRoots, rollbackActivate: async (target, context) => target ? activate({ ...target, node: persistentTargetNode(target?.node || serviceNode), }, { homeDir, platform, repairExecutable: context?.recovery?.candidate }) : { ok: true }, onLockAcquired: admit, lockIdentity, }); if (!recovered.ok) return recovered; if (recovered.recovered) return recovered; // npm is resolved beside the worker's own runtime; when that yields nothing (the // worker was handed a bare binary with no sibling npm), fall back to the durable // service node's npm rather than failing the whole transaction. const npmExecutable = npmCommand || runtimeNpmCommand(serviceNode, { platform }); const previous = readCanonicalRuntime({ homeDir, platform }); const legacy = previous ? null : legacyRuntimeTarget(runningPackageRoot, runningVersion, { node: serviceNode, platform }); if (!previous && !legacy) return { ok: false, phase: "input", reason: "legacy-runtime-invalid" }; return repair({ version, homeDir, platform, node: serviceNode, npmCommand: npmExecutable, installCandidate, protectedPackageRoots, normalizePreviousTarget: (target) => ({ ...target, node: persistentTargetNode(target?.node || serviceNode), }), onLockAcquired: admit, lockIdentity, rollbackTarget: legacy, preCommitVerify: async (candidate) => smoke(candidate), postCommitActivate: async (candidate) => { log(`activating ${candidate.version} from ${candidate.packageRoot}`); return activate(candidate, { homeDir, platform }); }, rollbackActivate: async (target, context) => { if (!target) return { ok: true }; log(`restoring runtime targets to ${target.packageRoot}`); return activate(target, { homeDir, platform, repairExecutable: context?.failed }); }, }); } function workerPath() { return fileURLToPath(import.meta.url); } function encodePayload(value) { return Buffer.from(JSON.stringify(value), "utf8").toString("base64url"); } // The pill launches `relay update` with ELECTRON_RUN_AS_NODE=1, which changes // behaviour but not process.execPath — so "the current runtime" reaching this // module can be the Electron binary itself. `launchctl submit` carries no // environment (the whole premise of the PATH bug above), so a submitted Electron // runs the worker as a GUI app: GPU and network helpers, no npm resolvable beside // it, and no exit when the work ends. That immortal worker is what Sven clicked // "Update" into (pid 15090) — it did nothing, and it never went away. export function isElectronExecutable(executable = process.execPath) { const value = String(executable || ""); if (!value) return false; if (/[\\/]electron\.app[\\/]/i.test(value)) return true; const base = value.split(/[\\/]/).pop().toLowerCase(); if (base === "electron" || base === "electron.exe") return true; return value === process.execPath && Boolean(process.versions?.electron); } export function resolveUpdateWorkerNode(node = process.execPath, { platform = process.platform, homeDir = os.homedir(), existsSync = fs.existsSync, run = defaultRun, } = {}) { if (node && !isElectronExecutable(node)) return node; const candidates = []; try { const current = readCanonicalRuntime({ homeDir, platform }); if (current?.node) candidates.push(current.node); } catch {} if (platform !== "win32") candidates.push("/opt/homebrew/bin/node", "/usr/local/bin/node", "/usr/bin/node"); const which = run(platform === "win32" ? "where.exe" : "/usr/bin/which", ["node"]); if (commandOk(which)) { const first = String(which.stdout || "").split(/\r?\n/)[0].trim(); if (first) candidates.push(first); } for (const candidate of candidates) { try { if ( candidate && !isElectronExecutable(candidate) && existsSync(candidate) && compatibleNodeRuntime(candidate, { runCommand: run }) ) return candidate; } catch {} } return null; } // `launchctl list` rows are "PID\tStatus\tLabel". Current builds own one fixed // label; suffixed labels identify legacy workers that must be removed wholesale. export function listUpdateWorkerJobs({ run = defaultRun } = {}) { const result = run("/bin/launchctl", ["list"]); if (!commandOk(result)) return []; const jobs = []; for (const line of String(result.stdout || "").split("\n")) { const parts = line.trim().split(/\s+/); if (parts.length < 3) continue; const [pid, , label] = parts; if (!label || (label !== UPDATE_WORKER_LABEL && !label.startsWith(UPDATE_WORKER_LABEL_PREFIX))) continue; const spawnedAt = Number(label.split(".").pop()) || 0; jobs.push({ label, kind: label === UPDATE_WORKER_LABEL ? "fixed" : "legacy", pid: pid === "-" ? null : Number(pid) || null, spawnedAt: label === UPDATE_WORKER_LABEL ? 0 : spawnedAt, }); } return jobs; } // Every suffixed label belongs to a pre-fixed-label build. A young PID is not // evidence that it is safe: the live process may itself be the stale storm // generator replaying a dead version. The first current client to run removes all // legacy jobs. The fixed label is deliberately retained while inactive; removing // it from a delayed old worker creates an ABA race with the next update. export function reconcileUpdateWorkerJobs({ run = defaultRun } = {}) { const jobs = listUpdateWorkerJobs({ run }); const legacy = jobs.filter((job) => job.kind === "legacy"); for (const job of legacy) run("/bin/launchctl", ["remove", job.label]); const fixed = jobs.find((job) => job.kind === "fixed") || null; return { removedLegacy: legacy.length, fixed }; } function requestDirectory(homeDir, platform) { const api = platform === "win32" ? path.win32 : path.posix; return api.join(canonicalRuntimeLayout({ homeDir, platform }).root, "update-requests"); } function atomicWriteText(destination, value, { fsImpl = fs, platform = process.platform } = {}) { const api = platform === "win32" ? path.win32 : path.posix; fsImpl.mkdirSync(api.dirname(destination), { recursive: true, mode: 0o700 }); const temporary = `${destination}.tmp-${process.pid}-${Math.random().toString(16).slice(2)}`; fsImpl.writeFileSync(temporary, value, { mode: 0o600 }); fsImpl.renameSync(temporary, destination); } function atomicWriteRequest(requestPath, value, options = {}) { atomicWriteText(requestPath, `${JSON.stringify(value)}\n`, options); } function plistEscape(value) { return String(value) .replaceAll("&", "&amp;") .replaceAll("<", "&lt;") .replaceAll(">", "&gt;") .replaceAll('"', "&quot;") .replaceAll("'", "&apos;"); } function updateWorkerPlist(programArguments, logPath) { const argumentsXml = programArguments.map((argument) => ` <string>${plistEscape(argument)}</string>`).join("\n"); return `<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> <key>Label</key><string>${UPDATE_WORKER_LABEL}</string> <key>ProgramArguments</key> <array> ${argumentsXml} </array> <key>RunAtLoad</key><true/> <key>KeepAlive</key><false/> <key>ProcessType</key><string>Background</string> <key>StandardOutPath</key><string>${plistEscape(logPath)}</string> <key>StandardErrorPath</key><string>${plistEscape(logPath)}</string> </dict> </plist> `; } function claimUpdateRequestDecision(requestPath, value, { fsImpl = fs } = {}) { const decisionPath = `${requestPath}.decision`; let descriptor = null; try { descriptor = fsImpl.openSync(decisionPath, "wx", 0o600); fsImpl.writeFileSync(descriptor, `${JSON.stringify(value)}\n`); fsImpl.closeSync(descriptor); descriptor = null; return { won: true, value }; } catch (error) { if (descriptor !== null) { try { fsImpl.closeSync(descriptor); } catch {} } if (error?.code !== "EEXIST") throw error; // `open("wx")` is the linearization point. The winner may still be writing // the tiny record, so tolerate that bounded publication window. for (let attempt = 0; attempt < 20; attempt += 1) { try { const existing = JSON.parse(fsImpl.readFileSync(decisionPath, "utf8")); if (existing?.requestId && existing?.workerId && existing?.state) { return { won: false, value: existing }; } } catch {} blockingSleep(5); } return { won: false, value: null }; } } export function readUpdateRequest(requestPath, { fsImpl = fs } = {}) { try { const value = JSON.parse(fsImpl.readFileSync(requestPath, "utf8")); return value?.schema === UPDATE_REQUEST_SCHEMA && value?.requestId ? value : null; } catch { return null; } } function blockingSleep(ms) { const signal = new Int32Array(new SharedArrayBuffer(4)); Atomics.wait(signal, 0, 0, Math.max(1, ms)); } export function waitForUpdateRequestAdmission(requestPath, { fsImpl = fs, now = Date.now, sleep = blockingSleep, timeoutMs = 5_000, pollMs = 50, } = {}) { const deadline = now() + timeoutMs; do { const value = readUpdateRequest(requestPath, { fsImpl }); if (value && ["admitted", "completed", "failed", "rejected"].includes(value.state)) return value; sleep(pollMs); } while (now() < deadline); return readUpdateRequest(requestPath, { fsImpl }); } export async function waitForUpdateRequestTerminal(requestPath, { fsImpl = fs, now = Date.now, sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)), timeoutMs = 20 * 60_000, pollMs = 250, } = {}) { const deadline = now() + timeoutMs; do { const value = readUpdateRequest(requestPath, { fsImpl }); if (value && ["completed", "failed", "rejected"].includes(value.state)) return value; await sleep(pollMs); } while (now() < deadline); return readUpdateRequest(requestPath, { fsImpl }); } export function spawnCanonicalUpdate({ version, runningPackageRoot, runningVersion, node = process.execPath, homeDir = os.homedir(), platform = process.platform, env = process.env, run = defaultRun, existsSync = fs.existsSync, fsImpl = fs, requestId = randomUUID(), workerId = randomUUID(), waitForAdmission = waitForUpdateRequestAdmission, } = {}) { const workerNode = resolveUpdateWorkerNode(node, { platform, homeDir, existsSync, run }); if (!workerNode) { throw new Error("canonical update worker needs a real Node runtime: refusing to launch it under Electron, and no public node was found"); } const api = platform === "win32" ? path.win32 : path.posix; const requestPath = api.join(requestDirectory(homeDir, platform), `${requestId}.json`); const prepared = { schema: UPDATE_REQUEST_SCHEMA, requestId, workerId, state: "prepared", requestedAt: Date.now(), version, runningVersion, }; atomicWriteRequest(requestPath, prepared, { fsImpl, platform }); const payload = encodePayload({ version, runningPackageRoot, runningVersion, node: workerNode, homeDir, platform, requestId, workerId, requestPath, }); let launchedPid = null; let linuxWorkerUnit = null; if (platform === "darwin") { const reconciled = reconcileUpdateWorkerJobs({ run }); if (reconciled.fixed?.pid) { atomicWriteRequest(requestPath, { ...prepared, state: "rejected", reason: "worker-busy", completedAt: Date.now() }, { fsImpl, platform }); return { status: "busy", requestId, workerId, requestPath }; } const domain = `gui/${typeof process.getuid === "function" ? process.getuid() : 0}`; if (reconciled.fixed) run("/bin/launchctl", ["bootout", `${domain}/${UPDATE_WORKER_LABEL}`]); const layout = canonicalRuntimeLayout({ homeDir, platform }); const workerPlistPath = api.join(layout.root, "update-worker.plist"); const logPath = api.join(homeDir, ".relay", "update.log"); atomicWriteText( workerPlistPath, updateWorkerPlist([workerNode, workerPath(), "--worker", payload], logPath), { fsImpl, platform }, ); const submitted = run("/bin/launchctl", ["bootstrap", domain, workerPlistPath]); if (!commandOk(submitted)) { atomicWriteRequest(requestPath, { ...prepared, state: "rejected", reason: "launch-failed", detail: submitted?.stderr || submitted?.stdout || "", completedAt: Date.now() }, { fsImpl, platform }); return { status: "launch-failed", requestId, workerId, requestPath }; } } else if (platform === "win32") { const quote = (value) => `"${String(value).replaceAll('"', '\\"')}"`; const commandLine = [workerNode, workerPath(), "--worker", payload].map(quote).join(" "); const ps = `$r=Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{CommandLine='${commandLine.replaceAll("'", "''")}'} -ErrorAction Stop;if($r.ReturnValue-ne 0){exit 1};Write-Output $r.ProcessId`; const encoded = Buffer.from(ps, "utf16le").toString("base64"); const systemRoot = process.env.SystemRoot || process.env.windir || "C:\\Windows"; const submitted = run(path.win32.join(systemRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"), [ "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-EncodedCommand", encoded, ]); if (!commandOk(submitted)) { atomicWriteRequest(requestPath, { ...prepared, state: "rejected", reason: "launch-failed", detail: submitted?.stderr || submitted?.stdout || "", completedAt: Date.now() }, { fsImpl, platform }); return { status: "launch-failed", requestId, workerId, requestPath }; } launchedPid = Number(String(submitted.stdout || "").trim()) || null; } else if (platform === "linux") { linuxWorkerUnit = `${UPDATE_WORKER_LABEL_PREFIX}${requestId}`; const logPath = api.join(homeDir, ".relay", "update.log"); const submitted = run("systemd-run", [ "--user", "--quiet", "--collect", `--unit=${linuxWorkerUnit}`, ...systemdRunEnvironmentArgs(env), "--property=Type=exec", `--property=StandardOutput=append:${logPath}`, `--property=StandardError=append:${logPath}`, workerNode, workerPath(), "--worker", payload, ]); if (!commandOk(submitted)) { atomicWriteRequest(requestPath, { ...prepared, state: "rejected", reason: "launch-failed", detail: submitted?.stderr || submitted?.stdout || "", completedAt: Date.now() }, { fsImpl, platform }); return { status: "launch-failed", requestId, workerId, requestPath }; } } else { throw new Error(`canonical updater unsupported on ${platform}`); } const admitted = waitForAdmission(requestPath, { fsImpl }); if (admitted?.state === "failed" && admitted.workerId === workerId && admitted.admittedAt) { return { status: "failed", requestId, workerId, requestPath, admittedAt: admitted.admittedAt, terminal: admitted }; } if (!admitted || !["admitted", "completed"].includes(admitted.state) || admitted.workerId !== workerId) { const rejected = { ...(admitted || prepared), schema: UPDATE_REQUEST_SCHEMA, requestId, workerId, state: "rejected", reason: admitted?.reason || "admission-timeout", completedAt: Date.now(), }; const decision = claimUpdateRequestDecision(requestPath, rejected, { fsImpl }); if (decision.value?.state === "admitted" && decision.value.workerId === workerId) { return { status: "admitted", requestId, workerId, requestPath, admittedAt: decision.value.admittedAt }; } if (decision.won) atomicWriteRequest(requestPath, rejected, { fsImpl, platform }); if (platform === "win32" && launchedPid && decision.won) { run("taskkill.exe", ["/PID", String(launchedPid), "/T", "/F"]); } if (platform === "linux" && linuxWorkerUnit && decision.won) { run("systemctl", ["--user", "stop", linuxWorkerUnit]); } // Never remove the fixed launchd label here. Another caller may have observed // its old inactive state, replaced it, and submitted a newer worker while this // caller waited. Removing by label would kill that newer owner (an ABA race). return { status: decision.value?.reason === "worker-busy" ? "busy" : "not-admitted", requestId, workerId, requestPath }; } return { status: "admitted", requestId, workerId, requestPath, admittedAt: admitted.admittedAt }; } async function workerMain(payload) { let options = null; try { options = JSON.parse(Buffer.from(payload, "base64url").toString("utf8")); } catch (error) { console.error(`[relay-update] worker payload invalid: ${error?.message || error}`); return null; } const requestApi = options.platform === "win32" ? path.win32 : path.posix; const expectedRequestPath = requestApi.join(requestDirectory(options.homeDir, options.platform), `${options.requestId}.json`); if ( !options.requestId || !options.workerId || requestApi.resolve(String(options.requestPath || "")) !== requestApi.resolve(expectedRequestPath) ) { console.error("[relay-update] worker request identity/path invalid"); return null; } const logPath = path.join(options.homeDir, ".relay", "update.log"); const log = (message) => { try { fs.mkdirSync(path.dirname(logPath), { recursive: true }); fs.appendFileSync(logPath, `[relay-update] ${new Date().toISOString()} ${message}\n`); } catch {} }; const writeState = (state, extra = {}) => { if (!options.requestPath || !options.requestId || !options.workerId) return; const current = readUpdateRequest(options.requestPath) || { schema: UPDATE_REQUEST_SCHEMA, requestId: options.requestId, workerId: options.workerId, requestedAt: Date.now(), }; if (current.requestId !== options.requestId || current.workerId !== options.workerId) { throw new Error("update request identity changed before worker admission"); } if (current.state === "rejected" && state !== "rejected") return current; atomicWriteRequest(options.requestPath, { ...current, ...extra, state }); return { ...current, ...extra, state }; }; try { const result = await runCanonicalUpdateTransaction({ ...options, log, onLockAcquired: (owner) => { const admittedAt = Date.now(); const admission = { schema: UPDATE_REQUEST_SCHEMA, requestId: options.requestId, workerId: options.workerId, state: "admitted", admittedAt, lockOwner: owner, }; const decision = claimUpdateRequestDecision(options.requestPath, admission); if (!decision.won && ( decision.value?.state !== "admitted" || decision.value?.requestId !== options.requestId || decision.value?.workerId !== options.workerId )) { throw new Error("update request was cancelled before worker admission"); } writeState("admitted", { admittedAt, lockOwner: owner }); }, }); const detail = String(result?.detail || "").trim().replace(/\s+/g, " ").slice(0, 2000); log(result.ok ? result.recovered ? "canonical activation journal recovered" : `canonical update committed: ${result.candidate.version}` : `canonical update failed at ${result.phase}: ${result.reason}${detail ? ` (${detail})` : ""}`); writeState(result.ok ? "completed" : "failed", { completedAt: Date.now(), result: { ok: Boolean(result.ok), phase: result.phase || null, reason: result.reason || null, detail }, }); } catch (error) { log(`canonical update worker crashed: ${error?.stack || error?.message || String(error)}`); try { writeState("failed", { completedAt: Date.now(), result: { ok: false, phase: "worker", reason: "worker-crashed", detail: error?.message || String(error) }, }); } catch {} } return options; } if (process.argv[2] === "--worker" && process.argv[3]) { const options = await workerMain(process.argv[3]); // ALWAYS exit 0. launchd restarts a submitted job that exits non-zero — forever, // with its original payload — so a failing worker used to be resurrected every // few seconds for hours, replaying a stale target version. Failure is reported // through update.log and the durable failure records, never the exit code // (nothing ever read it anyway). process.exitCode = 0; }