@braedonsaunders/appkit-desk 0.8.0 → 0.10.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.
@@ -1 +1 @@
1
- {"version":3,"file":"virtualization.js","sourceRoot":"","sources":["../src/virtualization.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process'\nimport { randomUUID } from 'node:crypto'\nimport { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { dirname, isAbsolute, join } from 'node:path'\nimport { createInterface } from 'node:readline'\nimport type { Duplex } from 'node:stream'\nimport type { DeskBackend, DeskProcessLauncher } from './backend'\nimport { DeskError } from './plan'\nimport { GUEST_AGENT_VSOCK_PORT } from './protocol'\nimport {\n attemptHandshake, connectWithRetry, createGuestMachine, defaultConnect, drainOutput,\n errorMessage, runToCompletion, terminate, watchExit,\n} from './machine'\n\n/** A signed helper is supplied by the host; it is not downloaded or executed at install time. */\nexport interface VirtualizationBackendOptions {\n helperPath: string\n launcher?: DeskProcessLauncher\n connect?: (socketPath: string) => Duplex\n connectTimeoutMs?: number\n /** Existing file for the guest serial console; omit to disable it. */\n consoleLogPath?: string\n requestTimeoutMs?: number\n guestAgentPort?: number\n platform?: NodeJS.Platform\n}\n\nexport function createVirtualizationBackend(options: VirtualizationBackendOptions): DeskBackend {\n if (options.consoleLogPath && !isAbsolute(options.consoleLogPath)) throw new DeskError('consoleLogPath must be an absolute path.')\n if (!isAbsolute(options.helperPath)) throw new DeskError('helperPath must be an absolute path.')\n const launcher: DeskProcessLauncher = options.launcher ?? ((command, args, { identity }) => spawn(command, [...args], {\n stdio: ['pipe', 'pipe', 'pipe'], uid: identity?.uid, gid: identity?.gid,\n }))\n const timeout = options.connectTimeoutMs ?? 30_000\n return {\n hypervisor: 'virtualization',\n vmmPath: options.helperPath,\n async boot(plan, hooks) {\n if (plan.hypervisor !== 'virtualization') throw new DeskError('Virtualization backend requires a virtualization plan.')\n if ((options.platform ?? process.platform) !== 'darwin') throw new DeskError('Virtualization requires macOS.')\n if (plan.overlay.create) await runToCompletion(launcher(plan.overlay.create.command, plan.overlay.create.args, { identity: plan.launcherIdentity }), `overlay creation for desk ${plan.deskId}`)\n await mkdir(dirname(plan.vsock.socketPath), { recursive: true, mode: 0o700 })\n const child = launcher(options.helperPath, [], { identity: plan.launcherIdentity })\n const exit = watchExit(child)\n const log = drainOutput(child, 8192, ['stderr'])\n if (!child.stdin || !child.stdout) {\n await terminate(child, exit, 2000)\n throw new DeskError('Virtualization helper requires piped stdin and stdout.')\n }\n const input = child.stdin\n // A helper exit may close stdin while a write is in flight.\n input.on('error', () => undefined)\n const pending = new Map<string, { resolve: (value: Record<string, unknown>) => void; reject: (error: Error) => void; timer: NodeJS.Timeout }>()\n const lines = createInterface({ input: child.stdout })\n lines.on('line', (line) => {\n let message: Record<string, unknown>\n try { message = JSON.parse(line) as Record<string, unknown> } catch { return }\n if (!message || typeof message !== 'object' || typeof message.id !== 'string') return\n const entry = pending.get(message.id)\n if (!entry) return\n pending.delete(message.id); clearTimeout(entry.timer)\n if (message.ok === true && message.result && typeof message.result === 'object') entry.resolve(message.result as Record<string, unknown>)\n else entry.reject(new DeskError(String(message.error ?? 'Helper request failed.')))\n })\n void exit.promise.then(() => {\n for (const entry of pending.values()) { clearTimeout(entry.timer); entry.reject(new DeskError(`Virtualization helper exited: ${log.tail()}`)) }\n pending.clear(); lines.close()\n })\n function call(op: string, fields: Record<string, unknown> = {}): Promise<Record<string, unknown>> {\n if (exit.done) return Promise.reject(new DeskError('Virtualization helper exited.'))\n const id = randomUUID()\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => { pending.delete(id); reject(new DeskError(`Virtualization helper ${op} timed out.`)) }, timeout)\n pending.set(id, { resolve, reject, timer })\n input.write(`${JSON.stringify({ ...fields, id, op })}\\n`, (error) => {\n if (error) { const entry = pending.get(id); if (entry) { clearTimeout(entry.timer); pending.delete(id); reject(error) } }\n })\n })\n }\n const stop = async () => {\n try { if (!exit.done) await call('stop') } finally { await terminate(child, exit, 2000) }\n }\n try {\n const identifierPath = join(plan.snapshot.dir, 'machine-id')\n const identifier = plan.snapshot.restoring ? await readFile(identifierPath, 'utf8') : undefined\n const boot = await call('boot', {\n kernel: plan.kernelPath, initrd: plan.initramfsPath, cmdline: plan.kernelCmdline,\n ...(options.consoleLogPath ? { consoleLog: options.consoleLogPath } : {}),\n disk: plan.overlay.path, memoryMb: plan.memoryMb, vcpus: plan.vcpus,\n vsockSocket: plan.vsock.socketPath, portSocket: plan.ports.socketPath,\n ...(plan.snapshot.restoring ? { restoreFrom: join(plan.snapshot.dir, 'state'), machineIdentifier: identifier } : {}),\n network: { ...plan.network, macAddress: plan.network.mac },\n })\n if (typeof boot.machineIdentifier !== 'string') throw new DeskError('Helper did not return a machine identifier.')\n const machineIdentifier = boot.machineIdentifier\n if (plan.snapshot.restoring) {\n if (!hooks?.onRestored) throw new DeskError('Restore requires snapshot invalidation before resume.')\n await hooks.onRestored()\n await call('resume')\n }\n const connectAgent = (abandoned?: () => boolean) => connectWithRetry({\n connect: options.connect ?? defaultConnect, now: Date.now, socketPath: plan.vsock.socketPath,\n port: options.guestAgentPort ?? GUEST_AGENT_VSOCK_PORT, timeoutMs: timeout, retryDelayMs: 100,\n attemptTimeoutMs: 5000, exited: exit, abandoned,\n })\n const stream = await connectAgent()\n return createGuestMachine({\n deskId: plan.deskId, exit, stream, reconnect: connectAgent, reconnectWindowMs: timeout,\n now: Date.now, idFactory: randomUUID, requestTimeoutMs: options.requestTimeoutMs ?? 30_000,\n stop,\n connect: (port) => attemptHandshake(options.connect ?? defaultConnect, plan.ports.socketPath, port, 5000),\n ...(boot.saveRestore === true ? {\n park: async (dir: string) => {\n try {\n await call('save', { path: join(dir, 'state') })\n await writeFile(join(dir, 'machine-id'), machineIdentifier, { mode: 0o600 })\n } finally { await stop() }\n },\n } : {}),\n })\n } catch (error) {\n await terminate(child, exit, 2000)\n throw new DeskError(`Virtualization desk ${plan.deskId} failed: ${errorMessage(error)}${log.tail() ? ` (${log.tail()})` : ''}`)\n }\n },\n }\n}\n"]}
1
+ {"version":3,"file":"virtualization.js","sourceRoot":"","sources":["../src/virtualization.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from 'node:child_process'\nimport { randomUUID } from 'node:crypto'\nimport { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport { dirname, isAbsolute, join } from 'node:path'\nimport { createInterface } from 'node:readline'\nimport type { Duplex } from 'node:stream'\nimport type { DeskBackend, DeskProcessLauncher } from './backend'\nimport { DeskError } from './plan'\nimport { GUEST_AGENT_VSOCK_PORT } from './protocol'\nimport {\n assertShareManifest, attemptHandshake, connectWithRetry, createGuestMachine, defaultConnect, drainOutput,\n errorMessage, runToCompletion, terminate, watchExit, writeShareManifest,\n} from './machine'\n\n/** A signed helper is supplied by the host; it is not downloaded or executed at install time. */\nexport interface VirtualizationBackendOptions {\n helperPath: string\n launcher?: DeskProcessLauncher\n connect?: (socketPath: string) => Duplex\n connectTimeoutMs?: number\n /** Existing file for the guest serial console; omit to disable it. */\n consoleLogPath?: string\n requestTimeoutMs?: number\n guestAgentPort?: number\n platform?: NodeJS.Platform\n}\n\nexport function createVirtualizationBackend(options: VirtualizationBackendOptions): DeskBackend {\n if (options.consoleLogPath && !isAbsolute(options.consoleLogPath)) throw new DeskError('consoleLogPath must be an absolute path.')\n if (!isAbsolute(options.helperPath)) throw new DeskError('helperPath must be an absolute path.')\n const launcher: DeskProcessLauncher = options.launcher ?? ((command, args, { identity }) => spawn(command, [...args], {\n stdio: ['pipe', 'pipe', 'pipe'], uid: identity?.uid, gid: identity?.gid,\n }))\n const timeout = options.connectTimeoutMs ?? 30_000\n return {\n hypervisor: 'virtualization',\n vmmPath: options.helperPath,\n async boot(plan, hooks) {\n if (plan.hypervisor !== 'virtualization') throw new DeskError('Virtualization backend requires a virtualization plan.')\n if ((options.platform ?? process.platform) !== 'darwin') throw new DeskError('Virtualization requires macOS.')\n if (plan.overlay.create) await runToCompletion(launcher(plan.overlay.create.command, plan.overlay.create.args, { identity: plan.launcherIdentity }), `overlay creation for desk ${plan.deskId}`)\n // A saved machine restores only into the device list it was saved with.\n if (plan.snapshot.restoring) {\n await assertShareManifest(plan.snapshot.dir, plan.shares).catch((error: unknown) => {\n throw new DeskError(`Virtualization desk ${plan.deskId} cannot be restored: ${errorMessage(error)}`)\n })\n }\n await mkdir(dirname(plan.vsock.socketPath), { recursive: true, mode: 0o700 })\n const child = launcher(options.helperPath, [], { identity: plan.launcherIdentity })\n const exit = watchExit(child)\n const log = drainOutput(child, 8192, ['stderr'])\n if (!child.stdin || !child.stdout) {\n await terminate(child, exit, 2000)\n throw new DeskError('Virtualization helper requires piped stdin and stdout.')\n }\n const input = child.stdin\n // A helper exit may close stdin while a write is in flight.\n input.on('error', () => undefined)\n const pending = new Map<string, { resolve: (value: Record<string, unknown>) => void; reject: (error: Error) => void; timer: NodeJS.Timeout }>()\n const lines = createInterface({ input: child.stdout })\n lines.on('line', (line) => {\n let message: Record<string, unknown>\n try { message = JSON.parse(line) as Record<string, unknown> } catch { return }\n if (!message || typeof message !== 'object' || typeof message.id !== 'string') return\n const entry = pending.get(message.id)\n if (!entry) return\n pending.delete(message.id); clearTimeout(entry.timer)\n if (message.ok === true && message.result && typeof message.result === 'object') entry.resolve(message.result as Record<string, unknown>)\n else entry.reject(new DeskError(String(message.error ?? 'Helper request failed.')))\n })\n void exit.promise.then(() => {\n for (const entry of pending.values()) { clearTimeout(entry.timer); entry.reject(new DeskError(`Virtualization helper exited: ${log.tail()}`)) }\n pending.clear(); lines.close()\n })\n function call(op: string, fields: Record<string, unknown> = {}): Promise<Record<string, unknown>> {\n if (exit.done) return Promise.reject(new DeskError('Virtualization helper exited.'))\n const id = randomUUID()\n return new Promise((resolve, reject) => {\n const timer = setTimeout(() => { pending.delete(id); reject(new DeskError(`Virtualization helper ${op} timed out.`)) }, timeout)\n pending.set(id, { resolve, reject, timer })\n input.write(`${JSON.stringify({ ...fields, id, op })}\\n`, (error) => {\n if (error) { const entry = pending.get(id); if (entry) { clearTimeout(entry.timer); pending.delete(id); reject(error) } }\n })\n })\n }\n const stop = async () => {\n try { if (!exit.done) await call('stop') } finally { await terminate(child, exit, 2000) }\n }\n try {\n const identifierPath = join(plan.snapshot.dir, 'machine-id')\n const identifier = plan.snapshot.restoring ? await readFile(identifierPath, 'utf8') : undefined\n const boot = await call('boot', {\n kernel: plan.kernelPath, initrd: plan.initramfsPath, cmdline: plan.kernelCmdline,\n ...(options.consoleLogPath ? { consoleLog: options.consoleLogPath } : {}),\n disk: plan.overlay.path, memoryMb: plan.memoryMb, vcpus: plan.vcpus,\n vsockSocket: plan.vsock.socketPath, portSocket: plan.ports.socketPath,\n shares: plan.shares.map(({ tag, path, readOnly }) => ({ tag, path, readOnly })),\n ...(plan.snapshot.restoring ? { restoreFrom: join(plan.snapshot.dir, 'state'), machineIdentifier: identifier } : {}),\n network: { ...plan.network, macAddress: plan.network.mac },\n })\n if (typeof boot.machineIdentifier !== 'string') throw new DeskError('Helper did not return a machine identifier.')\n const machineIdentifier = boot.machineIdentifier\n if (plan.snapshot.restoring) {\n if (!hooks?.onRestored) throw new DeskError('Restore requires snapshot invalidation before resume.')\n await hooks.onRestored()\n await call('resume')\n }\n const connectAgent = (abandoned?: () => boolean) => connectWithRetry({\n connect: options.connect ?? defaultConnect, now: Date.now, socketPath: plan.vsock.socketPath,\n port: options.guestAgentPort ?? GUEST_AGENT_VSOCK_PORT, timeoutMs: timeout, retryDelayMs: 100,\n attemptTimeoutMs: 5000, exited: exit, abandoned,\n })\n const stream = await connectAgent()\n return createGuestMachine({\n deskId: plan.deskId, exit, stream, reconnect: connectAgent, reconnectWindowMs: timeout,\n now: Date.now, idFactory: randomUUID, requestTimeoutMs: options.requestTimeoutMs ?? 30_000,\n stop,\n connect: (port) => attemptHandshake(options.connect ?? defaultConnect, plan.ports.socketPath, port, 5000),\n ...(boot.saveRestore === true ? {\n park: async (dir: string) => {\n try {\n await call('save', { path: join(dir, 'state') })\n await writeFile(join(dir, 'machine-id'), machineIdentifier, { mode: 0o600 })\n await writeShareManifest(dir, plan.shares)\n } finally { await stop() }\n },\n } : {}),\n })\n } catch (error) {\n await terminate(child, exit, 2000)\n throw new DeskError(`Virtualization desk ${plan.deskId} failed: ${errorMessage(error)}${log.tail() ? ` (${log.tail()})` : ''}`)\n }\n },\n }\n}\n"]}
@@ -2,6 +2,13 @@ import DeskNet
2
2
  import Foundation
