@namzu/sandbox 24.0.0 → 26.0.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.
Files changed (158) hide show
  1. package/CHANGELOG.md +284 -0
  2. package/dist/backends/http-worker-client.d.ts +3 -1
  3. package/dist/backends/http-worker-client.d.ts.map +1 -1
  4. package/dist/backends/http-worker-client.js +15 -4
  5. package/dist/backends/http-worker-client.js.map +1 -1
  6. package/dist/index.d.ts +2 -0
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/index.js +1 -0
  9. package/dist/index.js.map +1 -1
  10. package/dist/local-virtual-computer/client.d.ts +21 -0
  11. package/dist/local-virtual-computer/client.d.ts.map +1 -0
  12. package/dist/local-virtual-computer/client.js +503 -0
  13. package/dist/local-virtual-computer/client.js.map +1 -0
  14. package/dist/local-virtual-computer/detached-process.d.ts +15 -0
  15. package/dist/local-virtual-computer/detached-process.d.ts.map +1 -0
  16. package/dist/local-virtual-computer/detached-process.js +111 -0
  17. package/dist/local-virtual-computer/detached-process.js.map +1 -0
  18. package/dist/local-virtual-computer/detached-worker.d.ts +11 -0
  19. package/dist/local-virtual-computer/detached-worker.d.ts.map +1 -0
  20. package/dist/local-virtual-computer/detached-worker.js +97 -0
  21. package/dist/local-virtual-computer/detached-worker.js.map +1 -0
  22. package/dist/local-virtual-computer/dialect.d.ts +26 -0
  23. package/dist/local-virtual-computer/dialect.d.ts.map +1 -0
  24. package/dist/local-virtual-computer/dialect.js +61 -0
  25. package/dist/local-virtual-computer/dialect.js.map +1 -0
  26. package/dist/local-virtual-computer/engine.d.ts +63 -0
  27. package/dist/local-virtual-computer/engine.d.ts.map +1 -0
  28. package/dist/local-virtual-computer/engine.js +125 -0
  29. package/dist/local-virtual-computer/engine.js.map +1 -0
  30. package/dist/local-virtual-computer/guest-stdio.d.ts +15 -0
  31. package/dist/local-virtual-computer/guest-stdio.d.ts.map +1 -0
  32. package/dist/local-virtual-computer/guest-stdio.js +330 -0
  33. package/dist/local-virtual-computer/guest-stdio.js.map +1 -0
  34. package/dist/local-virtual-computer/index.d.ts +36 -0
  35. package/dist/local-virtual-computer/index.d.ts.map +1 -0
  36. package/dist/local-virtual-computer/index.js +341 -0
  37. package/dist/local-virtual-computer/index.js.map +1 -0
  38. package/dist/local-virtual-computer/podman-machine.d.ts +17 -0
  39. package/dist/local-virtual-computer/podman-machine.d.ts.map +1 -0
  40. package/dist/local-virtual-computer/podman-machine.js +181 -0
  41. package/dist/local-virtual-computer/podman-machine.js.map +1 -0
  42. package/dist/local-virtual-computer/setup/contract.d.ts +360 -0
  43. package/dist/local-virtual-computer/setup/contract.d.ts.map +1 -0
  44. package/dist/local-virtual-computer/setup/contract.js +3 -0
  45. package/dist/local-virtual-computer/setup/contract.js.map +1 -0
  46. package/dist/local-virtual-computer/setup/elevated.d.ts +78 -0
  47. package/dist/local-virtual-computer/setup/elevated.d.ts.map +1 -0
  48. package/dist/local-virtual-computer/setup/elevated.js +548 -0
  49. package/dist/local-virtual-computer/setup/elevated.js.map +1 -0
  50. package/dist/local-virtual-computer/setup/host.d.ts +12 -0
  51. package/dist/local-virtual-computer/setup/host.d.ts.map +1 -0
  52. package/dist/local-virtual-computer/setup/host.js +93 -0
  53. package/dist/local-virtual-computer/setup/host.js.map +1 -0
  54. package/dist/local-virtual-computer/setup/idle.d.ts +18 -0
  55. package/dist/local-virtual-computer/setup/idle.d.ts.map +1 -0
  56. package/dist/local-virtual-computer/setup/idle.js +38 -0
  57. package/dist/local-virtual-computer/setup/idle.js.map +1 -0
  58. package/dist/local-virtual-computer/setup/image.d.ts +7 -0
  59. package/dist/local-virtual-computer/setup/image.d.ts.map +1 -0
  60. package/dist/local-virtual-computer/setup/image.js +124 -0
  61. package/dist/local-virtual-computer/setup/image.js.map +1 -0
  62. package/dist/local-virtual-computer/setup/index.d.ts +17 -0
  63. package/dist/local-virtual-computer/setup/index.d.ts.map +1 -0
  64. package/dist/local-virtual-computer/setup/index.js +17 -0
  65. package/dist/local-virtual-computer/setup/index.js.map +1 -0
  66. package/dist/local-virtual-computer/setup/limits.d.ts +30 -0
  67. package/dist/local-virtual-computer/setup/limits.d.ts.map +1 -0
  68. package/dist/local-virtual-computer/setup/limits.js +58 -0
  69. package/dist/local-virtual-computer/setup/limits.js.map +1 -0
  70. package/dist/local-virtual-computer/setup/plan.d.ts +8 -0
  71. package/dist/local-virtual-computer/setup/plan.d.ts.map +1 -0
  72. package/dist/local-virtual-computer/setup/plan.js +131 -0
  73. package/dist/local-virtual-computer/setup/plan.js.map +1 -0
  74. package/dist/local-virtual-computer/setup/readiness.d.ts +11 -0
  75. package/dist/local-virtual-computer/setup/readiness.d.ts.map +1 -0
  76. package/dist/local-virtual-computer/setup/readiness.js +320 -0
  77. package/dist/local-virtual-computer/setup/readiness.js.map +1 -0
  78. package/dist/local-virtual-computer/setup/registry.d.ts +18 -0
  79. package/dist/local-virtual-computer/setup/registry.d.ts.map +1 -0
  80. package/dist/local-virtual-computer/setup/registry.js +50 -0
  81. package/dist/local-virtual-computer/setup/registry.js.map +1 -0
  82. package/dist/local-virtual-computer/setup/restart.d.ts +8 -0
  83. package/dist/local-virtual-computer/setup/restart.d.ts.map +1 -0
  84. package/dist/local-virtual-computer/setup/restart.js +60 -0
  85. package/dist/local-virtual-computer/setup/restart.js.map +1 -0
  86. package/dist/local-virtual-computer/setup/resume.d.ts +14 -0
  87. package/dist/local-virtual-computer/setup/resume.d.ts.map +1 -0
  88. package/dist/local-virtual-computer/setup/resume.js +58 -0
  89. package/dist/local-virtual-computer/setup/resume.js.map +1 -0
  90. package/dist/local-virtual-computer/setup/run.d.ts +8 -0
  91. package/dist/local-virtual-computer/setup/run.d.ts.map +1 -0
  92. package/dist/local-virtual-computer/setup/run.js +200 -0
  93. package/dist/local-virtual-computer/setup/run.js.map +1 -0
  94. package/dist/local-virtual-computer/setup/windows-folder.d.ts +11 -0
  95. package/dist/local-virtual-computer/setup/windows-folder.d.ts.map +1 -0
  96. package/dist/local-virtual-computer/setup/windows-folder.js +14 -0
  97. package/dist/local-virtual-computer/setup/windows-folder.js.map +1 -0
  98. package/dist/local-virtual-computer/setup/wsl-package.d.ts +19 -0
  99. package/dist/local-virtual-computer/setup/wsl-package.d.ts.map +1 -0
  100. package/dist/local-virtual-computer/setup/wsl-package.js +242 -0
  101. package/dist/local-virtual-computer/setup/wsl-package.js.map +1 -0
  102. package/dist/local-virtual-computer/setup/wslc-shared.d.ts +27 -0
  103. package/dist/local-virtual-computer/setup/wslc-shared.d.ts.map +1 -0
  104. package/dist/local-virtual-computer/setup/wslc-shared.js +137 -0
  105. package/dist/local-virtual-computer/setup/wslc-shared.js.map +1 -0
  106. package/dist/local-virtual-computer/ssh-forward.d.ts +19 -0
  107. package/dist/local-virtual-computer/ssh-forward.d.ts.map +1 -0
  108. package/dist/local-virtual-computer/ssh-forward.js +179 -0
  109. package/dist/local-virtual-computer/ssh-forward.js.map +1 -0
  110. package/dist/local-virtual-computer/wslc-engine.d.ts +27 -0
  111. package/dist/local-virtual-computer/wslc-engine.d.ts.map +1 -0
  112. package/dist/local-virtual-computer/wslc-engine.js +98 -0
  113. package/dist/local-virtual-computer/wslc-engine.js.map +1 -0
  114. package/local-computer/Dockerfile +99 -0
  115. package/local-computer/app-catalog.cjs +240 -0
  116. package/local-computer/app-native-host.cjs +144 -0
  117. package/local-computer/browser-home.cjs +249 -0
  118. package/local-computer/build-image.mjs +310 -0
  119. package/local-computer/desktop-worker.cjs +477 -0
  120. package/local-computer/entrypoint.sh +161 -0
  121. package/local-computer/home.html +28 -0
  122. package/local-computer/new-tab/home.css +255 -0
  123. package/local-computer/new-tab/home.js +65 -0
  124. package/local-computer/new-tab/manifest.json +12 -0
  125. package/local-computer/new-tab/wordmark.svg +33 -0
  126. package/local-computer/openbox-rc.xml +41 -0
  127. package/local-computer/prepare-home.cjs +121 -0
  128. package/local-computer/tint2rc +44 -0
  129. package/package.json +13 -4
  130. package/src/backends/http-worker-client.ts +31 -3
  131. package/src/index.ts +13 -0
  132. package/src/local-virtual-computer/client.ts +573 -0
  133. package/src/local-virtual-computer/detached-process.ts +133 -0
  134. package/src/local-virtual-computer/detached-worker.ts +124 -0
  135. package/src/local-virtual-computer/dialect.ts +83 -0
  136. package/src/local-virtual-computer/engine.ts +197 -0
  137. package/src/local-virtual-computer/guest-stdio.ts +332 -0
  138. package/src/local-virtual-computer/index.ts +447 -0
  139. package/src/local-virtual-computer/podman-machine.ts +264 -0
  140. package/src/local-virtual-computer/setup/contract.ts +395 -0
  141. package/src/local-virtual-computer/setup/elevated.ts +655 -0
  142. package/src/local-virtual-computer/setup/host.ts +100 -0
  143. package/src/local-virtual-computer/setup/idle.ts +39 -0
  144. package/src/local-virtual-computer/setup/image.ts +159 -0
  145. package/src/local-virtual-computer/setup/index.ts +16 -0
  146. package/src/local-virtual-computer/setup/limits.ts +68 -0
  147. package/src/local-virtual-computer/setup/plan.ts +144 -0
  148. package/src/local-virtual-computer/setup/readiness.ts +386 -0
  149. package/src/local-virtual-computer/setup/registry.ts +50 -0
  150. package/src/local-virtual-computer/setup/restart.ts +64 -0
  151. package/src/local-virtual-computer/setup/resume.ts +65 -0
  152. package/src/local-virtual-computer/setup/run.ts +225 -0
  153. package/src/local-virtual-computer/setup/windows-folder.ts +13 -0
  154. package/src/local-virtual-computer/setup/wsl-package.ts +242 -0
  155. package/src/local-virtual-computer/setup/wslc-shared.ts +147 -0
  156. package/src/local-virtual-computer/ssh-forward.ts +195 -0
  157. package/src/local-virtual-computer/wslc-engine.ts +124 -0
  158. package/worker/server.js +1499 -0
