@braedonsaunders/appkit-desk 0.5.1 → 0.7.0

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/host.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Buffer } from 'node:buffer';
2
+ import type { Duplex } from 'node:stream';
2
3
  import { type DeskLauncherIdentity } from './plan.js';
3
4
  import { type DeskBackend } from './backend.js';
4
5
  import type { A11yNode, DeskFrameFormat, DeskHandoverScope, DeskPoint, DeskPointerButton, DeskPorts, DeskVideoChunkKind, WindowInfo } from './events.js';
@@ -34,6 +35,8 @@ export interface DeskHostOptions {
34
35
  launcherIdentity?: DeskLauncherIdentity;
35
36
  backend?: DeskBackend;
36
37
  ports: DeskPorts;
38
+ /** Override TCP dialing for tests or a host-specific transport. */
39
+ connectGuest?: (address: string, port: number) => Promise<Duplex>;
37
40
  /** Hard cap on concurrently resident desks; starts beyond it queue FIFO. */
38
41
  capacity?: number;
39
42
  idleSuspendMs?: number;
@@ -48,6 +51,35 @@ export interface DeskHostOptions {
48
51
  * testable without a filesystem; defaults to `rm(path, { force: true })`.
49
52
  */
50
53
  removeOverlay?: (path: string) => Promise<void>;
54
+ /**
55
+ * Park a desk by snapshotting its memory instead of shutting it down, so
56
+ * resuming continues the guest's process tree rather than cold booting it.
57
+ *
58
+ * Off by default, because turning it on is a real operational change and not
59
+ * only a behavioural one: each parked desk holds a snapshot roughly the size
60
+ * of its guest RAM on the overlay volume, and the backend must offer `park`.
61
+ * A deployment opts in once it has sized for that.
62
+ *
63
+ * What it buys is the difference between a desk that is "parked" and one that
64
+ * was switched off. Shutdown parking means nothing inside the guest survives
65
+ * between runs: an agent that installs a daemon and believes it is watching
66
+ * something continuously is wrong, and the cold boot on every scheduled
67
+ * occurrence is indistinguishable, from inside the guest, from a failing host.
68
+ */
69
+ parkWithMemory?: boolean;
70
+ /** Identifies the host CPU for conservative snapshot portability checks. */
71
+ cpuSignature?: () => Promise<string>;
72
+ readSnapshotSignature?: (directory: string) => Promise<string | null>;
73
+ writeSnapshotSignature?: (directory: string, signature: string) => Promise<void>;
74
+ /** Create a snapshot directory; defaults to `mkdir(path, { recursive: true })`. */
75
+ makeSnapshotDir?: (path: string) => Promise<void>;
76
+ /**
77
+ * Destroy a snapshot; defaults to `rm(path, { recursive: true, force: true })`.
78
+ *
79
+ * Called at the one moment it is safe to — a restored guest still paused — so
80
+ * a failure here is not cosmetic. It is the reason the restore is abandoned.
81
+ */
82
+ removeSnapshot?: (path: string) => Promise<void>;
51
83
  /**
52
84
  * Bytes that must remain free on the overlay filesystem AFTER admitting a
53
85
  * desk. A desk whose overlay cannot be grown fails deep inside the guest —
@@ -87,6 +119,7 @@ export interface DeskStartOptions {
87
119
  network?: {
88
120
  tapDevice?: string;
89
121
  macAddress?: string;
122
+ guestAddress?: string;
90
123
  };
91
124
  }
92
125
  export interface DeskExecOptions {
@@ -272,6 +305,8 @@ export interface DeskScreenService {
272
305
  export interface DeskHandle {
273
306
  readonly deskId: string;
274
307
  renewLease(ms: number): void;
308
+ /** Open a host-to-guest TCP stream; the caller owns and closes it. */
309
+ connect(port: number): Promise<Duplex>;
275
310
  exec(options: DeskExecOptions & {
276
311
  keepAlive: true;
277
312
  }): Promise<DeskJob>;
@@ -300,11 +335,25 @@ export interface DeskHostStats {
300
335
  /** Which desk reconnected last: enough to go and look at the right one. */
301
336
  lastReconnectDeskId: string | null;
302
337
  lastError: string | null;
338
+ snapshotCpuFallbacks: number;
339
+ lastSnapshotCpuFallback: {
340
+ deskId: string;
341
+ at: string;
342
+ reason: 'signature_mismatch' | 'incompatible_cpuid';
343
+ } | null;
303
344
  }
304
345
  export interface DeskHost {
305
346
  start(options: DeskStartOptions): Promise<DeskHandle>;
306
347
  resume(deskId: string): Promise<DeskHandle>;
307
348
  suspend(deskId: string): Promise<void>;
349
+ /**
350
+ * Stop a desk WITHOUT its memory, keeping its overlay: the next `start` or
351
+ * `resume` cold boots from disk. This is a restart, not a destroy — the disk
352
+ * is the desk and stays exactly where it is. A resident desk is synced and
353
+ * shut down rather than parked; a suspended desk's parked memory snapshot is
354
+ * discarded. Unknown desks are a no-op.
355
+ */
356
+ shutdown(deskId: string): Promise<void>;
308
357
  destroy(deskId: string): Promise<void>;
309
358
  stats(): DeskHostStats;
310
359
  /**
@@ -393,4 +442,8 @@ export interface DeskHostVerification {
393
442
  * the backend, asks it two questions, and discards it.
394
443
  */
395
444
  export declare function verifyDeskHost(options: VerifyDeskHostOptions): Promise<DeskHostVerification>;
445
+ /** Cloud Hypervisor's specific restore error; do not classify generic CPUID mentions. */
446
+ export declare function isIncompatibleCpuSnapshotError(error: unknown): boolean;
447
+ /** Hash the host's CPU identity and advertised feature set, excluding per-core counters. */
448
+ export declare function hostCpuSignature(): Promise<string>;
396
449
  //# sourceMappingURL=host.d.ts.map
package/host.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../src/host.ts"],"names":[],"mappings":"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"}
package/host.js CHANGED
@@ -10,11 +10,12 @@
10
10
  */
11
11
  import { existsSync } from 'node:fs';
12
12
  import { execFile } from 'node:child_process';
13
- import { rm, statfs, writeFile } from 'node:fs/promises';
13
+ import { mkdir, readFile, rm, statfs, writeFile } from 'node:fs/promises';
14
14
  import { tmpdir } from 'node:os';
15
15
  import { dirname, join } from 'node:path';
16
- import { randomUUID } from 'node:crypto';
16
+ import { createHash, randomUUID } from 'node:crypto';
17
17
  import { Buffer } from 'node:buffer';
18
+ import { connect as tcpConnect, isIP } from 'node:net';
18
19
  import { buildDeskLaunchPlan, cleanDeskId, DEFAULT_KVM_PATH, DEFAULT_VMM_PATH, DeskError, } from './plan.js';
19
20
  import { cloudHypervisorBackend, } from './backend.js';
20
21
  import { parseCapabilities, parseClipboardText, parseExecResult, parseHandoverBegun, parseJobStarted, parseObservation, } from './protocol.js';
@@ -59,6 +60,9 @@ export function createDeskHost(options) {
59
60
  const pathExists = options.pathExists ?? existsSync;
60
61
  const deviceExists = options.deviceExists ?? existsSync;
61
62
  const removeOverlay = options.removeOverlay ?? ((path) => rm(path, { force: true }));
63
+ const parkWithMemory = options.parkWithMemory ?? false;
64
+ const makeSnapshotDir = options.makeSnapshotDir ?? ((path) => mkdir(path, { recursive: true }).then(() => undefined));
65
+ const removeSnapshot = options.removeSnapshot ?? ((path) => rm(path, { recursive: true, force: true }));
62
66
  const freeBytes = options.freeBytes ?? defaultFreeBytes;
63
67
  const minFreeBytes = nonNegativeInteger(options.minFreeBytes ?? 0, 'minFreeBytes');
64
68
  const trimOnSuspend = options.trimOnSuspend ?? true;
@@ -90,6 +94,8 @@ export function createDeskHost(options) {
90
94
  let lastStartedAt = null;
91
95
  let lastSuspendedAt = null;
92
96
  let lastError = null;
97
+ let snapshotCpuFallbacks = 0;
98
+ let lastSnapshotCpuFallback = null;
93
99
  let reconnects = 0;
94
100
  let lastReconnectAt = null;
95
101
  let lastReconnectDeskId = null;
@@ -251,13 +257,38 @@ export function createDeskHost(options) {
251
257
  lastError = `Trim before suspend failed for ${record.deskId}: ${errorMessage(error)}`;
252
258
  }
253
259
  }
254
- async function teardown(record) {
260
+ /**
261
+ * Flush the guest's filesystem, immediately before it stops.
262
+ *
263
+ * Only needed when parking keeps memory. A snapshot always matches the disk it
264
+ * was taken with, so the pair is consistent either way — but the disk ON ITS
265
+ * OWN is not, and that is what remains every time a snapshot has to be
266
+ * discarded. Without this, discarding one leaves a filesystem mid-transaction
267
+ * and the next cold boot recovers a journal and loses the last writes. Best
268
+ * effort and bounded: a guest too wedged to sync must still be parked.
269
+ */
270
+ async function syncGuest(record, machine) {
271
+ try {
272
+ await machine.request({ op: 'exec', command: 'sync', args: [], timeoutMs: trimTimeoutMs });
273
+ }
274
+ catch (error) {
275
+ lastError = `Sync before park failed for ${record.deskId}: ${errorMessage(error)}`;
276
+ }
277
+ }
278
+ /** Where this desk's memory snapshot lives; must agree with the launch plan. */
279
+ function snapshotDirFor(overlayPath, deskId) {
280
+ return join(dirname(overlayPath), `${deskId}.snapshot`);
281
+ }
282
+ async function teardown(record, keepMemory = true) {
255
283
  if (record.status !== 'resident')
256
284
  return false;
257
285
  await endHandover(record, 'revoked');
258
286
  for (const stop of [...record.streamStops])
259
287
  stop();
260
288
  record.streamStops.clear();
289
+ for (const stop of record.relayStops)
290
+ stop();
291
+ record.relayStops.clear();
261
292
  if (record.screen?.running) {
262
293
  record.screen.running = false;
263
294
  emit(record, { kind: 'screen_close' });
@@ -286,11 +317,38 @@ export function createDeskHost(options) {
286
317
  record.handle = null;
287
318
  if (machine) {
288
319
  await trimGuest(record, machine);
320
+ // Flush the guest's filesystem before anything stops.
321
+ //
322
+ // A paused guest still holds dirty pages that exist only in its memory, so
323
+ // a snapshot is consistent with the disk but the DISK ALONE is not — and
324
+ // the disk alone is exactly what is left whenever a snapshot is discarded,
325
+ // which happens on every failed restore. Syncing first makes discarding a
326
+ // snapshot always safe instead of usually safe.
327
+ const parking = keepMemory && parkWithMemory && typeof machine.park === 'function';
328
+ // A shutdown that throws memory away leaves the disk as the only state, so
329
+ // it is flushed exactly as a park is.
330
+ if (parking || !keepMemory)
331
+ await syncGuest(record, machine);
289
332
  try {
290
- await machine.shutdown();
333
+ if (parking) {
334
+ const snapshotDir = snapshotDirFor(record.startOptions.overlayPath, record.deskId);
335
+ await makeSnapshotDir(snapshotDir);
336
+ await machine.park(snapshotDir);
337
+ const signature = await (options.cpuSignature ?? hostCpuSignature)();
338
+ await (options.writeSnapshotSignature ?? writeSnapshotSignature)(snapshotDir, signature);
339
+ }
340
+ else {
341
+ await machine.shutdown();
342
+ }
291
343
  }
292
344
  catch (error) {
345
+ // `park` stops the VMM on its own way out, so the desk is down either
346
+ // way; what is lost is the memory, not the desk. Leave nothing behind
347
+ // that a later boot would try to restore from.
293
348
  lastError = errorMessage(error);
349
+ if (parking) {
350
+ await removeSnapshot(snapshotDirFor(record.startOptions.overlayPath, record.deskId)).catch(() => undefined);
351
+ }
294
352
  }
295
353
  }
296
354
  lastSuspendedAt = iso(now());
@@ -356,7 +414,12 @@ export function createDeskHost(options) {
356
414
  // Before the plan, so a full disk is reported as a full disk rather than
357
415
  // as whatever the overlay copy fails with.
358
416
  await admitDisk(startOptions.overlayPath);
359
- const plan = buildDeskLaunchPlan({
417
+ const snapshotDir = snapshotDirFor(startOptions.overlayPath, deskId);
418
+ // The SAME cid for both attempts below. A restored VM's snapshotted config
419
+ // names its vsock and tap, and a cold-boot retry that renumbered them would
420
+ // come up on a different tap than the one the egress rules were put on.
421
+ const cid = nextCid++;
422
+ const planFor = (restore) => buildDeskLaunchPlan({
360
423
  deskId,
361
424
  vmmPath: options.vmmPath,
362
425
  kvmPath: options.kvmPath,
@@ -366,7 +429,7 @@ export function createDeskHost(options) {
366
429
  overlayPath: startOptions.overlayPath,
367
430
  memoryMb: startOptions.memoryMb,
368
431
  vcpus: startOptions.vcpus,
369
- vsockCid: nextCid++,
432
+ vsockCid: cid,
370
433
  runtimeDir: options.runtimeDir,
371
434
  tapDevice: startOptions.network?.tapDevice,
372
435
  macAddress: startOptions.network?.macAddress,
@@ -374,16 +437,51 @@ export function createDeskHost(options) {
374
437
  // back to DEFAULT_KERNEL_CMDLINE.
375
438
  ...(kernelCmdline === undefined ? {} : { kernelCmdline }),
376
439
  launcherIdentity: options.launcherIdentity,
440
+ snapshotDir,
441
+ ...(restore ? { restore: true } : {}),
377
442
  }, { pathExists, deviceExists });
443
+ let resuming = parkWithMemory && pathExists(snapshotDir);
444
+ if (resuming) {
445
+ const saved = await (options.readSnapshotSignature ?? readSnapshotSignature)(snapshotDir);
446
+ if (saved !== null && saved !== await (options.cpuSignature ?? hostCpuSignature)()) {
447
+ await removeSnapshot(snapshotDir);
448
+ resuming = false;
449
+ recordCpuFallback(deskId, 'signature_mismatch');
450
+ }
451
+ }
378
452
  let machine;
379
453
  try {
380
- machine = await backend.boot(plan);
454
+ machine = resuming
455
+ ? await backend.boot(planFor(true), {
456
+ // The guest is restored and PAUSED here. Destroying the snapshot in
457
+ // this window is what stops a memory image ever outliving the disk
458
+ // state it belongs to. If it fails the restore is abandoned rather
459
+ // than resumed, because resuming would leave both alive at once.
460
+ onRestored: () => removeSnapshot(snapshotDir),
461
+ })
462
+ : await backend.boot(planFor(false));
381
463
  }
382
464
  catch (error) {
383
- lastError = errorMessage(error);
384
- throw error instanceof DeskError
385
- ? error
386
- : new DeskError(`Desk ${deskId} failed to boot: ${errorMessage(error)}`);
465
+ if (resuming) {
466
+ // A snapshot that will not come back is worse than no snapshot: it makes
467
+ // the desk unbootable for as long as it sits there. Drop it and cold
468
+ // boot. Safe by construction — the guest never ran, so the disk is
469
+ // exactly as the pre-park sync left it.
470
+ const why = errorMessage(error);
471
+ await removeSnapshot(snapshotDir);
472
+ if (isIncompatibleCpuSnapshotError(error))
473
+ recordCpuFallback(deskId, 'incompatible_cpuid');
474
+ else
475
+ lastError = `Desk ${deskId} could not resume from its snapshot and was cold booted: ${why}`;
476
+ // Falls through to the shared record path below with a cold machine.
477
+ machine = await backend.boot(planFor(false));
478
+ }
479
+ else {
480
+ lastError = errorMessage(error);
481
+ throw error instanceof DeskError
482
+ ? error
483
+ : new DeskError(`Desk ${deskId} failed to boot: ${errorMessage(error)}`);
484
+ }
387
485
  }
388
486
  let record = records.get(deskId);
389
487
  if (!record) {
@@ -402,6 +500,7 @@ export function createDeskHost(options) {
402
500
  handle: null,
403
501
  timer: null,
404
502
  streamStops: new Set(),
503
+ relayStops: new Set(),
405
504
  };
406
505
  records.set(deskId, record);
407
506
  }
@@ -413,6 +512,7 @@ export function createDeskHost(options) {
413
512
  record.jobs = new Map();
414
513
  record.screen = null;
415
514
  record.streamStops = new Set();
515
+ record.relayStops = new Set();
416
516
  }
417
517
  const current = record;
418
518
  const epoch = current.epoch;
@@ -658,6 +758,32 @@ export function createDeskHost(options) {
658
758
  touch(record);
659
759
  armTimer(record);
660
760
  },
761
+ async connect(port) {
762
+ requireMachine(record, epoch);
763
+ if (!Number.isInteger(port) || port < 1 || port > 65535)
764
+ throw new DeskError('Guest TCP port must be between 1 and 65535.');
765
+ const address = record.startOptions.network?.guestAddress;
766
+ if (!address || isIP(address) !== 4)
767
+ throw new DeskError('A guest IPv4 address is required to connect to a desk port.');
768
+ touch(record);
769
+ try {
770
+ const stream = await (options.connectGuest ?? connectGuestTcp)(address, port);
771
+ try {
772
+ requireMachine(record, epoch);
773
+ }
774
+ catch (error) {
775
+ stream.destroy();
776
+ throw error;
777
+ }
778
+ const stop = () => stream.destroy();
779
+ record.relayStops.add(stop);
780
+ stream.once('close', () => record.relayStops.delete(stop));
781
+ return stream;
782
+ }
783
+ catch (error) {
784
+ throw new DeskError(`Cannot connect to guest ${address}:${port}: ${errorMessage(error)}`);
785
+ }
786
+ },
661
787
  exec: execImplementation,
662
788
  jobs() {
663
789
  return [...record.jobs.values()]
@@ -1044,6 +1170,19 @@ export function createDeskHost(options) {
1044
1170
  await teardown(record);
1045
1171
  });
1046
1172
  }
1173
+ async function shutdown(deskId) {
1174
+ const id = cleanDeskId(deskId);
1175
+ await serializeLifecycle(id, async () => {
1176
+ const record = records.get(id);
1177
+ if (!record)
1178
+ return;
1179
+ await teardown(record, false);
1180
+ // A desk parked earlier still has its memory image on disk; dropping it is
1181
+ // what makes the next boot cold. The overlay is untouched — only `destroy`
1182
+ // may remove it.
1183
+ await removeSnapshot(snapshotDirFor(record.startOptions.overlayPath, record.deskId));
1184
+ });
1185
+ }
1047
1186
  /**
1048
1187
  * Delete a desk's overlay. The ONLY place in this module that removes a
1049
1188
  * desk's disk, and it is reachable from `destroy` alone — see the contract
@@ -1061,6 +1200,19 @@ export function createDeskHost(options) {
1061
1200
  catch (error) {
1062
1201
  lastError = `Overlay was not reclaimed for ${record.deskId}: ${errorMessage(error)}`;
1063
1202
  }
1203
+ // The snapshot goes with the disk, always — not only when parking is on.
1204
+ //
1205
+ // A snapshot outliving its overlay is the worst state this feature can
1206
+ // produce: the next desk created under the same id gets a FRESH overlay and
1207
+ // would restore a previous desk's memory on top of it. That is not stale
1208
+ // state, it is one machine's RAM over another machine's disk. Teardown just
1209
+ // ran, so nothing holds it open.
1210
+ try {
1211
+ await removeSnapshot(snapshotDirFor(record.startOptions.overlayPath, record.deskId));
1212
+ }
1213
+ catch (error) {
1214
+ lastError = `Snapshot was not reclaimed for ${record.deskId}: ${errorMessage(error)}`;
1215
+ }
1064
1216
  }
1065
1217
  async function destroy(deskId) {
1066
1218
  const id = cleanDeskId(deskId);
@@ -1101,6 +1253,11 @@ export function createDeskHost(options) {
1101
1253
  }
1102
1254
  return suspendedCount;
1103
1255
  }
1256
+ function recordCpuFallback(deskId, reason) {
1257
+ snapshotCpuFallbacks += 1;
1258
+ lastSnapshotCpuFallback = { deskId, reason, at: iso(now()) };
1259
+ ports.onEvent({ kind: 'snapshot_cpu_fallback', deskId, reason, at: iso(now()) });
1260
+ }
1104
1261
  function stats() {
1105
1262
  let suspended = 0;
1106
1263
  for (const record of records.values()) {
@@ -1118,9 +1275,11 @@ export function createDeskHost(options) {
1118
1275
  lastReconnectAt,
1119
1276
  lastReconnectDeskId,
1120
1277
  lastError,
1278
+ snapshotCpuFallbacks,
1279
+ lastSnapshotCpuFallback,
1121
1280
  };
1122
1281
  }
1123
- return { start, resume, suspend, destroy, stats, sweep };
1282
+ return { start, resume, suspend, shutdown, destroy, stats, sweep };
1124
1283
  }
1125
1284
  /**
1126
1285
  * Whether the overlay directory's filesystem can make reflink clones.
@@ -1312,4 +1471,38 @@ async function defaultFreeBytes(path) {
1312
1471
  function errorMessage(error) {
1313
1472
  return error instanceof Error ? error.message : String(error);
1314
1473
  }
1474
+ async function connectGuestTcp(address, port) {
1475
+ return new Promise((resolve, reject) => {
1476
+ const socket = tcpConnect({ host: address, port });
1477
+ socket.setTimeout(10_000, () => socket.destroy(new Error('Guest TCP connection timed out.')));
1478
+ socket.once('error', reject);
1479
+ socket.once('connect', () => { socket.setTimeout(0); socket.removeListener('error', reject); resolve(socket); });
1480
+ });
1481
+ }
1482
+ /** Cloud Hypervisor's specific restore error; do not classify generic CPUID mentions. */
1483
+ export function isIncompatibleCpuSnapshotError(error) {
1484
+ return /Detected incompatible CPUID entry/.test(errorMessage(error));
1485
+ }
1486
+ /** Hash the host's CPU identity and advertised feature set, excluding per-core counters. */
1487
+ export async function hostCpuSignature() {
1488
+ const cpuinfo = await readFile('/proc/cpuinfo', 'utf8');
1489
+ const first = cpuinfo.split(/\n\s*\n/)[0] ?? '';
1490
+ const identity = first.split('\n').filter((line) => /^(vendor_id|cpu family|model\s|model name|stepping|flags|microcode)\s*:/.test(line)).join('\n');
1491
+ if (!identity)
1492
+ throw new DeskError('Cannot determine host CPU signature.');
1493
+ return createHash('sha256').update(identity).digest('hex');
1494
+ }
1495
+ async function readSnapshotSignature(directory) {
1496
+ try {
1497
+ return (await readFile(join(directory, 'appkit-cpu-signature'), 'utf8')).trim();
1498
+ }
1499
+ catch (error) {
1500
+ if (error.code === 'ENOENT')
1501
+ return null;
1502
+ throw error;
1503
+ }
1504
+ }
1505
+ async function writeSnapshotSignature(directory, signature) {
1506
+ await writeFile(join(directory, 'appkit-cpu-signature'), signature, { mode: 0o600 });
1507
+ }
1315
1508
  //# sourceMappingURL=host.js.map