3
3
  import Virtualization
4
4
 
5
+ /// A host directory shared into the guest over virtio-fs.
6
+ struct ShareRequest {
7
+ let tag: String
8
+ let directory: URL
9
+ let readOnly: Bool
10
+ }
11
+
5
12
  /// Everything `boot` needs, validated before anything is created.
6
13
  struct BootRequest {
7
14
  let kernel: URL
@@ -16,6 +23,7 @@ struct BootRequest {
16
23
  let machineIdentifier: Data?
17
24
  let consoleLog: URL?
18
25
  let network: NetworkConfiguration
26
+ let shares: [ShareRequest]
19
27
 
20
28
  init(_ body: [String: Any]) throws {
21
29
  kernel = URL(fileURLWithPath: try body.path("kernel"))
@@ -39,6 +47,7 @@ struct BootRequest {
39
47
  if restoreFrom != nil && machineIdentifier == nil {
40
48
  throw RequestError("restoreFrom requires the machineIdentifier the machine was saved with.")
41
49
  }
50
+ shares = try Self.shares(body["shares"])
42
51
 
43
52
  let network = (body["network"] as? [String: Any]) ?? [:]
44
53
  var configuration = NetworkConfiguration(allowLan: try network.bool("allowLan", default: false))
@@ -74,6 +83,27 @@ struct BootRequest {
74
83
  }
75
84
  self.network = configuration
76
85
  }
86
+
87
+ /// Tags are checked against Virtualization.framework's own rules when the
88
+ /// devices are made; here, only that each is present and unique.
89
+ private static func shares(_ value: Any?) throws -> [ShareRequest] {
90
+ guard let value, !(value is NSNull) else { return [] }
91
+ guard let entries = value as? [[String: Any]] else { throw RequestError("shares must be an array of objects.") }
92
+ var tags = Set<String>()
93
+ return try entries.enumerated().map { index, entry in
94
+ let tag = try entry.string("tag")
95
+ guard tags.insert(tag).inserted else { throw RequestError("shares[\(index)].tag \(tag) is used by another share.") }
96
+ let path = try entry.path("path")
97
+ var isDirectory: ObjCBool = false
98
+ guard FileManager.default.fileExists(atPath: path, isDirectory: &isDirectory), isDirectory.boolValue else {
99
+ throw RequestError("shares[\(index)].path is not an existing directory: \(path)")
100
+ }
101
+ guard entry["readOnly"] is NSNumber else { throw RequestError("shares[\(index)].readOnly must be a boolean.") }
102
+ return ShareRequest(
103
+ tag: tag, directory: URL(fileURLWithPath: path, isDirectory: true),
104
+ readOnly: try entry.bool("readOnly", default: false))
105
+ }
106
+ }
77
107
  }
78
108
 
79
109
  /// One helper process runs one VM. It owns the VM, the guest's network, and
@@ -332,6 +362,21 @@ final class Helper: NSObject, VZVirtualMachineDelegate {
332
362
  network.guestMAC = MAC(nic.macAddress.string) ?? network.guestMAC
333
363
  attachNetwork(network, fd: helperEnd)
334
364
 
365
+ // Built from the request on every boot, so a restore has exactly the
366
+ // devices the machine was saved with as long as the host asks for the same
367
+ // shares — which it checks before restoring.
368
+ configuration.directorySharingDevices = try request.shares.map { share in
369
+ do {
370
+ try VZVirtioFileSystemDeviceConfiguration.validateTag(share.tag)
371
+ } catch {
372
+ throw RequestError("Share tag \(share.tag) is not a valid virtio-fs tag: \(describe(error))")
373
+ }
374
+ let device = VZVirtioFileSystemDeviceConfiguration(tag: share.tag)
375
+ device.share = VZSingleDirectoryShare(
376
+ directory: VZSharedDirectory(url: share.directory, readOnly: share.readOnly))
377
+ return device
378
+ }
379
+
335
380
  configuration.entropyDevices = [VZVirtioEntropyDeviceConfiguration()]
336
381
  configuration.memoryBalloonDevices = [VZVirtioTraditionalMemoryBalloonDeviceConfiguration()]
337
382
  configuration.socketDevices = [VZVirtioSocketDeviceConfiguration()]