@braedonsaunders/appkit-desk 0.6.0 → 0.8.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 +83 -1
- package/backend.d.ts +28 -7
- package/backend.d.ts.map +1 -1
- package/backend.js +18 -521
- 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 +32 -1
- package/host.d.ts.map +1 -1
- package/host.js +141 -23
- 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 +145 -0
- package/machine.d.ts.map +1 -0
- package/machine.js +518 -0
- package/machine.js.map +1 -0
- 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 +2 -2
- package/plan.d.ts +129 -31
- package/plan.d.ts.map +1 -1
- package/plan.js +165 -9
- package/plan.js.map +1 -1
- package/port-relay.d.ts +13 -0
- package/port-relay.d.ts.map +1 -0
- package/port-relay.js +56 -0
- package/port-relay.js.map +1 -0
- package/virtualization.d.ts +16 -0
- package/virtualization.d.ts.map +1 -0
- package/virtualization.js +148 -0
- package/virtualization.js.map +1 -0
- package/vz/Package.swift +23 -0
- package/vz/Sources/DeskNet/Addresses.swift +106 -0
- package/vz/Sources/DeskNet/DHCP.swift +111 -0
- package/vz/Sources/DeskNet/Fence.swift +57 -0
- package/vz/Sources/DeskNet/NetworkStack.swift +547 -0
- package/vz/Sources/DeskNet/Packets.swift +355 -0
- package/vz/Sources/DeskNet/Plumbing.swift +312 -0
- package/vz/Sources/DeskNet/Sockets.swift +221 -0
- package/vz/Sources/DeskNet/TCPFlow.swift +448 -0
- package/vz/Sources/appkit-desk-vz/Control.swift +152 -0
- package/vz/Sources/appkit-desk-vz/Helper.swift +505 -0
- package/vz/Sources/appkit-desk-vz/main.swift +38 -0
- package/vz/Tests/DeskNetTests/DeskNetTests.swift +426 -0
- package/vz/appkit-desk-vz.entitlements +8 -0
- package/vz/build.sh +23 -0
- package/vz/check/extract-boot.py +221 -0
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 { mkdir, rm, statfs, writeFile } from 'node:fs/promises';
|
|
14
|
-
import { tmpdir } from 'node:os';
|
|
13
|
+
import { mkdir, readFile, rm, statfs, writeFile } from 'node:fs/promises';
|
|
14
|
+
import { arch, cpus, release, 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';
|
|
@@ -76,7 +77,7 @@ export function createDeskHost(options) {
|
|
|
76
77
|
// cloud kernel boots without every caller having to name it; an explicit
|
|
77
78
|
// option always wins.
|
|
78
79
|
const defaultInitramfs = join(options.imageRoot, 'initrd');
|
|
79
|
-
const
|
|
80
|
+
const resolveInitramfsPath = () => options.initramfsPath ?? (pathExists(defaultInitramfs) ? defaultInitramfs : undefined);
|
|
80
81
|
const kernelCmdline = options.kernelCmdline;
|
|
81
82
|
const records = new Map();
|
|
82
83
|
/**
|
|
@@ -93,6 +94,8 @@ export function createDeskHost(options) {
|
|
|
93
94
|
let lastStartedAt = null;
|
|
94
95
|
let lastSuspendedAt = null;
|
|
95
96
|
let lastError = null;
|
|
97
|
+
let snapshotCpuFallbacks = 0;
|
|
98
|
+
let lastSnapshotCpuFallback = null;
|
|
96
99
|
let reconnects = 0;
|
|
97
100
|
let lastReconnectAt = null;
|
|
98
101
|
let lastReconnectDeskId = null;
|
|
@@ -276,13 +279,16 @@ export function createDeskHost(options) {
|
|
|
276
279
|
function snapshotDirFor(overlayPath, deskId) {
|
|
277
280
|
return join(dirname(overlayPath), `${deskId}.snapshot`);
|
|
278
281
|
}
|
|
279
|
-
async function teardown(record) {
|
|
282
|
+
async function teardown(record, keepMemory = true) {
|
|
280
283
|
if (record.status !== 'resident')
|
|
281
284
|
return false;
|
|
282
285
|
await endHandover(record, 'revoked');
|
|
283
286
|
for (const stop of [...record.streamStops])
|
|
284
287
|
stop();
|
|
285
288
|
record.streamStops.clear();
|
|
289
|
+
for (const stop of record.relayStops)
|
|
290
|
+
stop();
|
|
291
|
+
record.relayStops.clear();
|
|
286
292
|
if (record.screen?.running) {
|
|
287
293
|
record.screen.running = false;
|
|
288
294
|
emit(record, { kind: 'screen_close' });
|
|
@@ -318,14 +324,18 @@ export function createDeskHost(options) {
|
|
|
318
324
|
// the disk alone is exactly what is left whenever a snapshot is discarded,
|
|
319
325
|
// which happens on every failed restore. Syncing first makes discarding a
|
|
320
326
|
// snapshot always safe instead of usually safe.
|
|
321
|
-
const parking = parkWithMemory && typeof machine.park === 'function';
|
|
322
|
-
|
|
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)
|
|
323
331
|
await syncGuest(record, machine);
|
|
324
332
|
try {
|
|
325
333
|
if (parking) {
|
|
326
334
|
const snapshotDir = snapshotDirFor(record.startOptions.overlayPath, record.deskId);
|
|
327
335
|
await makeSnapshotDir(snapshotDir);
|
|
328
336
|
await machine.park(snapshotDir);
|
|
337
|
+
const signature = await (options.cpuSignature ?? hostCpuSignature)();
|
|
338
|
+
await (options.writeSnapshotSignature ?? writeSnapshotSignature)(snapshotDir, signature);
|
|
329
339
|
}
|
|
330
340
|
else {
|
|
331
341
|
await machine.shutdown();
|
|
@@ -409,8 +419,11 @@ export function createDeskHost(options) {
|
|
|
409
419
|
// names its vsock and tap, and a cold-boot retry that renumbered them would
|
|
410
420
|
// come up on a different tap than the one the egress rules were put on.
|
|
411
421
|
const cid = nextCid++;
|
|
422
|
+
const initramfsPath = resolveInitramfsPath();
|
|
412
423
|
const planFor = (restore) => buildDeskLaunchPlan({
|
|
413
424
|
deskId,
|
|
425
|
+
hypervisor: backend.hypervisor ?? 'cloud-hypervisor',
|
|
426
|
+
...(backend.hypervisor === 'virtualization' ? { guestNetwork: options.guestNetwork } : {}),
|
|
414
427
|
vmmPath: options.vmmPath,
|
|
415
428
|
kvmPath: options.kvmPath,
|
|
416
429
|
kernelPath,
|
|
@@ -430,7 +443,15 @@ export function createDeskHost(options) {
|
|
|
430
443
|
snapshotDir,
|
|
431
444
|
...(restore ? { restore: true } : {}),
|
|
432
445
|
}, { pathExists, deviceExists });
|
|
433
|
-
|
|
446
|
+
let resuming = parkWithMemory && pathExists(snapshotDir);
|
|
447
|
+
if (resuming) {
|
|
448
|
+
const saved = await (options.readSnapshotSignature ?? readSnapshotSignature)(snapshotDir);
|
|
449
|
+
if (saved !== null && saved !== await (options.cpuSignature ?? hostCpuSignature)()) {
|
|
450
|
+
await removeSnapshot(snapshotDir);
|
|
451
|
+
resuming = false;
|
|
452
|
+
recordCpuFallback(deskId, 'signature_mismatch');
|
|
453
|
+
}
|
|
454
|
+
}
|
|
434
455
|
let machine;
|
|
435
456
|
try {
|
|
436
457
|
machine = resuming
|
|
@@ -450,8 +471,11 @@ export function createDeskHost(options) {
|
|
|
450
471
|
// boot. Safe by construction — the guest never ran, so the disk is
|
|
451
472
|
// exactly as the pre-park sync left it.
|
|
452
473
|
const why = errorMessage(error);
|
|
453
|
-
await removeSnapshot(snapshotDir)
|
|
454
|
-
|
|
474
|
+
await removeSnapshot(snapshotDir);
|
|
475
|
+
if (isIncompatibleCpuSnapshotError(error))
|
|
476
|
+
recordCpuFallback(deskId, 'incompatible_cpuid');
|
|
477
|
+
else
|
|
478
|
+
lastError = `Desk ${deskId} could not resume from its snapshot and was cold booted: ${why}`;
|
|
455
479
|
// Falls through to the shared record path below with a cold machine.
|
|
456
480
|
machine = await backend.boot(planFor(false));
|
|
457
481
|
}
|
|
@@ -479,6 +503,7 @@ export function createDeskHost(options) {
|
|
|
479
503
|
handle: null,
|
|
480
504
|
timer: null,
|
|
481
505
|
streamStops: new Set(),
|
|
506
|
+
relayStops: new Set(),
|
|
482
507
|
};
|
|
483
508
|
records.set(deskId, record);
|
|
484
509
|
}
|
|
@@ -490,6 +515,7 @@ export function createDeskHost(options) {
|
|
|
490
515
|
record.jobs = new Map();
|
|
491
516
|
record.screen = null;
|
|
492
517
|
record.streamStops = new Set();
|
|
518
|
+
record.relayStops = new Set();
|
|
493
519
|
}
|
|
494
520
|
const current = record;
|
|
495
521
|
const epoch = current.epoch;
|
|
@@ -735,6 +761,33 @@ export function createDeskHost(options) {
|
|
|
735
761
|
touch(record);
|
|
736
762
|
armTimer(record);
|
|
737
763
|
},
|
|
764
|
+
async connect(port) {
|
|
765
|
+
requireMachine(record, epoch);
|
|
766
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
767
|
+
throw new DeskError('Guest TCP port must be between 1 and 65535.');
|
|
768
|
+
const machine = requireMachine(record, epoch);
|
|
769
|
+
const address = record.startOptions.network?.guestAddress;
|
|
770
|
+
if (!machine.connect && (!address || isIP(address) !== 4))
|
|
771
|
+
throw new DeskError('A guest IPv4 address is required to connect to a desk port.');
|
|
772
|
+
touch(record);
|
|
773
|
+
try {
|
|
774
|
+
const stream = machine.connect ? await machine.connect(port) : await (options.connectGuest ?? connectGuestTcp)(address, port);
|
|
775
|
+
try {
|
|
776
|
+
requireMachine(record, epoch);
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
stream.destroy();
|
|
780
|
+
throw error;
|
|
781
|
+
}
|
|
782
|
+
const stop = () => stream.destroy();
|
|
783
|
+
record.relayStops.add(stop);
|
|
784
|
+
stream.once('close', () => record.relayStops.delete(stop));
|
|
785
|
+
return stream;
|
|
786
|
+
}
|
|
787
|
+
catch (error) {
|
|
788
|
+
throw new DeskError(`Cannot connect to guest ${address}:${port}: ${errorMessage(error)}`);
|
|
789
|
+
}
|
|
790
|
+
},
|
|
738
791
|
exec: execImplementation,
|
|
739
792
|
jobs() {
|
|
740
793
|
return [...record.jobs.values()]
|
|
@@ -1121,6 +1174,19 @@ export function createDeskHost(options) {
|
|
|
1121
1174
|
await teardown(record);
|
|
1122
1175
|
});
|
|
1123
1176
|
}
|
|
1177
|
+
async function shutdown(deskId) {
|
|
1178
|
+
const id = cleanDeskId(deskId);
|
|
1179
|
+
await serializeLifecycle(id, async () => {
|
|
1180
|
+
const record = records.get(id);
|
|
1181
|
+
if (!record)
|
|
1182
|
+
return;
|
|
1183
|
+
await teardown(record, false);
|
|
1184
|
+
// A desk parked earlier still has its memory image on disk; dropping it is
|
|
1185
|
+
// what makes the next boot cold. The overlay is untouched — only `destroy`
|
|
1186
|
+
// may remove it.
|
|
1187
|
+
await removeSnapshot(snapshotDirFor(record.startOptions.overlayPath, record.deskId));
|
|
1188
|
+
});
|
|
1189
|
+
}
|
|
1124
1190
|
/**
|
|
1125
1191
|
* Delete a desk's overlay. The ONLY place in this module that removes a
|
|
1126
1192
|
* desk's disk, and it is reachable from `destroy` alone — see the contract
|
|
@@ -1191,6 +1257,11 @@ export function createDeskHost(options) {
|
|
|
1191
1257
|
}
|
|
1192
1258
|
return suspendedCount;
|
|
1193
1259
|
}
|
|
1260
|
+
function recordCpuFallback(deskId, reason) {
|
|
1261
|
+
snapshotCpuFallbacks += 1;
|
|
1262
|
+
lastSnapshotCpuFallback = { deskId, reason, at: iso(now()) };
|
|
1263
|
+
ports.onEvent({ kind: 'snapshot_cpu_fallback', deskId, reason, at: iso(now()) });
|
|
1264
|
+
}
|
|
1194
1265
|
function stats() {
|
|
1195
1266
|
let suspended = 0;
|
|
1196
1267
|
for (const record of records.values()) {
|
|
@@ -1208,12 +1279,15 @@ export function createDeskHost(options) {
|
|
|
1208
1279
|
lastReconnectAt,
|
|
1209
1280
|
lastReconnectDeskId,
|
|
1210
1281
|
lastError,
|
|
1282
|
+
snapshotCpuFallbacks,
|
|
1283
|
+
lastSnapshotCpuFallback,
|
|
1211
1284
|
};
|
|
1212
1285
|
}
|
|
1213
|
-
return { start, resume, suspend, destroy, stats, sweep };
|
|
1286
|
+
return { start, resume, suspend, shutdown, destroy, stats, sweep };
|
|
1214
1287
|
}
|
|
1215
1288
|
/**
|
|
1216
1289
|
* Whether the overlay directory's filesystem can make reflink clones.
|
|
1290
|
+
* On macOS this probes APFS cloning with `cp -c`. On Linux:
|
|
1217
1291
|
*
|
|
1218
1292
|
* This exists because `cp --reflink=auto` — what `buildDeskLaunchPlan` uses,
|
|
1219
1293
|
* deliberately, so a desk still boots on a filesystem without reflink — is
|
|
@@ -1246,7 +1320,7 @@ export async function verifyOverlayReflink(options) {
|
|
|
1246
1320
|
+ `support could not be determined: ${errorMessage(error)}`);
|
|
1247
1321
|
}
|
|
1248
1322
|
await copy(source, destination);
|
|
1249
|
-
return { supported: true, overlayDirectory, detail: 'cp --reflink=always succeeded.' };
|
|
1323
|
+
return { supported: true, overlayDirectory, detail: process.platform === 'darwin' ? 'cp -c succeeded.' : 'cp --reflink=always succeeded.' };
|
|
1250
1324
|
}
|
|
1251
1325
|
catch (error) {
|
|
1252
1326
|
if (error instanceof DeskError)
|
|
@@ -1265,16 +1339,18 @@ export async function verifyOverlayReflink(options) {
|
|
|
1265
1339
|
export async function assertOverlayReflink(options) {
|
|
1266
1340
|
const check = await verifyOverlayReflink(options);
|
|
1267
1341
|
if (!check.supported) {
|
|
1268
|
-
throw new DeskError(
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1342
|
+
throw new DeskError(process.platform === 'darwin'
|
|
1343
|
+
? `The overlay directory ${check.overlayDirectory} does not support cloning. Put it on APFS. Probe said: ${check.detail}`
|
|
1344
|
+
: `The overlay directory ${check.overlayDirectory} is on a filesystem without reflink `
|
|
1345
|
+
+ 'support, so every desk would FULL-COPY the base image instead of sharing its '
|
|
1346
|
+
+ 'blocks — gigabytes and minutes per desk, and a disk sized for sharing fills after '
|
|
1347
|
+
+ `a handful of them. Put the overlay directory on XFS or Btrfs. Probe said: ${check.detail}`);
|
|
1272
1348
|
}
|
|
1273
1349
|
return check;
|
|
1274
1350
|
}
|
|
1275
1351
|
async function reflinkAlways(source, destination) {
|
|
1276
1352
|
await new Promise((resolvePromise, rejectPromise) => {
|
|
1277
|
-
execFile('cp', ['--reflink=always', source, destination], (error, _stdout, stderr) => {
|
|
1353
|
+
execFile('cp', [process.platform === 'darwin' ? '-c' : '--reflink=always', source, destination], (error, _stdout, stderr) => {
|
|
1278
1354
|
if (error) {
|
|
1279
1355
|
rejectPromise(new Error(stderr.trim() || errorMessage(error)));
|
|
1280
1356
|
return;
|
|
@@ -1297,13 +1373,14 @@ export async function verifyDeskHost(options) {
|
|
|
1297
1373
|
const deviceExists = options.deviceExists ?? existsSync;
|
|
1298
1374
|
const idFactory = options.idFactory ?? randomUUID;
|
|
1299
1375
|
const backend = options.backend ?? cloudHypervisorBackend;
|
|
1300
|
-
const
|
|
1376
|
+
const isVirtualization = backend.hypervisor === 'virtualization';
|
|
1377
|
+
const vmmPath = options.vmmPath ?? backend.vmmPath ?? DEFAULT_VMM_PATH;
|
|
1301
1378
|
const kvmPath = options.kvmPath ?? DEFAULT_KVM_PATH;
|
|
1302
|
-
if (platform !== 'linux') {
|
|
1303
|
-
throw new DeskError(`The AppKit desk requires Linux; received ${platform}.`);
|
|
1379
|
+
if (platform !== (isVirtualization ? 'darwin' : 'linux')) {
|
|
1380
|
+
throw new DeskError(isVirtualization ? `Virtualization requires macOS; received ${platform}.` : `The AppKit desk requires Linux; received ${platform}.`);
|
|
1304
1381
|
}
|
|
1305
1382
|
if (!pathExists(vmmPath)) {
|
|
1306
|
-
throw new DeskError(`Cloud Hypervisor was not found at ${vmmPath}.`);
|
|
1383
|
+
throw new DeskError(isVirtualization ? `Virtualization helper was not found at ${vmmPath}.` : `Cloud Hypervisor was not found at ${vmmPath}.`);
|
|
1307
1384
|
}
|
|
1308
1385
|
if (!pathExists(options.kernelPath)) {
|
|
1309
1386
|
throw new DeskError(`Guest kernel does not exist: ${options.kernelPath}`);
|
|
@@ -1311,7 +1388,7 @@ export async function verifyDeskHost(options) {
|
|
|
1311
1388
|
if (!pathExists(options.baseImagePath)) {
|
|
1312
1389
|
throw new DeskError(`Base image does not exist: ${options.baseImagePath}`);
|
|
1313
1390
|
}
|
|
1314
|
-
if (!deviceExists(kvmPath)) {
|
|
1391
|
+
if (!isVirtualization && !deviceExists(kvmPath)) {
|
|
1315
1392
|
return { supported: false, vmmPath, kvm: false, vsock: false, virtioGpu: false };
|
|
1316
1393
|
}
|
|
1317
1394
|
const scratchDir = options.scratchDir ?? tmpdir();
|
|
@@ -1319,6 +1396,7 @@ export async function verifyDeskHost(options) {
|
|
|
1319
1396
|
const overlayPath = join(scratchDir, `${deskId}.raw`);
|
|
1320
1397
|
const plan = buildDeskLaunchPlan({
|
|
1321
1398
|
deskId,
|
|
1399
|
+
hypervisor: backend.hypervisor ?? 'cloud-hypervisor',
|
|
1322
1400
|
vmmPath,
|
|
1323
1401
|
kvmPath,
|
|
1324
1402
|
kernelPath: options.kernelPath,
|
|
@@ -1365,7 +1443,7 @@ export async function verifyDeskHost(options) {
|
|
|
1365
1443
|
}
|
|
1366
1444
|
await rm(overlayPath, { force: true });
|
|
1367
1445
|
}
|
|
1368
|
-
return { supported: vsock, vmmPath, kvm:
|
|
1446
|
+
return { supported: vsock, vmmPath, kvm: !isVirtualization, vsock, virtioGpu };
|
|
1369
1447
|
}
|
|
1370
1448
|
function nonEmptyString(value, field) {
|
|
1371
1449
|
if (typeof value !== 'string' || value.length === 0) {
|
|
@@ -1402,4 +1480,44 @@ async function defaultFreeBytes(path) {
|
|
|
1402
1480
|
function errorMessage(error) {
|
|
1403
1481
|
return error instanceof Error ? error.message : String(error);
|
|
1404
1482
|
}
|
|
1483
|
+
async function connectGuestTcp(address, port) {
|
|
1484
|
+
return new Promise((resolve, reject) => {
|
|
1485
|
+
const socket = tcpConnect({ host: address, port });
|
|
1486
|
+
socket.setTimeout(10_000, () => socket.destroy(new Error('Guest TCP connection timed out.')));
|
|
1487
|
+
socket.once('error', reject);
|
|
1488
|
+
socket.once('connect', () => { socket.setTimeout(0); socket.removeListener('error', reject); resolve(socket); });
|
|
1489
|
+
});
|
|
1490
|
+
}
|
|
1491
|
+
/** Cloud Hypervisor's specific restore error; do not classify generic CPUID mentions. */
|
|
1492
|
+
export function isIncompatibleCpuSnapshotError(error) {
|
|
1493
|
+
return /Detected incompatible CPUID entry/.test(errorMessage(error));
|
|
1494
|
+
}
|
|
1495
|
+
/** Hash the host's CPU identity and advertised feature set, excluding per-core counters. */
|
|
1496
|
+
export async function hostCpuSignature() {
|
|
1497
|
+
if (process.platform === 'darwin') {
|
|
1498
|
+
const model = cpus()[0]?.model;
|
|
1499
|
+
if (!model)
|
|
1500
|
+
throw new DeskError('Cannot determine host CPU signature.');
|
|
1501
|
+
return createHash('sha256').update(`darwin:${arch()}:${release()}:${model}`).digest('hex');
|
|
1502
|
+
}
|
|
1503
|
+
const cpuinfo = await readFile('/proc/cpuinfo', 'utf8');
|
|
1504
|
+
const first = cpuinfo.split(/\n\s*\n/)[0] ?? '';
|
|
1505
|
+
const identity = first.split('\n').filter((line) => /^(vendor_id|cpu family|model\s|model name|stepping|flags|microcode)\s*:/.test(line)).join('\n');
|
|
1506
|
+
if (!identity)
|
|
1507
|
+
throw new DeskError('Cannot determine host CPU signature.');
|
|
1508
|
+
return createHash('sha256').update(identity).digest('hex');
|
|
1509
|
+
}
|
|
1510
|
+
async function readSnapshotSignature(directory) {
|
|
1511
|
+
try {
|
|
1512
|
+
return (await readFile(join(directory, 'appkit-cpu-signature'), 'utf8')).trim();
|
|
1513
|
+
}
|
|
1514
|
+
catch (error) {
|
|
1515
|
+
if (error.code === 'ENOENT')
|
|
1516
|
+
return null;
|
|
1517
|
+
throw error;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
async function writeSnapshotSignature(directory, signature) {
|
|
1521
|
+
await writeFile(join(directory, 'appkit-cpu-signature'), signature, { mode: 0o600 });
|
|
1522
|
+
}
|
|
1405
1523
|
//# sourceMappingURL=host.js.map
|