@braedonsaunders/appkit-desk 0.10.0 → 0.11.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 +117 -0
- package/backend.d.ts +13 -1
- package/backend.d.ts.map +1 -1
- package/backend.js +23 -9
- package/backend.js.map +1 -1
- package/host.d.ts +33 -5
- package/host.d.ts.map +1 -1
- package/host.js +216 -18
- package/host.js.map +1 -1
- package/index.d.ts +3 -0
- package/index.d.ts.map +1 -1
- package/index.js +3 -0
- package/index.js.map +1 -1
- package/machine.d.ts +4 -2
- package/machine.d.ts.map +1 -1
- package/machine.js +5 -3
- package/machine.js.map +1 -1
- package/package.json +1 -1
- package/plan.d.ts +6 -1
- package/plan.d.ts.map +1 -1
- package/plan.js +7 -3
- package/plan.js.map +1 -1
- package/template-mapping.d.ts +5 -0
- package/template-mapping.d.ts.map +1 -0
- package/template-mapping.js +48 -0
- package/template-mapping.js.map +1 -0
- package/template-plan.d.ts +18 -0
- package/template-plan.d.ts.map +1 -0
- package/template-plan.js +52 -0
- package/template-plan.js.map +1 -0
- package/template-registry.d.ts +37 -0
- package/template-registry.d.ts.map +1 -0
- package/template-registry.js +190 -0
- package/template-registry.js.map +1 -0
- package/template-source.d.ts +15 -0
- package/template-source.d.ts.map +1 -0
- package/template-source.js +95 -0
- package/template-source.js.map +1 -0
package/host.js
CHANGED
|
@@ -20,6 +20,8 @@ import { buildDeskLaunchPlan, cleanDeskId, DEFAULT_KVM_PATH, DEFAULT_VMM_PATH, D
|
|
|
20
20
|
import { cloudHypervisorBackend, } from './backend.js';
|
|
21
21
|
import { readShareManifest } from './machine.js';
|
|
22
22
|
import { parseCapabilities, parseClipboardText, parseExecResult, parseHandoverBegun, parseJobStarted, parseObservation, } from './protocol.js';
|
|
23
|
+
/** Capability marker for the fenced template lifecycle and mapping proof. */
|
|
24
|
+
export const DESK_TEMPLATE_VERSION = 3;
|
|
23
25
|
export const DEFAULT_CAPACITY = 4;
|
|
24
26
|
export const DEFAULT_IDLE_SUSPEND_MS = 5 * 60_000;
|
|
25
27
|
export const DEFAULT_LEASE_MS = 15 * 60_000;
|
|
@@ -62,6 +64,8 @@ export function createDeskHost(options) {
|
|
|
62
64
|
const deviceExists = options.deviceExists ?? existsSync;
|
|
63
65
|
const removeOverlay = options.removeOverlay ?? ((path) => rm(path, { force: true }));
|
|
64
66
|
const parkWithMemory = options.parkWithMemory ?? false;
|
|
67
|
+
if (options.strictMemoryRestore && !parkWithMemory)
|
|
68
|
+
throw new DeskError('Strict memory restore requires parkWithMemory.');
|
|
65
69
|
if (options.memoryRestoreMode && (!parkWithMemory || backend.hypervisor === 'virtualization')) {
|
|
66
70
|
throw new DeskError('Copy-on-write restore requires Cloud Hypervisor and parkWithMemory.');
|
|
67
71
|
}
|
|
@@ -105,6 +109,7 @@ export function createDeskHost(options) {
|
|
|
105
109
|
const queue = [];
|
|
106
110
|
let occupied = 0;
|
|
107
111
|
let nextCid = positiveInteger(options.cidBase ?? DEFAULT_CID_BASE, 'cidBase');
|
|
112
|
+
const cidOwners = new Map();
|
|
108
113
|
let lastStartedAt = null;
|
|
109
114
|
let lastSuspendedAt = null;
|
|
110
115
|
let lastError = null;
|
|
@@ -293,7 +298,26 @@ export function createDeskHost(options) {
|
|
|
293
298
|
function snapshotDirFor(overlayPath, deskId) {
|
|
294
299
|
return join(dirname(overlayPath), `${deskId}.snapshot`);
|
|
295
300
|
}
|
|
296
|
-
|
|
301
|
+
function strictMemory(start) {
|
|
302
|
+
return Boolean(options.strictMemoryRestore || start.strictMemoryRestore || start.template
|
|
303
|
+
|| (start.memoryRestoreMode ?? options.memoryRestoreMode) === 'ondemand');
|
|
304
|
+
}
|
|
305
|
+
async function clearRestoreMarker(snapshotDir) {
|
|
306
|
+
await rm(`${snapshotDir}.restore-pending`, { force: true });
|
|
307
|
+
const parent = await open(dirname(snapshotDir), 'r');
|
|
308
|
+
try {
|
|
309
|
+
await parent.sync();
|
|
310
|
+
}
|
|
311
|
+
finally {
|
|
312
|
+
await parent.close();
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
async function teardown(record, keepMemory = true, hooks) {
|
|
316
|
+
let sidecarFailed = false;
|
|
317
|
+
if (hooks) {
|
|
318
|
+
const supplied = hooks;
|
|
319
|
+
hooks = { ...supplied, onPausedError: error => { sidecarFailed = true; supplied.onPausedError?.(error); } };
|
|
320
|
+
}
|
|
297
321
|
if (record.status === 'suspending' && record.machine) {
|
|
298
322
|
// A previous stop failed. Keep the overlay fenced until a retry confirms
|
|
299
323
|
// the VMM has exited; never boot another VM or delete it underneath one.
|
|
@@ -361,6 +385,13 @@ export function createDeskHost(options) {
|
|
|
361
385
|
// it is flushed exactly as a park is.
|
|
362
386
|
if (parking || !keepMemory)
|
|
363
387
|
await syncGuest(record, machine);
|
|
388
|
+
if (hooks && (!parking || !machine.supportsPausedParkHook)) {
|
|
389
|
+
try {
|
|
390
|
+
hooks.onPausedError?.(new DeskError('Paused park hooks are unavailable'));
|
|
391
|
+
}
|
|
392
|
+
catch { /* preserve memory */ }
|
|
393
|
+
hooks = undefined;
|
|
394
|
+
}
|
|
364
395
|
try {
|
|
365
396
|
if (parking) {
|
|
366
397
|
const snapshotDir = snapshotDirFor(record.startOptions.overlayPath, record.deskId);
|
|
@@ -385,9 +416,60 @@ export function createDeskHost(options) {
|
|
|
385
416
|
await directory.close();
|
|
386
417
|
}
|
|
387
418
|
}
|
|
388
|
-
|
|
419
|
+
if (hooks?.beforePark) {
|
|
420
|
+
try {
|
|
421
|
+
await hooks.beforePark(machine);
|
|
422
|
+
}
|
|
423
|
+
catch (error) {
|
|
424
|
+
try {
|
|
425
|
+
hooks.onPausedError?.(error);
|
|
426
|
+
}
|
|
427
|
+
catch { /* preserve memory */ }
|
|
428
|
+
;
|
|
429
|
+
hooks = undefined;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
await machine.park(snapshotDir, hooks);
|
|
389
433
|
const signature = await (options.cpuSignature ?? hostCpuSignature)();
|
|
390
434
|
await (options.writeSnapshotSignature ?? writeSnapshotSignature)(snapshotDir, signature);
|
|
435
|
+
if (strictMemory(record.startOptions) || (hooks?.onParked && !sidecarFailed)) {
|
|
436
|
+
for (const name of ['config.json', 'state.json', 'memory-ranges', 'appkit-cpu-signature']) {
|
|
437
|
+
const file = await open(join(snapshotDir, name), 'r');
|
|
438
|
+
try {
|
|
439
|
+
await file.sync();
|
|
440
|
+
}
|
|
441
|
+
finally {
|
|
442
|
+
await file.close();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const directory = await open(snapshotDir, 'r');
|
|
446
|
+
try {
|
|
447
|
+
await directory.sync();
|
|
448
|
+
}
|
|
449
|
+
finally {
|
|
450
|
+
await directory.close();
|
|
451
|
+
}
|
|
452
|
+
const parent = await open(dirname(snapshotDir), 'r');
|
|
453
|
+
try {
|
|
454
|
+
await parent.sync();
|
|
455
|
+
}
|
|
456
|
+
finally {
|
|
457
|
+
await parent.close();
|
|
458
|
+
}
|
|
459
|
+
if (strictMemory(record.startOptions))
|
|
460
|
+
await clearRestoreMarker(snapshotDir);
|
|
461
|
+
}
|
|
462
|
+
if (hooks?.onParked && !sidecarFailed) {
|
|
463
|
+
try {
|
|
464
|
+
await hooks.onParked(snapshotDir);
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
try {
|
|
468
|
+
hooks.onPausedError?.(error);
|
|
469
|
+
}
|
|
470
|
+
catch { /* preserve memory */ }
|
|
471
|
+
}
|
|
472
|
+
}
|
|
391
473
|
}
|
|
392
474
|
else {
|
|
393
475
|
await machine.shutdown();
|
|
@@ -404,7 +486,7 @@ export function createDeskHost(options) {
|
|
|
404
486
|
record.machine = machine;
|
|
405
487
|
throw new DeskError(`Desk ${record.deskId} could not stop after teardown failed: ${errorMessage(stopError)}`);
|
|
406
488
|
}
|
|
407
|
-
if (parking) {
|
|
489
|
+
if (parking && !strictMemory(record.startOptions)) {
|
|
408
490
|
await removeSnapshot(snapshotDirFor(record.startOptions.overlayPath, record.deskId)).catch(() => undefined);
|
|
409
491
|
}
|
|
410
492
|
}
|
|
@@ -473,10 +555,33 @@ export function createDeskHost(options) {
|
|
|
473
555
|
// as whatever the overlay copy fails with.
|
|
474
556
|
await admitDisk(startOptions.overlayPath);
|
|
475
557
|
const snapshotDir = snapshotDirFor(startOptions.overlayPath, deskId);
|
|
558
|
+
const strictRestore = strictMemory(startOptions);
|
|
559
|
+
const restoreMode = startOptions.template ? 'copyonwrite' : startOptions.memoryRestoreMode ?? options.memoryRestoreMode;
|
|
560
|
+
if ((strictRestore || restoreMode) && (!parkWithMemory || backend.hypervisor === 'virtualization'))
|
|
561
|
+
throw new DeskError('Strict or copy-on-write restore requires Cloud Hypervisor and parkWithMemory.');
|
|
562
|
+
if (strictRestore && existsSync(`${snapshotDir}.restore-pending`)) {
|
|
563
|
+
throw new DeskError(`Desk ${deskId} has an interrupted memory restore; operator recovery is required.`);
|
|
564
|
+
}
|
|
476
565
|
// The SAME cid for both attempts below. A restored VM's snapshotted config
|
|
477
566
|
// names its vsock and tap, and a cold-boot retry that renumbered them would
|
|
478
567
|
// come up on a different tap than the one the egress rules were put on.
|
|
479
|
-
|
|
568
|
+
let cid;
|
|
569
|
+
if (startOptions.vsockCid !== undefined) {
|
|
570
|
+
cid = startOptions.vsockCid;
|
|
571
|
+
if (!Number.isSafeInteger(cid) || cid < 3 || cid > 0xfffffffe)
|
|
572
|
+
throw new DeskError('Invalid guest vsock CID.');
|
|
573
|
+
const owner = cidOwners.get(cid);
|
|
574
|
+
if (owner !== undefined && owner !== deskId)
|
|
575
|
+
throw new DeskError('Guest vsock CID is already assigned.');
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
while (cidOwners.has(nextCid))
|
|
579
|
+
nextCid++;
|
|
580
|
+
cid = nextCid++;
|
|
581
|
+
if (cid > 0xfffffffe)
|
|
582
|
+
throw new DeskError('Guest vsock CID space exhausted.');
|
|
583
|
+
}
|
|
584
|
+
cidOwners.set(cid, deskId);
|
|
480
585
|
const initramfsPath = startOptions.initramfsPath ?? resolveInitramfsPath();
|
|
481
586
|
let restoreDir = snapshotDir;
|
|
482
587
|
const planFor = (restore, shares = startOptions.shares) => buildDeskLaunchPlan({
|
|
@@ -505,8 +610,8 @@ export function createDeskHost(options) {
|
|
|
505
610
|
...(kernelCmdline === undefined ? {} : { kernelCmdline }),
|
|
506
611
|
launcherIdentity: options.launcherIdentity,
|
|
507
612
|
snapshotDir: restore ? restoreDir : snapshotDir,
|
|
508
|
-
...(restore ? { restore: true, ...(
|
|
509
|
-
}, { pathExists, deviceExists, ...(options.isDirectory ? { isDirectory: options.isDirectory } : {}) });
|
|
613
|
+
...(restore ? { restore: true, ...(restoreMode ? { memoryRestoreMode: restoreMode } : {}) } : {}),
|
|
614
|
+
}, { pathExists: (path) => Boolean(startOptions.template && path === dirname(startOptions.overlayPath)) || pathExists(path), deviceExists, ...(options.isDirectory ? { isDirectory: options.isDirectory } : {}) });
|
|
510
615
|
const restorePlan = async () => {
|
|
511
616
|
const parked = await (options.readSnapshotShares ?? readShareManifest)(restoreDir);
|
|
512
617
|
try {
|
|
@@ -524,14 +629,51 @@ export function createDeskHost(options) {
|
|
|
524
629
|
if (hash.digest('hex') !== image.sha256)
|
|
525
630
|
throw new DeskError(`Read-only attachment digest mismatch: ${image.path}`);
|
|
526
631
|
}
|
|
632
|
+
let initializeTemplate = false;
|
|
633
|
+
if (startOptions.template) {
|
|
634
|
+
if (!strictRestore || restoreMode !== 'copyonwrite' || backend.hypervisor === 'virtualization') {
|
|
635
|
+
throw new DeskError('Template forks require strict Cloud Hypervisor copy-on-write restore.');
|
|
636
|
+
}
|
|
637
|
+
const childDir = dirname(startOptions.overlayPath);
|
|
638
|
+
const prepared = join(childDir, '.prepared');
|
|
639
|
+
const initialized = join(childDir, '.initialized');
|
|
640
|
+
if (existsSync(initialized) && !existsSync(snapshotDir))
|
|
641
|
+
throw new DeskError('Template descendant has no saved memory; recovery is required.');
|
|
642
|
+
if (existsSync(initialized))
|
|
643
|
+
await startOptions.template.verifyResume(planFor(true));
|
|
644
|
+
if (!existsSync(initialized)) {
|
|
645
|
+
await startOptions.template.holdFence();
|
|
646
|
+
if (!existsSync(prepared)) {
|
|
647
|
+
if (existsSync(startOptions.overlayPath) || existsSync(snapshotDir))
|
|
648
|
+
throw new DeskError('Incomplete template fork needs recovery.');
|
|
649
|
+
await startOptions.template.prepare(planFor(false));
|
|
650
|
+
}
|
|
651
|
+
if (await readFile(prepared, 'utf8') !== '1\n' || !existsSync(startOptions.overlayPath) || !existsSync(snapshotDir))
|
|
652
|
+
throw new DeskError('Template fork was not prepared completely.');
|
|
653
|
+
const planned = planFor(true);
|
|
654
|
+
if (planned.hypervisor !== 'cloud-hypervisor')
|
|
655
|
+
throw new DeskError('Template fork requires Cloud Hypervisor.');
|
|
656
|
+
const savedConfig = JSON.parse(await readFile(join(snapshotDir, 'config.json'), 'utf8'));
|
|
657
|
+
if (savedConfig.disks?.[0]?.path !== startOptions.overlayPath || savedConfig.net?.[0]?.tap !== planned.tap.device
|
|
658
|
+
|| savedConfig.net?.[0]?.mac?.toLowerCase() !== planned.tap.mac.toLowerCase()
|
|
659
|
+
|| savedConfig.vsock?.cid !== planned.vsock.cid || savedConfig.vsock.socket !== planned.vsock.socketPath
|
|
660
|
+
|| savedConfig.memory?.size !== planned.memoryMb * 1024 * 1024 || savedConfig.cpus?.boot_vcpus !== planned.vcpus) {
|
|
661
|
+
throw new DeskError('Prepared template device or resource identity does not match its launch plan.');
|
|
662
|
+
}
|
|
663
|
+
await startOptions.template.verifyPrepared(planned);
|
|
664
|
+
initializeTemplate = true;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
527
667
|
let resuming = parkWithMemory && pathExists(snapshotDir);
|
|
668
|
+
if (initializeTemplate && !resuming)
|
|
669
|
+
throw new DeskError('A template fork requires its prepared snapshot.');
|
|
528
670
|
// Shared guest memory, required by virtio-fs, makes Cloud Hypervisor fall
|
|
529
671
|
// back to eager restore. Reject before retiring the snapshot or cold booting.
|
|
530
|
-
if (
|
|
672
|
+
if (restoreMode) {
|
|
531
673
|
if (startOptions.shares?.length)
|
|
532
|
-
throw new DeskError('Copy-on-write restore cannot be combined with virtio-fs shares.');
|
|
674
|
+
throw new DeskError('Copy-on-write or on-demand restore cannot be combined with virtio-fs shares.');
|
|
533
675
|
if (resuming && (await (options.readSnapshotShares ?? readShareManifest)(snapshotDir)).length) {
|
|
534
|
-
throw new DeskError('The parked snapshot has virtio-fs shares and cannot use copy-on-write restore.');
|
|
676
|
+
throw new DeskError('The parked snapshot has virtio-fs shares and cannot use copy-on-write or on-demand restore.');
|
|
535
677
|
}
|
|
536
678
|
}
|
|
537
679
|
if (resuming) {
|
|
@@ -548,31 +690,84 @@ export function createDeskHost(options) {
|
|
|
548
690
|
throw new DeskError('Snapshot read-only attachment identity does not match; restore requires the original images.');
|
|
549
691
|
}
|
|
550
692
|
const saved = await (options.readSnapshotSignature ?? readSnapshotSignature)(snapshotDir);
|
|
693
|
+
if (strictRestore && saved === null)
|
|
694
|
+
throw new DeskError(`Desk ${deskId} snapshot has no CPU identity.`);
|
|
551
695
|
if (saved !== null && saved !== await (options.cpuSignature ?? hostCpuSignature)()) {
|
|
696
|
+
if (strictRestore)
|
|
697
|
+
throw new DeskError(`Desk ${deskId} snapshot CPU identity does not match.`);
|
|
552
698
|
await removeSnapshot(snapshotDir);
|
|
553
699
|
resuming = false;
|
|
554
700
|
recordCpuFallback(deskId, 'signature_mismatch');
|
|
555
701
|
}
|
|
556
702
|
}
|
|
703
|
+
if (strictRestore) {
|
|
704
|
+
const marker = await open(`${snapshotDir}.restore-pending`, 'wx', 0o600);
|
|
705
|
+
try {
|
|
706
|
+
await marker.writeFile('restore in progress\n');
|
|
707
|
+
await marker.sync();
|
|
708
|
+
}
|
|
709
|
+
finally {
|
|
710
|
+
await marker.close();
|
|
711
|
+
}
|
|
712
|
+
const parent = await open(dirname(snapshotDir), 'r');
|
|
713
|
+
try {
|
|
714
|
+
await parent.sync();
|
|
715
|
+
}
|
|
716
|
+
finally {
|
|
717
|
+
await parent.close();
|
|
718
|
+
}
|
|
719
|
+
}
|
|
557
720
|
// Retire before starting the VMM: after a crash the old name can never be
|
|
558
721
|
// replayed against a disk the guest may have changed. The retired file is
|
|
559
722
|
// private backing memory, not a restorable snapshot.
|
|
560
|
-
if (resuming &&
|
|
723
|
+
if (resuming && restoreMode) {
|
|
561
724
|
restoreDir = `${snapshotDir}.source-${randomUUID()}`;
|
|
562
725
|
await retireSnapshot(snapshotDir, restoreDir);
|
|
563
726
|
}
|
|
564
727
|
let machine;
|
|
565
728
|
try {
|
|
729
|
+
const launchPlan = resuming ? await restorePlan() : planFor(false);
|
|
730
|
+
if (resuming && (startOptions.template || (startOptions.strictMemoryRestore && restoreMode === 'copyonwrite')))
|
|
731
|
+
launchPlan.snapshot.requirePrivateMemoryMapping = true;
|
|
566
732
|
machine = resuming
|
|
567
|
-
? await backend.boot(
|
|
733
|
+
? await backend.boot(launchPlan, {
|
|
568
734
|
// Eager restore removes the parked snapshot before resume. A CoW
|
|
569
735
|
// source was already retired durably before the VMM started.
|
|
570
|
-
onRestored:
|
|
736
|
+
onRestored: restoreMode ? async () => { } : () => removeSnapshot(snapshotDir),
|
|
571
737
|
})
|
|
572
|
-
: await backend.boot(
|
|
738
|
+
: await backend.boot(launchPlan);
|
|
739
|
+
if (initializeTemplate && startOptions.template) {
|
|
740
|
+
try {
|
|
741
|
+
await startOptions.template.initialize(machine, launchPlan);
|
|
742
|
+
const initialized = await open(join(dirname(startOptions.overlayPath), '.initialized'), 'wx', 0o600);
|
|
743
|
+
try {
|
|
744
|
+
await initialized.writeFile('1\n');
|
|
745
|
+
await initialized.sync();
|
|
746
|
+
}
|
|
747
|
+
finally {
|
|
748
|
+
await initialized.close();
|
|
749
|
+
}
|
|
750
|
+
const directory = await open(dirname(startOptions.overlayPath), 'r');
|
|
751
|
+
try {
|
|
752
|
+
await directory.sync();
|
|
753
|
+
}
|
|
754
|
+
finally {
|
|
755
|
+
await directory.close();
|
|
756
|
+
}
|
|
757
|
+
await startOptions.template.releaseFence();
|
|
758
|
+
}
|
|
759
|
+
catch (error) {
|
|
760
|
+
await machine.shutdown();
|
|
761
|
+
throw error;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
573
764
|
}
|
|
574
765
|
catch (error) {
|
|
575
766
|
if (resuming) {
|
|
767
|
+
if (strictRestore) {
|
|
768
|
+
lastError = `Desk ${deskId} memory restore failed: ${errorMessage(error)}`;
|
|
769
|
+
throw new DeskError(lastError);
|
|
770
|
+
}
|
|
576
771
|
// A snapshot that will not come back is worse than no snapshot: it makes
|
|
577
772
|
// the desk unbootable for as long as it sits there. Drop it and cold
|
|
578
773
|
// boot. Safe by construction — the guest never ran, so the disk is
|
|
@@ -594,7 +789,7 @@ export function createDeskHost(options) {
|
|
|
594
789
|
: new DeskError(`Desk ${deskId} failed to boot: ${errorMessage(error)}`);
|
|
595
790
|
}
|
|
596
791
|
}
|
|
597
|
-
if (resuming &&
|
|
792
|
+
if (resuming && restoreMode) {
|
|
598
793
|
// The mapping remains file-backed until the VMM has exited. Never remove
|
|
599
794
|
// it after a failed stop; leaking it is safer than invalidating a live VM.
|
|
600
795
|
const restored = machine;
|
|
@@ -610,7 +805,7 @@ export function createDeskHost(options) {
|
|
|
610
805
|
};
|
|
611
806
|
restored.shutdown = async () => { await stop(); await releaseSource(); };
|
|
612
807
|
if (park)
|
|
613
|
-
restored.park = async (dir) => { await park(dir); await releaseSource(); };
|
|
808
|
+
restored.park = async (dir, hooks) => { await park(dir, hooks); await releaseSource(); };
|
|
614
809
|
}
|
|
615
810
|
let record = records.get(deskId);
|
|
616
811
|
if (!record) {
|
|
@@ -1331,13 +1526,13 @@ export function createDeskHost(options) {
|
|
|
1331
1526
|
return admit(() => boot(record.deskId, record.startOptions));
|
|
1332
1527
|
});
|
|
1333
1528
|
}
|
|
1334
|
-
async function suspend(deskId) {
|
|
1529
|
+
async function suspend(deskId, hooks) {
|
|
1335
1530
|
const id = cleanDeskId(deskId);
|
|
1336
1531
|
await serializeLifecycle(id, async () => {
|
|
1337
1532
|
const record = records.get(id);
|
|
1338
1533
|
if (!record)
|
|
1339
1534
|
return;
|
|
1340
|
-
await teardown(record);
|
|
1535
|
+
await teardown(record, true, hooks);
|
|
1341
1536
|
});
|
|
1342
1537
|
}
|
|
1343
1538
|
async function shutdown(deskId) {
|
|
@@ -1350,7 +1545,10 @@ export function createDeskHost(options) {
|
|
|
1350
1545
|
// A desk parked earlier still has its memory image on disk; dropping it is
|
|
1351
1546
|
// what makes the next boot cold. The overlay is untouched — only `destroy`
|
|
1352
1547
|
// may remove it.
|
|
1353
|
-
|
|
1548
|
+
const snapshot = snapshotDirFor(record.startOptions.overlayPath, record.deskId);
|
|
1549
|
+
await removeSnapshot(snapshot);
|
|
1550
|
+
if (strictMemory(record.startOptions))
|
|
1551
|
+
await clearRestoreMarker(snapshot);
|
|
1354
1552
|
});
|
|
1355
1553
|
}
|
|
1356
1554
|
/**
|