windows-development-sandbox 1.27.3 → 1.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-RU7XRN62.js → chunk-CB44HTTN.js} +196 -75
- package/dist/bin/{chunk-FA5VWLP4.js → chunk-UZ5S3EMB.js} +1 -1
- package/dist/bin/{chunk-4M5SZZ5P.js → chunk-ZMZI3625.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/core/schema/sandbox.js +1 -1
- package/dist/guest/daemon/heartbeat.js +1 -0
- package/dist/guest/governor/core.d.ts +53 -1
- package/dist/guest/governor/core.js +95 -1
- package/dist/guest/governor/decide.d.ts +4 -0
- package/dist/guest/governor/decide.js +2 -0
- package/dist/guest/governor/governor.js +23 -9
- package/dist/guest/toolchains/native/unreal.js +22 -3
- package/dist/guest/toolchains/runtimes.js +7 -6
- package/dist/host/life/crash-report.d.ts +4 -0
- package/dist/host/life/crash-report.js +30 -1
- package/dist/host/life/last-end.js +1 -0
- package/dist/host/tools/windows-terminal.d.ts +4 -1
- package/dist/host/tools/windows-terminal.js +25 -9
- package/dist/runtime/agent.js +5 -1
- package/dist/runtime/daemon.js +183 -70
- package/dist/runtime/db.js +5 -1
- package/dist/runtime/main.js +242 -56
- package/dist/runtime/netproxy.js +5 -1
- package/dist/runtime/tool.js +5 -1
- package/dist/runtime/view.js +125 -27
- package/dist/selftest/agent-paths.test.js +3 -1
- package/dist/selftest/channel.test.js +33 -0
- package/dist/selftest/heavy-load.test.js +116 -4
- package/dist/selftest/live-terminal.test.js +103 -0
- package/dist/selftest/personalization.test.js +2 -1
- package/dist/selftest/support/engine-golden.js +2 -0
- package/dist/shared/channel/schema.d.ts +2 -0
- package/dist/shared/channel/schema.js +4 -0
- package/dist/shared/memory/index.d.ts +1 -0
- package/dist/shared/memory/index.js +1 -0
- package/dist/shared/memory/policy.d.ts +12 -0
- package/dist/shared/memory/policy.js +4 -0
- package/dist/shared/memory/unreal-build.d.ts +2 -0
- package/dist/shared/memory/unreal-build.js +21 -0
- package/dist/shared/pty/clipboard-watch.d.ts +4 -0
- package/dist/shared/pty/clipboard-watch.js +40 -0
- package/dist/shared/pty/ctrl-c.d.ts +13 -7
- package/dist/shared/pty/ctrl-c.js +35 -19
- package/dist/shared/pty/index.d.ts +1 -0
- package/dist/shared/pty/index.js +1 -0
- package/dist/shared/pty/terminal.d.ts +1 -1
- package/dist/shared/pty/terminal.js +16 -13
- package/dist/shared/web/ctrl-c.d.ts +26 -0
- package/dist/shared/web/ctrl-c.js +64 -0
- package/dist/shared/web/index.d.ts +1 -0
- package/dist/shared/web/index.js +1 -0
- package/dist/web/index.html +1 -1
- package/dist/web/{main-NKM7ECSB.js → main-7NY4X6YU.js} +67 -67
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
- package/package.json +1 -1
- package/sandbox/csharp/ClipboardWatch.cs +59 -0
- package/sandbox/csharp/MemGov.cs +752 -7
- package/sandbox/scripts/watch-clipboard.ps1 +8 -0
- package/schemas/config.schema.json +1 -1
|
@@ -1,12 +1,62 @@
|
|
|
1
1
|
import type { PressureStatus } from '../../shared/channel/schema.js';
|
|
2
|
-
import
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { type LoadProfile } from '../../shared/memory/index.js';
|
|
3
4
|
import { type GovernorState, type MemSample } from './decide.js';
|
|
5
|
+
/** memgov.exe's arguments: samples every 2 s and the memory gate for this profile. */
|
|
6
|
+
export declare function memgovArgs(profile: LoadProfile): string[];
|
|
7
|
+
/** What the memory gate in memgov reports on its stdout (one JSON line each). */
|
|
8
|
+
export declare const GateEventSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
9
|
+
kind: z.ZodLiteral<"ready">;
|
|
10
|
+
mode: z.ZodEnum<{
|
|
11
|
+
notify: "notify";
|
|
12
|
+
poll: "poll";
|
|
13
|
+
}>;
|
|
14
|
+
growthMb: z.ZodNumber;
|
|
15
|
+
windowMs: z.ZodNumber;
|
|
16
|
+
event: z.ZodLiteral<"gate">;
|
|
17
|
+
at: z.ZodNumber;
|
|
18
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
19
|
+
kind: z.ZodLiteral<"off">;
|
|
20
|
+
reason: z.ZodString;
|
|
21
|
+
event: z.ZodLiteral<"gate">;
|
|
22
|
+
at: z.ZodNumber;
|
|
23
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
24
|
+
kind: z.ZodLiteral<"trip">;
|
|
25
|
+
grewMb: z.ZodNumber;
|
|
26
|
+
ms: z.ZodNumber;
|
|
27
|
+
jobMb: z.ZodNumber;
|
|
28
|
+
held: z.ZodArray<z.ZodObject<{
|
|
29
|
+
pid: z.ZodNumber;
|
|
30
|
+
name: z.ZodString;
|
|
31
|
+
}, z.core.$strict>>;
|
|
32
|
+
event: z.ZodLiteral<"gate">;
|
|
33
|
+
at: z.ZodNumber;
|
|
34
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
35
|
+
kind: z.ZodLiteral<"hold">;
|
|
36
|
+
pid: z.ZodNumber;
|
|
37
|
+
name: z.ZodString;
|
|
38
|
+
event: z.ZodLiteral<"gate">;
|
|
39
|
+
at: z.ZodNumber;
|
|
40
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
41
|
+
kind: z.ZodLiteral<"release">;
|
|
42
|
+
pid: z.ZodNumber;
|
|
43
|
+
name: z.ZodString;
|
|
44
|
+
heldMs: z.ZodNumber;
|
|
45
|
+
forced: z.ZodBoolean;
|
|
46
|
+
event: z.ZodLiteral<"gate">;
|
|
47
|
+
at: z.ZodNumber;
|
|
48
|
+
}, z.core.$strict>], "kind">;
|
|
49
|
+
export type GateEvent = z.output<typeof GateEventSchema>;
|
|
50
|
+
/** One memory-log line for a gate event (names cut to 64 characters). */
|
|
51
|
+
export declare function gateLine(e: GateEvent): string;
|
|
4
52
|
/** The newest numbers the governor saw (for the heartbeat file). */
|
|
5
53
|
export interface GovernorSnapshot {
|
|
6
54
|
readonly ramFreePercent: number;
|
|
7
55
|
readonly commitPercent: number;
|
|
8
56
|
readonly step: number;
|
|
9
57
|
readonly hostStep: number;
|
|
58
|
+
/** Build programs the memory gate holds now. */
|
|
59
|
+
readonly held: number;
|
|
10
60
|
readonly diskQueue: number | null;
|
|
11
61
|
readonly top: readonly {
|
|
12
62
|
readonly name: string;
|
|
@@ -57,6 +107,8 @@ export declare class GovernorCore {
|
|
|
57
107
|
get snapshot(): GovernorSnapshot | null;
|
|
58
108
|
get current(): GovernorState;
|
|
59
109
|
onSample(sample: MemSample): void;
|
|
110
|
+
/** An event from the memory gate in memgov: goes to the memory log. */
|
|
111
|
+
onGate(event: GateEvent): void;
|
|
60
112
|
private apply;
|
|
61
113
|
private note;
|
|
62
114
|
}
|
|
@@ -1,6 +1,94 @@
|
|
|
1
1
|
import { now } from '../../shared/channel/run-dir.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { BURST_GATE } from '../../shared/memory/index.js';
|
|
2
4
|
import { errMsg } from '../../core/util/errors.js';
|
|
3
|
-
import { decide, HOLD_MS, INITIAL_STATE } from './decide.js';
|
|
5
|
+
import { decide, GATE_NAMES, HOLD_MS, INITIAL_STATE, } from './decide.js';
|
|
6
|
+
/** memgov.exe's arguments: samples every 2 s and the memory gate for this profile. */
|
|
7
|
+
export function memgovArgs(profile) {
|
|
8
|
+
const g = BURST_GATE[profile];
|
|
9
|
+
return [
|
|
10
|
+
'serve',
|
|
11
|
+
'--interval',
|
|
12
|
+
'2',
|
|
13
|
+
'--gate',
|
|
14
|
+
[g.growthMb, g.windowMs, g.releaseMs, g.maxHoldMs].join(':'),
|
|
15
|
+
'--gate-names',
|
|
16
|
+
GATE_NAMES.join(','),
|
|
17
|
+
];
|
|
18
|
+
}
|
|
19
|
+
const GateBase = { event: z.literal('gate'), at: z.number() };
|
|
20
|
+
const GateName = z.string().max(128);
|
|
21
|
+
const GateCount = z.number().int().min(0);
|
|
22
|
+
/** What the memory gate in memgov reports on its stdout (one JSON line each). */
|
|
23
|
+
export const GateEventSchema = z.discriminatedUnion('kind', [
|
|
24
|
+
z.strictObject({
|
|
25
|
+
...GateBase,
|
|
26
|
+
kind: z.literal('ready'),
|
|
27
|
+
mode: z.enum(['notify', 'poll']),
|
|
28
|
+
growthMb: GateCount,
|
|
29
|
+
windowMs: GateCount,
|
|
30
|
+
}),
|
|
31
|
+
z.strictObject({ ...GateBase, kind: z.literal('off'), reason: z.string().max(200) }),
|
|
32
|
+
z.strictObject({
|
|
33
|
+
...GateBase,
|
|
34
|
+
kind: z.literal('trip'),
|
|
35
|
+
grewMb: GateCount,
|
|
36
|
+
ms: GateCount,
|
|
37
|
+
jobMb: GateCount,
|
|
38
|
+
held: z.array(z.strictObject({ pid: z.number().int(), name: GateName })).max(64),
|
|
39
|
+
}),
|
|
40
|
+
z.strictObject({ ...GateBase, kind: z.literal('hold'), pid: z.number().int(), name: GateName }),
|
|
41
|
+
z.strictObject({
|
|
42
|
+
...GateBase,
|
|
43
|
+
kind: z.literal('release'),
|
|
44
|
+
pid: z.number().int(),
|
|
45
|
+
name: GateName,
|
|
46
|
+
heldMs: GateCount,
|
|
47
|
+
forced: z.boolean(),
|
|
48
|
+
}),
|
|
49
|
+
]);
|
|
50
|
+
/** One memory-log line for a gate event (names cut to 64 characters). */
|
|
51
|
+
export function gateLine(e) {
|
|
52
|
+
const cut = (s) => s.slice(0, 64);
|
|
53
|
+
switch (e.kind) {
|
|
54
|
+
case 'ready':
|
|
55
|
+
return ('gate: on (' +
|
|
56
|
+
e.mode +
|
|
57
|
+
"): new compilers wait when the agents' programs grow by more than " +
|
|
58
|
+
e.growthMb +
|
|
59
|
+
' MB within ' +
|
|
60
|
+
e.windowMs / 1000 +
|
|
61
|
+
' s');
|
|
62
|
+
case 'off':
|
|
63
|
+
return 'gate: off (' + e.reason + '): builds are only slowed down by the dial';
|
|
64
|
+
case 'trip':
|
|
65
|
+
return ("gate: the agents' programs grew " +
|
|
66
|
+
e.grewMb +
|
|
67
|
+
' MB in ' +
|
|
68
|
+
(e.ms / 1000).toFixed(1) +
|
|
69
|
+
' s (now ' +
|
|
70
|
+
e.jobMb +
|
|
71
|
+
' MB); holding ' +
|
|
72
|
+
e.held.length +
|
|
73
|
+
': ' +
|
|
74
|
+
e.held
|
|
75
|
+
.slice(0, 8)
|
|
76
|
+
.map((p) => cut(p.name) + ' (' + p.pid + ')')
|
|
77
|
+
.join(', ') +
|
|
78
|
+
(e.held.length > 8 ? ' …' : ''));
|
|
79
|
+
case 'hold':
|
|
80
|
+
return 'gate: ' + cut(e.name) + ' (' + e.pid + ') waits at start while a build burst goes through in turn';
|
|
81
|
+
case 'release':
|
|
82
|
+
return ('gate: ' +
|
|
83
|
+
cut(e.name) +
|
|
84
|
+
' (' +
|
|
85
|
+
e.pid +
|
|
86
|
+
') goes on after ' +
|
|
87
|
+
(e.heldMs / 1000).toFixed(1) +
|
|
88
|
+
' s' +
|
|
89
|
+
(e.forced ? ' (the longest it may wait)' : ''));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
4
92
|
const STEP_TEXT = [
|
|
5
93
|
'builds back to normal',
|
|
6
94
|
'build programs at lower priority',
|
|
@@ -55,6 +143,7 @@ export class GovernorCore {
|
|
|
55
143
|
commitPercent: d.commitPercent,
|
|
56
144
|
step: d.state.step,
|
|
57
145
|
hostStep: nowMs < d.state.hostUntil ? d.state.hostStep : 0,
|
|
146
|
+
held: sample.gateHeld ?? 0,
|
|
58
147
|
diskQueue: null,
|
|
59
148
|
top: [...sample.procs]
|
|
60
149
|
.sort((a, b) => b.mb - a.mb)
|
|
@@ -89,6 +178,7 @@ export class GovernorCore {
|
|
|
89
178
|
paused: [...this.held]
|
|
90
179
|
.slice(0, 8)
|
|
91
180
|
.map(([pid, name]) => ({ name: name.slice(0, 64), pid, seconds: HOLD_MS / 1000 })),
|
|
181
|
+
held: sample.gateHeld ?? 0,
|
|
92
182
|
cpuPercent: d.state.cpuRate,
|
|
93
183
|
pcLow: nowMs < d.state.hostUntil,
|
|
94
184
|
lastAction: this.lastAction.slice(0, 300),
|
|
@@ -102,6 +192,10 @@ export class GovernorCore {
|
|
|
102
192
|
this.deps.saveStatus(status);
|
|
103
193
|
}
|
|
104
194
|
}
|
|
195
|
+
/** An event from the memory gate in memgov: goes to the memory log. */
|
|
196
|
+
onGate(event) {
|
|
197
|
+
this.note(gateLine(event));
|
|
198
|
+
}
|
|
105
199
|
apply(action) {
|
|
106
200
|
const port = this.deps.port;
|
|
107
201
|
try {
|
|
@@ -16,6 +16,8 @@ export interface MemSample {
|
|
|
16
16
|
readonly commitAvailMb: number;
|
|
17
17
|
/** Only the processes in the agents' job. */
|
|
18
18
|
readonly procs: readonly ProcSample[];
|
|
19
|
+
/** Build programs the memory gate holds now (memgov). */
|
|
20
|
+
readonly gateHeld?: number;
|
|
19
21
|
}
|
|
20
22
|
export declare const isNeverTouched: (name: string) => boolean;
|
|
21
23
|
export declare const canLower: (name: string) => boolean;
|
|
@@ -24,6 +26,8 @@ export declare const isLinker: (name: string) => boolean;
|
|
|
24
26
|
export declare function linkerSlots(totalMb: number, cpus: number): number;
|
|
25
27
|
export declare const isHeavyCompiler: (name: string) => boolean;
|
|
26
28
|
export declare const canPause: (name: string) => boolean;
|
|
29
|
+
/** What the memory gate in memgov may freeze (compilers, linkers, Unreal's shader workers); passed as --gate-names. */
|
|
30
|
+
export declare const GATE_NAMES: readonly string[];
|
|
27
31
|
export type Level = 'ok' | 'throttled' | 'suspending';
|
|
28
32
|
export interface GovernorState {
|
|
29
33
|
/** The dial: 0 nothing wrong, 1 lower priority, 2 less CPU and fewer compilers, 3 and 4 harder. */
|
|
@@ -109,6 +109,8 @@ export function linkerSlots(totalMb, cpus) {
|
|
|
109
109
|
}
|
|
110
110
|
export const isHeavyCompiler = (name) => !isNeverTouched(name) && HEAVY.has(key(name));
|
|
111
111
|
export const canPause = (name) => !isNeverTouched(name) && COMPILERS.has(key(name));
|
|
112
|
+
/** What the memory gate in memgov may freeze (compilers, linkers, Unreal's shader workers); passed as --gate-names. */
|
|
113
|
+
export const GATE_NAMES = [...COMPILERS, 'shadercompileworker'].filter((n) => !NEVER.has(n)).sort();
|
|
112
114
|
export const INITIAL_STATE = {
|
|
113
115
|
step: 0,
|
|
114
116
|
calm: 0,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The memory governor: samples the sandbox's memory every 2 seconds (memgov.exe, sandbox/csharp/MemGov.cs) and turns
|
|
2
2
|
// a dial as it fills: the build programs the agents started get lower priority, lower memory priority, less CPU, and
|
|
3
|
-
// take turns (decide.ts).
|
|
3
|
+
// take turns (decide.ts). A memory gate in memgov also holds new compilers when memory grows faster than a few GB in
|
|
4
|
+
// seconds. Builds slow down; nothing fails. What it does goes to the memory log and run\pressure.json.
|
|
4
5
|
import fs from 'node:fs';
|
|
5
6
|
import os from 'node:os';
|
|
6
7
|
import path from 'node:path';
|
|
@@ -8,14 +9,14 @@ import { spawn } from 'node:child_process';
|
|
|
8
9
|
import { createInterface } from 'node:readline';
|
|
9
10
|
import { z } from 'zod';
|
|
10
11
|
import { writeJsonAtomic } from '../../shared/io/json.js';
|
|
11
|
-
import { DIAL_STEPS, maxCompilers } from '../../shared/memory/index.js';
|
|
12
|
+
import { BURST_GATE, DIAL_STEPS, maxCompilers } from '../../shared/memory/index.js';
|
|
12
13
|
import { ensureCompiled } from '../runtime/csc.js';
|
|
13
14
|
import { dataDir } from '../runtime/paths.js';
|
|
14
15
|
import { warn } from '../runtime/log.js';
|
|
15
16
|
import { run } from '../runtime/paths.js';
|
|
16
17
|
import { session } from '../runtime/session.js';
|
|
17
18
|
import { errMsg } from '../../core/util/errors.js';
|
|
18
|
-
import { GovernorCore } from './core.js';
|
|
19
|
+
import { GateEventSchema, GovernorCore, memgovArgs, } from './core.js';
|
|
19
20
|
const SampleSchema = z.object({
|
|
20
21
|
ramTotalMb: z.number(),
|
|
21
22
|
ramAvailMb: z.number(),
|
|
@@ -29,27 +30,36 @@ const SampleSchema = z.object({
|
|
|
29
30
|
prio: z.enum(['idle', 'below', 'normal']).catch('normal'),
|
|
30
31
|
}))
|
|
31
32
|
.max(1024),
|
|
33
|
+
gateHeld: z.number().int().min(0).max(1024).optional().catch(undefined),
|
|
32
34
|
});
|
|
33
35
|
/** memgov.exe as a child process: samples come out of its stdout, commands go into its stdin. */
|
|
34
36
|
class MemGovProcess {
|
|
37
|
+
args;
|
|
35
38
|
onSample;
|
|
39
|
+
onGate;
|
|
36
40
|
child = null;
|
|
37
|
-
constructor(onSample) {
|
|
41
|
+
constructor(args, onSample, onGate) {
|
|
42
|
+
this.args = args;
|
|
38
43
|
this.onSample = onSample;
|
|
44
|
+
this.onGate = onGate;
|
|
39
45
|
}
|
|
40
46
|
start() {
|
|
41
47
|
try {
|
|
42
|
-
const child = spawn(ensureCompiled('memgov'), [
|
|
48
|
+
const child = spawn(ensureCompiled('memgov'), [...this.args], {
|
|
43
49
|
stdio: ['pipe', 'pipe', 'ignore'],
|
|
44
50
|
windowsHide: true,
|
|
45
51
|
});
|
|
46
52
|
this.child = child;
|
|
47
53
|
createInterface({ input: child.stdout }).on('line', (line) => {
|
|
48
54
|
try {
|
|
49
|
-
|
|
55
|
+
const raw = JSON.parse(line);
|
|
56
|
+
if (typeof raw === 'object' && raw !== null && raw.event === 'gate')
|
|
57
|
+
this.onGate(GateEventSchema.parse(raw));
|
|
58
|
+
else
|
|
59
|
+
this.onSample(SampleSchema.parse(raw));
|
|
50
60
|
}
|
|
51
61
|
catch (e) {
|
|
52
|
-
warn('governor: bad
|
|
62
|
+
warn('governor: bad line from memgov: ' + errMsg(e));
|
|
53
63
|
}
|
|
54
64
|
});
|
|
55
65
|
child.stdin.on('error', () => undefined);
|
|
@@ -120,13 +130,17 @@ export class Governor {
|
|
|
120
130
|
first.availablePercent +
|
|
121
131
|
'% free RAM or ' +
|
|
122
132
|
first.commitPercent +
|
|
123
|
-
'% commit, in steps'
|
|
133
|
+
'% commit, in steps; holds new compilers when memory grows faster than ' +
|
|
134
|
+
BURST_GATE[profile].growthMb +
|
|
135
|
+
' MB in ' +
|
|
136
|
+
BURST_GATE[profile].windowMs / 1000 +
|
|
137
|
+
' s';
|
|
124
138
|
}
|
|
125
139
|
start() {
|
|
126
140
|
if (session.pressureResponse === false)
|
|
127
141
|
return;
|
|
128
142
|
const profile = session.loadProfile === 'gentle' ? 'gentle' : 'normal';
|
|
129
|
-
const proc = new MemGovProcess((s) => this.core?.onSample(s));
|
|
143
|
+
const proc = new MemGovProcess(memgovArgs(profile), (s) => this.core?.onSample(s), (e) => this.core?.onGate(e));
|
|
130
144
|
this.core = new GovernorCore({
|
|
131
145
|
port: proc,
|
|
132
146
|
profile,
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
// Unreal Engine development, machine-wide (toolchains.unreal, once per boot): the DirectX End-User Runtime the engine
|
|
2
2
|
// lists and the Epic Games Launcher (real installers: they run every boot), the launcher pointed at the data disk for
|
|
3
|
-
// engines and given back the engines it installed there, the Windows SDK debuggers for crash dumps,
|
|
4
|
-
// UnrealBuildTool finds Visual Studio the way it looks (the Setup Configuration API)
|
|
5
|
-
// step (visual-studio.ts), which the plan runs first; each project's engine is set up
|
|
3
|
+
// engines and given back the engines it installed there, the Windows SDK debuggers for crash dumps, a check that
|
|
4
|
+
// UnrealBuildTool finds Visual Studio the way it looks (the Setup Configuration API), and its parallel compiles capped.
|
|
5
|
+
// Visual Studio itself is its own step (visual-studio.ts), which the plan runs first; each project's engine is set up
|
|
6
|
+
// by engines/unreal-engines.ts.
|
|
6
7
|
import fs from 'node:fs';
|
|
7
8
|
import os from 'node:os';
|
|
8
9
|
import path from 'node:path';
|
|
9
10
|
import { errMsg } from '../../../core/util/errors.js';
|
|
10
11
|
import { readValid } from '../../../shared/io/json.js';
|
|
11
12
|
import { safeRemove } from '../../../shared/io/safe-remove.js';
|
|
13
|
+
import { buildJobs, ubtBuildConfiguration, usableCpus } from '../../../shared/memory/index.js';
|
|
12
14
|
import { VsSetupReportSchema, vsVerdict } from '../../../shared/unreal/index.js';
|
|
13
15
|
import { runInstaller } from '../../runtime/pools.js';
|
|
14
16
|
import { dryRun } from '../../runtime/paths.js';
|
|
15
17
|
import { exists, mkdirp } from '../../runtime/fs.js';
|
|
16
18
|
import { log, warn } from '../../runtime/log.js';
|
|
17
19
|
import { runScriptAsync } from '../../runtime/process.js';
|
|
20
|
+
import { session } from '../../runtime/session.js';
|
|
18
21
|
import { ensureLauncher, fetchLauncher, placeEngines, restoreLauncherItems } from '../engines/epic-launcher.js';
|
|
19
22
|
import { resetUnrealStatus, updateUnrealStatus } from '../engines/unreal-status.js';
|
|
20
23
|
import { SIGNERS, signedInstaller } from '../installers.js';
|
|
@@ -68,6 +71,18 @@ async function checkVisualStudio() {
|
|
|
68
71
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
69
72
|
}
|
|
70
73
|
}
|
|
74
|
+
/**
|
|
75
|
+
* UnrealBuildTool's per-user BuildConfiguration.xml (%APPDATA%, wiped with the sandbox): compiles at once from the
|
|
76
|
+
* sandbox's build budget, so a build start cannot outrun the memory Windows backs the sandbox with. Returns the cap.
|
|
77
|
+
*/
|
|
78
|
+
function capUnrealBuilds() {
|
|
79
|
+
const cpus = usableCpus(os.cpus().length, session.cpuLimitPercent ?? null);
|
|
80
|
+
const jobs = buildJobs(cpus, os.totalmem() / 1024 ** 3);
|
|
81
|
+
const appData = process.env.APPDATA ?? 'C:\\Users\\WDAGUtilityAccount\\AppData\\Roaming';
|
|
82
|
+
const dir = mkdirp(path.join(appData, 'Unreal Engine', 'UnrealBuildTool'));
|
|
83
|
+
fs.writeFileSync(path.join(dir, 'BuildConfiguration.xml'), ubtBuildConfiguration(jobs), 'utf8');
|
|
84
|
+
return jobs;
|
|
85
|
+
}
|
|
71
86
|
/** Runs one part, warning (not failing the step) when it does not work. */
|
|
72
87
|
async function part(what, work, fallback) {
|
|
73
88
|
try {
|
|
@@ -86,6 +101,9 @@ export async function setupUnreal(env, config, _ctx) {
|
|
|
86
101
|
return;
|
|
87
102
|
}
|
|
88
103
|
resetUnrealStatus();
|
|
104
|
+
const jobs = await part('capping parallel compiles', async () => capUnrealBuilds(), 0);
|
|
105
|
+
if (jobs)
|
|
106
|
+
log('Unreal: UnrealBuildTool runs at most ' + jobs + ' compiles at once (the build budget)');
|
|
89
107
|
await part('the DirectX End-User Runtime', installDirectX, undefined);
|
|
90
108
|
const launcher = await part('the Epic Games Launcher', ensureLauncher, null);
|
|
91
109
|
// Before anyone opens the launcher: engines go to the data disk, and it knows the ones it installed there before.
|
|
@@ -131,6 +149,7 @@ export async function setupUnreal(env, config, _ctx) {
|
|
|
131
149
|
vs.version +
|
|
132
150
|
(check.ok ? ' (UnrealBuildTool finds it)' : ' (UnrealBuildTool would NOT find it)')
|
|
133
151
|
: 'no Visual Studio') +
|
|
152
|
+
(jobs ? ', at most ' + jobs + ' compiles at once' : '') +
|
|
134
153
|
', DirectX End-User Runtime' +
|
|
135
154
|
(launcher ? ', Epic Games Launcher' : '') +
|
|
136
155
|
(debuggers ? ', cdb/windbg' : ''));
|
|
@@ -28,12 +28,13 @@ export async function setupSingleExe(env, name, repo, pattern, exeName, dir, opt
|
|
|
28
28
|
}
|
|
29
29
|
export async function setupBun(env) {
|
|
30
30
|
await setupSingleExe(env, 'bun', 'oven-sh/bun', arch === 'arm64' ? /^bun-windows-aarch64\.zip$/ : /^bun-windows-x64\.zip$/, 'bun.exe', tool('bun', 'bin'), { size: '35 MB' });
|
|
31
|
-
// Bun cannot keep its cache or temp files on a mapped folder (EINVAL); they go to the sandbox's own disk.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
31
|
+
// Bun cannot keep its cache or temp files on a mapped folder (EINVAL); they go to the sandbox's own disk. The temp
|
|
32
|
+
// folder must exist: programs compiled with Bun (opencode) unpack their DLLs there and fail when it is missing.
|
|
33
|
+
const cache = path.join(localDir, 'bun-cache');
|
|
34
|
+
const temp = path.join(localDir, 'bun-tmp');
|
|
35
|
+
if (!dryRun)
|
|
36
|
+
[cache, temp].forEach((dir) => mkdirp(dir));
|
|
37
|
+
Object.assign(env.vars, { BUN_INSTALL: tool('bun'), BUN_INSTALL_CACHE_DIR: cache, BUN_TMPDIR: temp });
|
|
37
38
|
}
|
|
38
39
|
export async function setupDeno(env) {
|
|
39
40
|
await setupSingleExe(env, 'deno', 'denoland/deno', arch === 'arm64' ? /^deno-aarch64-pc-windows-msvc[.]zip$/ : /^deno-x86_64-pc-windows-msvc[.]zip$/, 'deno.exe', tool('deno', 'bin'), { size: '40 MB' });
|
|
@@ -22,6 +22,10 @@ export declare function findingLines(events: readonly StopEvent[]): string[];
|
|
|
22
22
|
export declare function findDumps(dirs: readonly string[], since: Date): DumpFile[];
|
|
23
23
|
/** Whether the newest launch started at or before `lastSeen` had vGPU on (host-only .wsb in <storage>\launches). */
|
|
24
24
|
export declare function launchHadVgpu(storageRoot: string, lastSeen: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* "This PC still had ..." from the last line of its memory log, when RAM and commit were both 4 GB or more free (pure).
|
|
27
|
+
*/
|
|
28
|
+
export declare function memoryWasFineLine(hostMemoryLog: string): string | null;
|
|
25
29
|
/**
|
|
26
30
|
* The report text (pure): what happened (the sandbox's or this PC's stop code, where the dump is) and the memory log's
|
|
27
31
|
* last lines. `memoryLog` is the sandbox's own file, so it is shown through safeText.
|
|
@@ -54,7 +54,10 @@ export function findingLines(events) {
|
|
|
54
54
|
lines.push('WHEA-Logger: Windows reported a hardware error (CPU, memory, a bus or a disk): heat or failing hardware.');
|
|
55
55
|
if (has((e) => lower(e).startsWith('hyper-v')))
|
|
56
56
|
lines.push("Hyper-V events: the sandbox's virtual machine (or the service that runs it) reported a problem.");
|
|
57
|
-
|
|
57
|
+
const isAppCrash = (e) => /^(application error|windows error reporting)$/i.test(e.provider);
|
|
58
|
+
if (has((e) => isAppCrash(e) && /vmwp\.exe/i.test(e.process)))
|
|
59
|
+
lines.push("vmwp.exe, the Windows program that runs the sandbox's VM, crashed. That is a fault in Windows' Hyper-V code or a driver it uses (with vGPU on, usually the graphics driver), not a program inside the sandbox running out of memory.");
|
|
60
|
+
else if (has(isAppCrash))
|
|
58
61
|
lines.push("A crash report names vmwp.exe or vmmem, the programs that hold the sandbox's VM.");
|
|
59
62
|
return lines;
|
|
60
63
|
}
|
|
@@ -122,6 +125,30 @@ function hostMemoryTail(storageRoot, dataPath) {
|
|
|
122
125
|
return '';
|
|
123
126
|
}
|
|
124
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* "This PC still had ..." from the last line of its memory log, when RAM and commit were both 4 GB or more free (pure).
|
|
130
|
+
*/
|
|
131
|
+
export function memoryWasFineLine(hostMemoryLog) {
|
|
132
|
+
const last = hostMemoryLog
|
|
133
|
+
.split(/\r?\n/)
|
|
134
|
+
.filter((l) => l.trim() !== '')
|
|
135
|
+
.pop();
|
|
136
|
+
if (!last)
|
|
137
|
+
return null;
|
|
138
|
+
const ram = /RAM free\/total MB (\d{1,9})\/(\d{1,9})/.exec(last);
|
|
139
|
+
const commit = /commit free\/limit MB (\d{1,9})\/(\d{1,9})/.exec(last);
|
|
140
|
+
if (!ram || !commit)
|
|
141
|
+
return null;
|
|
142
|
+
const freeRam = Number(ram[1]);
|
|
143
|
+
const freeCommit = Number(commit[1]);
|
|
144
|
+
if (freeRam < 4096 || freeCommit < 4096)
|
|
145
|
+
return null;
|
|
146
|
+
return ('This PC still had ' +
|
|
147
|
+
Math.floor(freeRam / 1024) +
|
|
148
|
+
' GB of RAM and ' +
|
|
149
|
+
Math.floor(freeCommit / 1024) +
|
|
150
|
+
' GB of memory (RAM plus paging file) free just before, so it did not run out of memory.');
|
|
151
|
+
}
|
|
125
152
|
/**
|
|
126
153
|
* The report text (pure): what happened (the sandbox's or this PC's stop code, where the dump is) and the memory log's
|
|
127
154
|
* last lines. `memoryLog` is the sandbox's own file, so it is shown through safeText.
|
|
@@ -152,6 +179,7 @@ export function crashReportText(cause, events, memoryLog, lastSeen, extra = {})
|
|
|
152
179
|
.map((l) => ' ' + safeText(l));
|
|
153
180
|
const freeze = freezeLine(cause, lastSeen);
|
|
154
181
|
const meaning = findingLines(unique);
|
|
182
|
+
const fine = memoryWasFineLine(extra.hostMemoryLog ?? '');
|
|
155
183
|
const dumps = extra.dumps ?? [];
|
|
156
184
|
const vgpu = extra.vgpu ? ['vGPU was on for this sandbox. ' + vgpuWarning('its launch settings'), ''] : [];
|
|
157
185
|
return [
|
|
@@ -165,6 +193,7 @@ export function crashReportText(cause, events, memoryLog, lastSeen, extra = {})
|
|
|
165
193
|
...vgpu,
|
|
166
194
|
'What that usually means:',
|
|
167
195
|
...(meaning.length ? meaning.map((l) => ' ' + l) : [' (no memory, blue screen or hardware event was logged)']),
|
|
196
|
+
...(fine ? [' ' + fine] : []),
|
|
168
197
|
'',
|
|
169
198
|
'Crash dumps Windows left (C:\\Windows\\Minidump, MEMORY.DMP):',
|
|
170
199
|
...(dumps.length ? dumps.map((d) => ' ' + d.name + ' ' + d.at) : [' (none from that time)']),
|
|
@@ -39,6 +39,7 @@ export function heartbeatLine(hb) {
|
|
|
39
39
|
hb.step +
|
|
40
40
|
(hb.hostStep > 0 ? ' (this PC asked for step ' + hb.hostStep + ')' : '') +
|
|
41
41
|
(hb.diskQueueReported !== undefined ? ', this PC disk queue ' + hb.diskQueueReported : '') +
|
|
42
|
+
(hb.held ? ', ' + hb.held + ' builds held by the memory gate' : '') +
|
|
42
43
|
(top ? '; biggest: ' + top : ''));
|
|
43
44
|
}
|
|
44
45
|
/** The likeliest reason, from the last heartbeat (pure). */
|
|
@@ -19,10 +19,13 @@ declare const TerminalSettingsSchema: z.ZodObject<{
|
|
|
19
19
|
export declare function withPwshProfile(settings: z.output<typeof TerminalSettingsSchema>, visible: boolean): z.output<typeof TerminalSettingsSchema> | null;
|
|
20
20
|
/** settings.json that makes new tabs read the current environment; null when it already does. Pure. */
|
|
21
21
|
export declare function withReloadEnvironment(settings: z.output<typeof TerminalSettingsSchema>): z.output<typeof TerminalSettingsSchema> | null;
|
|
22
|
+
/** settings.json that copies on select; null when the user already chose (true or false). Pure. */
|
|
23
|
+
export declare function withCopyOnSelect(settings: z.output<typeof TerminalSettingsSchema>): z.output<typeof TerminalSettingsSchema> | null;
|
|
22
24
|
/**
|
|
23
25
|
* Shows the PowerShell 7 profile once pwsh is installed in this data folder (and `allowed`, i.e. toolchains.pwsh is
|
|
24
26
|
* not off), hides it otherwise. settings.json is written by the sandbox too: read with a size cap, through no links,
|
|
25
|
-
* shape-checked, only rewritten when the profile changes
|
|
27
|
+
* shape-checked, only rewritten when the profile changes or a default switch is missing (reload the environment,
|
|
28
|
+
* copy on select; a value the user set stays); a file that does not parse is left alone.
|
|
26
29
|
*/
|
|
27
30
|
export declare function syncPwshProfile(dataPath: string, allowed: boolean): void;
|
|
28
31
|
export declare function ensureTerminal(dataPath: string, refresh: boolean, options?: {
|
|
@@ -24,6 +24,9 @@ const SETTINGS = {
|
|
|
24
24
|
theme: 'dark',
|
|
25
25
|
firstWindowPreference: 'defaultProfile',
|
|
26
26
|
confirmCloseAllTabs: false,
|
|
27
|
+
// Copied the moment the mouse is released, so an agent's redraw cannot clear the selection first.
|
|
28
|
+
copyOnSelect: true,
|
|
29
|
+
copyFormatting: 'none',
|
|
27
30
|
// New tabs pick up the PATH and variables the runner writes machine-wide after the window opened.
|
|
28
31
|
compatibility: { reloadEnvironmentVariables: true },
|
|
29
32
|
warning: { confirmCloseAllTabs: false, largePaste: false, multiLinePaste: false },
|
|
@@ -75,10 +78,17 @@ export function withReloadEnvironment(settings) {
|
|
|
75
78
|
return null;
|
|
76
79
|
return { ...settings, compatibility: { ...compatibility, reloadEnvironmentVariables: true } };
|
|
77
80
|
}
|
|
81
|
+
/** settings.json that copies on select; null when the user already chose (true or false). Pure. */
|
|
82
|
+
export function withCopyOnSelect(settings) {
|
|
83
|
+
if ('copyOnSelect' in settings)
|
|
84
|
+
return null;
|
|
85
|
+
return { ...settings, copyOnSelect: true };
|
|
86
|
+
}
|
|
78
87
|
/**
|
|
79
88
|
* Shows the PowerShell 7 profile once pwsh is installed in this data folder (and `allowed`, i.e. toolchains.pwsh is
|
|
80
89
|
* not off), hides it otherwise. settings.json is written by the sandbox too: read with a size cap, through no links,
|
|
81
|
-
* shape-checked, only rewritten when the profile changes
|
|
90
|
+
* shape-checked, only rewritten when the profile changes or a default switch is missing (reload the environment,
|
|
91
|
+
* copy on select; a value the user set stays); a file that does not parse is left alone.
|
|
82
92
|
*/
|
|
83
93
|
export function syncPwshProfile(dataPath, allowed) {
|
|
84
94
|
const file = path.join(terminalDir(dataPath), 'settings', 'settings.json');
|
|
@@ -100,16 +110,22 @@ export function syncPwshProfile(dataPath, allowed) {
|
|
|
100
110
|
}
|
|
101
111
|
const withProfile = withPwshProfile(parsed, visible);
|
|
102
112
|
if (withProfile) {
|
|
103
|
-
|
|
113
|
+
const reloaded = withReloadEnvironment(withProfile) ?? withProfile;
|
|
114
|
+
writeJson(file, withCopyOnSelect(reloaded) ?? reloaded);
|
|
104
115
|
return;
|
|
105
116
|
}
|
|
106
|
-
// Only
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
// Only default switches are missing: set those values in the text, so comments and the layout stay.
|
|
118
|
+
const missing = [];
|
|
119
|
+
if (withReloadEnvironment(parsed))
|
|
120
|
+
missing.push([['compatibility', 'reloadEnvironmentVariables'], true]);
|
|
121
|
+
if (withCopyOnSelect(parsed))
|
|
122
|
+
missing.push([['copyOnSelect'], true]);
|
|
123
|
+
if (missing.length === 0)
|
|
124
|
+
return;
|
|
125
|
+
let edited = text;
|
|
126
|
+
for (const [at, value] of missing)
|
|
127
|
+
edited = applyEdits(edited, modify(edited, at, value, { formattingOptions: { insertSpaces: true, tabSize: 4 } }));
|
|
128
|
+
writeFileAtomic(file, edited);
|
|
113
129
|
}
|
|
114
130
|
export async function ensureTerminal(dataPath, refresh, options = {}) {
|
|
115
131
|
const result = await installTerminal(dataPath, refresh);
|
package/dist/runtime/agent.js
CHANGED
|
@@ -25502,7 +25502,7 @@ var SandboxSchema = external_exports.strictObject({
|
|
|
25502
25502
|
'The last resort that keeps the sandbox up, in MB below its commit limit (RAM plus paging file): builds are slowed down long before (sandbox.pressureResponse); only if everything the agents start together would take the sandbox to the very edge of its memory, where Windows inside it would start failing, is a build refused memory instead of Windows. "auto" = the larger of 1536 MB and 3 % of the commit limit; "none" = no backstop. Yours only: a project file cannot change it.'
|
|
25503
25503
|
),
|
|
25504
25504
|
pressureResponse: external_exports.boolean().describe(
|
|
25505
|
-
"When the sandbox (or this PC) runs low on memory, slow builds down in steps, like a PC that gets slower instead of failing: build programs get lower CPU and memory priority, then less CPU, then compilers take turns (the waiting ones briefly suspended and trimmed to the paging file). Nothing is stopped or killed, and the agents themselves are never touched. Yours only: a project file cannot change it."
|
|
25505
|
+
"When the sandbox (or this PC) runs low on memory, slow builds down in steps, like a PC that gets slower instead of failing: build programs get lower CPU and memory priority, then less CPU, then compilers take turns (the waiting ones briefly suspended and trimmed to the paging file). Nothing is stopped or killed, and the agents themselves are never touched. A memory gate also holds new compilers and linkers for a moment when the memory of the build programs grows faster than 4 GB in 3 seconds (3 GB with vGPU), and lets them go one at a time. Yours only: a project file cannot change it."
|
|
25506
25506
|
),
|
|
25507
25507
|
vGpu: external_exports.boolean().describe(
|
|
25508
25508
|
"Virtual GPU. Off by default: Microsoft notes it widens the sandbox attack surface. The sandbox then uses this PC's graphics card, and with some graphics drivers heavy use can freeze the whole PC: turn it on only when a program in the sandbox needs the GPU (the mesa toolchain gives software OpenGL without it). Browsers in the sandbox never use it (they draw in software)."
|
|
@@ -27700,6 +27700,8 @@ var PressureStatusSchema = external_exports.object({
|
|
|
27700
27700
|
lowered: external_exports.number().int().min(0).max(1024),
|
|
27701
27701
|
/** Compilers waiting their turn right now (name, process id, seconds until they may go on). */
|
|
27702
27702
|
paused: external_exports.array(external_exports.object({ name: Text(64), pid: external_exports.number().int(), seconds: external_exports.number().int().min(0).max(60) })).max(8),
|
|
27703
|
+
/** Build programs the memory gate holds right now (let go one at a time). */
|
|
27704
|
+
held: external_exports.number().int().min(0).max(1024).optional(),
|
|
27703
27705
|
/** The CPU share the compilers get together (percent; 100 = no cap). */
|
|
27704
27706
|
cpuPercent: external_exports.number().int().min(0).max(100).optional(),
|
|
27705
27707
|
/** The PC itself asked for slowing down (its memory or CPU is low). */
|
|
@@ -27720,6 +27722,8 @@ var HeartbeatSchema = external_exports.object({
|
|
|
27720
27722
|
topProcesses: external_exports.array(external_exports.object({ name: Text(64), privateMb: external_exports.number().int().min(0).max(1e7) })).max(5),
|
|
27721
27723
|
/** The disk queue this PC last reported while asking the sandbox to slow down. */
|
|
27722
27724
|
diskQueueReported: external_exports.number().min(0).max(1e5).optional(),
|
|
27725
|
+
/** Build programs the memory gate holds right now (let go one at a time). */
|
|
27726
|
+
held: external_exports.number().int().min(0).max(1024).optional(),
|
|
27723
27727
|
/** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
|
|
27724
27728
|
phase: external_exports.enum(["ready", "working", "ending"])
|
|
27725
27729
|
});
|