windows-development-sandbox 1.18.3 → 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/dist/app/features/defs.d.ts +18 -0
- package/dist/app/features/defs.js +2 -1
- package/dist/app/features/sandbox/sandbox.def.d.ts +22 -0
- package/dist/app/features/sandbox/sandbox.def.js +41 -14
- package/dist/app/features/sandbox/sandbox.run.d.ts +20 -0
- package/dist/app/features/sandbox/sandbox.run.js +41 -1
- package/dist/app/registry.js +2 -1
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-FQWYCF6L.js → chunk-FRPLQLSY.js} +3453 -3145
- package/dist/bin/{chunk-M6XUQK53.js → chunk-MY7NLQQY.js} +1 -1
- package/dist/bin/{chunk-3G53NNZP.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.js +29 -2
- package/dist/guest/daemon/heartbeat.d.ts +7 -2
- package/dist/guest/daemon/heartbeat.js +34 -15
- package/dist/guest/daemon/idle-shutdown.js +2 -0
- package/dist/guest/entries/daemon.js +1 -1
- 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/life/crash-report.d.ts +1 -1
- package/dist/host/life/crash-report.js +5 -3
- package/dist/host/life/last-end.d.ts +7 -0
- package/dist/host/life/last-end.js +21 -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/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 +3 -1
- package/dist/runtime/daemon.js +368 -351
- package/dist/runtime/db.js +3 -1
- package/dist/runtime/main.js +3 -1
- package/dist/runtime/netproxy.js +3 -1
- package/dist/runtime/tool.js +3 -1
- package/dist/runtime/view.js +3 -1
- package/dist/selftest/heavy-load.test.js +119 -2
- package/dist/shared/channel/schema.d.ts +1 -0
- package/dist/shared/channel/schema.js +2 -1
- package/dist/shared/system.d.ts +1 -0
- package/dist/shared/system.js +1 -0
- package/dist/sync/watcher.js +9 -4
- package/dist/web/index.html +1 -1
- package/dist/web/{main-6XYO5FZC.js → main-GVZWWP4I.js} +62 -62
- 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/dist/bin/claude-host.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
run
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-N4GXYTLV.js";
|
|
6
|
+
import "./chunk-FRPLQLSY.js";
|
|
7
7
|
|
|
8
8
|
// src/bin/claude-host.ts
|
|
9
9
|
var args = process.argv.slice(2);
|
package/dist/bin/claude-sbx.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
run
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-N4GXYTLV.js";
|
|
6
|
+
import "./chunk-FRPLQLSY.js";
|
|
7
7
|
|
|
8
8
|
// src/bin/claude-sbx.ts
|
|
9
9
|
run(process.argv.slice(2), { harness: "claude" });
|
package/dist/bin/codex-sbx.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
run
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-N4GXYTLV.js";
|
|
6
|
+
import "./chunk-FRPLQLSY.js";
|
|
7
7
|
|
|
8
8
|
// src/bin/codex-sbx.ts
|
|
9
9
|
run(process.argv.slice(2), { harness: "codex" });
|
package/dist/bin/gemini-sbx.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
run
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-N4GXYTLV.js";
|
|
6
|
+
import "./chunk-FRPLQLSY.js";
|
|
7
7
|
|
|
8
8
|
// src/bin/gemini-sbx.ts
|
|
9
9
|
run(process.argv.slice(2), { harness: "gemini" });
|
package/dist/bin/sandbox.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
run
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-N4GXYTLV.js";
|
|
6
|
+
import "./chunk-FRPLQLSY.js";
|
|
7
7
|
|
|
8
8
|
// src/bin/sandbox.ts
|
|
9
9
|
run(process.argv.slice(2));
|
package/dist/bin/sbx-hide.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
run
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-N4GXYTLV.js";
|
|
6
|
+
import "./chunk-FRPLQLSY.js";
|
|
7
7
|
|
|
8
8
|
// src/bin/sbx-hide.ts
|
|
9
9
|
run(process.argv.slice(2), { command: "hide" });
|
package/dist/bin/sbx-show.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
|
|
3
3
|
import {
|
|
4
4
|
run
|
|
5
|
-
} from "./chunk-
|
|
6
|
-
import "./chunk-
|
|
5
|
+
} from "./chunk-N4GXYTLV.js";
|
|
6
|
+
import "./chunk-FRPLQLSY.js";
|
|
7
7
|
|
|
8
8
|
// src/bin/sbx-show.ts
|
|
9
9
|
run(process.argv.slice(2), { command: "show" });
|
|
@@ -21,6 +21,7 @@ import { watcherRunning } from '../../../sync/index.js';
|
|
|
21
21
|
import { protectionsOf } from '../../../core/local-config/index.js';
|
|
22
22
|
import { errMsg } from '../../../core/util/index.js';
|
|
23
23
|
import { askYesNo } from '../../../core/prompt.js';
|
|
24
|
+
import { offerLeftoverCleanup } from './leftover-offer.js';
|
|
24
25
|
import { resolveRun } from '../../../core/run/index.js';
|
|
25
26
|
import { c, table } from '../../output/index.js';
|
|
26
27
|
import { syncAgentCommands } from '../agent-commands.js';
|
|
@@ -133,8 +134,10 @@ class Launcher {
|
|
|
133
134
|
const provider = providerFor(config, config.storage.contained === 'off'
|
|
134
135
|
? {}
|
|
135
136
|
: { contained: realContainedDeps(ctx.storageRoot, config.storage.containedMaxGb, Boolean(ctx.options.config)) });
|
|
136
|
-
if (!generateOnly)
|
|
137
|
+
if (!generateOnly) {
|
|
138
|
+
await offerLeftoverCleanup(ctx);
|
|
137
139
|
assertCanLaunch(isSandboxRunning() ? runningOwner() : undefined);
|
|
140
|
+
}
|
|
138
141
|
// Contained storage: the image is mounted now (before anything reads or writes the data set), and let go of again
|
|
139
142
|
// when the launch fails before the sandbox starts. The background watcher holds it from then on.
|
|
140
143
|
const dataMappings = await this.prepareStorage(provider, run);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// A launch that finds Windows Sandbox programs left behind by a sandbox that is gone (a crash, a kill from outside):
|
|
2
|
+
// Windows then says "already running" or that the sandbox was signed out. Asks to close them and goes on.
|
|
3
|
+
import { askYesNo } from '../../../core/prompt.js';
|
|
4
|
+
import { fail } from '../../../core/util/index.js';
|
|
5
|
+
import { readSandboxFacts } from '../../../host/health/index.js';
|
|
6
|
+
import { cleanUpLeftovers, endSandboxPrograms, isSandboxRunning, restartSandboxService, } from '../../../host/sandbox/index.js';
|
|
7
|
+
import { c } from '../../output/index.js';
|
|
8
|
+
/** Does nothing unless leftovers are found; then asks (needs a terminal) and fails with the way out when declined. */
|
|
9
|
+
export async function offerLeftoverCleanup(ctx) {
|
|
10
|
+
if (!isSandboxRunning())
|
|
11
|
+
return;
|
|
12
|
+
const { storageRoot, dataPath } = ctx;
|
|
13
|
+
const outcome = await cleanUpLeftovers({
|
|
14
|
+
facts: () => readSandboxFacts(storageRoot, dataPath, true),
|
|
15
|
+
end: endSandboxPrograms,
|
|
16
|
+
restartService: restartSandboxService,
|
|
17
|
+
confirm: (q) => {
|
|
18
|
+
for (const line of q.lines)
|
|
19
|
+
console.log(c.yellow(line));
|
|
20
|
+
return askYesNo(q.title, true);
|
|
21
|
+
},
|
|
22
|
+
say: (line) => console.log(line),
|
|
23
|
+
});
|
|
24
|
+
if (outcome === 'declined' || outcome === 'failed')
|
|
25
|
+
fail('Windows Sandbox programs from a sandbox that is gone are still there, so a new one cannot start.', 'Run sandbox cleanup (it asks first), or end the WindowsSandbox programs in Task Manager.');
|
|
26
|
+
}
|
|
@@ -6,10 +6,10 @@ import path from 'node:path';
|
|
|
6
6
|
import { diskUsage, humanBytes } from '../../../../core/disk.js';
|
|
7
7
|
import { dirExists, fileExists, isLink } from '../../../../core/util/index.js';
|
|
8
8
|
import { SandboxChannel } from '../../../../host/channel/index.js';
|
|
9
|
-
import { orphanWatchers, runStateProblems, staleLaunchFolders, watcherRecords } from '../../../../host/health/index.js';
|
|
9
|
+
import { judgeSandbox, orphanWatchers, readSandboxFacts, runStateProblems, staleLaunchFolders, watcherRecords, } from '../../../../host/health/index.js';
|
|
10
10
|
import { vgpuSource, vgpuWarning } from '../../../../host/launch/index.js';
|
|
11
11
|
import { hostToolsExe } from '../../../../host/native.js';
|
|
12
|
-
import { sandboxFeatureInstalled } from '../../../../host/sandbox/index.js';
|
|
12
|
+
import { endSandboxPrograms, sandboxFeatureInstalled } from '../../../../host/sandbox/index.js';
|
|
13
13
|
import { assertTestWritable, refuseInSelfTest } from '../../../../host/test-guard.js';
|
|
14
14
|
import { conptyDir, terminalDir, terminalExe } from '../../../../host/tools/index.js';
|
|
15
15
|
import { RunDir } from '../../../../shared/channel/index.js';
|
|
@@ -90,6 +90,33 @@ export const SANDBOX_CHECKS = [
|
|
|
90
90
|
};
|
|
91
91
|
},
|
|
92
92
|
},
|
|
93
|
+
{
|
|
94
|
+
id: 'sandbox-leftover',
|
|
95
|
+
group: 'Sandbox',
|
|
96
|
+
name: 'Leftover Windows Sandbox programs',
|
|
97
|
+
run: async (env) => {
|
|
98
|
+
if (!env.running)
|
|
99
|
+
return ok('none');
|
|
100
|
+
const found = judgeSandbox(await readSandboxFacts(env.storageRoot, env.dataPath, false));
|
|
101
|
+
if (found.state !== 'leftover')
|
|
102
|
+
return ok(found.state === 'none' ? 'none' : 'the sandbox is working');
|
|
103
|
+
return {
|
|
104
|
+
status: 'needs-you',
|
|
105
|
+
detail: found.why,
|
|
106
|
+
means: 'A sandbox that died leaves its Windows programs behind, and Windows then refuses to start a new one ("the sandbox was signed out" or "already running").',
|
|
107
|
+
plan: 'close ' + found.images.join(', ') + ' (only the ones in your Windows session)',
|
|
108
|
+
data: found.pids,
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
fix: (_env, finding) => {
|
|
112
|
+
const pids = Array.isArray(finding.data) ? finding.data : [];
|
|
113
|
+
return endSandboxPrograms(pids)
|
|
114
|
+
? { done: 'closed the leftover Windows Sandbox programs' }
|
|
115
|
+
: {
|
|
116
|
+
done: 'Windows would not close some of them; sandbox cleanup can also restart the Windows Sandbox service',
|
|
117
|
+
};
|
|
118
|
+
},
|
|
119
|
+
},
|
|
93
120
|
{
|
|
94
121
|
id: 'sandbox-run-state',
|
|
95
122
|
group: 'Sandbox',
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { Heartbeat } from '../../shared/channel/schema.js';
|
|
2
2
|
import { type GovernorSnapshot } from '../governor/index.js';
|
|
3
3
|
/** The heartbeat for these numbers (the governor's snapshot when it has one, else Windows' own free memory). */
|
|
4
|
-
export declare function buildHeartbeat(snapshot: GovernorSnapshot | null, sessionCount: number, uptimeSeconds: number, freeBytes: number, totalBytes: number, at: string): Heartbeat;
|
|
4
|
+
export declare function buildHeartbeat(snapshot: GovernorSnapshot | null, sessionCount: number, uptimeSeconds: number, freeBytes: number, totalBytes: number, at: string, ending?: boolean): Heartbeat;
|
|
5
|
+
/**
|
|
6
|
+
* The last heartbeat of an orderly end (idle shutdown, `sandbox stop`, Windows shutting down): the host then knows the
|
|
7
|
+
* sandbox was not lost. A sandbox that just vanishes leaves a busy heartbeat instead.
|
|
8
|
+
*/
|
|
9
|
+
export declare function markEnding(): void;
|
|
5
10
|
/** Writes the heartbeat now and every 10 seconds; a failed write is only a warning. */
|
|
6
|
-
export declare function startHeartbeat(): void;
|
|
11
|
+
export declare function startHeartbeat(sessionCount: () => number): void;
|
|
@@ -7,11 +7,10 @@ import { governor } from '../governor/index.js';
|
|
|
7
7
|
import { run } from '../runtime/paths.js';
|
|
8
8
|
import { warn } from '../runtime/log.js';
|
|
9
9
|
import { errMsg } from '../../core/util/errors.js';
|
|
10
|
-
import { sessions } from './sessions.js';
|
|
11
10
|
const BEAT_MS = 10000;
|
|
12
11
|
const clamp = (n, low, high) => Math.max(low, Math.min(high, n));
|
|
13
12
|
/** The heartbeat for these numbers (the governor's snapshot when it has one, else Windows' own free memory). */
|
|
14
|
-
export function buildHeartbeat(snapshot, sessionCount, uptimeSeconds, freeBytes, totalBytes, at) {
|
|
13
|
+
export function buildHeartbeat(snapshot, sessionCount, uptimeSeconds, freeBytes, totalBytes, at, ending = false) {
|
|
15
14
|
const ramFree = snapshot?.ramFreePercent ?? Math.round((freeBytes / Math.max(1, totalBytes)) * 100);
|
|
16
15
|
return {
|
|
17
16
|
at,
|
|
@@ -21,20 +20,40 @@ export function buildHeartbeat(snapshot, sessionCount, uptimeSeconds, freeBytes,
|
|
|
21
20
|
step: clamp(snapshot?.step ?? 0, 0, 4),
|
|
22
21
|
hostStep: clamp(snapshot?.hostStep ?? 0, 0, 4),
|
|
23
22
|
topProcesses: (snapshot?.top ?? []).slice(0, 5).map((p) => ({ name: p.name.slice(0, 64), privateMb: p.mb })),
|
|
24
|
-
...(snapshot?.diskQueue
|
|
25
|
-
|
|
23
|
+
...(snapshot?.diskQueue !== null && snapshot?.diskQueue !== undefined
|
|
24
|
+
? { diskQueueReported: snapshot.diskQueue }
|
|
25
|
+
: {}),
|
|
26
|
+
phase: ending ? 'ending' : sessionCount > 0 ? 'working' : 'ready',
|
|
26
27
|
};
|
|
27
28
|
}
|
|
29
|
+
/** How many sessions run (set by startHeartbeat; this file must not import the sessions, which end through it). */
|
|
30
|
+
let countSessions = () => 0;
|
|
31
|
+
function writeBeat(ending) {
|
|
32
|
+
try {
|
|
33
|
+
writeJsonAtomic(run.heartbeatFile, buildHeartbeat(governor.snapshot(), countSessions(), os.uptime(), os.freemem(), os.totalmem(), now(), ending));
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
warn('heartbeat: ' + errMsg(e));
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The last heartbeat of an orderly end (idle shutdown, `sandbox stop`, Windows shutting down): the host then knows the
|
|
41
|
+
* sandbox was not lost. A sandbox that just vanishes leaves a busy heartbeat instead.
|
|
42
|
+
*/
|
|
43
|
+
export function markEnding() {
|
|
44
|
+
writeBeat(true);
|
|
45
|
+
}
|
|
28
46
|
/** Writes the heartbeat now and every 10 seconds; a failed write is only a warning. */
|
|
29
|
-
export function startHeartbeat() {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
47
|
+
export function startHeartbeat(sessionCount) {
|
|
48
|
+
countSessions = sessionCount;
|
|
49
|
+
writeBeat(false);
|
|
50
|
+
const timer = setInterval(() => writeBeat(false), BEAT_MS);
|
|
51
|
+
timer.unref();
|
|
52
|
+
// Windows shutting down or the console closing: say so, then end (the default for these signals).
|
|
53
|
+
for (const signal of ['SIGHUP', 'SIGBREAK', 'SIGTERM'])
|
|
54
|
+
process.on(signal, () => {
|
|
55
|
+
clearInterval(timer);
|
|
56
|
+
markEnding();
|
|
57
|
+
process.exit(0);
|
|
58
|
+
});
|
|
40
59
|
}
|
|
@@ -9,6 +9,7 @@ import { log } from '../runtime/log.js';
|
|
|
9
9
|
import { session } from '../runtime/session.js';
|
|
10
10
|
import { safely } from './state.js';
|
|
11
11
|
import { flushAccounts } from './accounts.js';
|
|
12
|
+
import { markEnding } from './heartbeat.js';
|
|
12
13
|
/** How long the sandbox must stay idle before it shuts down. */
|
|
13
14
|
const GRACE_MS = 15000;
|
|
14
15
|
export class IdleShutdown {
|
|
@@ -27,6 +28,7 @@ export class IdleShutdown {
|
|
|
27
28
|
if (!this.idle())
|
|
28
29
|
return;
|
|
29
30
|
log('Headless sandbox has no sessions left; shutting down.');
|
|
31
|
+
safely('heartbeat', markEnding);
|
|
30
32
|
safely('claude-mem save', saveClaudeMem);
|
|
31
33
|
safely('agent databases save', saveLocalState);
|
|
32
34
|
// Renewed sign-ins go to the PC, and a held one (a Unity seat) is let go, before Windows stops.
|
|
@@ -25,5 +25,5 @@ safely('leftover processes', () => sessions.sweepLeftovers());
|
|
|
25
25
|
safely('memory governor', () => governor.start());
|
|
26
26
|
log('Memory governor: ' + governor.describe());
|
|
27
27
|
safely('service sign-ins', startAccountWatch);
|
|
28
|
-
safely('heartbeat', startHeartbeat);
|
|
28
|
+
safely('heartbeat', () => startHeartbeat(() => sessions.count));
|
|
29
29
|
new Daemon().start();
|
|
@@ -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
|
+
}
|
|
@@ -35,4 +35,4 @@ export declare function newestCrashReport(storageRoot: string): {
|
|
|
35
35
|
/**
|
|
36
36
|
* When the sandbox ended in a crash (its own, this PC's, power, low memory): writes the report and returns its path.
|
|
37
37
|
*/
|
|
38
|
-
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;
|
|
@@ -12,6 +12,8 @@ const KEEP_REPORTS = 20;
|
|
|
12
12
|
const MEMORY_TAIL_BYTES = 16 * 1024;
|
|
13
13
|
/** A gap longer than this between "last seen running" and the restart means the PC stopped responding first. */
|
|
14
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'];
|
|
15
17
|
function crashDir(storageRoot) {
|
|
16
18
|
return path.join(storageRoot, 'logs', 'crashes');
|
|
17
19
|
}
|
|
@@ -230,10 +232,10 @@ function memoryLogTail(dataPath) {
|
|
|
230
232
|
/**
|
|
231
233
|
* When the sandbox ended in a crash (its own, this PC's, power, low memory): writes the report and returns its path.
|
|
232
234
|
*/
|
|
233
|
-
export function saveCrashReport(storageRoot, dataPath, lastSeen) {
|
|
235
|
+
export function saveCrashReport(storageRoot, dataPath, lastSeen, known = null) {
|
|
234
236
|
const events = readStopEvents(new Date(Date.parse(lastSeen) - 2 * 60 * 1000), storageRoot);
|
|
235
|
-
const cause = explainStop(events);
|
|
236
|
-
if (!cause || !
|
|
237
|
+
const cause = explainStop(events) ?? known;
|
|
238
|
+
if (!cause || !REPORTED.includes(cause.kind))
|
|
237
239
|
return null;
|
|
238
240
|
const dir = crashDir(storageRoot);
|
|
239
241
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Heartbeat } from '../../shared/channel/index.js';
|
|
2
2
|
import type { Life } from './life-record.js';
|
|
3
|
+
import type { StopCause } from './stop-events.js';
|
|
3
4
|
/** How the last sandbox ended, when nobody closed it. */
|
|
4
5
|
export interface LastEnd {
|
|
5
6
|
/** "ended unexpectedly 21:31: this PC's disks were overloaded (queue 40)". */
|
|
@@ -12,6 +13,12 @@ export declare function readHeartbeat(dataPath: string): Heartbeat | null;
|
|
|
12
13
|
export declare function heartbeatLine(hb: Heartbeat): string;
|
|
13
14
|
/** The likeliest reason, from the last heartbeat (pure). */
|
|
14
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;
|
|
15
22
|
/**
|
|
16
23
|
* How the last sandbox ended (pure): null when it was closed normally, or is still running. `heartbeat` only counts
|
|
17
24
|
* when it was written during that run.
|
|
@@ -49,6 +49,27 @@ export function guessReason(hb) {
|
|
|
49
49
|
return 'the sandbox was running out of memory';
|
|
50
50
|
return 'Windows stopped it (no reason was logged)';
|
|
51
51
|
}
|
|
52
|
+
/** A heartbeat older than this before the end is not "it was working when it went". */
|
|
53
|
+
const FRESH_MS = 2 * 60 * 1000;
|
|
54
|
+
/**
|
|
55
|
+
* The sandbox went and nothing was logged (pure): its last heartbeat is fresh, from this run, says it was working
|
|
56
|
+
* (a session running, or builds slowed down) and does not say it was shutting down in order, and nobody stopped it on
|
|
57
|
+
* purpose (`sandbox stop`, a restart). The guest cannot tell a closed window from a crash, so the wording says both.
|
|
58
|
+
*/
|
|
59
|
+
export function vanishedCause(hb, startedIso, closedOnPurpose, endedAtMs) {
|
|
60
|
+
if (!hb || closedOnPurpose || hb.phase === 'ending')
|
|
61
|
+
return null;
|
|
62
|
+
const seen = Date.parse(hb.at);
|
|
63
|
+
if (!Number.isFinite(seen) || seen < Date.parse(startedIso) - 60000 || endedAtMs - seen > FRESH_MS)
|
|
64
|
+
return null;
|
|
65
|
+
if (hb.phase !== 'working' && hb.step === 0 && hb.hostStep === 0)
|
|
66
|
+
return null;
|
|
67
|
+
return {
|
|
68
|
+
kind: 'vanished',
|
|
69
|
+
at: new Date(endedAtMs).toISOString(),
|
|
70
|
+
text: 'the sandbox ended suddenly while it was working (its window was closed, or it crashed: Windows logged nothing)',
|
|
71
|
+
};
|
|
72
|
+
}
|
|
52
73
|
/**
|
|
53
74
|
* How the last sandbox ended (pure): null when it was closed normally, or is still running. `heartbeat` only counts
|
|
54
75
|
* when it was written during that run.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { JsonFile } from '../../shared/io/index.js';
|
|
7
7
|
import { hostStateFile } from '../storage/index.js';
|
|
8
|
-
import { causeSince, IsoSchema } from './stop-events.js';
|
|
8
|
+
import { causeSince, IsoSchema, STOP_KINDS } from './stop-events.js';
|
|
9
9
|
import { stopWarning } from './stop-warning.js';
|
|
10
10
|
import { saveCrashReport } from './crash-report.js';
|
|
11
11
|
const LifeSchema = z.object({
|
|
@@ -17,7 +17,7 @@ const LifeSchema = z.object({
|
|
|
17
17
|
at: IsoSchema,
|
|
18
18
|
cause: z
|
|
19
19
|
.object({
|
|
20
|
-
kind: z.enum(
|
|
20
|
+
kind: z.enum(STOP_KINDS),
|
|
21
21
|
at: IsoSchema,
|
|
22
22
|
text: z.string().max(400),
|
|
23
23
|
})
|
|
@@ -6,7 +6,9 @@ export interface StopEvent {
|
|
|
6
6
|
provider: string;
|
|
7
7
|
process: string;
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
/** The ways a sandbox can end that nobody asked for (the life record stores one of these). */
|
|
10
|
+
export declare const STOP_KINDS: readonly ["updates", "restart", "crash", "sleep", "memory", "sandboxCrash", "gpu", "hyperv", "vanished"];
|
|
11
|
+
export type StopKind = (typeof STOP_KINDS)[number];
|
|
10
12
|
export interface StopCause {
|
|
11
13
|
kind: StopKind;
|
|
12
14
|
at: string;
|