@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.
- package/CHANGELOG.md +284 -0
- package/dist/backends/http-worker-client.d.ts +3 -1
- package/dist/backends/http-worker-client.d.ts.map +1 -1
- package/dist/backends/http-worker-client.js +15 -4
- package/dist/backends/http-worker-client.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/local-virtual-computer/client.d.ts +21 -0
- package/dist/local-virtual-computer/client.d.ts.map +1 -0
- package/dist/local-virtual-computer/client.js +503 -0
- package/dist/local-virtual-computer/client.js.map +1 -0
- package/dist/local-virtual-computer/detached-process.d.ts +15 -0
- package/dist/local-virtual-computer/detached-process.d.ts.map +1 -0
- package/dist/local-virtual-computer/detached-process.js +111 -0
- package/dist/local-virtual-computer/detached-process.js.map +1 -0
- package/dist/local-virtual-computer/detached-worker.d.ts +11 -0
- package/dist/local-virtual-computer/detached-worker.d.ts.map +1 -0
- package/dist/local-virtual-computer/detached-worker.js +97 -0
- package/dist/local-virtual-computer/detached-worker.js.map +1 -0
- package/dist/local-virtual-computer/dialect.d.ts +26 -0
- package/dist/local-virtual-computer/dialect.d.ts.map +1 -0
- package/dist/local-virtual-computer/dialect.js +61 -0
- package/dist/local-virtual-computer/dialect.js.map +1 -0
- package/dist/local-virtual-computer/engine.d.ts +63 -0
- package/dist/local-virtual-computer/engine.d.ts.map +1 -0
- package/dist/local-virtual-computer/engine.js +125 -0
- package/dist/local-virtual-computer/engine.js.map +1 -0
- package/dist/local-virtual-computer/guest-stdio.d.ts +15 -0
- package/dist/local-virtual-computer/guest-stdio.d.ts.map +1 -0
- package/dist/local-virtual-computer/guest-stdio.js +330 -0
- package/dist/local-virtual-computer/guest-stdio.js.map +1 -0
- package/dist/local-virtual-computer/index.d.ts +36 -0
- package/dist/local-virtual-computer/index.d.ts.map +1 -0
- package/dist/local-virtual-computer/index.js +341 -0
- package/dist/local-virtual-computer/index.js.map +1 -0
- package/dist/local-virtual-computer/podman-machine.d.ts +17 -0
- package/dist/local-virtual-computer/podman-machine.d.ts.map +1 -0
- package/dist/local-virtual-computer/podman-machine.js +181 -0
- package/dist/local-virtual-computer/podman-machine.js.map +1 -0
- package/dist/local-virtual-computer/setup/contract.d.ts +360 -0
- package/dist/local-virtual-computer/setup/contract.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/contract.js +3 -0
- package/dist/local-virtual-computer/setup/contract.js.map +1 -0
- package/dist/local-virtual-computer/setup/elevated.d.ts +78 -0
- package/dist/local-virtual-computer/setup/elevated.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/elevated.js +548 -0
- package/dist/local-virtual-computer/setup/elevated.js.map +1 -0
- package/dist/local-virtual-computer/setup/host.d.ts +12 -0
- package/dist/local-virtual-computer/setup/host.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/host.js +93 -0
- package/dist/local-virtual-computer/setup/host.js.map +1 -0
- package/dist/local-virtual-computer/setup/idle.d.ts +18 -0
- package/dist/local-virtual-computer/setup/idle.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/idle.js +38 -0
- package/dist/local-virtual-computer/setup/idle.js.map +1 -0
- package/dist/local-virtual-computer/setup/image.d.ts +7 -0
- package/dist/local-virtual-computer/setup/image.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/image.js +124 -0
- package/dist/local-virtual-computer/setup/image.js.map +1 -0
- package/dist/local-virtual-computer/setup/index.d.ts +17 -0
- package/dist/local-virtual-computer/setup/index.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/index.js +17 -0
- package/dist/local-virtual-computer/setup/index.js.map +1 -0
- package/dist/local-virtual-computer/setup/limits.d.ts +30 -0
- package/dist/local-virtual-computer/setup/limits.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/limits.js +58 -0
- package/dist/local-virtual-computer/setup/limits.js.map +1 -0
- package/dist/local-virtual-computer/setup/plan.d.ts +8 -0
- package/dist/local-virtual-computer/setup/plan.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/plan.js +131 -0
- package/dist/local-virtual-computer/setup/plan.js.map +1 -0
- package/dist/local-virtual-computer/setup/readiness.d.ts +11 -0
- package/dist/local-virtual-computer/setup/readiness.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/readiness.js +320 -0
- package/dist/local-virtual-computer/setup/readiness.js.map +1 -0
- package/dist/local-virtual-computer/setup/registry.d.ts +18 -0
- package/dist/local-virtual-computer/setup/registry.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/registry.js +50 -0
- package/dist/local-virtual-computer/setup/registry.js.map +1 -0
- package/dist/local-virtual-computer/setup/restart.d.ts +8 -0
- package/dist/local-virtual-computer/setup/restart.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/restart.js +60 -0
- package/dist/local-virtual-computer/setup/restart.js.map +1 -0
- package/dist/local-virtual-computer/setup/resume.d.ts +14 -0
- package/dist/local-virtual-computer/setup/resume.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/resume.js +58 -0
- package/dist/local-virtual-computer/setup/resume.js.map +1 -0
- package/dist/local-virtual-computer/setup/run.d.ts +8 -0
- package/dist/local-virtual-computer/setup/run.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/run.js +200 -0
- package/dist/local-virtual-computer/setup/run.js.map +1 -0
- package/dist/local-virtual-computer/setup/windows-folder.d.ts +11 -0
- package/dist/local-virtual-computer/setup/windows-folder.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/windows-folder.js +14 -0
- package/dist/local-virtual-computer/setup/windows-folder.js.map +1 -0
- package/dist/local-virtual-computer/setup/wsl-package.d.ts +19 -0
- package/dist/local-virtual-computer/setup/wsl-package.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/wsl-package.js +242 -0
- package/dist/local-virtual-computer/setup/wsl-package.js.map +1 -0
- package/dist/local-virtual-computer/setup/wslc-shared.d.ts +27 -0
- package/dist/local-virtual-computer/setup/wslc-shared.d.ts.map +1 -0
- package/dist/local-virtual-computer/setup/wslc-shared.js +137 -0
- package/dist/local-virtual-computer/setup/wslc-shared.js.map +1 -0
- package/dist/local-virtual-computer/ssh-forward.d.ts +19 -0
- package/dist/local-virtual-computer/ssh-forward.d.ts.map +1 -0
- package/dist/local-virtual-computer/ssh-forward.js +179 -0
- package/dist/local-virtual-computer/ssh-forward.js.map +1 -0
- package/dist/local-virtual-computer/wslc-engine.d.ts +27 -0
- package/dist/local-virtual-computer/wslc-engine.d.ts.map +1 -0
- package/dist/local-virtual-computer/wslc-engine.js +98 -0
- package/dist/local-virtual-computer/wslc-engine.js.map +1 -0
- package/local-computer/Dockerfile +99 -0
- package/local-computer/app-catalog.cjs +240 -0
- package/local-computer/app-native-host.cjs +144 -0
- package/local-computer/browser-home.cjs +249 -0
- package/local-computer/build-image.mjs +310 -0
- package/local-computer/desktop-worker.cjs +477 -0
- package/local-computer/entrypoint.sh +161 -0
- package/local-computer/home.html +28 -0
- package/local-computer/new-tab/home.css +255 -0
- package/local-computer/new-tab/home.js +65 -0
- package/local-computer/new-tab/manifest.json +12 -0
- package/local-computer/new-tab/wordmark.svg +33 -0
- package/local-computer/openbox-rc.xml +41 -0
- package/local-computer/prepare-home.cjs +121 -0
- package/local-computer/tint2rc +44 -0
- package/package.json +13 -4
- package/src/backends/http-worker-client.ts +31 -3
- package/src/index.ts +13 -0
- package/src/local-virtual-computer/client.ts +573 -0
- package/src/local-virtual-computer/detached-process.ts +133 -0
- package/src/local-virtual-computer/detached-worker.ts +124 -0
- package/src/local-virtual-computer/dialect.ts +83 -0
- package/src/local-virtual-computer/engine.ts +197 -0
- package/src/local-virtual-computer/guest-stdio.ts +332 -0
- package/src/local-virtual-computer/index.ts +447 -0
- package/src/local-virtual-computer/podman-machine.ts +264 -0
- package/src/local-virtual-computer/setup/contract.ts +395 -0
- package/src/local-virtual-computer/setup/elevated.ts +655 -0
- package/src/local-virtual-computer/setup/host.ts +100 -0
- package/src/local-virtual-computer/setup/idle.ts +39 -0
- package/src/local-virtual-computer/setup/image.ts +159 -0
- package/src/local-virtual-computer/setup/index.ts +16 -0
- package/src/local-virtual-computer/setup/limits.ts +68 -0
- package/src/local-virtual-computer/setup/plan.ts +144 -0
- package/src/local-virtual-computer/setup/readiness.ts +386 -0
- package/src/local-virtual-computer/setup/registry.ts +50 -0
- package/src/local-virtual-computer/setup/restart.ts +64 -0
- package/src/local-virtual-computer/setup/resume.ts +65 -0
- package/src/local-virtual-computer/setup/run.ts +225 -0
- package/src/local-virtual-computer/setup/windows-folder.ts +13 -0
- package/src/local-virtual-computer/setup/wsl-package.ts +242 -0
- package/src/local-virtual-computer/setup/wslc-shared.ts +147 -0
- package/src/local-virtual-computer/ssh-forward.ts +195 -0
- package/src/local-virtual-computer/wslc-engine.ts +124 -0
- 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
|
+
}
|