windows-development-sandbox 1.18.2 → 1.18.4
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/config.example.json +1 -1
- package/dist/app/features/defs.d.ts +26 -0
- package/dist/app/features/defs.js +2 -1
- package/dist/app/features/sandbox/sandbox.def.d.ts +34 -0
- package/dist/app/features/sandbox/sandbox.def.js +52 -10
- package/dist/app/features/sandbox/sandbox.run.d.ts +24 -0
- package/dist/app/features/sandbox/sandbox.run.js +45 -1
- package/dist/app/registry.js +2 -1
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-MQIA7GNB.js → chunk-FRPLQLSY.js} +3188 -2765
- package/dist/bin/{chunk-M4OM632I.js → chunk-MY7NLQQY.js} +1 -1
- package/dist/bin/{chunk-IIHL7W6B.js → chunk-N4GXYTLV.js} +1 -1
- package/dist/bin/claude-host.js +2 -2
- package/dist/bin/claude-sbx.js +2 -2
- package/dist/bin/codex-sbx.js +2 -2
- package/dist/bin/gemini-sbx.js +2 -2
- package/dist/bin/sandbox.js +2 -2
- package/dist/bin/sbx-hide.js +2 -2
- package/dist/bin/sbx-show.js +2 -2
- package/dist/cli/commands/launch/launch.js +4 -1
- package/dist/cli/commands/launch/leftover-offer.d.ts +3 -0
- package/dist/cli/commands/launch/leftover-offer.js +26 -0
- package/dist/cli/commands/repair/checks/sandbox.d.ts +3 -1
- package/dist/cli/commands/repair/checks/sandbox.js +53 -2
- package/dist/core/config/defaults.js +1 -1
- package/dist/core/schema/sandbox.js +1 -1
- package/dist/core/schema/toolchains.js +2 -2
- package/dist/guest/daemon/daemon.js +1 -1
- package/dist/guest/daemon/heartbeat.d.ts +11 -0
- package/dist/guest/daemon/heartbeat.js +59 -0
- package/dist/guest/daemon/idle-shutdown.js +2 -0
- package/dist/guest/entries/daemon.js +2 -0
- package/dist/guest/governor/core.d.ts +19 -1
- package/dist/guest/governor/core.js +23 -1
- package/dist/guest/governor/decide.d.ts +14 -0
- package/dist/guest/governor/decide.js +52 -7
- package/dist/guest/governor/governor.d.ts +4 -1
- package/dist/guest/governor/governor.js +9 -2
- package/dist/guest/toolchains/native/rust-extras.d.ts +7 -0
- package/dist/guest/toolchains/native/rust-extras.js +26 -3
- package/dist/guest/toolchains/plan.js +6 -4
- package/dist/host/channel/channel.d.ts +1 -1
- package/dist/host/channel/channel.js +10 -2
- package/dist/host/health/index.d.ts +1 -0
- package/dist/host/health/index.js +1 -0
- package/dist/host/health/leftovers.d.ts +3 -0
- package/dist/host/health/leftovers.js +40 -0
- package/dist/host/health/stale.d.ts +48 -0
- package/dist/host/health/stale.js +40 -0
- package/dist/host/launch/wsb-file.d.ts +6 -1
- package/dist/host/launch/wsb-file.js +9 -5
- package/dist/host/life/crash-report.d.ts +3 -1
- package/dist/host/life/crash-report.js +9 -3
- package/dist/host/life/index.d.ts +1 -0
- package/dist/host/life/index.js +1 -0
- package/dist/host/life/last-end.d.ts +28 -0
- package/dist/host/life/last-end.js +91 -0
- package/dist/host/life/life-record.d.ts +3 -0
- package/dist/host/life/life-record.js +2 -2
- package/dist/host/life/stop-events.d.ts +3 -1
- package/dist/host/life/stop-events.js +59 -0
- package/dist/host/life/stop-warning.js +3 -0
- package/dist/host/life/watchdog.d.ts +2 -0
- package/dist/host/life/watchdog.js +8 -2
- package/dist/host/sandbox/cleanup.d.ts +37 -0
- package/dist/host/sandbox/cleanup.js +82 -0
- package/dist/host/sandbox/index.d.ts +1 -0
- package/dist/host/sandbox/index.js +1 -0
- package/dist/host/sandbox/platform.js +1 -1
- package/dist/runtime/agent.js +28 -3
- package/dist/runtime/daemon.js +629 -479
- package/dist/runtime/db.js +28 -3
- package/dist/runtime/main.js +59 -11
- package/dist/runtime/netproxy.js +28 -3
- package/dist/runtime/tool.js +28 -3
- package/dist/runtime/view.js +28 -3
- package/dist/selftest/heavy-load.test.js +212 -14
- package/dist/selftest/live-terminal.test.js +4 -0
- package/dist/shared/channel/run-dir.d.ts +4 -1
- package/dist/shared/channel/run-dir.js +7 -1
- package/dist/shared/channel/schema.d.ts +24 -0
- package/dist/shared/channel/schema.js +22 -0
- package/dist/shared/memory/policy.d.ts +10 -0
- package/dist/shared/memory/policy.js +17 -1
- package/dist/shared/system.d.ts +1 -0
- package/dist/shared/system.js +1 -0
- package/dist/sync/watcher.js +10 -5
- package/dist/web/index.html +1 -1
- package/dist/web/{main-CWIF4VQI.js → main-GVZWWP4I.js} +63 -63
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/Program.cs +5 -1
- package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
- package/native/scripts/stop-events.ps1 +7 -3
- package/package.json +1 -1
- package/sandbox/csharp/MemGov.cs +58 -0
- package/schemas/config.schema.json +3 -3
- package/schemas/sandbox-config.schema.json +3 -3
|
@@ -100,6 +100,13 @@ const HEAVY = new Set([
|
|
|
100
100
|
'ld',
|
|
101
101
|
'ld.lld',
|
|
102
102
|
]);
|
|
103
|
+
/** Linkers: the biggest memory and disk users of a build, so they run one at a time from step 2. */
|
|
104
|
+
const LINKERS = new Set(['link', 'lld', 'lld-link', 'ld', 'ld.lld']);
|
|
105
|
+
export const isLinker = (name) => !isNeverTouched(name) && LINKERS.has(key(name));
|
|
106
|
+
/** Links that may run at once ("auto"): one per 6 GB of RAM, 1 to 4 (never more than the processors). */
|
|
107
|
+
export function linkerSlots(totalMb, cpus) {
|
|
108
|
+
return Math.max(1, Math.min(4, Math.max(1, Math.floor(cpus)), Math.floor(totalMb / 6144)));
|
|
109
|
+
}
|
|
103
110
|
export const isHeavyCompiler = (name) => !isNeverTouched(name) && HEAVY.has(key(name));
|
|
104
111
|
export const canPause = (name) => !isNeverTouched(name) && COMPILERS.has(key(name));
|
|
105
112
|
export const INITIAL_STATE = {
|
|
@@ -109,6 +116,7 @@ export const INITIAL_STATE = {
|
|
|
109
116
|
memLow: [],
|
|
110
117
|
throttled: [],
|
|
111
118
|
trimmed: [],
|
|
119
|
+
ioLowered: {},
|
|
112
120
|
cpuRate: 100,
|
|
113
121
|
lastCommitMb: 0,
|
|
114
122
|
lastAt: 0,
|
|
@@ -172,6 +180,10 @@ export function decide(prev, sample, nowMs, profile, maxCompilers = null) {
|
|
|
172
180
|
calm = 0;
|
|
173
181
|
}
|
|
174
182
|
}
|
|
183
|
+
// vGPU on (the gentle profile): a GPU game and a build together have killed the whole VM, so while any heavy compiler
|
|
184
|
+
// or linker runs the dial is at step 2 at least. The game itself is never touched (only build programs are).
|
|
185
|
+
if (profile === 'gentle' && step < 2 && sample.procs.some((p) => isHeavyCompiler(p.name)))
|
|
186
|
+
step = 2;
|
|
175
187
|
const actions = [];
|
|
176
188
|
const present = new Set(sample.procs.map((p) => p.pid));
|
|
177
189
|
const keep = (pids) => pids.filter((pid) => present.has(pid));
|
|
@@ -198,6 +210,25 @@ export function decide(prev, sample, nowMs, profile, maxCompilers = null) {
|
|
|
198
210
|
else
|
|
199
211
|
lowered[p.pid] = target;
|
|
200
212
|
}
|
|
213
|
+
// I/O priority of compilers and linkers: low while their CPU priority is below normal, very low from step 3 (a PC
|
|
214
|
+
// whose disks are overloaded asks for step 4). Restored to normal when they are released.
|
|
215
|
+
const ioLowered = {};
|
|
216
|
+
for (const [pid, to] of Object.entries(prev.ioLowered))
|
|
217
|
+
if (present.has(Number(pid)))
|
|
218
|
+
ioLowered[Number(pid)] = to;
|
|
219
|
+
for (const p of sample.procs) {
|
|
220
|
+
if (!canPause(p.name))
|
|
221
|
+
continue;
|
|
222
|
+
const cpu = lowered[p.pid] ?? 'normal';
|
|
223
|
+
const target = step >= 3 ? 'verylow' : cpu !== 'normal' ? 'low' : 'normal';
|
|
224
|
+
const was = ioLowered[p.pid] ?? 'normal';
|
|
225
|
+
if (was !== target)
|
|
226
|
+
actions.push({ kind: 'io', pid: p.pid, name: p.name, to: target });
|
|
227
|
+
if (target === 'normal')
|
|
228
|
+
delete ioLowered[p.pid];
|
|
229
|
+
else
|
|
230
|
+
ioLowered[p.pid] = target;
|
|
231
|
+
}
|
|
201
232
|
// Memory priority: low from step 1, so Windows trims the builds' pages before the agents'.
|
|
202
233
|
const lowMemory = step >= 1;
|
|
203
234
|
for (const p of sample.procs) {
|
|
@@ -224,20 +255,33 @@ export function decide(prev, sample, nowMs, profile, maxCompilers = null) {
|
|
|
224
255
|
let allowed = compilersAllowed(step, heavy.length);
|
|
225
256
|
// The cap is a memory measure: with memory to spare, Windows shares the CPU (the priorities above) and no compiler
|
|
226
257
|
// waits. A waiting rustc also holds cargo's job tokens, which stalls the whole build.
|
|
227
|
-
if (maxCompilers !== null && step >=
|
|
258
|
+
if (maxCompilers !== null && step >= 1)
|
|
228
259
|
allowed = Math.min(allowed, Math.max(1, maxCompilers));
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
260
|
+
const turn = heavy.length > 0 ? Math.floor(nowMs / ROTATE_MS) % heavy.length : 0;
|
|
261
|
+
const order = [...heavy.slice(turn), ...heavy.slice(0, turn)];
|
|
262
|
+
let waiting = order.slice(Math.min(allowed, order.length));
|
|
263
|
+
// From step 2 the linkers that would run go one at a time, whatever the cap: a link reads and writes the most.
|
|
264
|
+
if (step >= 2) {
|
|
265
|
+
const waitingPids = new Set(waiting.map((p) => p.pid));
|
|
266
|
+
let linking = 0;
|
|
267
|
+
for (const p of order)
|
|
268
|
+
if (isLinker(p.name) && !waitingPids.has(p.pid) && ++linking > 1) {
|
|
269
|
+
waiting = [...waiting, p];
|
|
270
|
+
waitingPids.add(p.pid);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (waiting.length > 0) {
|
|
274
|
+
const waitingPids = new Set(waiting.map((p) => p.pid));
|
|
275
|
+
for (const p of waiting) {
|
|
233
276
|
actions.push({ kind: 'hold', pid: p.pid, name: p.name, ms: HOLD_MS });
|
|
234
277
|
if (step >= 3 && !trimmed.has(p.pid)) {
|
|
235
278
|
actions.push({ kind: 'trim', pid: p.pid, name: p.name });
|
|
236
279
|
trimmed.add(p.pid);
|
|
237
280
|
}
|
|
238
281
|
}
|
|
239
|
-
for (const p of order
|
|
240
|
-
|
|
282
|
+
for (const p of order)
|
|
283
|
+
if (!waitingPids.has(p.pid))
|
|
284
|
+
trimmed.delete(p.pid);
|
|
241
285
|
}
|
|
242
286
|
else
|
|
243
287
|
trimmed.clear();
|
|
@@ -250,6 +294,7 @@ export function decide(prev, sample, nowMs, profile, maxCompilers = null) {
|
|
|
250
294
|
memLow: [...memLow],
|
|
251
295
|
throttled: [...throttled],
|
|
252
296
|
trimmed: [...trimmed],
|
|
297
|
+
ioLowered,
|
|
253
298
|
cpuRate,
|
|
254
299
|
lastCommitMb: usedMb,
|
|
255
300
|
lastAt: nowMs,
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import { type GovernorSnapshot } from './core.js';
|
|
1
2
|
/** The daemon's governor: on unless sandbox.pressureResponse is off. */
|
|
2
3
|
export declare class Governor {
|
|
3
4
|
private core;
|
|
4
5
|
describe(): string;
|
|
5
6
|
start(): void;
|
|
6
7
|
/** The host's watchdog: this PC is low (or nearly out of memory) for a while. */
|
|
7
|
-
hostPressure(seconds: number, reason: string, level?: 'slow' | 'severe'): void;
|
|
8
|
+
hostPressure(seconds: number, reason: string, level?: 'slow' | 'severe', diskQueue?: number): void;
|
|
9
|
+
/** The newest numbers, for the heartbeat (null when the governor is off or has not sampled yet). */
|
|
10
|
+
snapshot(): GovernorSnapshot | null;
|
|
8
11
|
}
|
|
9
12
|
export declare const governor: Governor;
|
|
@@ -72,6 +72,9 @@ class MemGovProcess {
|
|
|
72
72
|
setMemoryPriority(pid, low) {
|
|
73
73
|
this.send('memprio ' + pid + ' ' + (low ? 'low' : 'normal'));
|
|
74
74
|
}
|
|
75
|
+
setIoPriority(pid, to) {
|
|
76
|
+
this.send('iopriority ' + pid + ' ' + to);
|
|
77
|
+
}
|
|
75
78
|
throttle(pid) {
|
|
76
79
|
this.send('throttle ' + pid);
|
|
77
80
|
}
|
|
@@ -143,8 +146,12 @@ export class Governor {
|
|
|
143
146
|
proc.start();
|
|
144
147
|
}
|
|
145
148
|
/** The host's watchdog: this PC is low (or nearly out of memory) for a while. */
|
|
146
|
-
hostPressure(seconds, reason, level = 'slow') {
|
|
147
|
-
this.core?.hostPressure(seconds, reason, level);
|
|
149
|
+
hostPressure(seconds, reason, level = 'slow', diskQueue) {
|
|
150
|
+
this.core?.hostPressure(seconds, reason, level, diskQueue);
|
|
151
|
+
}
|
|
152
|
+
/** The newest numbers, for the heartbeat (null when the governor is off or has not sampled yet). */
|
|
153
|
+
snapshot() {
|
|
154
|
+
return this.core?.snapshot ?? null;
|
|
148
155
|
}
|
|
149
156
|
}
|
|
150
157
|
export const governor = new Governor();
|
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { Toolchains } from '../../../core/types.js';
|
|
2
2
|
import { type ToolEnv } from '../tool-env.js';
|
|
3
|
+
/**
|
|
4
|
+
* "auto" (the default): build on the sandbox's own disk when C: has 25 GB free, else keep the target next to the
|
|
5
|
+
* project, which writes every object file through the mapped workspace to this PC's disk. Other values stay as set.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveTargetDir(rust: Toolchains['rust'], free?: number, note?: (text: string) => void): Toolchains['rust'];
|
|
8
|
+
/** One summary line for the agent: where cargo builds. */
|
|
9
|
+
export declare function targetSummary(rust: Toolchains['rust']): string;
|
|
3
10
|
/** "separate": tools\shims\cargo\cargo.exe in front of the real cargo, which it runs with the checkout's target dir. */
|
|
4
11
|
export declare function cargoShim(env: ToolEnv, rust: Toolchains['rust']): void;
|
|
5
12
|
/**
|
|
@@ -7,7 +7,31 @@ import { execOutput } from '../../runtime/process.js';
|
|
|
7
7
|
import { ensureCompiled } from '../../runtime/csc.js';
|
|
8
8
|
import { arch, tool } from '../tool-env.js';
|
|
9
9
|
import { wanted } from '../detect.js';
|
|
10
|
-
import { cargoJobs,
|
|
10
|
+
import { cargoJobs, chooseTargetDir } from '../../../shared/memory/index.js';
|
|
11
|
+
import { linkerSlots } from '../../governor/decide.js';
|
|
12
|
+
import { warn } from '../../runtime/log.js';
|
|
13
|
+
import { freeBytes } from '../persisted.js';
|
|
14
|
+
/**
|
|
15
|
+
* "auto" (the default): build on the sandbox's own disk when C: has 25 GB free, else keep the target next to the
|
|
16
|
+
* project, which writes every object file through the mapped workspace to this PC's disk. Other values stay as set.
|
|
17
|
+
*/
|
|
18
|
+
export function resolveTargetDir(rust, free = freeBytes((process.env.SystemDrive ?? 'C:') + '\\'), note = warn) {
|
|
19
|
+
if (rust.targetDir !== 'auto')
|
|
20
|
+
return rust;
|
|
21
|
+
const chosen = chooseTargetDir(rust.targetDir, free);
|
|
22
|
+
if (chosen.warning)
|
|
23
|
+
note(chosen.warning);
|
|
24
|
+
return { ...rust, targetDir: chosen.targetDir };
|
|
25
|
+
}
|
|
26
|
+
/** One summary line for the agent: where cargo builds. */
|
|
27
|
+
export function targetSummary(rust) {
|
|
28
|
+
const target = cargoTarget(rust.targetDir);
|
|
29
|
+
if (target)
|
|
30
|
+
return 'rust builds: ' + target + ' (CARGO_TARGET_DIR)';
|
|
31
|
+
return rust.targetDir === 'shared'
|
|
32
|
+
? 'rust builds: target\\ in each project (shared with this PC)'
|
|
33
|
+
: 'rust builds: target\\sandbox in each project (on the mapped workspace)';
|
|
34
|
+
}
|
|
11
35
|
/**
|
|
12
36
|
* Where cargo builds: shared with the PC's target\, one on the sandbox's disk, or a path. "separate" sets no variable:
|
|
13
37
|
* the cargo shim (sandbox/csharp/CargoShim.cs) gives every checkout its own target\sandbox, so git worktrees of several
|
|
@@ -40,8 +64,7 @@ export function linkGate(env, rust, host) {
|
|
|
40
64
|
return;
|
|
41
65
|
const gate = path.join(mkdirp(tool('shims')), 'linkgate.exe');
|
|
42
66
|
fs.copyFileSync(ensureCompiled('linkgate'), gate);
|
|
43
|
-
const
|
|
44
|
-
const slots = rust.linkSlots === 'auto' ? linkSlotsFor(memoryGb) : rust.linkSlots;
|
|
67
|
+
const slots = rust.linkSlots === 'auto' ? linkerSlots(os.totalmem() / 1024 ** 2, os.cpus().length) : rust.linkSlots;
|
|
45
68
|
env.vars.AGENT_LINK_SLOTS = String(slots);
|
|
46
69
|
env.vars.AGENT_REAL_LINKER = gcc;
|
|
47
70
|
env.vars.CARGO_TARGET_X86_64_PC_WINDOWS_GNU_LINKER = gate;
|
|
@@ -32,7 +32,7 @@ import { setupFlutter } from './mobile/flutter.js';
|
|
|
32
32
|
import { setupDart } from './mobile/dart.js';
|
|
33
33
|
import { setupReactNative } from './mobile/react-native.js';
|
|
34
34
|
import { setupKotlin } from './languages/kotlin.js';
|
|
35
|
-
import { rustHost, rustCaches, cargoDefaults, cargoShim, linkGate } from './native/rust-extras.js';
|
|
35
|
+
import { rustHost, rustCaches, cargoDefaults, cargoShim, linkGate, resolveTargetDir, targetSummary, } from './native/rust-extras.js';
|
|
36
36
|
import { setupPythonTools } from './languages/python-tools.js';
|
|
37
37
|
export function plan(config, versions, o) {
|
|
38
38
|
const apps = Boolean(o.apps && (o.apps.install.length || Object.keys(o.apps.custom).length));
|
|
@@ -92,11 +92,13 @@ export function plan(config, versions, o) {
|
|
|
92
92
|
after: ['C/C++', 'MSVC', 'Visual Studio', 'Unreal'],
|
|
93
93
|
install: async (env) => {
|
|
94
94
|
await setupRust(env, { ...config.rust, host: rustHost(config) }, o.refresh);
|
|
95
|
-
|
|
95
|
+
const rust = resolveTargetDir(config.rust);
|
|
96
|
+
Object.assign(env.vars, rustCaches(rust));
|
|
97
|
+
env.summary.push(targetSummary(rust));
|
|
96
98
|
if (!dryRun) {
|
|
97
99
|
cargoDefaults(rustHost(config));
|
|
98
|
-
cargoShim(env,
|
|
99
|
-
linkGate(env,
|
|
100
|
+
cargoShim(env, rust);
|
|
101
|
+
linkGate(env, rust, rustHost(config));
|
|
100
102
|
}
|
|
101
103
|
},
|
|
102
104
|
},
|
|
@@ -51,7 +51,7 @@ export declare class SandboxChannel {
|
|
|
51
51
|
/** Ends a running session (agent or shell) and everything it started; waits for the sandbox's answer. */
|
|
52
52
|
closeSession(sessionId: string): Promise<GuestResponse>;
|
|
53
53
|
/** The PC is low on memory or CPU: the sandbox slows its builds down for `seconds`. Never waits for an answer. */
|
|
54
|
-
pressure(reason: string, seconds: number, level?: 'slow' | 'severe'): void;
|
|
54
|
+
pressure(reason: string, seconds: number, level?: 'slow' | 'severe', diskQueue?: number): void;
|
|
55
55
|
ping(): Promise<GuestResponse>;
|
|
56
56
|
runningSessions(): SessionStatus[];
|
|
57
57
|
/** Running sessions of the sandbox that is running now (its launch id), newest last, capped. */
|
|
@@ -192,8 +192,16 @@ export class SandboxChannel {
|
|
|
192
192
|
return this.waitForResponse(requestId, 30000);
|
|
193
193
|
}
|
|
194
194
|
/** The PC is low on memory or CPU: the sandbox slows its builds down for `seconds`. Never waits for an answer. */
|
|
195
|
-
pressure(reason, seconds, level = 'slow') {
|
|
196
|
-
this.send({
|
|
195
|
+
pressure(reason, seconds, level = 'slow', diskQueue) {
|
|
196
|
+
this.send({
|
|
197
|
+
kind: 'pressure',
|
|
198
|
+
id: newId(),
|
|
199
|
+
reason,
|
|
200
|
+
seconds,
|
|
201
|
+
level,
|
|
202
|
+
...(diskQueue === undefined ? {} : { diskQueue }),
|
|
203
|
+
createdAt: now(),
|
|
204
|
+
});
|
|
197
205
|
}
|
|
198
206
|
async ping() {
|
|
199
207
|
const requestId = newId();
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type SandboxFacts } from './stale.js';
|
|
2
|
+
/** The facts for judgeSandbox, from the process list, our heartbeat and our life record. `ping`: ask the daemon too. */
|
|
3
|
+
export declare function readSandboxFacts(storageRoot: string, dataPath: string, ping: boolean): Promise<SandboxFacts>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Reads what is needed to judge "are the Windows Sandbox programs ours, alive, or left over?" (see judgeSandbox).
|
|
2
|
+
import { LifeRecord, readHeartbeat } from '../life/index.js';
|
|
3
|
+
import { SandboxChannel } from '../channel/index.js';
|
|
4
|
+
import { listProcesses, sessionOf } from '../sharing/index.js';
|
|
5
|
+
import { judgeSandbox } from './stale.js';
|
|
6
|
+
const PING_WAIT_MS = 5000;
|
|
7
|
+
/** The facts for judgeSandbox, from the process list, our heartbeat and our life record. `ping`: ask the daemon too. */
|
|
8
|
+
export async function readSandboxFacts(storageRoot, dataPath, ping) {
|
|
9
|
+
const rows = listProcesses();
|
|
10
|
+
const life = new LifeRecord(storageRoot, dataPath).read();
|
|
11
|
+
const heartbeat = readHeartbeat(dataPath);
|
|
12
|
+
const nowMs = Date.now();
|
|
13
|
+
// A heartbeat from before this launch belongs to an earlier sandbox.
|
|
14
|
+
const mine = heartbeat && (!life || Date.parse(heartbeat.at) >= Date.parse(life.started) - 60000) ? heartbeat : null;
|
|
15
|
+
const base = {
|
|
16
|
+
rows,
|
|
17
|
+
mySession: sessionOf(rows),
|
|
18
|
+
heartbeatAgeMs: mine ? Math.max(0, nowMs - Date.parse(mine.at)) : null,
|
|
19
|
+
lifeEnded: Boolean(life?.ended),
|
|
20
|
+
startedAgeMs: life ? Math.max(0, nowMs - Date.parse(life.started)) : null,
|
|
21
|
+
answers: null,
|
|
22
|
+
};
|
|
23
|
+
if (!ping || judgeSandbox(base).state !== 'leftover')
|
|
24
|
+
return base;
|
|
25
|
+
return { ...base, answers: await answers(dataPath) };
|
|
26
|
+
}
|
|
27
|
+
/** Does the sandbox's daemon answer a ping within a few seconds? */
|
|
28
|
+
async function answers(dataPath) {
|
|
29
|
+
try {
|
|
30
|
+
const reply = await Promise.race([
|
|
31
|
+
new SandboxChannel(dataPath).ping().then((r) => r.ok),
|
|
32
|
+
new Promise((resolve) => setTimeout(() => resolve(false), PING_WAIT_MS).unref()),
|
|
33
|
+
]);
|
|
34
|
+
return reply;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// No answer, or a channel that cannot be used: it does not answer.
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -43,3 +43,51 @@ export declare function orphanWatchers(infos: readonly WatcherInfo[], o: {
|
|
|
43
43
|
}): WatcherInfo[];
|
|
44
44
|
/** null when a small file can be created in the folder, else the reason (the file is removed again). */
|
|
45
45
|
export declare function writeProblem(dir: string): string | null;
|
|
46
|
+
/** The Windows Sandbox programs of the user that a dead sandbox leaves behind (never the VM's own vmwp.exe). */
|
|
47
|
+
export declare const LEFTOVER_IMAGES: readonly ["windowssandbox.exe", "windowssandboxclient.exe", "windowssandboxremotesession.exe", "windowssandboxserver.exe"];
|
|
48
|
+
/** A heartbeat this recent means the sandbox is alive (the sandbox writes one every 10 seconds). */
|
|
49
|
+
export declare const ALIVE_MS: number;
|
|
50
|
+
/** A heartbeat older than this, with the sandbox's programs still there, means the sandbox is gone. */
|
|
51
|
+
export declare const GONE_MS: number;
|
|
52
|
+
/** A sandbox started this recently may not have written a heartbeat yet (a first boot takes minutes). */
|
|
53
|
+
export declare const STARTING_MS: number;
|
|
54
|
+
/** What is known about the Windows Sandbox programs on this PC and our sandbox (read by the caller). */
|
|
55
|
+
export interface SandboxFacts {
|
|
56
|
+
/** Every process of every session (tasklist). */
|
|
57
|
+
readonly rows: readonly TaskRow[];
|
|
58
|
+
/** The Windows session this program runs in; null when unknown (then nothing is ever ended). */
|
|
59
|
+
readonly mySession: number | null;
|
|
60
|
+
/** Age of our sandbox's last heartbeat from this run; null when there is none. */
|
|
61
|
+
readonly heartbeatAgeMs: number | null;
|
|
62
|
+
/** Our life record says the last sandbox ended (the watcher saw it go). */
|
|
63
|
+
readonly lifeEnded: boolean;
|
|
64
|
+
/** Age of our last launch; null when unknown. */
|
|
65
|
+
readonly startedAgeMs: number | null;
|
|
66
|
+
/** Did the sandbox answer a ping just now? null: not asked. */
|
|
67
|
+
readonly answers: boolean | null;
|
|
68
|
+
}
|
|
69
|
+
export type SandboxState =
|
|
70
|
+
/** No Windows Sandbox program runs. */
|
|
71
|
+
'none'
|
|
72
|
+
/** Ours, and alive (or not known to be gone). */
|
|
73
|
+
| 'running'
|
|
74
|
+
/** Ours, started a moment ago. */
|
|
75
|
+
| 'starting'
|
|
76
|
+
/** Programs of a sandbox that is gone: safe to end after asking. */
|
|
77
|
+
| 'leftover'
|
|
78
|
+
/** Another Windows session's sandbox: never touched. */
|
|
79
|
+
| 'foreign';
|
|
80
|
+
export interface SandboxJudgement {
|
|
81
|
+
readonly state: SandboxState;
|
|
82
|
+
/** The programs of this user's session that cleaning up would end. */
|
|
83
|
+
readonly pids: readonly number[];
|
|
84
|
+
readonly images: readonly string[];
|
|
85
|
+
/** Why, in plain words (for the question). */
|
|
86
|
+
readonly why: string;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Is the Windows Sandbox on this PC ours and alive, ours and gone, or someone else's? (pure) It is "leftover" only
|
|
90
|
+
* with positive evidence: our own programs in our own session, no answer, and either the watcher saw the sandbox end
|
|
91
|
+
* or its heartbeat stopped long ago. No heartbeat at all (an older sandbox) counts as running.
|
|
92
|
+
*/
|
|
93
|
+
export declare function judgeSandbox(f: SandboxFacts): SandboxJudgement;
|
|
@@ -118,3 +118,43 @@ export function writeProblem(dir) {
|
|
|
118
118
|
fs.rmSync(probe, { force: true });
|
|
119
119
|
}
|
|
120
120
|
}
|
|
121
|
+
/** The Windows Sandbox programs of the user that a dead sandbox leaves behind (never the VM's own vmwp.exe). */
|
|
122
|
+
export const LEFTOVER_IMAGES = [
|
|
123
|
+
'windowssandbox.exe',
|
|
124
|
+
'windowssandboxclient.exe',
|
|
125
|
+
'windowssandboxremotesession.exe',
|
|
126
|
+
'windowssandboxserver.exe',
|
|
127
|
+
];
|
|
128
|
+
/** A heartbeat this recent means the sandbox is alive (the sandbox writes one every 10 seconds). */
|
|
129
|
+
export const ALIVE_MS = 45 * 1000;
|
|
130
|
+
/** A heartbeat older than this, with the sandbox's programs still there, means the sandbox is gone. */
|
|
131
|
+
export const GONE_MS = 2 * 60 * 1000;
|
|
132
|
+
/** A sandbox started this recently may not have written a heartbeat yet (a first boot takes minutes). */
|
|
133
|
+
export const STARTING_MS = 10 * 60 * 1000;
|
|
134
|
+
const isLeftoverImage = (image) => LEFTOVER_IMAGES.includes(image.toLowerCase());
|
|
135
|
+
/**
|
|
136
|
+
* Is the Windows Sandbox on this PC ours and alive, ours and gone, or someone else's? (pure) It is "leftover" only
|
|
137
|
+
* with positive evidence: our own programs in our own session, no answer, and either the watcher saw the sandbox end
|
|
138
|
+
* or its heartbeat stopped long ago. No heartbeat at all (an older sandbox) counts as running.
|
|
139
|
+
*/
|
|
140
|
+
export function judgeSandbox(f) {
|
|
141
|
+
const sandbox = f.rows.filter((r) => isLeftoverImage(r.image));
|
|
142
|
+
const mine = f.mySession === null ? [] : sandbox.filter((r) => r.session === f.mySession);
|
|
143
|
+
const none = (state, why) => ({ state, pids: [], images: [], why });
|
|
144
|
+
if (sandbox.length === 0)
|
|
145
|
+
return none('none', 'No Windows Sandbox program is running.');
|
|
146
|
+
if (mine.length === 0)
|
|
147
|
+
return none('foreign', 'The Windows Sandbox that runs belongs to another Windows session.');
|
|
148
|
+
const pids = mine.map((r) => r.pid);
|
|
149
|
+
const images = [...new Set(mine.map((r) => r.image))];
|
|
150
|
+
const found = (state, why) => ({ state, pids, images, why });
|
|
151
|
+
if (f.answers === true || (f.heartbeatAgeMs !== null && f.heartbeatAgeMs <= ALIVE_MS))
|
|
152
|
+
return found('running', 'The sandbox is working.');
|
|
153
|
+
if (f.startedAgeMs !== null && f.startedAgeMs <= STARTING_MS && !f.lifeEnded)
|
|
154
|
+
return found('starting', 'The sandbox started a moment ago.');
|
|
155
|
+
if (f.lifeEnded)
|
|
156
|
+
return found('leftover', 'The last sandbox has ended, but its Windows programs are still there.');
|
|
157
|
+
if (f.heartbeatAgeMs !== null && f.heartbeatAgeMs > GONE_MS)
|
|
158
|
+
return found('leftover', 'The sandbox has not shown a sign of life for ' + Math.round(f.heartbeatAgeMs / 60000) + ' minutes.');
|
|
159
|
+
return found('running', 'A sandbox is running (there is no sign that it is gone).');
|
|
160
|
+
}
|
|
@@ -18,7 +18,12 @@ export interface MemoryDecision {
|
|
|
18
18
|
readonly commitAvailableMb: number | null;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
|
-
*
|
|
21
|
+
* GPU-PV (vGPU) needs room of its own on this PC: the GPU's shared memory for the sandbox comes out of this PC's RAM,
|
|
22
|
+
* and a GPU game plus heavy builds with vGPU on has killed the VM when the PC had no RAM left for it.
|
|
23
|
+
*/
|
|
24
|
+
export declare const VGPU_RESERVE_MB = 4096;
|
|
25
|
+
/**
|
|
26
|
+
* The most memory this PC can keep in RAM for the sandbox (pure): its RAM minus max(reserveHostMb, 25 %), and 4 GB
|
|
22
27
|
* more with vGPU. Windows Sandbox's memory is this PC's memory taken as needed; beyond what the PC can keep resident it
|
|
23
28
|
* is paged to this PC's disk while the sandbox pages to its own, and that is what can freeze the whole PC.
|
|
24
29
|
*/
|
|
@@ -11,10 +11,13 @@ export const GUEST = {
|
|
|
11
11
|
const MB = 1048576;
|
|
12
12
|
const MIN_AUTO_MEMORY_MB = 4096;
|
|
13
13
|
const roundDown = (mb) => Math.floor(mb / 1024) * 1024;
|
|
14
|
-
/** GPU-PV (vGPU) needs room of its own on this PC: memory the graphics driver pins for the sandbox. */
|
|
15
|
-
const VGPU_RESERVE_MB = 2048;
|
|
16
14
|
/**
|
|
17
|
-
*
|
|
15
|
+
* GPU-PV (vGPU) needs room of its own on this PC: the GPU's shared memory for the sandbox comes out of this PC's RAM,
|
|
16
|
+
* and a GPU game plus heavy builds with vGPU on has killed the VM when the PC had no RAM left for it.
|
|
17
|
+
*/
|
|
18
|
+
export const VGPU_RESERVE_MB = 4096;
|
|
19
|
+
/**
|
|
20
|
+
* The most memory this PC can keep in RAM for the sandbox (pure): its RAM minus max(reserveHostMb, 25 %), and 4 GB
|
|
18
21
|
* more with vGPU. Windows Sandbox's memory is this PC's memory taken as needed; beyond what the PC can keep resident it
|
|
19
22
|
* is paged to this PC's disk while the sandbox pages to its own, and that is what can freeze the whole PC.
|
|
20
23
|
*/
|
|
@@ -46,8 +49,9 @@ export function residentNote(asked, given) {
|
|
|
46
49
|
*/
|
|
47
50
|
export function decideMemory(sandbox, freeMb, totalMb, commitAvailableMb) {
|
|
48
51
|
const shareMb = Math.floor((totalMb * sandbox.maxMemoryPercent) / 100);
|
|
49
|
-
const
|
|
50
|
-
const
|
|
52
|
+
const reserveMb = sandbox.reserveHostMb + (sandbox.vGpu ? VGPU_RESERVE_MB : 0);
|
|
53
|
+
const byFree = freeMb - reserveMb;
|
|
54
|
+
const byCommit = commitAvailableMb === null ? Infinity : commitAvailableMb - reserveMb;
|
|
51
55
|
const candidates = [
|
|
52
56
|
['max', sandbox.maxMemoryMb],
|
|
53
57
|
['share', shareMb],
|
|
@@ -11,6 +11,8 @@ export interface CrashExtra {
|
|
|
11
11
|
readonly hostMemoryNow?: string;
|
|
12
12
|
/** The frozen sandbox's launch had vGPU on (read from its .wsb). */
|
|
13
13
|
readonly vgpu?: boolean;
|
|
14
|
+
/** The sandbox's last heartbeat as a line (see heartbeatLine): what it was doing inside. */
|
|
15
|
+
readonly lastInside?: string;
|
|
14
16
|
}
|
|
15
17
|
/** "This PC stopped responding at X and was restarted at Y (n minutes later)", or null when there was no long gap. */
|
|
16
18
|
export declare function freezeLine(cause: StopCause, lastSeen: string): string | null;
|
|
@@ -33,4 +35,4 @@ export declare function newestCrashReport(storageRoot: string): {
|
|
|
33
35
|
/**
|
|
34
36
|
* When the sandbox ended in a crash (its own, this PC's, power, low memory): writes the report and returns its path.
|
|
35
37
|
*/
|
|
36
|
-
export declare function saveCrashReport(storageRoot: string, dataPath: string, lastSeen: string): string | null;
|
|
38
|
+
export declare function saveCrashReport(storageRoot: string, dataPath: string, lastSeen: string, known?: StopCause | null): string | null;
|
|
@@ -6,11 +6,14 @@ import { assertNoLinks, safeText } from '../../core/util/index.js';
|
|
|
6
6
|
import { vgpuWarning } from '../launch/risk-notes.js';
|
|
7
7
|
import { hostMemoryLine, readHostMemory } from './host-memory.js';
|
|
8
8
|
import { hostDataFolder, hostDataRoot } from '../storage/index.js';
|
|
9
|
+
import { heartbeatLine, readHeartbeat } from './last-end.js';
|
|
9
10
|
import { bugcheckCode, explainStop, readStopEvents } from './stop-events.js';
|
|
10
11
|
const KEEP_REPORTS = 20;
|
|
11
12
|
const MEMORY_TAIL_BYTES = 16 * 1024;
|
|
12
13
|
/** A gap longer than this between "last seen running" and the restart means the PC stopped responding first. */
|
|
13
14
|
const FREEZE_GAP_MS = 5 * 60 * 1000;
|
|
15
|
+
/** The ends that get a crash report (not updates, restarts and sleep: those are plain). */
|
|
16
|
+
const REPORTED = ['sandboxCrash', 'crash', 'memory', 'gpu', 'hyperv', 'vanished'];
|
|
14
17
|
function crashDir(storageRoot) {
|
|
15
18
|
return path.join(storageRoot, 'logs', 'crashes');
|
|
16
19
|
}
|
|
@@ -170,6 +173,7 @@ export function crashReportText(cause, events, memoryLog, lastSeen, extra = {})
|
|
|
170
173
|
...(hostLines.length ? hostLines : [' (none: the background sync was not running)']),
|
|
171
174
|
...(extra.hostMemoryNow ? [' after the restart: ' + extra.hostMemoryNow] : []),
|
|
172
175
|
'',
|
|
176
|
+
...(extra.lastInside ? [safeText(extra.lastInside), ''] : []),
|
|
173
177
|
"The sandbox's memory log, last lines (C:\\AgentData\\logs\\memory-*.log inside it):",
|
|
174
178
|
...memoryLines,
|
|
175
179
|
'',
|
|
@@ -228,10 +232,10 @@ function memoryLogTail(dataPath) {
|
|
|
228
232
|
/**
|
|
229
233
|
* When the sandbox ended in a crash (its own, this PC's, power, low memory): writes the report and returns its path.
|
|
230
234
|
*/
|
|
231
|
-
export function saveCrashReport(storageRoot, dataPath, lastSeen) {
|
|
235
|
+
export function saveCrashReport(storageRoot, dataPath, lastSeen, known = null) {
|
|
232
236
|
const events = readStopEvents(new Date(Date.parse(lastSeen) - 2 * 60 * 1000), storageRoot);
|
|
233
|
-
const cause = explainStop(events);
|
|
234
|
-
if (!cause || !
|
|
237
|
+
const cause = explainStop(events) ?? known;
|
|
238
|
+
if (!cause || !REPORTED.includes(cause.kind))
|
|
235
239
|
return null;
|
|
236
240
|
const dir = crashDir(storageRoot);
|
|
237
241
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -239,9 +243,11 @@ export function saveCrashReport(storageRoot, dataPath, lastSeen) {
|
|
|
239
243
|
const since = new Date(Date.parse(lastSeen) - 10 * 60 * 1000);
|
|
240
244
|
const windir = process.env.SystemRoot ?? 'C:\\Windows';
|
|
241
245
|
const memoryNow = readHostMemory(storageRoot);
|
|
246
|
+
const heartbeat = readHeartbeat(dataPath);
|
|
242
247
|
fs.writeFileSync(file, crashReportText(cause, events, memoryLogTail(dataPath), lastSeen, {
|
|
243
248
|
hostMemoryLog: hostMemoryTail(storageRoot, dataPath),
|
|
244
249
|
vgpu: launchHadVgpu(storageRoot, lastSeen),
|
|
250
|
+
...(heartbeat ? { lastInside: heartbeatLine(heartbeat) } : {}),
|
|
245
251
|
dumps: findDumps([path.join(windir, 'Minidump'), windir], since),
|
|
246
252
|
...(memoryNow ? { hostMemoryNow: hostMemoryLine(memoryNow) } : {}),
|
|
247
253
|
}));
|
|
@@ -4,6 +4,7 @@ export * from './stop-warning.js';
|
|
|
4
4
|
export * from './keep-awake.js';
|
|
5
5
|
export * from './windows-update.js';
|
|
6
6
|
export * from './crash-report.js';
|
|
7
|
+
export * from './last-end.js';
|
|
7
8
|
export * from './session-ledger.js';
|
|
8
9
|
export * from './host-memory.js';
|
|
9
10
|
export * from './watchdog.js';
|
package/dist/host/life/index.js
CHANGED
|
@@ -4,6 +4,7 @@ export * from './stop-warning.js';
|
|
|
4
4
|
export * from './keep-awake.js';
|
|
5
5
|
export * from './windows-update.js';
|
|
6
6
|
export * from './crash-report.js';
|
|
7
|
+
export * from './last-end.js';
|
|
7
8
|
export * from './session-ledger.js';
|
|
8
9
|
export * from './host-memory.js';
|
|
9
10
|
export * from './watchdog.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Heartbeat } from '../../shared/channel/index.js';
|
|
2
|
+
import type { Life } from './life-record.js';
|
|
3
|
+
import type { StopCause } from './stop-events.js';
|
|
4
|
+
/** How the last sandbox ended, when nobody closed it. */
|
|
5
|
+
export interface LastEnd {
|
|
6
|
+
/** "ended unexpectedly 21:31: this PC's disks were overloaded (queue 40)". */
|
|
7
|
+
readonly text: string;
|
|
8
|
+
readonly at: string;
|
|
9
|
+
}
|
|
10
|
+
/** The sandbox's last heartbeat, or null (none written, damaged, or reached through a link). */
|
|
11
|
+
export declare function readHeartbeat(dataPath: string): Heartbeat | null;
|
|
12
|
+
/** One line of what the sandbox was doing at its last heartbeat (pure; guest text goes through safeText). */
|
|
13
|
+
export declare function heartbeatLine(hb: Heartbeat): string;
|
|
14
|
+
/** The likeliest reason, from the last heartbeat (pure). */
|
|
15
|
+
export declare function guessReason(hb: Heartbeat | null): string;
|
|
16
|
+
/**
|
|
17
|
+
* The sandbox went and nothing was logged (pure): its last heartbeat is fresh, from this run, says it was working
|
|
18
|
+
* (a session running, or builds slowed down) and does not say it was shutting down in order, and nobody stopped it on
|
|
19
|
+
* purpose (`sandbox stop`, a restart). The guest cannot tell a closed window from a crash, so the wording says both.
|
|
20
|
+
*/
|
|
21
|
+
export declare function vanishedCause(hb: Heartbeat | null, startedIso: string, closedOnPurpose: boolean, endedAtMs: number): StopCause | null;
|
|
22
|
+
/**
|
|
23
|
+
* How the last sandbox ended (pure): null when it was closed normally, or is still running. `heartbeat` only counts
|
|
24
|
+
* when it was written during that run.
|
|
25
|
+
*/
|
|
26
|
+
export declare function lastEndOf(life: Life | null, heartbeat: Heartbeat | null, running: boolean): LastEnd | null;
|
|
27
|
+
/** The status page's "Last sandbox" fact for this data folder. */
|
|
28
|
+
export declare function readLastEnd(lifeRead: () => Life | null, dataPath: string, running: boolean): LastEnd | null;
|