@@ -0,0 +1,386 @@
1
+ import { stat } from 'node:fs/promises'
2
+ import {
3
+ LocalCommandError,
4
+ type LocalComputerCommandRunner,
5
+ LocalComputerSetupError,
6
+ } from '../engine.js'
7
+ import { WSLC_MINIMUM_VERSION, mapWslcFailure, parseWslcVersion } from '../wslc-engine.js'
8
+ import type {
9
+ InspectWslcReadiness,
10
+ WslcReadiness,
11
+ WslcReadinessState,
12
+ WslcSetupHost,
13
+ WslcUnsupportedReason,
14
+ } from './contract.js'
15
+ import { createWslcSetupHost } from './host.js'
16
+ import { PROBE_LIMIT_MS, ProbeTimeoutError, runLimited } from './limits.js'
17
+ import { clearRestartMarker, markerPath, readRestartMarker } from './restart.js'
18
+ import {
19
+ archivePathOf,
20
+ parseImageInspect,
21
+ readImageRecord,
22
+ readManifest,
23
+ wslcBinaryOf,
24
+ } from './wslc-shared.js'
25
+
26
+ const POLICY_KEY = 'HKLM\\Software\\Policies\\WSL'
27
+ /** Windows' own pending-restart markers; both can be read without administrator rights. */
28
+ const PENDING_RESTART_KEYS = [
29
+ 'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootPending',
30
+ 'HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\WindowsUpdate\\Auto Update\\RebootRequired',
31
+ ] as const
32
+ const MINIMUM_BUILD = 19041
33
+ const WINDOWS_11_BUILD = 22000
34
+
35
+ /** `wsl.exe --version` is UTF-16LE and localised: strip NULs, never read a label. */
36
+ export function parseWslVersion(output: string): string | undefined {
37
+ const text = output.replace(/\0/g, '').replace(/^/, '')
38
+ const firstLine = text.split(/\r?\n/).find((line) => line.trim() !== '') ?? ''
39
+ return /\d+\.\d+\.\d+\.\d+/.exec(firstLine)?.[0]
40
+ }
41
+
42
+ export function wslcAtLeastMinimum(version: string): boolean {
43
+ const parts = version.split('.').map(Number)
44
+ for (let i = 0; i < WSLC_MINIMUM_VERSION.length; i++) {
45
+ const found = parts[i] ?? 0
46
+ const floor = WSLC_MINIMUM_VERSION[i] ?? 0
47
+ if (found !== floor) return found > floor
48
+ }
49
+ return true
50
+ }
51
+
52
+ /** 1060 is "service does not exist"; any other failure leaves it unknown. */
53
+ async function serviceExists(
54
+ runner: LocalComputerCommandRunner,
55
+ name: string,
56
+ signal: AbortSignal | undefined,
57
+ ): Promise<boolean | 'unknown'> {
58
+ try {
59
+ await runLimited(runner, 'sc.exe', ['query', name], PROBE_LIMIT_MS.command, { signal })
60
+ return true
61
+ } catch (error) {
62
+ signal?.throwIfAborted()
63
+ if (error instanceof LocalCommandError && /\(1060\)/.test(error.message)) return false
64
+ return 'unknown'
65
+ }
66
+ }
67
+
68
+ async function hypervisorPresent(
69
+ runner: LocalComputerCommandRunner,
70
+ signal: AbortSignal | undefined,
71
+ ): Promise<boolean | 'unknown'> {
72
+ try {
73
+ const output = await runLimited(
74
+ runner,
75
+ 'powershell.exe',
76
+ [
77
+ '-NoProfile',
78
+ '-NonInteractive',
79
+ '-Command',
80
+ '(Get-CimInstance Win32_ComputerSystem).HypervisorPresent',
81
+ ],
82
+ PROBE_LIMIT_MS.powershell,
83
+ { signal },
84
+ )
85
+ const answer = output.trim().toLowerCase()
86
+ if (answer === 'true') return true
87
+ if (answer === 'false') return false
88
+ return 'unknown'
89
+ } catch {
90
+ signal?.throwIfAborted()
91
+ return 'unknown'
92
+ }
93
+ }
94
+
95
+ async function wslVersionOf(
96
+ runner: LocalComputerCommandRunner,
97
+ signal: AbortSignal | undefined,
98
+ ): Promise<string | undefined> {
99
+ try {
100
+ return parseWslVersion(
101
+ await runLimited(runner, 'wsl.exe', ['--version'], PROBE_LIMIT_MS.command, { signal }),
102
+ )
103
+ } catch {
104
+ signal?.throwIfAborted()
105
+ return undefined
106
+ }
107
+ }
108
+
109
+ async function wslcVersionOf(
110
+ runner: LocalComputerCommandRunner,
111
+ binary: string,
112
+ signal: AbortSignal | undefined,
113
+ ): Promise<string | undefined> {
114
+ try {
115
+ const parsed = parseWslcVersion(
116
+ await runLimited(runner, binary, ['version'], PROBE_LIMIT_MS.command, { signal }),
117
+ )
118
+ return parsed?.join('.')
119
+ } catch (error) {
120
+ signal?.throwIfAborted()
121
+ // Without an answer there is no telling "not installed" from "slow": the
122
+ // state cannot be decided, so say so rather than plan an install.
123
+ if (error instanceof ProbeTimeoutError) throw couldNotCheck(error)
124
+ return undefined
125
+ }
126
+ }
127
+
128
+ /** A policy value; a read that did not answer in time cannot be taken as "not set". */
129
+ async function readPolicy(host: WslcSetupHost, name: string): Promise<number | undefined> {
130
+ try {
131
+ return await host.readDword(POLICY_KEY, name)
132
+ } catch (error) {
133
+ if (error instanceof ProbeTimeoutError) throw couldNotCheck(error)
134
+ throw error
135
+ }
136
+ }
137
+
138
+ /** The state cannot be decided: a plain message the CLI and Desktop show with Details. */
139
+ function couldNotCheck(cause: unknown): LocalComputerSetupError {
140
+ return new LocalComputerSetupError(
141
+ 'Linux container support did not answer, so Namzu could not check this PC. Try again in a moment.',
142
+ undefined,
143
+ { cause },
144
+ )
145
+ }
146
+
147
+ /**
148
+ * Windows' own markers that a restart is pending. A marker that cannot be
149
+ * read is neither set nor clear: say so, because guessing "clear" would send a
150
+ * PC that only needs a restart to the firmware settings.
151
+ */
152
+ async function windowsWantsRestart(host: WslcSetupHost): Promise<boolean | 'unknown'> {
153
+ let unknown = false
154
+ for (const key of PENDING_RESTART_KEYS) {
155
+ const set = await host.keyExists(key)
156
+ if (set === true) return true
157
+ if (set === 'unknown') unknown = true
158
+ }
159
+ return unknown ? 'unknown' : false
160
+ }
161
+
162
+ async function bundledOf(
163
+ manifestPath: string | undefined,
164
+ ): Promise<{ manifestPath: string; archivePath: string; imageId: string } | undefined> {
165
+ if (manifestPath === undefined) return undefined
166
+ try {
167
+ const manifest = await readManifest(manifestPath)
168
+ const archivePath = archivePathOf(manifestPath, manifest)
169
+ await stat(archivePath)
170
+ return { manifestPath, archivePath, imageId: manifest.imageId }
171
+ } catch {
172
+ return undefined
173
+ }
174
+ }
175
+
176
+ type ProbeResult =
177
+ | { readonly kind: 'loaded' | 'absent' }
178
+ | { readonly kind: 'state'; readonly state: WslcReadinessState }
179
+
180
+ /** Start the session (wslc image inspect does) and see what answers. */
181
+ async function probeImage(
182
+ host: WslcSetupHost,
183
+ binary: string,
184
+ ref: string,
185
+ expectedId: string | undefined,
186
+ signal: AbortSignal | undefined,
187
+ ): Promise<ProbeResult> {
188
+ try {
189
+ const output = await runLimited(
190
+ host.runner,
191
+ binary,
192
+ ['image', 'inspect', '-f', 'json', ref],
193
+ PROBE_LIMIT_MS.session,
194
+ { signal },
195
+ )
196
+ const found = parseImageInspect(output)
197
+ if (found === undefined) return { kind: 'absent' }
198
+ return expectedId === undefined || found.id === expectedId
199
+ ? { kind: 'loaded' }
200
+ : { kind: 'absent' }
201
+ } catch (error) {
202
+ signal?.throwIfAborted()
203
+ const mapped = mapWslcFailure(error)
204
+ if (mapped?.code === 'virtualization-off') return { kind: 'state', state: 'virtualization-off' }
205
+ if (mapped?.code === 'blocked-by-policy') return { kind: 'state', state: 'blocked-by-policy' }
206
+ if (mapped?.code === 'sdk-mismatch') return { kind: 'state', state: 'sdk-mismatch' }
207
+ if (mapped?.code === 'image-missing') return { kind: 'absent' }
208
+ // A missing image makes the command exit non-zero with the not-found code
209
+ // or with a bare exit status 1 and no code (observed on wslc 3.0.1, where the PC answers
210
+ // "not found" and nothing else). A command that ran and failed with any
211
+ // other code, or did not start, is the engine failing.
212
+ if (
213
+ error instanceof LocalCommandError &&
214
+ (error.errorCodes.includes('WSLC_E_IMAGE_NOT_FOUND') ||
215
+ (error.errorCodes.length === 0 && error.exitCode === 1))
216
+ )
217
+ return { kind: 'absent' }
218
+ throw couldNotCheck(error)
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Inspect the PC without changing anything. Without `probeSession` no command
224
+ * runs that starts the wslc session (`wslc version` does not; `ps` and `image`
225
+ * do), so opening a panel never starts a virtual machine.
226
+ */
227
+ export const inspectWslcReadiness: InspectWslcReadiness = async (options) => {
228
+ const host = createWslcSetupHost(options.host)
229
+ const { signal } = options
230
+ const checkedAt = host.now().toISOString()
231
+ const bundled = await bundledOf(options.bundledManifestPath)
232
+ const bundledPaths = bundled
233
+ ? { manifestPath: bundled.manifestPath, archivePath: bundled.archivePath }
234
+ : undefined
235
+ const base = {
236
+ policy: { allowWsl: true, allowWslContainer: true },
237
+ components: {
238
+ virtualMachinePlatform: 'unknown' as const,
239
+ wslOptionalComponent: 'unknown' as const,
240
+ hypervisorPresent: 'unknown' as const,
241
+ },
242
+ wsl: {},
243
+ image: {
244
+ ref: options.imageRef,
245
+ loaded: false,
246
+ ...(bundledPaths ? { bundled: bundledPaths } : {}),
247
+ },
248
+ checkedAt,
249
+ }
250
+
251
+ let unsupported: WslcUnsupportedReason | undefined
252
+ if (host.platform !== 'win32') unsupported = 'platform'
253
+ else if (host.arch !== 'x64') unsupported = 'architecture'
254
+ else if (host.windowsBuild === undefined || host.windowsBuild < MINIMUM_BUILD)
255
+ unsupported = 'windows-build'
256
+ if (unsupported !== undefined || host.windowsBuild === undefined) {
257
+ return {
258
+ state: 'unsupported',
259
+ unsupportedReason: unsupported ?? 'windows-build',
260
+ ...base,
261
+ }
262
+ }
263
+
264
+ const build = host.windowsBuild
265
+ const isWindows11 = build >= WINDOWS_11_BUILD
266
+ const binary = wslcBinaryOf(host)
267
+ const { runner } = host
268
+
269
+ const [
270
+ allowWsl,
271
+ allowContainer,
272
+ markerRead,
273
+ hns,
274
+ vmcompute,
275
+ lxss,
276
+ hypervisor,
277
+ wslVersion,
278
+ wslcVersion,
279
+ ] = await Promise.all([
280
+ readPolicy(host, 'AllowWSL'),
281
+ readPolicy(host, 'AllowWSLContainer'),
282
+ readRestartMarker(host).then(
283
+ (marker) => ({ marker }),
284
+ () => ({ marker: undefined, unreadable: true as const }),
285
+ ),
286
+ serviceExists(runner, 'HNS', signal),
287
+ serviceExists(runner, 'vmcompute', signal),
288
+ isWindows11
289
+ ? Promise.resolve('not-needed' as const)
290
+ : serviceExists(runner, 'LxssManager', signal),
291
+ hypervisorPresent(runner, signal),
292
+ wslVersionOf(runner, signal),
293
+ wslcVersionOf(runner, binary, signal),
294
+ ])
295
+
296
+ const policy = { allowWsl: allowWsl !== 0, allowWslContainer: allowContainer !== 0 }
297
+ const vmp: boolean | 'unknown' =
298
+ hns === false || vmcompute === false
299
+ ? false
300
+ : hns === true && vmcompute === true
301
+ ? true
302
+ : 'unknown'
303
+ const components = {
304
+ virtualMachinePlatform: vmp,
305
+ wslOptionalComponent: lxss,
306
+ hypervisorPresent: hypervisor,
307
+ }
308
+
309
+ let restart: WslcReadiness['restart']
310
+ let restartUnrecorded = false
311
+ const { marker } = markerRead
312
+ let recordedAt: string | undefined = marker?.recordedAt
313
+ if ('unreadable' in markerRead) {
314
+ // A marker that cannot be read still has a file date, and a restart since
315
+ // then ends it like any other. With no date either, the state cannot be
316
+ // decided: it reads as restart-pending with no record to compare against
317
+ // and no remaining steps, rather than failing the inspection.
318
+ try {
319
+ recordedAt = (await stat(markerPath(host.stateDirectory))).mtime.toISOString()
320
+ } catch {
321
+ restartUnrecorded = true
322
+ }
323
+ }
324
+ if (recordedAt !== undefined) {
325
+ let boot: Date | undefined
326
+ try {
327
+ boot = await host.bootTime()
328
+ } catch {
329
+ signal?.throwIfAborted()
330
+ }
331
+ if (boot === undefined) restart = { recordedAt }
332
+ else if (boot.getTime() > Date.parse(recordedAt)) {
333
+ try {
334
+ await clearRestartMarker(host)
335
+ } catch {
336
+ // A marker that stays is checked again next time; the PC has restarted.
337
+ }
338
+ } else restart = { recordedAt, bootTime: boot.toISOString() }
339
+ }
340
+
341
+ const wsl = {
342
+ ...(wslVersion ? { version: wslVersion } : {}),
343
+ ...(wslcVersion ? { wslcBinary: binary, wslcVersion } : {}),
344
+ }
345
+ const wslcUsable = wslcVersion !== undefined && wslcAtLeastMinimum(wslcVersion)
346
+
347
+ let loaded = false
348
+ if (options.probeSession !== true) {
349
+ const record = await readImageRecord(host)
350
+ loaded =
351
+ record !== undefined &&
352
+ record.imageRef === options.imageRef &&
353
+ (bundled === undefined || record.imageId === bundled.imageId)
354
+ }
355
+
356
+ const result = (state: WslcReadinessState, imageLoaded = loaded): WslcReadiness => ({
357
+ state,
358
+ windows: { build, architecture: 'x64', isWindows11 },
359
+ policy,
360
+ components,
361
+ wsl,
362
+ image: { ...base.image, loaded: imageLoaded },
363
+ ...(restart ? { restart } : {}),
364
+ checkedAt,
365
+ })
366
+
367
+ if (!policy.allowWsl || !policy.allowWslContainer) return result('blocked-by-policy')
368
+ if (restart || restartUnrecorded) return result('restart-pending')
369
+ if (vmp === false || lxss === false) return result('platform-off')
370
+ if (!wslcUsable) return result(wslVersion ? 'wsl-outdated' : 'wsl-missing')
371
+ if (vmp === true && hypervisor === false && options.probeSession !== true) {
372
+ // Turning the platform on outside Namzu leaves no hypervisor until the PC
373
+ // restarts; Windows says so in its own markers. Only without one is it the
374
+ // firmware setting. With a probe the session's own failure decides.
375
+ const pending = await windowsWantsRestart(host)
376
+ if (pending === 'unknown') throw couldNotCheck(undefined)
377
+ return result(pending ? 'restart-pending' : 'virtualization-off')
378
+ }
379
+
380
+ if (options.probeSession === true) {
381
+ const probe = await probeImage(host, binary, options.imageRef, bundled?.imageId, signal)
382
+ if (probe.kind === 'state') return result(probe.state)
383
+ loaded = probe.kind === 'loaded'
384
+ }
385
+ return result(loaded ? 'ready' : 'image-missing', loaded)
386
+ }
@@ -0,0 +1,50 @@
1
+ import { LocalCommandError } from '../engine.js'
2
+
3
+ /** Run `reg.exe` with these arguments and return what it printed. */
4
+ export type RegistryRun = (args: readonly string[]) => Promise<string>
5
+
6
+ function parentOf(key: string): string | undefined {
7
+ const cut = key.lastIndexOf('\\')
8
+ return cut > 0 ? key.slice(0, cut) : undefined
9
+ }
10
+
11
+ /**
12
+ * `reg query` exits 1 both for a key that is not there and for one the caller
13
+ * may not read, and its words are localised, so the exit status alone cannot
14
+ * tell them apart. Ask the parent instead: its listing of subkeys names the
15
+ * key in every language. When the parent is absent too, the same test climbs
16
+ * to the grandparent, and so on. A key an ancestor lists but the query cannot
17
+ * read is not absent, and neither is an ancestor that cannot be read or a
18
+ * climb that reaches a hive root without an answer.
19
+ *
20
+ * Returns the key's listing, or undefined when the key is provably absent.
21
+ * Anything else rethrows the failure.
22
+ */
23
+ export async function queryRegistryKey(run: RegistryRun, key: string): Promise<string | undefined> {
24
+ try {
25
+ return await run(['query', key])
26
+ } catch (error) {
27
+ if (!(error instanceof LocalCommandError) || error.exitCode !== 1) throw error
28
+ const parent = parentOf(key)
29
+ if (parent === undefined) throw error
30
+ // The parent may itself be absent (no Windows Update key on an offline or
31
+ // managed image): walk up until an ancestor answers.
32
+ const listing = await queryRegistryKey(run, parent)
33
+ if (listing === undefined) return undefined
34
+ const leaf = key.slice(parent.length + 1).toLowerCase()
35
+ const listed = listing
36
+ .split(/\r?\n/)
37
+ .some((line) => line.trim().toLowerCase().endsWith(`\\${leaf}`))
38
+ if (listed) throw error
39
+ return undefined
40
+ }
41
+ }
42
+
43
+ /** True when a `reg query <key>` listing carries a value of this name. */
44
+ export function listingHasValue(listing: string, name: string): boolean {
45
+ const wanted = name.toLowerCase()
46
+ return listing.split(/\r?\n/).some((line) => {
47
+ const match = /^\s+(.+?)\s+REG_[A-Z_]+(?:\s|$)/.exec(line)
48
+ return match?.[1]?.toLowerCase() === wanted
49
+ })
50
+ }
@@ -0,0 +1,64 @@
1
+ import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import type {
4
+ ClearRestartMarker,
5
+ ReadRestartMarker,
6
+ WriteRestartMarker,
7
+ WslcRestartMarker,
8
+ WslcSetupStepKind,
9
+ } from './contract.js'
10
+
11
+ const STEP_KINDS: readonly WslcSetupStepKind[] = [
12
+ 'download-wsl',
13
+ 'enable-windows-features',
14
+ 'install-wsl',
15
+ 'restart',
16
+ 'load-image',
17
+ ]
18
+
19
+ export function markerPath(directory: string): string {
20
+ return join(directory, 'restart.json')
21
+ }
22
+
23
+ function parseMarker(text: string): WslcRestartMarker | undefined {
24
+ let value: unknown
25
+ try {
26
+ value = JSON.parse(text)
27
+ } catch {
28
+ return undefined
29
+ }
30
+ if (typeof value !== 'object' || value === null) return undefined
31
+ const { recordedAt, planId, remaining } = value as Record<string, unknown>
32
+ if (typeof recordedAt !== 'string' || Number.isNaN(Date.parse(recordedAt))) return undefined
33
+ if (typeof planId !== 'string' || !Array.isArray(remaining)) return undefined
34
+ if (!remaining.every((step) => STEP_KINDS.includes(step as WslcSetupStepKind))) return undefined
35
+ return { recordedAt, planId, remaining: remaining as WslcSetupStepKind[] }
36
+ }
37
+
38
+ /** The recorded restart, or undefined when absent or not one this code wrote. */
39
+ export const readRestartMarker: ReadRestartMarker = async (host) => {
40
+ try {
41
+ return parseMarker(await readFile(markerPath(host.stateDirectory), 'utf8'))
42
+ } catch (error) {
43
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') return undefined
44
+ throw error
45
+ }
46
+ }
47
+
48
+ /** Temp file plus rename, so a reader never sees half a marker. */
49
+ export const writeRestartMarker: WriteRestartMarker = async (host, marker) => {
50
+ await mkdir(host.stateDirectory, { recursive: true })
51
+ const target = markerPath(host.stateDirectory)
52
+ const temp = `${target}.${process.pid}.tmp`
53
+ await writeFile(temp, `${JSON.stringify(marker)}\n`, 'utf8')
54
+ try {
55
+ await rename(temp, target)
56
+ } catch (error) {
57
+ await rm(temp, { force: true })
58
+ throw error
59
+ }
60
+ }
61
+
62
+ export const clearRestartMarker: ClearRestartMarker = async (host) => {
63
+ await rm(markerPath(host.stateDirectory), { force: true })
64
+ }
@@ -0,0 +1,65 @@
1
+ import type { ClearResumeAtSignIn, RegisterResumeAtSignIn, WslcSetupHost } from './contract.js'
2
+ import { listingHasValue, queryRegistryKey } from './registry.js'
3
+ import { trustedWindowsDirectory } from './windows-folder.js'
4
+
5
+ /** The registry value the resume command is stored under. */
6
+ const RESUME_VALUE_NAME = 'NamzuPalComputerSetup'
7
+ /** Windows runs a RunOnce command line of at most this many characters. */
8
+ const MAX_COMMAND_LENGTH = 2048
9
+
10
+ function regBinary(host: WslcSetupHost): string {
11
+ return `${trustedWindowsDirectory(host.env)}\\System32\\reg.exe`
12
+ }
13
+
14
+ function checkKey(key: string): void {
15
+ if (!/^HK(CU|LM|CR|U|CC)\\[^\r\n\0"]+$/.test(key)) {
16
+ throw new Error('The sign-in registry key is not a valid registry path.')
17
+ }
18
+ }
19
+
20
+ function checkCommand(command: string): void {
21
+ if (command.trim().length === 0) throw new Error('The sign-in command is empty.')
22
+ if (command.length > MAX_COMMAND_LENGTH) {
23
+ throw new Error(`The sign-in command is longer than ${MAX_COMMAND_LENGTH} characters.`)
24
+ }
25
+ if (/[\r\n\0]/.test(command)) {
26
+ throw new Error('The sign-in command must be a single line.')
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Ask Windows to run `command` once at the next sign-in by writing the
32
+ * `NamzuPalComputerSetup` value (REG_SZ) under `host.resumeRegistryKey`. No
33
+ * administrator is needed for the default per-user key. Windows deletes a
34
+ * RunOnce value just before it runs it; writing the value again replaces it.
35
+ */
36
+ export const registerResumeAtSignIn: RegisterResumeAtSignIn = async (host, command) => {
37
+ checkKey(host.resumeRegistryKey)
38
+ checkCommand(command)
39
+ await host.runner.run(regBinary(host), [
40
+ 'add',
41
+ host.resumeRegistryKey,
42
+ '/v',
43
+ RESUME_VALUE_NAME,
44
+ '/t',
45
+ 'REG_SZ',
46
+ '/d',
47
+ command,
48
+ '/f',
49
+ ])
50
+ }
51
+
52
+ /**
53
+ * Take the command back. A value that is not there is not an error; a key
54
+ * that cannot be read is, because the command might still be registered.
55
+ */
56
+ export const clearResumeAtSignIn: ClearResumeAtSignIn = async (host) => {
57
+ checkKey(host.resumeRegistryKey)
58
+ const reg = regBinary(host)
59
+ const listing = await queryRegistryKey(
60
+ (args) => host.runner.run(reg, [...args]),
61
+ host.resumeRegistryKey,
62
+ )
63
+ if (listing === undefined || !listingHasValue(listing, RESUME_VALUE_NAME)) return
64
+ await host.runner.run(reg, ['delete', host.resumeRegistryKey, '/v', RESUME_VALUE_NAME, '/f'])
65
+ }