@braedonsaunders/appkit-desk 0.11.0 → 0.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend.d.ts +5 -1
- package/backend.d.ts.map +1 -1
- package/backend.js +26 -0
- package/backend.js.map +1 -1
- package/package.json +1 -1
package/backend.d.ts
CHANGED
|
@@ -203,9 +203,13 @@ export interface CloudHypervisorBackendOptions {
|
|
|
203
203
|
serviceVsock?: boolean;
|
|
204
204
|
/** Whether a share daemon's socket exists yet; injectable for tests. */
|
|
205
205
|
pathExists?: (path: string) => boolean;
|
|
206
|
-
/** Removes a stale share socket before
|
|
206
|
+
/** Removes a stale share or vsock socket before the process that binds it starts; injectable for tests. */
|
|
207
207
|
removeFile?: (path: string) => Promise<void>;
|
|
208
|
+
/** Whether a process listens on a Unix socket (a running VMM on its vsock socket); injectable for tests. */
|
|
209
|
+
socketListening?: (path: string) => Promise<boolean>;
|
|
208
210
|
}
|
|
211
|
+
/** Whether something accepts connections on the Unix socket at `path`: false when nothing is there or nothing listens. */
|
|
212
|
+
export declare function unixSocketListening(path: string): Promise<boolean>;
|
|
209
213
|
export declare function createCloudHypervisorBackend(options?: CloudHypervisorBackendOptions): DeskBackend;
|
|
210
214
|
/**
|
|
211
215
|
* Cloud Hypervisor's control plane: HTTP/1.1 over the `--api-socket` Unix
|
package/backend.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"backend.d.ts","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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"}
|
package/backend.js
CHANGED
|
@@ -3,6 +3,7 @@ import { assertPrivateMemoryMapping } from './template-mapping.js';
|
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { rm } from 'node:fs/promises';
|
|
5
5
|
import { request as httpRequest } from 'node:http';
|
|
6
|
+
import { createConnection } from 'node:net';
|
|
6
7
|
import { GUEST_AGENT_VSOCK_PORT } from './protocol.js';
|
|
7
8
|
import { DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_CONNECT_RETRY_DELAY_MS, DEFAULT_HANDSHAKE_TIMEOUT_MS, DEFAULT_RECONNECT_WINDOW_MS, DEFAULT_RECONNECT_RETRY_DELAY_MS, DEFAULT_KILL_GRACE_MS, RECONNECT_BACKOFF_CEILING, connectWithRetry, createGuestMachine, attemptHandshake, defaultLauncher, defaultConnect, drainOutput, watchExit, terminate, runToCompletion, errorMessage, assertShareManifest, writeShareManifest, } from './machine.js';
|
|
8
9
|
import { DeskError, } from './plan.js';
|
|
@@ -20,6 +21,23 @@ import { DeskError, } from './plan.js';
|
|
|
20
21
|
export class DeskRequestFateUnknownError extends DeskError {
|
|
21
22
|
name = 'DeskRequestFateUnknownError';
|
|
22
23
|
}
|
|
24
|
+
/** Whether something accepts connections on the Unix socket at `path`: false when nothing is there or nothing listens. */
|
|
25
|
+
export function unixSocketListening(path) {
|
|
26
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
27
|
+
const socket = createConnection(path);
|
|
28
|
+
socket.once('connect', () => {
|
|
29
|
+
socket.destroy();
|
|
30
|
+
resolvePromise(true);
|
|
31
|
+
});
|
|
32
|
+
socket.once('error', (error) => {
|
|
33
|
+
socket.destroy();
|
|
34
|
+
if (error.code === 'ECONNREFUSED' || error.code === 'ENOENT')
|
|
35
|
+
resolvePromise(false);
|
|
36
|
+
else
|
|
37
|
+
rejectPromise(error);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
}
|
|
23
41
|
export function createCloudHypervisorBackend(options = {}) {
|
|
24
42
|
const api = options.vmmApi ?? vmmApi;
|
|
25
43
|
const launcher = options.launcher ?? defaultLauncher;
|
|
@@ -37,6 +55,7 @@ export function createCloudHypervisorBackend(options = {}) {
|
|
|
37
55
|
const guestAgentPort = options.guestAgentPort ?? GUEST_AGENT_VSOCK_PORT;
|
|
38
56
|
const pathExists = options.pathExists ?? existsSync;
|
|
39
57
|
const removeFile = options.removeFile ?? ((path) => rm(path, { force: true }));
|
|
58
|
+
const socketListening = options.socketListening ?? unixSocketListening;
|
|
40
59
|
/**
|
|
41
60
|
* Start one `virtiofsd` per share and wait until every socket exists.
|
|
42
61
|
*
|
|
@@ -111,6 +130,13 @@ export function createCloudHypervisorBackend(options = {}) {
|
|
|
111
130
|
throw new DeskError(`Desk ${plan.deskId} could not be restored from ${plan.snapshot.dir}: ${errorMessage(error)}`);
|
|
112
131
|
}
|
|
113
132
|
}
|
|
133
|
+
// Cloud Hypervisor binds the vsock's host socket and fails with "Address in use" when a
|
|
134
|
+
// VMM that did not exit cleanly left it behind. A socket nothing listens on is that
|
|
135
|
+
// leftover and is removed; one a VMM still serves is this desk running already.
|
|
136
|
+
if (await socketListening(plan.vsock.socketPath)) {
|
|
137
|
+
throw new DeskError(`Desk ${plan.deskId} is already running: a VMM is listening on ${plan.vsock.socketPath}.`);
|
|
138
|
+
}
|
|
139
|
+
await removeFile(plan.vsock.socketPath);
|
|
114
140
|
let shares;
|
|
115
141
|
try {
|
|
116
142
|
shares = await startShareDaemons(plan);
|
package/backend.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from 'node:crypto'\nimport { assertPrivateMemoryMapping } from './template-mapping'\nimport { existsSync } from 'node:fs'\nimport { rm } from 'node:fs/promises'\nimport { request as httpRequest } from 'node:http'\nimport { GUEST_AGENT_VSOCK_PORT } from './protocol'\nimport {\n DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_CONNECT_RETRY_DELAY_MS,\n DEFAULT_HANDSHAKE_TIMEOUT_MS, DEFAULT_RECONNECT_WINDOW_MS, DEFAULT_RECONNECT_RETRY_DELAY_MS,\n DEFAULT_KILL_GRACE_MS, RECONNECT_BACKOFF_CEILING, connectWithRetry, createGuestMachine, attemptHandshake,\n defaultLauncher, defaultConnect, drainOutput, watchExit, terminate, runToCompletion, errorMessage,\n assertShareManifest, writeShareManifest, type ExitWatcher,\n} from './machine'\n/**\n * The backend port.\n *\n * `DeskBackend` is the seam that keeps the package testable without a\n * hypervisor: the host asks a backend to boot a `DeskLaunchPlan` and gets a\n * `DeskMachine` — a request/response channel to the in-guest agent plus an\n * event subscription. CI and non-KVM developer machines inject an in-memory\n * fake; production uses `cloudHypervisorBackend` on Linux or\n * `createVirtualizationBackend` on an Apple silicon Mac, whose process glue is\n * deliberately thin because everything that can be pure lives in the plan\n * builder, the protocol module, and the shared guest channel.\n */\nimport type { ChildProcess } from 'node:child_process'\nimport type { Duplex } from 'node:stream'\nimport {\n DeskError, type CloudHypervisorLaunchPlan, type DeskHypervisor, type DeskLaunchPlan, type DeskLauncherIdentity,\n} from './plan'\nimport type { GuestCommand, GuestEventMessage } from './protocol'\n\n/**\n * A request that was in flight when the guest connection dropped.\n *\n * Distinct from a plain `DeskError` because the caller's next move is\n * different: this request may ALREADY HAVE RUN. The frame reached the guest,\n * and whether the guest executed it before the channel went away is not\n * knowable from the host. An `exec` may have sent mail; a `click` may have\n * landed. So the transport refuses to guess — it names the ambiguity and lets\n * the caller decide whether the operation is safe to repeat. Anything that\n * rejects with a plain `DeskError` instead never reached the guest at all.\n */\nexport class DeskRequestFateUnknownError extends DeskError {\n override readonly name: string = 'DeskRequestFateUnknownError'\n}\n\n/**\n * What happened to the transport underneath a machine.\n *\n * `reconnected` means the host lost the guest channel mid-lease and got it\n * back; the desk is usable again. It is NOT a promise that the guest is\n * unchanged — see the note on `onConnectionChange`. `lost` means it will not\n * come back on this VM and the desk needs a fresh boot.\n */\nexport type DeskConnectionChange =\n | {\n state: 'reconnected'\n deskId: string\n /** How the previous connection went away. */\n reason: string\n /** Wall-clock time the desk spent with no channel to its guest. */\n downtimeMs: number\n /** Reconnects this machine has survived, including this one. */\n reconnects: number\n }\n | {\n state: 'lost'\n deskId: string\n /** Why it is gone for good: the VMM exited, or the window ran out. */\n reason: string\n }\n\n/** A booted microVM as the host sees it: one desk, one guest-agent channel. */\nexport interface DeskMachine {\n supportsPausedParkHook?: true\n readonly deskId: string\n /**\n * Send one request to the in-guest agent and await its correlated result.\n *\n * Rejects with `DeskError` when the guest reports an error, the request\n * times out, or the connection is gone for good — in that last case the\n * request never left the host, so repeating it is safe.\n *\n * A connection lost mid-lease is re-established rather than fatal: a call\n * that arrives while that is happening WAITS for it instead of failing.\n * A call that was already in flight when the channel dropped rejects with\n * `DeskRequestFateUnknownError`, because the host cannot tell whether the\n * guest ran it.\n */\n request(command: GuestCommand, options?: { timeoutMs?: number }): Promise<unknown>\n /**\n * Subscribe to unsolicited guest events (job exits, frames, focus changes).\n *\n * Subscriptions belong to the machine, not to the socket under it, so they\n * survive a reconnect. What does NOT survive is the guest's own capture\n * state: a guest agent that restarted has no screen, no frame encoder and\n * no video encoder, and a subscriber will simply stop receiving. Watch\n * `onConnectionChange` to notice.\n */\n subscribe(listener: (event: GuestEventMessage) => void): () => void\n /**\n * Watch the transport under this machine.\n *\n * Optional: a backend whose channel cannot drop has nothing to report. The\n * default backend reports every reconnect, because a reconnect that leaves\n * no trace turns a guest agent restarting every few minutes into a desk that\n * misbehaves for reasons nobody can find.\n */\n onConnectionChange?(listener: (change: DeskConnectionChange) => void): () => void\n /**\n * Open a TCP stream from the host to `port` on the guest.\n *\n * Optional: a backend whose guest has a routable address (Cloud Hypervisor's\n * TAP) leaves this out and the host dials the guest address directly. A\n * backend whose guest sits behind a userspace NAT (Virtualization.framework)\n * provides it, because there is no address on the Mac to dial. Rejects with\n * `DeskError` when the guest refuses or does not answer.\n */\n connect?(port: number): Promise<Duplex>\n /** Optional resident pause and resume. Callers must serialize these with lifecycle operations. */\n pause?(): Promise<void>\n resume?(): Promise<void>\n /** Stop the VM. Idempotent; resolves once the VMM process has exited. */\n shutdown(): Promise<void>\n /**\n * Park the VM with its memory, so resuming continues the process tree.\n *\n * Optional: a backend with no snapshot facility simply does not offer it, and\n * the host falls back to `shutdown()`. Pauses the guest, writes a snapshot to\n * `snapshotDir`, then stops the VMM — and resolves only once all three have\n * happened, because a caller that believed a snapshot exists when it does not\n * would later refuse to boot the desk at all.\n *\n * The caller is responsible for quiescing the guest filesystem first. A paused\n * guest still holds dirty pages that exist only in the snapshot, so a disk\n * parked without a prior `sync` is consistent only *with that snapshot* —\n * which matters the moment the snapshot is discarded.\n */\n park?(snapshotDir: string, hooks?: DeskParkHooks): Promise<void>\n}\n\nexport interface DeskParkHooks {\n /** Called while the VM is paused, before its memory snapshot is written. */\n onPaused?: () => Promise<void>\n /** Reports a sidecar failure; ordinary memory parking continues. */\n onPausedError?: (error: unknown) => void\n}\n\nexport interface DeskBootHooks {\n /**\n * Called on a restore, after the snapshot has been read back and before the\n * guest is allowed to run.\n *\n * The snapshot must no longer be available for replay by this point. A memory image\n * is valid only against the disk it was taken with, so once the guest resumes\n * and writes, any surviving snapshot describes a past that no longer matches —\n * and replaying it later would corrupt the desk. Restoring paused and\n * invalidating here is what makes that impossible rather than unlikely.\n *\n * Throwing aborts the restore: the VM is stopped and the caller is free to\n * cold boot, which is safe because a paused guest has written nothing.\n */\n onRestored?: () => Promise<void>\n}\n\nexport interface DeskBackend {\n /**\n * Which hypervisor this backend boots, and so which plan the host builds\n * for it. Absent means Cloud Hypervisor.\n */\n readonly hypervisor?: DeskHypervisor\n /**\n * The binary the backend launches — the VMM or the helper — for readiness\n * reporting. Optional; `verifyDeskHost` falls back to its own option.\n */\n readonly vmmPath?: string\n boot(plan: DeskLaunchPlan, hooks?: DeskBootHooks): Promise<DeskMachine>\n}\n\nexport type DeskProcessLauncher = (\n command: string,\n args: readonly string[],\n options: { identity?: DeskLauncherIdentity; inheritedFds?: readonly number[] },\n) => ChildProcess\n\nexport interface CloudHypervisorBackendOptions {\n /** VMM API transport, injectable for tests. */\n vmmApi?: VmmApiCall\n /** Descriptors retained by the VMM and share daemons, mapped consecutively from fd 3. */\n inheritedFds?: readonly number[]\n /** Spawns the VMM and image tooling; injectable for tests. */\n launcher?: DeskProcessLauncher\n /** Opens the host side of the vsock Unix socket; injectable for tests. */\n connect?: (socketPath: string) => Duplex\n platform?: NodeJS.Platform\n /**\n * Clock behind every connect and reconnect deadline. Injectable for the same\n * reason `DeskHostOptions.now` is: a test drives a window to its end by\n * moving the clock, rather than sleeping for it and hoping the machine it\n * runs on is fast enough.\n */\n now?: () => number\n idFactory?: () => string\n requestTimeoutMs?: number\n connectTimeoutMs?: number\n connectRetryDelayMs?: number\n /** Bound on a single handshake attempt; see attemptHandshake. */\n handshakeTimeoutMs?: number\n /**\n * How long a machine keeps trying to get its guest channel back after an\n * unexpected drop before declaring the desk lost. Bounded on purpose: the\n * VMM is still alive so a reconnect is cheap and usually fast, but retrying\n * forever against a guest that is never coming back is worse than failing.\n */\n reconnectWindowMs?: number\n /**\n * First delay between reconnect attempts. It doubles toward a ceiling, so\n * this is the fast first poll rather than the steady rate. Separate from\n * `connectRetryDelayMs`, which is sized for a guest that is still cold\n * booting; here the guest was up a moment ago.\n */\n reconnectRetryDelayMs?: number\n killGraceMs?: number\n guestAgentPort?: number\n /** Use guest-provided vsock service bridges for DeskHandle.connect. Opt in only when the image provides them. */\n serviceVsock?: boolean\n /** Whether a share daemon's socket exists yet; injectable for tests. */\n pathExists?: (path: string) => boolean\n /** Removes a stale share socket before its daemon starts; injectable for tests. */\n removeFile?: (path: string) => Promise<void>\n}\n\nexport function createCloudHypervisorBackend(\n options: CloudHypervisorBackendOptions = {},\n): DeskBackend {\n const api = options.vmmApi ?? vmmApi\n const launcher = options.launcher ?? defaultLauncher\n const connect = options.connect ?? defaultConnect\n const platform = options.platform ?? process.platform\n const now = options.now ?? Date.now\n const idFactory = options.idFactory ?? randomUUID\n const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS\n const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS\n const connectRetryDelayMs = options.connectRetryDelayMs ?? DEFAULT_CONNECT_RETRY_DELAY_MS\n const handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS\n const reconnectWindowMs = options.reconnectWindowMs ?? DEFAULT_RECONNECT_WINDOW_MS\n const reconnectRetryDelayMs = options.reconnectRetryDelayMs ?? DEFAULT_RECONNECT_RETRY_DELAY_MS\n const killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS\n const guestAgentPort = options.guestAgentPort ?? GUEST_AGENT_VSOCK_PORT\n const pathExists = options.pathExists ?? existsSync\n const removeFile = options.removeFile ?? ((path: string) => rm(path, { force: true }))\n\n /**\n * Start one `virtiofsd` per share and wait until every socket exists.\n *\n * The VMM connects to these at boot and at restore alike, so they come up\n * first. A stale socket is removed beforehand, or its presence would read as\n * a daemon that is listening. Existence is the readiness signal rather than\n * a probe connection, because `virtiofsd` serves exactly one connection and\n * exits when it closes; the moment between bind and listen is covered by\n * Cloud Hypervisor retrying a refused vhost-user connect.\n */\n async function startShareDaemons(plan: CloudHypervisorLaunchPlan): Promise<ShareDaemons> {\n const daemons: { tag: string; child: ChildProcess; exit: ExitWatcher; log: { tail: () => string } }[] = []\n const stop = async () => {\n await Promise.all(daemons.map((daemon) => terminate(daemon.child, daemon.exit, killGraceMs)))\n }\n try {\n for (const share of plan.shares) {\n await removeFile(share.socketPath)\n const child = launcher(share.daemon.command, share.daemon.args, { identity: plan.launcherIdentity, inheritedFds: options.inheritedFds })\n daemons.push({ tag: share.tag, child, exit: watchExit(child), log: drainOutput(child) })\n }\n const deadline = now() + connectTimeoutMs\n for (;;) {\n const exited = daemons.find((daemon) => daemon.exit.done)\n if (exited) {\n const said = exited.log.tail()\n throw new DeskError(`virtiofsd for share ${exited.tag} exited before it listened${said === '' ? '.' : `: ${said}`}`)\n }\n const waiting = plan.shares.filter((share) => !pathExists(share.socketPath))\n if (waiting.length === 0) break\n if (now() >= deadline) {\n throw new DeskError(\n `virtiofsd did not create ${waiting.map((share) => share.socketPath).join(', ')} in ${connectTimeoutMs}ms.`,\n )\n }\n // A held timer: the wait is bounded by the deadline, and nothing else may be keeping\n // the process alive while a daemon starts.\n await new Promise<void>((resolvePromise) => setTimeout(resolvePromise, connectRetryDelayMs))\n }\n } catch (error) {\n await stop()\n throw error\n }\n return {\n stop,\n // The VMM cannot serve a share whose daemon is gone; neither can it be\n // handed a new one mid-run. So a daemon that dies stops the VMM, and the\n // desk is lost loudly instead of hanging every guest access to the share.\n supervise: (vmm, vmmExit) => {\n for (const daemon of daemons) {\n void daemon.exit.promise.then(() => terminate(vmm, vmmExit, killGraceMs))\n }\n },\n }\n }\n\n async function boot(plan: DeskLaunchPlan, hooks?: DeskBootHooks): Promise<DeskMachine> {\n if (plan.hypervisor !== 'cloud-hypervisor') throw new DeskError('Cloud Hypervisor requires a cloud-hypervisor plan.')\n if (platform !== 'linux') {\n throw new DeskError(`The AppKit desk requires Linux; received ${platform}.`)\n }\n if (plan.overlay.create) {\n await runToCompletion(\n launcher(plan.overlay.create.command, plan.overlay.create.args, {\n identity: plan.launcherIdentity,\n }),\n `overlay creation for desk ${plan.deskId}`,\n )\n }\n if (plan.snapshot.restoring) {\n try {\n await assertShareManifest(plan.snapshot.dir, plan.shares)\n } catch (error) {\n throw new DeskError(`Desk ${plan.deskId} could not be restored from ${plan.snapshot.dir}: ${errorMessage(error)}`)\n }\n }\n\n let shares: ShareDaemons\n try {\n shares = await startShareDaemons(plan)\n } catch (error) {\n throw new DeskError(`Desk ${plan.deskId} could not start its shares: ${errorMessage(error)}`)\n }\n const child = launcher(plan.vmm.command, plan.vmm.args, { identity: plan.launcherIdentity, inheritedFds: options.inheritedFds })\n const log = drainOutput(child)\n const exit = watchExit(child)\n shares.supervise(child, exit)\n // The VMM first: a daemon outliving it is harmless, while a daemon stopped\n // under a running VMM is a share failing in the guest.\n const stop = async () => {\n try {\n await terminate(child, exit, killGraceMs)\n } finally {\n await shares.stop()\n }\n }\n\n // A restored guest comes back PAUSED (see the `resume=false` reasoning in\n // the plan). Three things have to happen in this order and nowhere else:\n // prove the restore finished, let the caller invalidate replay while the\n // guest still cannot write, then start it.\n if (plan.snapshot.restoring) {\n try {\n await waitForVmm(api, plan.api.socketPath, { now, exited: exit, timeoutMs: connectTimeoutMs })\n if (plan.snapshot.requirePrivateMemoryMapping) await assertPrivateMemoryMapping(child.pid, plan.snapshot.dir)\n await hooks?.onRestored?.()\n await api(plan.api.socketPath, '/api/v1/vm.resume')\n } catch (error) {\n await stop()\n const said = log.tail()\n const detail = said === '' ? '' : ` The VMM said: ${said}`\n throw new DeskError(\n `Desk ${plan.deskId} could not be restored from ${plan.snapshot.dir}: ${errorMessage(error)}${detail}`,\n )\n }\n }\n\n let stream: Duplex\n try {\n stream = await connectWithRetry({\n connect,\n now,\n socketPath: plan.vsock.socketPath,\n port: guestAgentPort,\n timeoutMs: connectTimeoutMs,\n retryDelayMs: connectRetryDelayMs,\n attemptTimeoutMs: handshakeTimeoutMs,\n exited: exit,\n })\n } catch (error) {\n await stop()\n const said = log.tail()\n const detail = said === '' ? '' : ` The VMM said: ${said}`\n throw error instanceof DeskError\n ? new DeskError(`${error.message}${detail}`)\n : new DeskError(\n `Could not reach the guest agent for desk ${plan.deskId}: ${errorMessage(error)}${detail}`,\n )\n }\n return createGuestMachine({\n deskId: plan.deskId,\n exit,\n stream,\n // The same path the first connection took, deliberately: a reconnect\n // that skipped confirmGuest would adopt the very hollow handshake the\n // boot path learned not to trust.\n reconnect: (abandoned) =>\n connectWithRetry({\n connect,\n now,\n socketPath: plan.vsock.socketPath,\n port: guestAgentPort,\n timeoutMs: reconnectWindowMs,\n retryDelayMs: reconnectRetryDelayMs,\n maxRetryDelayMs: reconnectRetryDelayMs * RECONNECT_BACKOFF_CEILING,\n attemptTimeoutMs: handshakeTimeoutMs,\n exited: exit,\n exitedMessage: `the VMM for desk ${plan.deskId} exited.`,\n abandoned,\n }),\n reconnectWindowMs,\n now,\n idFactory,\n requestTimeoutMs,\n ...(options.serviceVsock ? { connect: (port: number) => {\n if (!Number.isInteger(port) || port < 1 || port > 65535) throw new DeskError('Guest port must be between 1 and 65535.')\n return attemptHandshake(connect, plan.vsock.socketPath, port, handshakeTimeoutMs)\n } } : {}),\n stop,\n pause: () => setVmmState(api, plan.api.socketPath, 'Paused'),\n resume: () => setVmmState(api, plan.api.socketPath, 'Running'),\n supportsPausedParkHook: true,\n park: async (snapshotDir, hooks) => {\n try {\n await api(plan.api.socketPath, '/api/v1/vm.pause')\n try { await hooks?.onPaused?.() }\n catch (error) { try { hooks?.onPausedError?.(error) } catch { /* preserve memory */ } }\n await api(plan.api.socketPath, '/api/v1/vm.snapshot', { destination_url: `file://${snapshotDir}` })\n await writeShareManifest(snapshotDir, plan.shares)\n } finally {\n await stop()\n }\n },\n })\n }\n\n return { boot }\n}\n\ninterface ShareDaemons {\n /** Stop every daemon; resolves once all have exited. */\n stop: () => Promise<void>\n /** Stop the VMM if any daemon exits while it runs. */\n supervise: (vmm: ChildProcess, vmmExit: ExitWatcher) => void\n}\n\n/**\n * Cloud Hypervisor's control plane: HTTP/1.1 over the `--api-socket` Unix\n * socket. The plan has always created that socket; until now nothing spoke to\n * it, because stopping a VM needed nothing more than killing the process.\n *\n * Pausing and snapshotting do need it, and `ch-remote` — the tool that usually\n * does this — is not in the runner image. Writing the two requests by hand is a\n * smaller change than adding a binary to the image, and keeps the whole\n * lifecycle inside one process that already owns the socket path.\n */\nexport type VmmApiCall = (\n socketPath: string,\n path: string,\n body?: Record<string, unknown>,\n method?: 'PUT' | 'GET',\n) => Promise<void | string>\n\n/** Reconcile an ambiguous command failure against the VMM before releasing the fence. */\nexport async function setVmmState(call: VmmApiCall, socketPath: string, desired: 'Paused' | 'Running'): Promise<void> {\n try {\n await call(socketPath, desired === 'Paused' ? '/api/v1/vm.pause' : '/api/v1/vm.resume')\n } catch (error) {\n try {\n const response = await call(socketPath, '/api/v1/vm.info', undefined, 'GET')\n const info: unknown = typeof response === 'string' ? JSON.parse(response) : null\n if (info && typeof info === 'object' && 'state' in info && info.state === desired) return\n } catch { /* State is unknown: preserve the original failure and the request fence. */ }\n throw error\n }\n}\n\nconst vmmApi: VmmApiCall = async (socketPath, path, body, method = 'PUT') => {\n const payload = body === undefined ? undefined : Buffer.from(JSON.stringify(body), 'utf8')\n return await new Promise<string>((resolve, reject) => {\n const request = httpRequest(\n {\n socketPath,\n path,\n method,\n headers: {\n ...(payload ? { 'content-type': 'application/json', 'content-length': String(payload.length) } : {}),\n },\n },\n (response) => {\n // CH answers 204 with no body on success. Drain regardless: an unread\n // response keeps the socket open and the next call queues behind it.\n const chunks: Buffer[] = []\n response.on('data', (chunk: Buffer) => chunks.push(chunk))\n response.on('end', () => {\n const status = response.statusCode ?? 0\n if (status >= 200 && status < 300) {\n resolve(Buffer.concat(chunks).toString('utf8'))\n return\n }\n const said = Buffer.concat(chunks).toString('utf8').trim().slice(0, 400)\n reject(new DeskError(`${path} returned ${status}${said === '' ? '' : `: ${said}`}`))\n })\n },\n )\n request.on('error', (error) => reject(new DeskError(`${path} failed: ${errorMessage(error)}`)))\n if (payload) request.write(payload)\n request.end()\n })\n}\n\n/**\n * Wait until the VMM answers its control socket.\n *\n * On a restore this is also the only honest signal that the snapshot was read\n * back: Cloud Hypervisor creates the API socket early but does not serve\n * `vm.info` until it holds a VM, so a successful call means the restore\n * completed rather than merely that the process started. A VMM that exits\n * instead — a corrupt snapshot, a config the host can no longer satisfy — stops\n * the wait immediately rather than burning the whole timeout.\n */\nasync function waitForVmm(\n call: VmmApiCall,\n socketPath: string,\n options: { now: () => number; exited: ExitWatcher; timeoutMs: number; retryDelayMs?: number },\n): Promise<void> {\n const deadline = options.now() + options.timeoutMs\n const retryDelayMs = options.retryDelayMs ?? 50\n let lastFailure = 'it never answered'\n for (;;) {\n if (options.exited.done) throw new DeskError(`the VMM exited before it served its control socket`)\n try {\n await call(socketPath, '/api/v1/vm.info', undefined, 'GET')\n return\n } catch (error) {\n lastFailure = errorMessage(error)\n }\n if (options.now() >= deadline) {\n throw new DeskError(`the VMM did not answer ${socketPath} in ${options.timeoutMs}ms: ${lastFailure}`)\n }\n await new Promise((resolve) => setTimeout(resolve, retryDelayMs))\n }\n}\n\n/** The default backend: real Cloud Hypervisor over a real vsock socket. */\nexport const cloudHypervisorBackend: DeskBackend = {\n boot: (plan, hooks) => createCloudHypervisorBackend().boot(plan, hooks),\n}\n"]}
|
|
1
|
+
{"version":3,"file":"backend.js","sourceRoot":"","sources":["../src/backend.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from 'node:crypto'\nimport { assertPrivateMemoryMapping } from './template-mapping'\nimport { existsSync } from 'node:fs'\nimport { rm } from 'node:fs/promises'\nimport { request as httpRequest } from 'node:http'\nimport { createConnection } from 'node:net'\nimport { GUEST_AGENT_VSOCK_PORT } from './protocol'\nimport {\n DEFAULT_REQUEST_TIMEOUT_MS, DEFAULT_CONNECT_TIMEOUT_MS, DEFAULT_CONNECT_RETRY_DELAY_MS,\n DEFAULT_HANDSHAKE_TIMEOUT_MS, DEFAULT_RECONNECT_WINDOW_MS, DEFAULT_RECONNECT_RETRY_DELAY_MS,\n DEFAULT_KILL_GRACE_MS, RECONNECT_BACKOFF_CEILING, connectWithRetry, createGuestMachine, attemptHandshake,\n defaultLauncher, defaultConnect, drainOutput, watchExit, terminate, runToCompletion, errorMessage,\n assertShareManifest, writeShareManifest, type ExitWatcher,\n} from './machine'\n/**\n * The backend port.\n *\n * `DeskBackend` is the seam that keeps the package testable without a\n * hypervisor: the host asks a backend to boot a `DeskLaunchPlan` and gets a\n * `DeskMachine` — a request/response channel to the in-guest agent plus an\n * event subscription. CI and non-KVM developer machines inject an in-memory\n * fake; production uses `cloudHypervisorBackend` on Linux or\n * `createVirtualizationBackend` on an Apple silicon Mac, whose process glue is\n * deliberately thin because everything that can be pure lives in the plan\n * builder, the protocol module, and the shared guest channel.\n */\nimport type { ChildProcess } from 'node:child_process'\nimport type { Duplex } from 'node:stream'\nimport {\n DeskError, type CloudHypervisorLaunchPlan, type DeskHypervisor, type DeskLaunchPlan, type DeskLauncherIdentity,\n} from './plan'\nimport type { GuestCommand, GuestEventMessage } from './protocol'\n\n/**\n * A request that was in flight when the guest connection dropped.\n *\n * Distinct from a plain `DeskError` because the caller's next move is\n * different: this request may ALREADY HAVE RUN. The frame reached the guest,\n * and whether the guest executed it before the channel went away is not\n * knowable from the host. An `exec` may have sent mail; a `click` may have\n * landed. So the transport refuses to guess — it names the ambiguity and lets\n * the caller decide whether the operation is safe to repeat. Anything that\n * rejects with a plain `DeskError` instead never reached the guest at all.\n */\nexport class DeskRequestFateUnknownError extends DeskError {\n override readonly name: string = 'DeskRequestFateUnknownError'\n}\n\n/**\n * What happened to the transport underneath a machine.\n *\n * `reconnected` means the host lost the guest channel mid-lease and got it\n * back; the desk is usable again. It is NOT a promise that the guest is\n * unchanged — see the note on `onConnectionChange`. `lost` means it will not\n * come back on this VM and the desk needs a fresh boot.\n */\nexport type DeskConnectionChange =\n | {\n state: 'reconnected'\n deskId: string\n /** How the previous connection went away. */\n reason: string\n /** Wall-clock time the desk spent with no channel to its guest. */\n downtimeMs: number\n /** Reconnects this machine has survived, including this one. */\n reconnects: number\n }\n | {\n state: 'lost'\n deskId: string\n /** Why it is gone for good: the VMM exited, or the window ran out. */\n reason: string\n }\n\n/** A booted microVM as the host sees it: one desk, one guest-agent channel. */\nexport interface DeskMachine {\n supportsPausedParkHook?: true\n readonly deskId: string\n /**\n * Send one request to the in-guest agent and await its correlated result.\n *\n * Rejects with `DeskError` when the guest reports an error, the request\n * times out, or the connection is gone for good — in that last case the\n * request never left the host, so repeating it is safe.\n *\n * A connection lost mid-lease is re-established rather than fatal: a call\n * that arrives while that is happening WAITS for it instead of failing.\n * A call that was already in flight when the channel dropped rejects with\n * `DeskRequestFateUnknownError`, because the host cannot tell whether the\n * guest ran it.\n */\n request(command: GuestCommand, options?: { timeoutMs?: number }): Promise<unknown>\n /**\n * Subscribe to unsolicited guest events (job exits, frames, focus changes).\n *\n * Subscriptions belong to the machine, not to the socket under it, so they\n * survive a reconnect. What does NOT survive is the guest's own capture\n * state: a guest agent that restarted has no screen, no frame encoder and\n * no video encoder, and a subscriber will simply stop receiving. Watch\n * `onConnectionChange` to notice.\n */\n subscribe(listener: (event: GuestEventMessage) => void): () => void\n /**\n * Watch the transport under this machine.\n *\n * Optional: a backend whose channel cannot drop has nothing to report. The\n * default backend reports every reconnect, because a reconnect that leaves\n * no trace turns a guest agent restarting every few minutes into a desk that\n * misbehaves for reasons nobody can find.\n */\n onConnectionChange?(listener: (change: DeskConnectionChange) => void): () => void\n /**\n * Open a TCP stream from the host to `port` on the guest.\n *\n * Optional: a backend whose guest has a routable address (Cloud Hypervisor's\n * TAP) leaves this out and the host dials the guest address directly. A\n * backend whose guest sits behind a userspace NAT (Virtualization.framework)\n * provides it, because there is no address on the Mac to dial. Rejects with\n * `DeskError` when the guest refuses or does not answer.\n */\n connect?(port: number): Promise<Duplex>\n /** Optional resident pause and resume. Callers must serialize these with lifecycle operations. */\n pause?(): Promise<void>\n resume?(): Promise<void>\n /** Stop the VM. Idempotent; resolves once the VMM process has exited. */\n shutdown(): Promise<void>\n /**\n * Park the VM with its memory, so resuming continues the process tree.\n *\n * Optional: a backend with no snapshot facility simply does not offer it, and\n * the host falls back to `shutdown()`. Pauses the guest, writes a snapshot to\n * `snapshotDir`, then stops the VMM — and resolves only once all three have\n * happened, because a caller that believed a snapshot exists when it does not\n * would later refuse to boot the desk at all.\n *\n * The caller is responsible for quiescing the guest filesystem first. A paused\n * guest still holds dirty pages that exist only in the snapshot, so a disk\n * parked without a prior `sync` is consistent only *with that snapshot* —\n * which matters the moment the snapshot is discarded.\n */\n park?(snapshotDir: string, hooks?: DeskParkHooks): Promise<void>\n}\n\nexport interface DeskParkHooks {\n /** Called while the VM is paused, before its memory snapshot is written. */\n onPaused?: () => Promise<void>\n /** Reports a sidecar failure; ordinary memory parking continues. */\n onPausedError?: (error: unknown) => void\n}\n\nexport interface DeskBootHooks {\n /**\n * Called on a restore, after the snapshot has been read back and before the\n * guest is allowed to run.\n *\n * The snapshot must no longer be available for replay by this point. A memory image\n * is valid only against the disk it was taken with, so once the guest resumes\n * and writes, any surviving snapshot describes a past that no longer matches —\n * and replaying it later would corrupt the desk. Restoring paused and\n * invalidating here is what makes that impossible rather than unlikely.\n *\n * Throwing aborts the restore: the VM is stopped and the caller is free to\n * cold boot, which is safe because a paused guest has written nothing.\n */\n onRestored?: () => Promise<void>\n}\n\nexport interface DeskBackend {\n /**\n * Which hypervisor this backend boots, and so which plan the host builds\n * for it. Absent means Cloud Hypervisor.\n */\n readonly hypervisor?: DeskHypervisor\n /**\n * The binary the backend launches — the VMM or the helper — for readiness\n * reporting. Optional; `verifyDeskHost` falls back to its own option.\n */\n readonly vmmPath?: string\n boot(plan: DeskLaunchPlan, hooks?: DeskBootHooks): Promise<DeskMachine>\n}\n\nexport type DeskProcessLauncher = (\n command: string,\n args: readonly string[],\n options: { identity?: DeskLauncherIdentity; inheritedFds?: readonly number[] },\n) => ChildProcess\n\nexport interface CloudHypervisorBackendOptions {\n /** VMM API transport, injectable for tests. */\n vmmApi?: VmmApiCall\n /** Descriptors retained by the VMM and share daemons, mapped consecutively from fd 3. */\n inheritedFds?: readonly number[]\n /** Spawns the VMM and image tooling; injectable for tests. */\n launcher?: DeskProcessLauncher\n /** Opens the host side of the vsock Unix socket; injectable for tests. */\n connect?: (socketPath: string) => Duplex\n platform?: NodeJS.Platform\n /**\n * Clock behind every connect and reconnect deadline. Injectable for the same\n * reason `DeskHostOptions.now` is: a test drives a window to its end by\n * moving the clock, rather than sleeping for it and hoping the machine it\n * runs on is fast enough.\n */\n now?: () => number\n idFactory?: () => string\n requestTimeoutMs?: number\n connectTimeoutMs?: number\n connectRetryDelayMs?: number\n /** Bound on a single handshake attempt; see attemptHandshake. */\n handshakeTimeoutMs?: number\n /**\n * How long a machine keeps trying to get its guest channel back after an\n * unexpected drop before declaring the desk lost. Bounded on purpose: the\n * VMM is still alive so a reconnect is cheap and usually fast, but retrying\n * forever against a guest that is never coming back is worse than failing.\n */\n reconnectWindowMs?: number\n /**\n * First delay between reconnect attempts. It doubles toward a ceiling, so\n * this is the fast first poll rather than the steady rate. Separate from\n * `connectRetryDelayMs`, which is sized for a guest that is still cold\n * booting; here the guest was up a moment ago.\n */\n reconnectRetryDelayMs?: number\n killGraceMs?: number\n guestAgentPort?: number\n /** Use guest-provided vsock service bridges for DeskHandle.connect. Opt in only when the image provides them. */\n serviceVsock?: boolean\n /** Whether a share daemon's socket exists yet; injectable for tests. */\n pathExists?: (path: string) => boolean\n /** Removes a stale share or vsock socket before the process that binds it starts; injectable for tests. */\n removeFile?: (path: string) => Promise<void>\n /** Whether a process listens on a Unix socket (a running VMM on its vsock socket); injectable for tests. */\n socketListening?: (path: string) => Promise<boolean>\n}\n\n/** Whether something accepts connections on the Unix socket at `path`: false when nothing is there or nothing listens. */\nexport function unixSocketListening(path: string): Promise<boolean> {\n return new Promise((resolvePromise, rejectPromise) => {\n const socket = createConnection(path)\n socket.once('connect', () => {\n socket.destroy()\n resolvePromise(true)\n })\n socket.once('error', (error: NodeJS.ErrnoException) => {\n socket.destroy()\n if (error.code === 'ECONNREFUSED' || error.code === 'ENOENT') resolvePromise(false)\n else rejectPromise(error)\n })\n })\n}\n\nexport function createCloudHypervisorBackend(\n options: CloudHypervisorBackendOptions = {},\n): DeskBackend {\n const api = options.vmmApi ?? vmmApi\n const launcher = options.launcher ?? defaultLauncher\n const connect = options.connect ?? defaultConnect\n const platform = options.platform ?? process.platform\n const now = options.now ?? Date.now\n const idFactory = options.idFactory ?? randomUUID\n const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS\n const connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS\n const connectRetryDelayMs = options.connectRetryDelayMs ?? DEFAULT_CONNECT_RETRY_DELAY_MS\n const handshakeTimeoutMs = options.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS\n const reconnectWindowMs = options.reconnectWindowMs ?? DEFAULT_RECONNECT_WINDOW_MS\n const reconnectRetryDelayMs = options.reconnectRetryDelayMs ?? DEFAULT_RECONNECT_RETRY_DELAY_MS\n const killGraceMs = options.killGraceMs ?? DEFAULT_KILL_GRACE_MS\n const guestAgentPort = options.guestAgentPort ?? GUEST_AGENT_VSOCK_PORT\n const pathExists = options.pathExists ?? existsSync\n const removeFile = options.removeFile ?? ((path: string) => rm(path, { force: true }))\n const socketListening = options.socketListening ?? unixSocketListening\n\n /**\n * Start one `virtiofsd` per share and wait until every socket exists.\n *\n * The VMM connects to these at boot and at restore alike, so they come up\n * first. A stale socket is removed beforehand, or its presence would read as\n * a daemon that is listening. Existence is the readiness signal rather than\n * a probe connection, because `virtiofsd` serves exactly one connection and\n * exits when it closes; the moment between bind and listen is covered by\n * Cloud Hypervisor retrying a refused vhost-user connect.\n */\n async function startShareDaemons(plan: CloudHypervisorLaunchPlan): Promise<ShareDaemons> {\n const daemons: { tag: string; child: ChildProcess; exit: ExitWatcher; log: { tail: () => string } }[] = []\n const stop = async () => {\n await Promise.all(daemons.map((daemon) => terminate(daemon.child, daemon.exit, killGraceMs)))\n }\n try {\n for (const share of plan.shares) {\n await removeFile(share.socketPath)\n const child = launcher(share.daemon.command, share.daemon.args, { identity: plan.launcherIdentity, inheritedFds: options.inheritedFds })\n daemons.push({ tag: share.tag, child, exit: watchExit(child), log: drainOutput(child) })\n }\n const deadline = now() + connectTimeoutMs\n for (;;) {\n const exited = daemons.find((daemon) => daemon.exit.done)\n if (exited) {\n const said = exited.log.tail()\n throw new DeskError(`virtiofsd for share ${exited.tag} exited before it listened${said === '' ? '.' : `: ${said}`}`)\n }\n const waiting = plan.shares.filter((share) => !pathExists(share.socketPath))\n if (waiting.length === 0) break\n if (now() >= deadline) {\n throw new DeskError(\n `virtiofsd did not create ${waiting.map((share) => share.socketPath).join(', ')} in ${connectTimeoutMs}ms.`,\n )\n }\n // A held timer: the wait is bounded by the deadline, and nothing else may be keeping\n // the process alive while a daemon starts.\n await new Promise<void>((resolvePromise) => setTimeout(resolvePromise, connectRetryDelayMs))\n }\n } catch (error) {\n await stop()\n throw error\n }\n return {\n stop,\n // The VMM cannot serve a share whose daemon is gone; neither can it be\n // handed a new one mid-run. So a daemon that dies stops the VMM, and the\n // desk is lost loudly instead of hanging every guest access to the share.\n supervise: (vmm, vmmExit) => {\n for (const daemon of daemons) {\n void daemon.exit.promise.then(() => terminate(vmm, vmmExit, killGraceMs))\n }\n },\n }\n }\n\n async function boot(plan: DeskLaunchPlan, hooks?: DeskBootHooks): Promise<DeskMachine> {\n if (plan.hypervisor !== 'cloud-hypervisor') throw new DeskError('Cloud Hypervisor requires a cloud-hypervisor plan.')\n if (platform !== 'linux') {\n throw new DeskError(`The AppKit desk requires Linux; received ${platform}.`)\n }\n if (plan.overlay.create) {\n await runToCompletion(\n launcher(plan.overlay.create.command, plan.overlay.create.args, {\n identity: plan.launcherIdentity,\n }),\n `overlay creation for desk ${plan.deskId}`,\n )\n }\n if (plan.snapshot.restoring) {\n try {\n await assertShareManifest(plan.snapshot.dir, plan.shares)\n } catch (error) {\n throw new DeskError(`Desk ${plan.deskId} could not be restored from ${plan.snapshot.dir}: ${errorMessage(error)}`)\n }\n }\n\n // Cloud Hypervisor binds the vsock's host socket and fails with \"Address in use\" when a\n // VMM that did not exit cleanly left it behind. A socket nothing listens on is that\n // leftover and is removed; one a VMM still serves is this desk running already.\n if (await socketListening(plan.vsock.socketPath)) {\n throw new DeskError(`Desk ${plan.deskId} is already running: a VMM is listening on ${plan.vsock.socketPath}.`)\n }\n await removeFile(plan.vsock.socketPath)\n\n let shares: ShareDaemons\n try {\n shares = await startShareDaemons(plan)\n } catch (error) {\n throw new DeskError(`Desk ${plan.deskId} could not start its shares: ${errorMessage(error)}`)\n }\n const child = launcher(plan.vmm.command, plan.vmm.args, { identity: plan.launcherIdentity, inheritedFds: options.inheritedFds })\n const log = drainOutput(child)\n const exit = watchExit(child)\n shares.supervise(child, exit)\n // The VMM first: a daemon outliving it is harmless, while a daemon stopped\n // under a running VMM is a share failing in the guest.\n const stop = async () => {\n try {\n await terminate(child, exit, killGraceMs)\n } finally {\n await shares.stop()\n }\n }\n\n // A restored guest comes back PAUSED (see the `resume=false` reasoning in\n // the plan). Three things have to happen in this order and nowhere else:\n // prove the restore finished, let the caller invalidate replay while the\n // guest still cannot write, then start it.\n if (plan.snapshot.restoring) {\n try {\n await waitForVmm(api, plan.api.socketPath, { now, exited: exit, timeoutMs: connectTimeoutMs })\n if (plan.snapshot.requirePrivateMemoryMapping) await assertPrivateMemoryMapping(child.pid, plan.snapshot.dir)\n await hooks?.onRestored?.()\n await api(plan.api.socketPath, '/api/v1/vm.resume')\n } catch (error) {\n await stop()\n const said = log.tail()\n const detail = said === '' ? '' : ` The VMM said: ${said}`\n throw new DeskError(\n `Desk ${plan.deskId} could not be restored from ${plan.snapshot.dir}: ${errorMessage(error)}${detail}`,\n )\n }\n }\n\n let stream: Duplex\n try {\n stream = await connectWithRetry({\n connect,\n now,\n socketPath: plan.vsock.socketPath,\n port: guestAgentPort,\n timeoutMs: connectTimeoutMs,\n retryDelayMs: connectRetryDelayMs,\n attemptTimeoutMs: handshakeTimeoutMs,\n exited: exit,\n })\n } catch (error) {\n await stop()\n const said = log.tail()\n const detail = said === '' ? '' : ` The VMM said: ${said}`\n throw error instanceof DeskError\n ? new DeskError(`${error.message}${detail}`)\n : new DeskError(\n `Could not reach the guest agent for desk ${plan.deskId}: ${errorMessage(error)}${detail}`,\n )\n }\n return createGuestMachine({\n deskId: plan.deskId,\n exit,\n stream,\n // The same path the first connection took, deliberately: a reconnect\n // that skipped confirmGuest would adopt the very hollow handshake the\n // boot path learned not to trust.\n reconnect: (abandoned) =>\n connectWithRetry({\n connect,\n now,\n socketPath: plan.vsock.socketPath,\n port: guestAgentPort,\n timeoutMs: reconnectWindowMs,\n retryDelayMs: reconnectRetryDelayMs,\n maxRetryDelayMs: reconnectRetryDelayMs * RECONNECT_BACKOFF_CEILING,\n attemptTimeoutMs: handshakeTimeoutMs,\n exited: exit,\n exitedMessage: `the VMM for desk ${plan.deskId} exited.`,\n abandoned,\n }),\n reconnectWindowMs,\n now,\n idFactory,\n requestTimeoutMs,\n ...(options.serviceVsock ? { connect: (port: number) => {\n if (!Number.isInteger(port) || port < 1 || port > 65535) throw new DeskError('Guest port must be between 1 and 65535.')\n return attemptHandshake(connect, plan.vsock.socketPath, port, handshakeTimeoutMs)\n } } : {}),\n stop,\n pause: () => setVmmState(api, plan.api.socketPath, 'Paused'),\n resume: () => setVmmState(api, plan.api.socketPath, 'Running'),\n supportsPausedParkHook: true,\n park: async (snapshotDir, hooks) => {\n try {\n await api(plan.api.socketPath, '/api/v1/vm.pause')\n try { await hooks?.onPaused?.() }\n catch (error) { try { hooks?.onPausedError?.(error) } catch { /* preserve memory */ } }\n await api(plan.api.socketPath, '/api/v1/vm.snapshot', { destination_url: `file://${snapshotDir}` })\n await writeShareManifest(snapshotDir, plan.shares)\n } finally {\n await stop()\n }\n },\n })\n }\n\n return { boot }\n}\n\ninterface ShareDaemons {\n /** Stop every daemon; resolves once all have exited. */\n stop: () => Promise<void>\n /** Stop the VMM if any daemon exits while it runs. */\n supervise: (vmm: ChildProcess, vmmExit: ExitWatcher) => void\n}\n\n/**\n * Cloud Hypervisor's control plane: HTTP/1.1 over the `--api-socket` Unix\n * socket. The plan has always created that socket; until now nothing spoke to\n * it, because stopping a VM needed nothing more than killing the process.\n *\n * Pausing and snapshotting do need it, and `ch-remote` — the tool that usually\n * does this — is not in the runner image. Writing the two requests by hand is a\n * smaller change than adding a binary to the image, and keeps the whole\n * lifecycle inside one process that already owns the socket path.\n */\nexport type VmmApiCall = (\n socketPath: string,\n path: string,\n body?: Record<string, unknown>,\n method?: 'PUT' | 'GET',\n) => Promise<void | string>\n\n/** Reconcile an ambiguous command failure against the VMM before releasing the fence. */\nexport async function setVmmState(call: VmmApiCall, socketPath: string, desired: 'Paused' | 'Running'): Promise<void> {\n try {\n await call(socketPath, desired === 'Paused' ? '/api/v1/vm.pause' : '/api/v1/vm.resume')\n } catch (error) {\n try {\n const response = await call(socketPath, '/api/v1/vm.info', undefined, 'GET')\n const info: unknown = typeof response === 'string' ? JSON.parse(response) : null\n if (info && typeof info === 'object' && 'state' in info && info.state === desired) return\n } catch { /* State is unknown: preserve the original failure and the request fence. */ }\n throw error\n }\n}\n\nconst vmmApi: VmmApiCall = async (socketPath, path, body, method = 'PUT') => {\n const payload = body === undefined ? undefined : Buffer.from(JSON.stringify(body), 'utf8')\n return await new Promise<string>((resolve, reject) => {\n const request = httpRequest(\n {\n socketPath,\n path,\n method,\n headers: {\n ...(payload ? { 'content-type': 'application/json', 'content-length': String(payload.length) } : {}),\n },\n },\n (response) => {\n // CH answers 204 with no body on success. Drain regardless: an unread\n // response keeps the socket open and the next call queues behind it.\n const chunks: Buffer[] = []\n response.on('data', (chunk: Buffer) => chunks.push(chunk))\n response.on('end', () => {\n const status = response.statusCode ?? 0\n if (status >= 200 && status < 300) {\n resolve(Buffer.concat(chunks).toString('utf8'))\n return\n }\n const said = Buffer.concat(chunks).toString('utf8').trim().slice(0, 400)\n reject(new DeskError(`${path} returned ${status}${said === '' ? '' : `: ${said}`}`))\n })\n },\n )\n request.on('error', (error) => reject(new DeskError(`${path} failed: ${errorMessage(error)}`)))\n if (payload) request.write(payload)\n request.end()\n })\n}\n\n/**\n * Wait until the VMM answers its control socket.\n *\n * On a restore this is also the only honest signal that the snapshot was read\n * back: Cloud Hypervisor creates the API socket early but does not serve\n * `vm.info` until it holds a VM, so a successful call means the restore\n * completed rather than merely that the process started. A VMM that exits\n * instead — a corrupt snapshot, a config the host can no longer satisfy — stops\n * the wait immediately rather than burning the whole timeout.\n */\nasync function waitForVmm(\n call: VmmApiCall,\n socketPath: string,\n options: { now: () => number; exited: ExitWatcher; timeoutMs: number; retryDelayMs?: number },\n): Promise<void> {\n const deadline = options.now() + options.timeoutMs\n const retryDelayMs = options.retryDelayMs ?? 50\n let lastFailure = 'it never answered'\n for (;;) {\n if (options.exited.done) throw new DeskError(`the VMM exited before it served its control socket`)\n try {\n await call(socketPath, '/api/v1/vm.info', undefined, 'GET')\n return\n } catch (error) {\n lastFailure = errorMessage(error)\n }\n if (options.now() >= deadline) {\n throw new DeskError(`the VMM did not answer ${socketPath} in ${options.timeoutMs}ms: ${lastFailure}`)\n }\n await new Promise((resolve) => setTimeout(resolve, retryDelayMs))\n }\n}\n\n/** The default backend: real Cloud Hypervisor over a real vsock socket. */\nexport const cloudHypervisorBackend: DeskBackend = {\n boot: (plan, hooks) => createCloudHypervisorBackend().boot(plan, hooks),\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@braedonsaunders/appkit-desk",
|
|
3
|
-
"version": "0.11.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Per-agent Debian microVMs on Linux and macOS, with a framed guest-agent protocol, leases, snapshots, and governed networking.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|