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
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
|
+
}
|
|
@@ -1,2 +1,4 @@
|
|
|
1
|
-
import type { Check } from '../types.js';
|
|
1
|
+
import type { Check, Finding } from '../types.js';
|
|
2
|
+
/** A warning when the sandbox's paging file takes more than 40 % of the drive (pure); null when it is fine. */
|
|
3
|
+
export declare function pageFileFinding(pageFileMb: number, driveBytes: number): Finding | null;
|
|
2
4
|
export declare const SANDBOX_CHECKS: Check[];
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
// on the PC.
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import path from 'node:path';
|
|
6
|
+
import { diskUsage, humanBytes } from '../../../../core/disk.js';
|
|
6
7
|
import { dirExists, fileExists, isLink } from '../../../../core/util/index.js';
|
|
7
8
|
import { SandboxChannel } from '../../../../host/channel/index.js';
|
|
8
|
-
import { orphanWatchers, runStateProblems, staleLaunchFolders, watcherRecords } from '../../../../host/health/index.js';
|
|
9
|
+
import { judgeSandbox, orphanWatchers, readSandboxFacts, runStateProblems, staleLaunchFolders, watcherRecords, } from '../../../../host/health/index.js';
|
|
9
10
|
import { vgpuSource, vgpuWarning } from '../../../../host/launch/index.js';
|
|
10
11
|
import { hostToolsExe } from '../../../../host/native.js';
|
|
11
|
-
import { sandboxFeatureInstalled } from '../../../../host/sandbox/index.js';
|
|
12
|
+
import { endSandboxPrograms, sandboxFeatureInstalled } from '../../../../host/sandbox/index.js';
|
|
12
13
|
import { assertTestWritable, refuseInSelfTest } from '../../../../host/test-guard.js';
|
|
13
14
|
import { conptyDir, terminalDir, terminalExe } from '../../../../host/tools/index.js';
|
|
14
15
|
import { RunDir } from '../../../../shared/channel/index.js';
|
|
@@ -18,6 +19,19 @@ import { deadPidFiles } from '../finders.js';
|
|
|
18
19
|
import { fromFacts } from './pc.js';
|
|
19
20
|
import { ok, plural, rel } from './common.js';
|
|
20
21
|
const LAUNCH_DAYS = 7;
|
|
22
|
+
/** The paging file may take at most this share of the disk it sits on. */
|
|
23
|
+
const PAGE_FILE_MAX_SHARE = 0.4;
|
|
24
|
+
/** A warning when the sandbox's paging file takes more than 40 % of the drive (pure); null when it is fine. */
|
|
25
|
+
export function pageFileFinding(pageFileMb, driveBytes) {
|
|
26
|
+
if (pageFileMb <= 0 || driveBytes <= 0 || pageFileMb * 1024 * 1024 <= PAGE_FILE_MAX_SHARE * driveBytes)
|
|
27
|
+
return null;
|
|
28
|
+
return {
|
|
29
|
+
status: 'warn',
|
|
30
|
+
detail: 'the paging file is ' + pageFileMb + ' MB on a ' + humanBytes(driveBytes) + ' drive',
|
|
31
|
+
means: 'sandbox.pageFileMb takes more than 40% of the drive, so builds and installs have little room left and the disk fills up (a full disk can freeze the sandbox).',
|
|
32
|
+
fix: 'Run: sandbox config set sandbox.pageFileMb 8192 (or 0 to let Windows decide).',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
21
35
|
const hiddenWindowsFile = (env) => path.join(env.storageRoot, 'run', 'hidden-windows.json');
|
|
22
36
|
export const SANDBOX_CHECKS = [
|
|
23
37
|
{
|
|
@@ -33,6 +47,16 @@ export const SANDBOX_CHECKS = [
|
|
|
33
47
|
}
|
|
34
48
|
: ok('off'),
|
|
35
49
|
},
|
|
50
|
+
{
|
|
51
|
+
id: 'sandbox-pagefile',
|
|
52
|
+
group: 'Sandbox',
|
|
53
|
+
name: 'Paging file size',
|
|
54
|
+
run: (env) => {
|
|
55
|
+
const mb = env.ctx.loaded.config.sandbox.pageFileMb;
|
|
56
|
+
const drive = diskUsage(env.storageRoot).find((d) => d.drive.toUpperCase() === 'C:\\');
|
|
57
|
+
return pageFileFinding(mb, drive?.totalBytes ?? 0) ?? ok(mb > 0 ? mb + ' MB' : "Windows' own");
|
|
58
|
+
},
|
|
59
|
+
},
|
|
36
60
|
{
|
|
37
61
|
id: 'sandbox-exe',
|
|
38
62
|
group: 'Sandbox',
|
|
@@ -66,6 +90,33 @@ export const SANDBOX_CHECKS = [
|
|
|
66
90
|
};
|
|
67
91
|
},
|
|
68
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
|
+
},
|
|
69
120
|
{
|
|
70
121
|
id: 'sandbox-run-state',
|
|
71
122
|
group: 'Sandbox',
|
|
@@ -21,7 +21,7 @@ export const SandboxSchema = z.strictObject({
|
|
|
21
21
|
.describe('(alpha) What runs the sandbox. "windows-sandbox" = Windows Sandbox (the default: desktop, Windows Terminal, GPU, clipboard, audio, printers). "hcs" = a headless Hyper-V isolated Server Core container for agents (no desktop, GPU or clipboard); it needs Windows features and an image on this PC (sandbox engine check hcs lists what is missing), and starting it is not enabled yet in this alpha. sandbox engine shows what each engine can do. Yours only: a project file (.sandbox-config.json) cannot set it.'),
|
|
22
22
|
memoryMb: z
|
|
23
23
|
.union([z.literal('auto'), z.number().int().min(2048)])
|
|
24
|
-
.describe('Sandbox RAM in MB (>= 2048), or "auto": free RAM minus reserveHostMb, capped at maxMemoryMb. Never more than this PC can keep in RAM for it (its RAM minus the larger of reserveHostMb and 25 %,
|
|
24
|
+
.describe('Sandbox RAM in MB (>= 2048), or "auto": free RAM minus reserveHostMb, capped at maxMemoryMb. Never more than this PC can keep in RAM for it (its RAM minus the larger of reserveHostMb and 25 %, 4 GB more with vGPU): a bigger value is lowered.'),
|
|
25
25
|
maxMemoryMb: z.number().int().positive().describe('Upper limit for memoryMb "auto".'),
|
|
26
26
|
maxMemoryPercent: z
|
|
27
27
|
.number()
|
|
@@ -37,13 +37,13 @@ export const ToolchainsSchema = z.strictObject({
|
|
|
37
37
|
targetDir: z
|
|
38
38
|
.string()
|
|
39
39
|
.min(1)
|
|
40
|
-
.describe('"shared": cargo\'s default target\\ (shared with builds on this PC, so builds on either side invalidate the other), "separate": <workspace>\\target\\sandbox (kept, no cross-talk), "local": on the sandbox\'s own disk (fastest, lost every boot), or a guest path.'),
|
|
40
|
+
.describe('"auto" (default): on the sandbox\'s own disk when C: has 25 GB free at boot, else "separate" (builds then write through the mapped workspace, which can overload this PC\'s disk). "shared": cargo\'s default target\\ (shared with builds on this PC, so builds on either side invalidate the other), "separate": <workspace>\\target\\sandbox (kept, no cross-talk), "local": on the sandbox\'s own disk (fastest, lost every boot), or a guest path.'),
|
|
41
41
|
linker: z
|
|
42
42
|
.enum(['lld', 'gnu'])
|
|
43
43
|
.describe('GNU host: "lld" links with Rust\'s own lld through MinGW\'s gcc (much less memory than GNU ld), "gnu" with GNU ld.'),
|
|
44
44
|
linkSlots: z
|
|
45
45
|
.union([z.literal('auto'), z.number().int().min(1).max(32)])
|
|
46
|
-
.describe('GNU host: how many programs may link at the same time in the whole sandbox (other links wait). "auto" = one per
|
|
46
|
+
.describe('GNU host: how many programs may link at the same time in the whole sandbox (other links wait). "auto" = one per 6 GB of sandbox memory, 1 to 4 (the memory governor slows links down too when memory runs short).'),
|
|
47
47
|
}),
|
|
48
48
|
msvc: z.strictObject({
|
|
49
49
|
enabled: enabled("MSVC (cl, link) and the Windows SDK, portable from Microsoft's Visual Studio manifest (~1-2 GB once), with vcvars64.bat and vswhere.", 'a .vcxproj, CMakePresets.json, a script calling vcvars/vswhere, or a Rust project when this PC builds Rust with MSVC'),
|
|
@@ -63,7 +63,7 @@ export class Daemon {
|
|
|
63
63
|
return sessions.close(req);
|
|
64
64
|
case 'pressure':
|
|
65
65
|
// No response: the host repeats it every few seconds while its memory stays low.
|
|
66
|
-
return governor.hostPressure(req.seconds, req.reason, req.level ?? 'slow');
|
|
66
|
+
return governor.hostPressure(req.seconds, req.reason, req.level ?? 'slow', req.diskQueue);
|
|
67
67
|
case 'ping':
|
|
68
68
|
return respond(req.id, { ok: true, message: 'pong (' + sessions.count + ' session(s) running)' });
|
|
69
69
|
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Heartbeat } from '../../shared/channel/schema.js';
|
|
2
|
+
import { type GovernorSnapshot } from '../governor/index.js';
|
|
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, 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;
|
|
10
|
+
/** Writes the heartbeat now and every 10 seconds; a failed write is only a warning. */
|
|
11
|
+
export declare function startHeartbeat(sessionCount: () => number): void;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// The heartbeat: every 10 seconds the daemon writes run\heartbeat.json (memory, the governor's dial, the biggest
|
|
2
|
+
// programs). When the whole sandbox vanishes, the host reads the last one to say what it was doing.
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { writeJsonAtomic } from '../../shared/io/json.js';
|
|
5
|
+
import { now } from '../../shared/channel/run-dir.js';
|
|
6
|
+
import { governor } from '../governor/index.js';
|
|
7
|
+
import { run } from '../runtime/paths.js';
|
|
8
|
+
import { warn } from '../runtime/log.js';
|
|
9
|
+
import { errMsg } from '../../core/util/errors.js';
|
|
10
|
+
const BEAT_MS = 10000;
|
|
11
|
+
const clamp = (n, low, high) => Math.max(low, Math.min(high, n));
|
|
12
|
+
/** The heartbeat for these numbers (the governor's snapshot when it has one, else Windows' own free memory). */
|
|
13
|
+
export function buildHeartbeat(snapshot, sessionCount, uptimeSeconds, freeBytes, totalBytes, at, ending = false) {
|
|
14
|
+
const ramFree = snapshot?.ramFreePercent ?? Math.round((freeBytes / Math.max(1, totalBytes)) * 100);
|
|
15
|
+
return {
|
|
16
|
+
at,
|
|
17
|
+
uptimeS: Math.max(0, Math.round(uptimeSeconds)),
|
|
18
|
+
ramFreePct: clamp(ramFree, 0, 100),
|
|
19
|
+
commitPct: clamp(snapshot?.commitPercent ?? 0, 0, 100),
|
|
20
|
+
step: clamp(snapshot?.step ?? 0, 0, 4),
|
|
21
|
+
hostStep: clamp(snapshot?.hostStep ?? 0, 0, 4),
|
|
22
|
+
topProcesses: (snapshot?.top ?? []).slice(0, 5).map((p) => ({ name: p.name.slice(0, 64), privateMb: p.mb })),
|
|
23
|
+
...(snapshot?.diskQueue !== null && snapshot?.diskQueue !== undefined
|
|
24
|
+
? { diskQueueReported: snapshot.diskQueue }
|
|
25
|
+
: {}),
|
|
26
|
+
phase: ending ? 'ending' : sessionCount > 0 ? 'working' : 'ready',
|
|
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
|
+
}
|
|
46
|
+
/** Writes the heartbeat now and every 10 seconds; a failed write is only a warning. */
|
|
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
|
+
});
|
|
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.
|
|
@@ -13,6 +13,7 @@ import { startLiveFolders } from '../daemon/live.js';
|
|
|
13
13
|
import { sessions } from '../daemon/sessions.js';
|
|
14
14
|
import { governor } from '../governor/index.js';
|
|
15
15
|
import { startAccountWatch } from '../daemon/accounts.js';
|
|
16
|
+
import { startHeartbeat } from '../daemon/heartbeat.js';
|
|
16
17
|
process.on('uncaughtException', (e) => warn('daemon: unexpected error (continuing): ' + (e.stack ?? errMsg(e))));
|
|
17
18
|
process.on('unhandledRejection', (e) => warn('daemon: unexpected error (continuing): ' + errMsg(e)));
|
|
18
19
|
const liveKey = adoptBootSecrets();
|
|
@@ -24,4 +25,5 @@ safely('leftover processes', () => sessions.sweepLeftovers());
|
|
|
24
25
|
safely('memory governor', () => governor.start());
|
|
25
26
|
log('Memory governor: ' + governor.describe());
|
|
26
27
|
safely('service sign-ins', startAccountWatch);
|
|
28
|
+
safely('heartbeat', () => startHeartbeat(() => sessions.count));
|
|
27
29
|
new Daemon().start();
|
|
@@ -1,12 +1,26 @@
|
|
|
1
1
|
import type { PressureStatus } from '../../shared/channel/schema.js';
|
|
2
2
|
import type { LoadProfile } from '../../shared/memory/index.js';
|
|
3
3
|
import { type GovernorState, type MemSample } from './decide.js';
|
|
4
|
+
/** The newest numbers the governor saw (for the heartbeat file). */
|
|
5
|
+
export interface GovernorSnapshot {
|
|
6
|
+
readonly ramFreePercent: number;
|
|
7
|
+
readonly commitPercent: number;
|
|
8
|
+
readonly step: number;
|
|
9
|
+
readonly hostStep: number;
|
|
10
|
+
readonly diskQueue: number | null;
|
|
11
|
+
readonly top: readonly {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly mb: number;
|
|
14
|
+
}[];
|
|
15
|
+
}
|
|
4
16
|
/** What the governor needs from Windows (injectable: the tests use a fake). */
|
|
5
17
|
export interface GovernorPort {
|
|
6
18
|
setPriority(pid: number, to: 'idle' | 'below' | 'normal'): void;
|
|
7
19
|
/** Suspends for `ms`, then resumes (memgov resumes it even if the daemon goes away). */
|
|
8
20
|
hold(pid: number, ms: number): void;
|
|
9
21
|
setMemoryPriority(pid: number, low: boolean): void;
|
|
22
|
+
/** The disk queue priority (very low, low, normal). */
|
|
23
|
+
setIoPriority(pid: number, to: 'verylow' | 'low' | 'normal'): void;
|
|
10
24
|
/** Into the throttle job (whose CPU cap setCpuRate sets). */
|
|
11
25
|
throttle(pid: number): void;
|
|
12
26
|
setCpuRate(percent: number): void;
|
|
@@ -35,8 +49,12 @@ export declare class GovernorCore {
|
|
|
35
49
|
private lastWriteAt;
|
|
36
50
|
/** Compilers waiting their turn now, by pid (name), to log a hold once. */
|
|
37
51
|
private held;
|
|
52
|
+
private latest;
|
|
53
|
+
private diskQueue;
|
|
38
54
|
constructor(deps: GovernorDeps);
|
|
39
|
-
hostPressure(seconds: number, reason: string, level?: 'slow' | 'severe'): void;
|
|
55
|
+
hostPressure(seconds: number, reason: string, level?: 'slow' | 'severe', diskQueue?: number): void;
|
|
56
|
+
/** The newest numbers, null before the first sample. */
|
|
57
|
+
get snapshot(): GovernorSnapshot | null;
|
|
40
58
|
get current(): GovernorState;
|
|
41
59
|
onSample(sample: MemSample): void;
|
|
42
60
|
private apply;
|
|
@@ -17,10 +17,14 @@ export class GovernorCore {
|
|
|
17
17
|
lastWriteAt = 0;
|
|
18
18
|
/** Compilers waiting their turn now, by pid (name), to log a hold once. */
|
|
19
19
|
held = new Map();
|
|
20
|
+
latest = null;
|
|
21
|
+
diskQueue = null;
|
|
20
22
|
constructor(deps) {
|
|
21
23
|
this.deps = deps;
|
|
22
24
|
}
|
|
23
|
-
hostPressure(seconds, reason, level = 'slow') {
|
|
25
|
+
hostPressure(seconds, reason, level = 'slow', diskQueue) {
|
|
26
|
+
if (diskQueue !== undefined)
|
|
27
|
+
this.diskQueue = diskQueue;
|
|
24
28
|
const nowMs = this.deps.now();
|
|
25
29
|
const until = nowMs + seconds * 1000;
|
|
26
30
|
const active = this.state.hostUntil > nowMs;
|
|
@@ -34,6 +38,10 @@ export class GovernorCore {
|
|
|
34
38
|
step);
|
|
35
39
|
this.state = { ...this.state, hostUntil: Math.max(until, this.state.hostUntil), hostStep: step };
|
|
36
40
|
}
|
|
41
|
+
/** The newest numbers, null before the first sample. */
|
|
42
|
+
get snapshot() {
|
|
43
|
+
return this.latest ? { ...this.latest, diskQueue: this.diskQueue } : null;
|
|
44
|
+
}
|
|
37
45
|
get current() {
|
|
38
46
|
return this.state;
|
|
39
47
|
}
|
|
@@ -42,6 +50,17 @@ export class GovernorCore {
|
|
|
42
50
|
const before = this.state.step;
|
|
43
51
|
const d = decide(this.state, sample, nowMs, this.deps.profile, this.deps.maxCompilers ?? null);
|
|
44
52
|
this.state = d.state;
|
|
53
|
+
this.latest = {
|
|
54
|
+
ramFreePercent: d.ramFreePercent,
|
|
55
|
+
commitPercent: d.commitPercent,
|
|
56
|
+
step: d.state.step,
|
|
57
|
+
hostStep: nowMs < d.state.hostUntil ? d.state.hostStep : 0,
|
|
58
|
+
diskQueue: null,
|
|
59
|
+
top: [...sample.procs]
|
|
60
|
+
.sort((a, b) => b.mb - a.mb)
|
|
61
|
+
.slice(0, 5)
|
|
62
|
+
.map((p) => ({ name: p.name.slice(0, 64), mb: Math.round(p.mb) })),
|
|
63
|
+
};
|
|
45
64
|
const holding = new Set();
|
|
46
65
|
for (const action of d.actions) {
|
|
47
66
|
if (action.kind === 'hold')
|
|
@@ -98,6 +117,9 @@ export class GovernorCore {
|
|
|
98
117
|
port.setPriority(action.pid, action.to);
|
|
99
118
|
this.note(action.name + ' (' + action.pid + ') priority ' + action.to);
|
|
100
119
|
return;
|
|
120
|
+
case 'io':
|
|
121
|
+
port.setIoPriority(action.pid, action.to);
|
|
122
|
+
return;
|
|
101
123
|
case 'memory':
|
|
102
124
|
port.setMemoryPriority(action.pid, action.low);
|
|
103
125
|
return;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { type LoadProfile } from '../../shared/memory/index.js';
|
|
2
2
|
export type Priority = 'idle' | 'below' | 'normal';
|
|
3
|
+
/** I/O priority of a process (NtSetInformationProcess ProcessIoPriority): its disk requests queue behind the rest. */
|
|
4
|
+
export type IoPriority = 'verylow' | 'low' | 'normal';
|
|
3
5
|
export interface ProcSample {
|
|
4
6
|
readonly pid: number;
|
|
5
7
|
readonly name: string;
|
|
@@ -17,6 +19,9 @@ export interface MemSample {
|
|
|
17
19
|
}
|
|
18
20
|
export declare const isNeverTouched: (name: string) => boolean;
|
|
19
21
|
export declare const canLower: (name: string) => boolean;
|
|
22
|
+
export declare const isLinker: (name: string) => boolean;
|
|
23
|
+
/** Links that may run at once ("auto"): one per 6 GB of RAM, 1 to 4 (never more than the processors). */
|
|
24
|
+
export declare function linkerSlots(totalMb: number, cpus: number): number;
|
|
20
25
|
export declare const isHeavyCompiler: (name: string) => boolean;
|
|
21
26
|
export declare const canPause: (name: string) => boolean;
|
|
22
27
|
export type Level = 'ok' | 'throttled' | 'suspending';
|
|
@@ -33,6 +38,8 @@ export interface GovernorState {
|
|
|
33
38
|
readonly throttled: readonly number[];
|
|
34
39
|
/** Pids whose working set was trimmed while they waited. */
|
|
35
40
|
readonly trimmed: readonly number[];
|
|
41
|
+
/** Pids whose I/O priority was lowered, and to what. */
|
|
42
|
+
readonly ioLowered: Readonly<Record<number, IoPriority>>;
|
|
36
43
|
/** The CPU cap last set on the throttle job (percent; 100 = none). */
|
|
37
44
|
readonly cpuRate: number;
|
|
38
45
|
/** Used commit (MB) and when, at the last sample: how fast it grows. */
|
|
@@ -51,6 +58,13 @@ export type Action = {
|
|
|
51
58
|
readonly pid: number;
|
|
52
59
|
readonly name: string;
|
|
53
60
|
readonly to: Priority;
|
|
61
|
+
}
|
|
62
|
+
/** Disk requests of a compiler or linker queue behind everything else (the PC's disks are the scarce thing). */
|
|
63
|
+
| {
|
|
64
|
+
readonly kind: 'io';
|
|
65
|
+
readonly pid: number;
|
|
66
|
+
readonly name: string;
|
|
67
|
+
readonly to: IoPriority;
|
|
54
68
|
} | {
|
|
55
69
|
readonly kind: 'memory';
|
|
56
70
|
readonly pid: number;
|