@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/README.md +28 -1
- package/backend.d.ts +43 -1
- package/backend.d.ts.map +1 -1
- package/backend.js +110 -1
- package/backend.js.map +1 -1
- package/events.d.ts +3 -0
- package/events.d.ts.map +1 -1
- package/events.js.map +1 -1
- package/host.d.ts +53 -0
- package/host.d.ts.map +1 -1
- package/host.js +205 -12
- package/host.js.map +1 -1
- package/index.d.ts +1 -0
- package/index.d.ts.map +1 -1
- package/index.js +1 -0
- package/index.js.map +1 -1
- package/network.d.ts +37 -0
- package/network.d.ts.map +1 -0
- package/network.js +173 -0
- package/network.js.map +1 -0
- package/package.json +1 -1
- package/plan.d.ts +21 -0
- package/plan.d.ts.map +1 -1
- package/plan.js +58 -17
- package/plan.js.map +1 -1
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":"AAgBA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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 =
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|