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
package/dist/runtime/view.js
CHANGED
|
@@ -5743,7 +5743,7 @@ var OutputFilter = class {
|
|
|
5743
5743
|
}
|
|
5744
5744
|
};
|
|
5745
5745
|
|
|
5746
|
-
// src/shared/pty/
|
|
5746
|
+
// src/shared/pty/clipboard-watch.ts
|
|
5747
5747
|
import { spawn } from "node:child_process";
|
|
5748
5748
|
|
|
5749
5749
|
// src/shared/system.ts
|
|
@@ -5796,7 +5796,36 @@ function scriptArgs(name, args = []) {
|
|
|
5796
5796
|
];
|
|
5797
5797
|
}
|
|
5798
5798
|
|
|
5799
|
+
// src/shared/pty/clipboard-watch.ts
|
|
5800
|
+
var MAX_PENDING = 1024;
|
|
5801
|
+
function watchClipboard(onChange) {
|
|
5802
|
+
if (process.platform !== "win32") return { stop: () => void 0 };
|
|
5803
|
+
let child = null;
|
|
5804
|
+
try {
|
|
5805
|
+
child = spawn(SYSTEM.powershell(), scriptArgs("watch-clipboard.ps1", ["-ProcessId", String(process.pid)]), {
|
|
5806
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
5807
|
+
windowsHide: true
|
|
5808
|
+
});
|
|
5809
|
+
} catch {
|
|
5810
|
+
return { stop: () => void 0 };
|
|
5811
|
+
}
|
|
5812
|
+
let pending = "";
|
|
5813
|
+
child.stdout?.setEncoding("latin1");
|
|
5814
|
+
child.stdout?.on("data", (text) => {
|
|
5815
|
+
const lines = (pending + text).split("\n");
|
|
5816
|
+
pending = (lines.pop() ?? "").slice(-MAX_PENDING);
|
|
5817
|
+
for (const line of lines) if (line.trim() === "changed") onChange();
|
|
5818
|
+
});
|
|
5819
|
+
child.on("error", () => void 0);
|
|
5820
|
+
return {
|
|
5821
|
+
stop: () => {
|
|
5822
|
+
if (child && child.exitCode === null && child.signalCode === null) child.kill();
|
|
5823
|
+
}
|
|
5824
|
+
};
|
|
5825
|
+
}
|
|
5826
|
+
|
|
5799
5827
|
// src/shared/pty/console-input.ts
|
|
5828
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
5800
5829
|
function enableVtInput() {
|
|
5801
5830
|
if (process.platform !== "win32" || !process.stdin.isTTY)
|
|
5802
5831
|
return { ready: Promise.resolve(false), stop: () => Promise.resolve() };
|
|
@@ -5804,7 +5833,7 @@ function enableVtInput() {
|
|
|
5804
5833
|
let exited = Promise.resolve();
|
|
5805
5834
|
const ready = new Promise((resolve) => {
|
|
5806
5835
|
try {
|
|
5807
|
-
child =
|
|
5836
|
+
child = spawn2(SYSTEM.powershell(), scriptArgs("enable-vt-input.ps1", ["-ProcessId", String(process.pid)]), {
|
|
5808
5837
|
stdio: "ignore",
|
|
5809
5838
|
windowsHide: true
|
|
5810
5839
|
});
|
|
@@ -5863,30 +5892,94 @@ function unfinishedSequence(data) {
|
|
|
5863
5892
|
return data.length;
|
|
5864
5893
|
}
|
|
5865
5894
|
|
|
5866
|
-
// src/shared/
|
|
5895
|
+
// src/shared/web/protocol.ts
|
|
5896
|
+
var MAX_CLIENT_MESSAGE_BYTES = 256 * 1024;
|
|
5897
|
+
|
|
5898
|
+
// src/shared/web/ctrl-c.ts
|
|
5867
5899
|
var CTRL_C_CONFIRM_MS = 1500;
|
|
5868
5900
|
var CTRL_C_HINT = "Ctrl+C again to send it to the program (Esc stops an agent)";
|
|
5869
|
-
var
|
|
5870
|
-
var
|
|
5901
|
+
var CTRL_C_COPY_HINT = "Copied: Ctrl+C after selecting text is not sent to the program (Esc stops an agent)";
|
|
5902
|
+
var CtrlCDecider = class {
|
|
5903
|
+
constructor(confirm = true) {
|
|
5904
|
+
this.confirm = confirm;
|
|
5905
|
+
}
|
|
5906
|
+
confirm;
|
|
5871
5907
|
armedAt = Number.NEGATIVE_INFINITY;
|
|
5872
|
-
|
|
5873
|
-
/**
|
|
5874
|
-
|
|
5875
|
-
|
|
5908
|
+
selected = false;
|
|
5909
|
+
/** Text was selected in the program: Ctrl+C is "copy" from now on. */
|
|
5910
|
+
selection() {
|
|
5911
|
+
this.selected = true;
|
|
5912
|
+
this.armedAt = Number.NEGATIVE_INFINITY;
|
|
5913
|
+
}
|
|
5914
|
+
/** Any other key or a click: Ctrl+C is an interrupt again. */
|
|
5915
|
+
otherInput() {
|
|
5916
|
+
this.selected = false;
|
|
5917
|
+
}
|
|
5918
|
+
/** What to do with a lone Ctrl+C pressed at `nowMs`. */
|
|
5919
|
+
ctrlC(nowMs) {
|
|
5920
|
+
if (this.selected) return "copy";
|
|
5921
|
+
if (!this.confirm) return "send";
|
|
5876
5922
|
if (nowMs - this.armedAt <= CTRL_C_CONFIRM_MS) {
|
|
5877
5923
|
this.armedAt = Number.NEGATIVE_INFINITY;
|
|
5878
|
-
return
|
|
5924
|
+
return "send";
|
|
5879
5925
|
}
|
|
5880
5926
|
this.armedAt = nowMs;
|
|
5881
|
-
return
|
|
5927
|
+
return "hold";
|
|
5928
|
+
}
|
|
5929
|
+
};
|
|
5930
|
+
var MOUSE_REPORT = /\x1b\[<(\d{1,4});\d{1,5};\d{1,5}([Mm])|\x1b\[M([\s\S])[\s\S]{2}/g;
|
|
5931
|
+
function classifyInput(text) {
|
|
5932
|
+
let drag = false;
|
|
5933
|
+
let click = false;
|
|
5934
|
+
for (const m of text.matchAll(MOUSE_REPORT)) {
|
|
5935
|
+
const sgr = m[1] !== void 0;
|
|
5936
|
+
const b = sgr ? Number(m[1]) : (m[3] ?? " ").charCodeAt(0) - 32;
|
|
5937
|
+
if (b & 64) continue;
|
|
5938
|
+
if (b & 32) {
|
|
5939
|
+
if ((b & 3) !== 3) drag = true;
|
|
5940
|
+
} else if (sgr ? m[2] === "M" : (b & 3) !== 3) click = true;
|
|
5941
|
+
}
|
|
5942
|
+
return { mouse: drag ? "drag" : click ? "click" : null, keys: text.replace(MOUSE_REPORT, "").length > 0 };
|
|
5943
|
+
}
|
|
5944
|
+
function hasClipboardWrite(text) {
|
|
5945
|
+
return /\x1b\]0{0,2}52;/.test(text) || /\u009d0{0,2}52;/.test(text);
|
|
5946
|
+
}
|
|
5947
|
+
|
|
5948
|
+
// src/shared/pty/ctrl-c.ts
|
|
5949
|
+
var OSC_TITLE = /\x1b\][02];([^\x07\x1b]{0,256})(?:\x07|\x1b\\)/g;
|
|
5950
|
+
var CtrlCGuard = class {
|
|
5951
|
+
decider;
|
|
5952
|
+
lastTitle = "";
|
|
5953
|
+
constructor(options = {}) {
|
|
5954
|
+
this.decider = new CtrlCDecider(options.confirm !== false);
|
|
5955
|
+
}
|
|
5956
|
+
/** What to send for one read from the keyboard: a lone Ctrl+C only when it is not held back or taken as "copy". */
|
|
5957
|
+
push(chunk, nowMs) {
|
|
5958
|
+
if (chunk.length === 1 && chunk[0] === 3) {
|
|
5959
|
+
const action = this.decider.ctrlC(nowMs);
|
|
5960
|
+
if (action === "send") return { send: chunk, held: false, copied: false };
|
|
5961
|
+
return { send: Buffer.alloc(0), held: action === "hold", copied: action === "copy" };
|
|
5962
|
+
}
|
|
5963
|
+
const c = classifyInput(chunk.toString("latin1"));
|
|
5964
|
+
if (c.keys || c.mouse === "click") this.decider.otherInput();
|
|
5965
|
+
else if (c.mouse === "drag") this.decider.selection();
|
|
5966
|
+
return { send: chunk, held: false, copied: false };
|
|
5967
|
+
}
|
|
5968
|
+
/** The clipboard changed since the last key: you just copied. */
|
|
5969
|
+
clipboardChanged() {
|
|
5970
|
+
this.decider.selection();
|
|
5971
|
+
}
|
|
5972
|
+
/** The program wrote to the clipboard: that is a selection too. */
|
|
5973
|
+
watchClipboard(raw) {
|
|
5974
|
+
if (hasClipboardWrite(raw)) this.decider.selection();
|
|
5882
5975
|
}
|
|
5883
5976
|
/** Remembers the title the program last set (from its output), to put it back after the hint. */
|
|
5884
5977
|
watchOutput(text) {
|
|
5885
5978
|
for (const m of text.matchAll(OSC_TITLE)) this.lastTitle = m[1];
|
|
5886
5979
|
}
|
|
5887
5980
|
/** The escape sequence that shows the hint, and the one that puts the program's title back. */
|
|
5888
|
-
hintTitle() {
|
|
5889
|
-
return "\x1B]0;" + CTRL_C_HINT + "\x07";
|
|
5981
|
+
hintTitle(kind = "hold") {
|
|
5982
|
+
return "\x1B]0;" + (kind === "copy" ? CTRL_C_COPY_HINT : CTRL_C_HINT) + "\x07";
|
|
5890
5983
|
}
|
|
5891
5984
|
restoreTitle() {
|
|
5892
5985
|
return "\x1B]0;" + this.lastTitle.replace(/[\x00-\x1f\x7f]/g, "") + "\x07";
|
|
@@ -6090,6 +6183,7 @@ function runTerminalClient(socket, token, options = {}) {
|
|
|
6090
6183
|
clearTimeout(joinTimer);
|
|
6091
6184
|
clearTimeout(hintTimer);
|
|
6092
6185
|
clearTimeout(vtTimer);
|
|
6186
|
+
clipboard.stop();
|
|
6093
6187
|
socket.destroy();
|
|
6094
6188
|
const tracked = mouse.tracking();
|
|
6095
6189
|
const reset = "\x1B[0m\x1B[?25h" + MOUSE_OFF + (mouse.has(2004) ? "\x1B[?2004l" : "") + "\r\n";
|
|
@@ -6107,23 +6201,22 @@ function runTerminalClient(socket, token, options = {}) {
|
|
|
6107
6201
|
if (data.length && !done) socket.write(encodeFrame(FrameType.Input, data));
|
|
6108
6202
|
};
|
|
6109
6203
|
const sendInput = (data) => write(keys.push(passthrough ? mouse.filter(data) : data));
|
|
6110
|
-
const guard = options.confirmCtrlC
|
|
6204
|
+
const guard = new CtrlCGuard({ confirm: options.confirmCtrlC !== false });
|
|
6111
6205
|
let hintTimer;
|
|
6206
|
+
const clipboard = watchClipboard(() => guard.clipboardChanged());
|
|
6112
6207
|
const onInput = (chunk) => {
|
|
6113
6208
|
if (options.detachKey !== void 0 && chunk.includes(options.detachKey)) {
|
|
6114
6209
|
finish({ reason: "detach" });
|
|
6115
6210
|
return;
|
|
6116
6211
|
}
|
|
6117
|
-
|
|
6118
|
-
|
|
6119
|
-
|
|
6120
|
-
|
|
6121
|
-
|
|
6122
|
-
|
|
6123
|
-
return;
|
|
6124
|
-
}
|
|
6125
|
-
if (decided.send.length === 1 && decided.send[0] === 3) stdout.write(guard.restoreTitle());
|
|
6212
|
+
const decided = guard.push(chunk, Date.now());
|
|
6213
|
+
clearTimeout(hintTimer);
|
|
6214
|
+
if (decided.held || decided.copied) {
|
|
6215
|
+
stdout.write(guard.hintTitle(decided.copied ? "copy" : "hold"));
|
|
6216
|
+
hintTimer = setTimeout(() => stdout.write(guard.restoreTitle()), CTRL_C_CONFIRM_MS);
|
|
6217
|
+
return;
|
|
6126
6218
|
}
|
|
6219
|
+
if (decided.send.length === 1 && decided.send[0] === 3) stdout.write(guard.restoreTitle());
|
|
6127
6220
|
clearTimeout(joinTimer);
|
|
6128
6221
|
sendInput(joiner.push(chunk));
|
|
6129
6222
|
if (joiner.holding()) joinTimer = setTimeout(() => sendInput(joiner.flush()), JOIN_MS);
|
|
@@ -6156,9 +6249,10 @@ function runTerminalClient(socket, token, options = {}) {
|
|
|
6156
6249
|
}
|
|
6157
6250
|
for (const frame of frames) {
|
|
6158
6251
|
if (frame.type === FrameType.Output) {
|
|
6252
|
+
guard.watchClipboard(frame.payload.toString("latin1"));
|
|
6159
6253
|
const out = filter.push(frame.payload);
|
|
6160
6254
|
mouse.watch(out);
|
|
6161
|
-
guard
|
|
6255
|
+
guard.watchOutput(out.toString("latin1"));
|
|
6162
6256
|
stdout.write(out);
|
|
6163
6257
|
} else if (frame.type === FrameType.Console) passthrough = frame.payload.toString("latin1") === "conpty.dll";
|
|
6164
6258
|
else if (frame.type === FrameType.Restarting) {
|
|
@@ -26106,7 +26200,7 @@ var SandboxSchema = external_exports.strictObject({
|
|
|
26106
26200
|
'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.'
|
|
26107
26201
|
),
|
|
26108
26202
|
pressureResponse: external_exports.boolean().describe(
|
|
26109
|
-
"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."
|
|
26203
|
+
"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."
|
|
26110
26204
|
),
|
|
26111
26205
|
vGpu: external_exports.boolean().describe(
|
|
26112
26206
|
"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)."
|
|
@@ -28304,6 +28398,8 @@ var PressureStatusSchema = external_exports.object({
|
|
|
28304
28398
|
lowered: external_exports.number().int().min(0).max(1024),
|
|
28305
28399
|
/** Compilers waiting their turn right now (name, process id, seconds until they may go on). */
|
|
28306
28400
|
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),
|
|
28401
|
+
/** Build programs the memory gate holds right now (let go one at a time). */
|
|
28402
|
+
held: external_exports.number().int().min(0).max(1024).optional(),
|
|
28307
28403
|
/** The CPU share the compilers get together (percent; 100 = no cap). */
|
|
28308
28404
|
cpuPercent: external_exports.number().int().min(0).max(100).optional(),
|
|
28309
28405
|
/** The PC itself asked for slowing down (its memory or CPU is low). */
|
|
@@ -28324,6 +28420,8 @@ var HeartbeatSchema = external_exports.object({
|
|
|
28324
28420
|
topProcesses: external_exports.array(external_exports.object({ name: Text(64), privateMb: external_exports.number().int().min(0).max(1e7) })).max(5),
|
|
28325
28421
|
/** The disk queue this PC last reported while asking the sandbox to slow down. */
|
|
28326
28422
|
diskQueueReported: external_exports.number().min(0).max(1e5).optional(),
|
|
28423
|
+
/** Build programs the memory gate holds right now (let go one at a time). */
|
|
28424
|
+
held: external_exports.number().int().min(0).max(1024).optional(),
|
|
28327
28425
|
/** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
|
|
28328
28426
|
phase: external_exports.enum(["ready", "working", "ending"])
|
|
28329
28427
|
});
|
|
@@ -28652,7 +28750,7 @@ import crypto from "node:crypto";
|
|
|
28652
28750
|
|
|
28653
28751
|
// src/shared/download/spawn.ts
|
|
28654
28752
|
import os3 from "node:os";
|
|
28655
|
-
import { spawn as
|
|
28753
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
28656
28754
|
function lowerPriority(pid) {
|
|
28657
28755
|
if (pid === void 0) return;
|
|
28658
28756
|
try {
|
|
@@ -28663,7 +28761,7 @@ function lowerPriority(pid) {
|
|
|
28663
28761
|
function spawnAsync(file2, args, options = {}) {
|
|
28664
28762
|
const { lowPriority, ...spawnOptions } = options;
|
|
28665
28763
|
return new Promise((resolve, reject) => {
|
|
28666
|
-
const child =
|
|
28764
|
+
const child = spawn3(file2, args, { stdio: "ignore", windowsHide: true, ...spawnOptions });
|
|
28667
28765
|
if (lowPriority) lowerPriority(child.pid);
|
|
28668
28766
|
child.once("error", reject);
|
|
28669
28767
|
child.once("close", (code) => resolve(code ?? -1));
|
|
@@ -7,7 +7,7 @@ import { isWin } from '../core/util/index.js';
|
|
|
7
7
|
import { SYSTEM } from '../shared/system.js';
|
|
8
8
|
import { agentCommand, agentShims, bootResetPaths, isCopyableDatabaseFile, isDatabaseFile, localStateEntries, shimText, } from '../guest/agents/agent-files.js';
|
|
9
9
|
import { appendMissing, expandVars, installedToolPaths, isGitUsrBin, isMsvcToolsDir, isPinnedNodeDir, msvcBeforeGitUsr, parseRegPath, pathKey, planPath, rebuildPath, sessionPath, splitPath, } from '../guest/setup/path-plan.js';
|
|
10
|
-
import { withReloadEnvironment } from '../host/tools/index.js';
|
|
10
|
+
import { withCopyOnSelect, withReloadEnvironment } from '../host/tools/index.js';
|
|
11
11
|
import { HarnessSchema } from '../core/schema/index.js';
|
|
12
12
|
export const agentPathsSuite = {
|
|
13
13
|
name: 'agent-paths',
|
|
@@ -136,6 +136,8 @@ export const agentPathsSuite = {
|
|
|
136
136
|
t.section('Windows Terminal reloads the environment');
|
|
137
137
|
t.equal(withReloadEnvironment({ compatibility: { reloadEnvironmentVariables: true } }), null, 'already set: unchanged');
|
|
138
138
|
t.equal(withReloadEnvironment({ theme: 'dark', compatibility: { other: 1 } })?.compatibility, { other: 1, reloadEnvironmentVariables: true }, 'set without touching the rest of compatibility');
|
|
139
|
+
t.equal(withCopyOnSelect({ theme: 'dark' })?.copyOnSelect, true, 'copy on select is added when the key is missing');
|
|
140
|
+
t.equal(withCopyOnSelect({ copyOnSelect: false }), null, 'a copy-on-select value the user set stays');
|
|
139
141
|
if (!isWin)
|
|
140
142
|
return;
|
|
141
143
|
t.section('shell start-up (pathsync.exe and the start-up files)');
|
|
@@ -4,6 +4,7 @@ import { safeId } from '../shared/channel/index.js';
|
|
|
4
4
|
import { GuestRequestSchema, SandboxStatusSchema, SessionStatusSchema } from '../shared/channel/index.js';
|
|
5
5
|
import { bootLine, firstStartNotice, pendingDownloads, readyIn, stillWorking } from '../cli/commands/launch/index.js';
|
|
6
6
|
import { CTRL_C_CONFIRM_MS, CTRL_C_HINT, CtrlCGuard, encodeFrame, FrameReader, FrameType, fixWindowsKeys, helloFrame, InputJoiner, KeyGate, MAX_FRAME_BYTES, MOUSE_OFF, MouseModes, OutputFilter, } from '../shared/pty/index.js';
|
|
7
|
+
import { classifyInput } from '../shared/web/index.js';
|
|
7
8
|
import { bootSilent, FIRST_STATUS_MS } from '../host/channel/index.js';
|
|
8
9
|
export const channelSuite = {
|
|
9
10
|
name: 'channel',
|
|
@@ -251,6 +252,38 @@ export const channelSuite = {
|
|
|
251
252
|
!g.push(Buffer.from('x'), 6000).held, 'a lone Ctrl+C is held back, a second one soon after is sent, a late second one is held again, other input passes');
|
|
252
253
|
g.watchOutput('\x1b]0;claude: working\x07more \x1b]2;\u0001evil\x07');
|
|
253
254
|
t.check(g.hintTitle().includes(CTRL_C_HINT) && g.restoreTitle() === '\x1b]0;evil\x07', "the hint shows in the title, and the program's last title comes back without control characters");
|
|
255
|
+
const drag = Buffer.from('\x1b[<32;10;5M');
|
|
256
|
+
const release = Buffer.from('\x1b[<0;12;5m');
|
|
257
|
+
const copyG = new CtrlCGuard();
|
|
258
|
+
copyG.push(drag, 0);
|
|
259
|
+
copyG.push(release, 10);
|
|
260
|
+
const copies = [100, 200, 300].map((at) => copyG.push(ctrlC, at));
|
|
261
|
+
t.check(copies.every((c) => c.send.length === 0 && c.copied && !c.held), 'after a mouse drag in the program every Ctrl+C is copied and never sent, however often it is pressed');
|
|
262
|
+
copyG.push(Buffer.from('x'), 400);
|
|
263
|
+
t.check(copyG.push(ctrlC, 500).held, 'after another key a lone Ctrl+C is held back again');
|
|
264
|
+
const clickG = new CtrlCGuard();
|
|
265
|
+
clickG.push(drag, 0);
|
|
266
|
+
clickG.push(Buffer.from('\x1b[<0;3;3M'), 10);
|
|
267
|
+
t.check(clickG.push(ctrlC, 20).held, 'a mouse click after a drag ends copy mode');
|
|
268
|
+
const quietG = new CtrlCGuard();
|
|
269
|
+
quietG.push(Buffer.from('\x1b[<64;3;3M'), 0);
|
|
270
|
+
quietG.push(Buffer.from('\x1b[<35;3;3M'), 1);
|
|
271
|
+
t.check(quietG.push(ctrlC, 2).held, 'mouse wheel and hover reports do not start copy mode');
|
|
272
|
+
const clipG = new CtrlCGuard();
|
|
273
|
+
clipG.watchClipboard('\x1b]52;c;aGk=');
|
|
274
|
+
t.check(clipG.push(ctrlC, 0).copied, 'the program writing the clipboard (OSC 52) starts copy mode');
|
|
275
|
+
const copiedG = new CtrlCGuard();
|
|
276
|
+
copiedG.clipboardChanged();
|
|
277
|
+
const afterCopy = copiedG.push(ctrlC, 0);
|
|
278
|
+
copiedG.push(Buffer.from('x'), 1);
|
|
279
|
+
t.check(afterCopy.copied && !afterCopy.held && afterCopy.send.length === 0 && copiedG.push(ctrlC, 2).held, 'after the clipboard changed a lone Ctrl+C is copied (not sent, not held) until another key is pressed');
|
|
280
|
+
const noConfirm = new CtrlCGuard({ confirm: false });
|
|
281
|
+
const sentAtOnce = noConfirm.push(ctrlC, 0);
|
|
282
|
+
noConfirm.push(drag, 1);
|
|
283
|
+
t.check(sentAtOnce.send.equals(ctrlC) && noConfirm.push(ctrlC, 2).copied, 'without confirmation a lone Ctrl+C is sent at once, but after a drag it is still copied');
|
|
284
|
+
const x10 = classifyInput('\x1b[M' + String.fromCharCode(32 + 32) + '!!');
|
|
285
|
+
const arrow = classifyInput('\x1b[A');
|
|
286
|
+
t.check(x10.mouse === 'drag' && !x10.keys && arrow.mouse === null && arrow.keys, 'an X10 drag is a drag without keys, and an arrow key is a key without a mouse report');
|
|
254
287
|
t.section('typed input keeps escape sequences whole');
|
|
255
288
|
const joined = (...chunks) => {
|
|
256
289
|
const j = new InputJoiner();
|
|
@@ -8,7 +8,7 @@ import { DEFAULTS } from '../core/config/index.js';
|
|
|
8
8
|
import { isWin } from '../core/util/index.js';
|
|
9
9
|
import { SYSTEM } from '../shared/system.js';
|
|
10
10
|
import { GuestRequestSchema, PressureStatusSchema } from '../shared/channel/index.js';
|
|
11
|
-
import { compilersAllowed, DIAL_CALM_SAMPLES, DIAL_STEPS, HOST_THRESHOLDS, buildJobs, cargoJobs, linkSlotsFor, usableCpus, effectiveCpuLimit, gentleCpuLimit, maxCompilers, chooseTargetDir, cargoLinkerVar, } from '../shared/memory/index.js';
|
|
11
|
+
import { BURST_GATE, compilersAllowed, DIAL_CALM_SAMPLES, DIAL_STEPS, HOST_THRESHOLDS, buildJobs, cargoJobs, linkSlotsFor, usableCpus, effectiveCpuLimit, gentleCpuLimit, maxCompilers, chooseTargetDir, cargoLinkerVar, ubtBuildConfiguration, } from '../shared/memory/index.js';
|
|
12
12
|
import { HeartbeatSchema } from '../shared/channel/index.js';
|
|
13
13
|
import { heartbeatLine, lastEndOf, vanishedCause, stopWarning } from '../host/life/index.js';
|
|
14
14
|
import { cappedMemoryMb, decideMemory, memoryNote, residentCapMb, residentNote, resolveMemoryMb, } from '../host/launch/index.js';
|
|
@@ -16,10 +16,10 @@ import { vgpuWarning, GENTLE_NOTE } from '../host/launch/risk-notes.js';
|
|
|
16
16
|
import { judgeSandbox } from '../host/health/index.js';
|
|
17
17
|
import { cleanUpLeftovers, cleanupQuestion, killArgs } from '../host/sandbox/index.js';
|
|
18
18
|
import { parseTasklist } from '../host/sharing/index.js';
|
|
19
|
-
import { bugcheckCode, cpuPercentBetween, explainStop, findDumps, findingLines, freezeLine, hostPressure, lowReason, severeHost, pageFileRisk, parseHostMemory, crashReportText, watchdogStep, WATCHDOG_START, } from '../host/life/index.js';
|
|
19
|
+
import { bugcheckCode, cpuPercentBetween, explainStop, findDumps, findingLines, freezeLine, hostPressure, lowReason, memoryWasFineLine, severeHost, pageFileRisk, parseHostMemory, crashReportText, watchdogStep, WATCHDOG_START, } from '../host/life/index.js';
|
|
20
20
|
// The pure governor files only: governor.ts loads the guest runtime (session.json), which exists only in a sandbox.
|
|
21
|
-
import { canLower, canPause, decide, INITIAL_STATE, isLinker, linkerSlots, ROTATE_MS, } from '../guest/governor/decide.js';
|
|
22
|
-
import { GovernorCore } from '../guest/governor/core.js';
|
|
21
|
+
import { canLower, canPause, decide, GATE_NAMES, INITIAL_STATE, isLinker, isNeverTouched, linkerSlots, ROTATE_MS, } from '../guest/governor/decide.js';
|
|
22
|
+
import { GateEventSchema, GovernorCore, memgovArgs } from '../guest/governor/core.js';
|
|
23
23
|
const GB = 1024;
|
|
24
24
|
const MB_SANDBOX = DEFAULTS.sandbox;
|
|
25
25
|
const proc = (pid, name, mb, prio = 'normal') => ({
|
|
@@ -96,6 +96,10 @@ export const heavyLoadSuite = {
|
|
|
96
96
|
t.check(cargoJobs(16, 32, 3) === 2 && cargoJobs(16, 32, 8) === 1, 'fewer when several sessions build');
|
|
97
97
|
t.check(cargoJobs(32, 32, 1) === 8, '32 processors, 32 GB: 8 jobs for one session');
|
|
98
98
|
t.check(buildJobs(32, 32) === 8 && buildJobs(32, 64) === 19 && buildJobs(4, 64) === 4 && buildJobs(16, 4) === 2, 'the build budget: 3 GB per job after 6 GB, never more than the processors, at least 2');
|
|
99
|
+
const ubt = ubtBuildConfiguration(buildJobs(20, 36));
|
|
100
|
+
t.check(ubt.includes('<MaxParallelActions>10</MaxParallelActions>') &&
|
|
101
|
+
ubt.includes('xmlns="https://www.unrealengine.com/BuildConfiguration"') &&
|
|
102
|
+
ubtBuildConfiguration(0).includes('<MaxParallelActions>1</MaxParallelActions>'), "UnrealBuildTool's compiles at once follow the build budget (at least 1)", ubt);
|
|
99
103
|
t.check(linkSlotsFor(32) === 8 && linkSlotsFor(16) === 3 && linkSlotsFor(8) === 2 && linkSlotsFor(128) === 8, 'links at once ("auto"): one per 3 GB after 6 GB, 2 to 8');
|
|
100
104
|
t.check(usableCpus(32, 80) === 25 && usableCpus(32, null) === 32 && usableCpus(4, 10) === 2 && usableCpus(8, 100) === 8, 'the processors programs see follow the CPU cap (at least 2)');
|
|
101
105
|
t.section('the governor: which programs');
|
|
@@ -374,6 +378,105 @@ export const heavyLoadSuite = {
|
|
|
374
378
|
t.check(decide({ ...INITIAL_STATE, hostUntil: clock + 60000, hostStep: 4 }, sample(60, 40, procs), clock, 'normal').state
|
|
375
379
|
.step === 4, 'with plenty of memory in the sandbox, a severe PC still turns the dial to 4');
|
|
376
380
|
t.check(core.current.hostUntil === clock + 60000, 'a host request is remembered for its seconds');
|
|
381
|
+
t.section('the memory gate (rate of growth)');
|
|
382
|
+
t.check(['cl', 'link', 'rustc', 'clang-cl', 'lld-link', 'shadercompileworker'].every((n) => GATE_NAMES.includes(n)), 'the gate may freeze compilers, linkers and Unreal shader workers', GATE_NAMES.join(','));
|
|
383
|
+
t.check([
|
|
384
|
+
'node',
|
|
385
|
+
'claude',
|
|
386
|
+
'codex',
|
|
387
|
+
'opencode',
|
|
388
|
+
'cmd',
|
|
389
|
+
'pwsh',
|
|
390
|
+
'powershell',
|
|
391
|
+
'msbuild',
|
|
392
|
+
'dotnet',
|
|
393
|
+
'cargo',
|
|
394
|
+
'ninja',
|
|
395
|
+
'unrealbuildtool',
|
|
396
|
+
'ubahost',
|
|
397
|
+
'unrealeditor',
|
|
398
|
+
'unrealeditor-cmd',
|
|
399
|
+
'mspdbsrv',
|
|
400
|
+
'ptyhost',
|
|
401
|
+
'memgov',
|
|
402
|
+
].every((n) => !GATE_NAMES.includes(n)) &&
|
|
403
|
+
GATE_NAMES.every((n) => (canPause(n) || n === 'shadercompileworker') && !isNeverTouched(n)), 'agents, shells, build coordinators, the editor and the tools themselves are never frozen');
|
|
404
|
+
t.check(JSON.stringify(memgovArgs('normal')) ===
|
|
405
|
+
JSON.stringify([
|
|
406
|
+
'serve',
|
|
407
|
+
'--interval',
|
|
408
|
+
'2',
|
|
409
|
+
'--gate',
|
|
410
|
+
'4096:3000:1500:30000',
|
|
411
|
+
'--gate-names',
|
|
412
|
+
GATE_NAMES.join(','),
|
|
413
|
+
]) && memgovArgs('gentle').includes('3072:3000:2000:30000'), 'memgov starts with the gate for the profile', memgovArgs('gentle').join(' '));
|
|
414
|
+
t.check(['normal', 'gentle'].every((p) => {
|
|
415
|
+
const g = BURST_GATE[p];
|
|
416
|
+
return g.growthMb >= 1024 && g.maxHoldMs <= 30000 && g.releaseMs < g.maxHoldMs && g.windowMs >= 1000;
|
|
417
|
+
}) && BURST_GATE.gentle.growthMb <= BURST_GATE.normal.growthMb, 'the gate is sane: a real burst size, every hold ends within 30 s, vGPU is not looser');
|
|
418
|
+
const gateExamples = [
|
|
419
|
+
{ event: 'gate', at: 1, kind: 'ready', mode: 'notify', growthMb: 4096, windowMs: 3000 },
|
|
420
|
+
{ event: 'gate', at: 1, kind: 'off', reason: 'bad --gate' },
|
|
421
|
+
{ event: 'gate', at: 1, kind: 'trip', grewMb: 4210, ms: 1800, jobMb: 23456, held: [{ pid: 101, name: 'cl' }] },
|
|
422
|
+
{ event: 'gate', at: 1, kind: 'hold', pid: 101, name: 'cl' },
|
|
423
|
+
{ event: 'gate', at: 1, kind: 'release', pid: 101, name: 'cl', heldMs: 1500, forced: false },
|
|
424
|
+
];
|
|
425
|
+
const heldOf = (n) => Array.from({ length: n }, (_, i) => ({ pid: i, name: 'cl' }));
|
|
426
|
+
t.check(gateExamples.every((e) => GateEventSchema.safeParse(e).success) &&
|
|
427
|
+
!GateEventSchema.safeParse({ event: 'gate', at: 1, kind: 'hold', pid: 1, name: 'x'.repeat(129) }).success &&
|
|
428
|
+
!GateEventSchema.safeParse({ event: 'gate', at: 1, kind: 'trip', grewMb: 1, ms: 1, jobMb: 1, held: heldOf(65) })
|
|
429
|
+
.success &&
|
|
430
|
+
!GateEventSchema.safeParse({ event: 'gate', at: 1, kind: 'boom' }).success &&
|
|
431
|
+
!GateEventSchema.safeParse({ at: 1, kind: 'hold', pid: 1, name: 'cl' }).success, 'gate events from memgov are checked: one of each kind passes; a long name, 65 held, a bad kind or no event fail');
|
|
432
|
+
{
|
|
433
|
+
const gateLogs = [];
|
|
434
|
+
const gateCore = new GovernorCore({
|
|
435
|
+
port,
|
|
436
|
+
profile: 'normal',
|
|
437
|
+
now: () => 0,
|
|
438
|
+
logLine: (l) => gateLogs.push(l),
|
|
439
|
+
saveStatus: () => undefined,
|
|
440
|
+
warn: (line) => gateLogs.push(line),
|
|
441
|
+
});
|
|
442
|
+
gateCore.onGate({
|
|
443
|
+
event: 'gate',
|
|
444
|
+
at: 1,
|
|
445
|
+
kind: 'trip',
|
|
446
|
+
grewMb: 4210,
|
|
447
|
+
ms: 1800,
|
|
448
|
+
jobMb: 23456,
|
|
449
|
+
held: [{ pid: 101, name: 'cl' }],
|
|
450
|
+
});
|
|
451
|
+
gateCore.onGate({
|
|
452
|
+
event: 'gate',
|
|
453
|
+
at: 2,
|
|
454
|
+
kind: 'release',
|
|
455
|
+
pid: 101,
|
|
456
|
+
name: 'cl',
|
|
457
|
+
heldMs: 30000,
|
|
458
|
+
forced: true,
|
|
459
|
+
});
|
|
460
|
+
t.check(gateLogs.some((l) => l.includes("governor: gate: the agents' programs grew 4210 MB in 1.8 s (now 23456 MB); holding 1: cl (101)")) &&
|
|
461
|
+
gateLogs.some((l) => l.endsWith('(the longest it may wait)')) &&
|
|
462
|
+
gateCore.current.step === 0, 'gate events go to the memory log and leave the dial alone', gateLogs.join('\n'));
|
|
463
|
+
const heldStatuses = [];
|
|
464
|
+
const heldCore = new GovernorCore({
|
|
465
|
+
port,
|
|
466
|
+
profile: 'normal',
|
|
467
|
+
now: () => 0,
|
|
468
|
+
logLine: () => undefined,
|
|
469
|
+
saveStatus: (s) => heldStatuses.push(s),
|
|
470
|
+
warn: () => undefined,
|
|
471
|
+
});
|
|
472
|
+
heldCore.onSample({ ...sample(60, 40, procs), gateHeld: 3 });
|
|
473
|
+
const heldStatus = heldStatuses[heldStatuses.length - 1];
|
|
474
|
+
t.check(PressureStatusSchema.safeParse(heldStatus).success &&
|
|
475
|
+
heldStatus.held === 3 &&
|
|
476
|
+
heldCore.snapshot?.held === 3, 'the number of held builds reaches the status file and the snapshot', JSON.stringify(heldStatus));
|
|
477
|
+
t.check(HeartbeatSchema.safeParse({ ...beat, held: 3 }).success &&
|
|
478
|
+
heartbeatLine({ ...beat, held: 3 }).includes('3 builds held by the memory gate'), 'the heartbeat carries the held builds into the crash report line');
|
|
479
|
+
}
|
|
377
480
|
t.section("the PC's watchdog");
|
|
378
481
|
const input = (memory, nowMs, cpu = 10, profile = 'normal') => ({
|
|
379
482
|
memory,
|
|
@@ -541,6 +644,13 @@ export const heavyLoadSuite = {
|
|
|
541
644
|
meaning.includes('froze, lost power') &&
|
|
542
645
|
meaning.includes('hardware error') &&
|
|
543
646
|
meaning.includes('vmwp.exe'), 'what each finding usually means, in plain words', meaning);
|
|
647
|
+
t.check(findingLines([ev(1000, 'Application Error', '2026-10-15T19:41:12Z', 'Faulting application name: vmwp.exe')])
|
|
648
|
+
.join('\n')
|
|
649
|
+
.includes('not a program inside the sandbox running out of memory'), 'a crash of vmwp.exe is named as a fault in Windows or a driver, not memory');
|
|
650
|
+
t.check(memoryWasFineLine('x\n2026-11-08T15:30:05.411Z | RAM free/total MB 42325/63170 | commit free/limit MB 52051/75458 | page file auto\n')?.includes('still had 41 GB of RAM and 50 GB') === true &&
|
|
651
|
+
memoryWasFineLine('2026-10-15T19:40:50Z | RAM free/total MB 900/32768 | commit free/limit MB 9000/40000') ===
|
|
652
|
+
null &&
|
|
653
|
+
memoryWasFineLine('') === null, 'the report says when this PC still had plenty of memory, and says nothing when it was low');
|
|
544
654
|
const dumps = path.join(t.at('crash-dumps'), 'Minidump');
|
|
545
655
|
fs.mkdirSync(dumps, { recursive: true });
|
|
546
656
|
fs.writeFileSync(path.join(dumps, '101526-1.dmp'), 'x');
|
|
@@ -576,6 +686,7 @@ export const heavyLoadSuite = {
|
|
|
576
686
|
};
|
|
577
687
|
const sandboxCs = (n) => path.join(t.packageRoot, 'sandbox', 'csharp', n);
|
|
578
688
|
build('ptyhost', [sandboxCs('PtyHost.cs')]);
|
|
689
|
+
build('clipboardwatch', [sandboxCs('ClipboardWatch.cs')], 'library');
|
|
579
690
|
const memgov = build('memgov', [sandboxCs('MemGov.cs')]);
|
|
580
691
|
const hostDir = path.join(t.packageRoot, 'native', 'csharp', 'HostTools');
|
|
581
692
|
const tools = build('HostTools', fs
|
|
@@ -607,6 +718,7 @@ export const heavyLoadSuite = {
|
|
|
607
718
|
// Not JSON: the check below fails.
|
|
608
719
|
}
|
|
609
720
|
t.check(sampleOk && served.status === 0, 'memgov serve prints a well-formed sample and ends when its input closes', firstLine);
|
|
721
|
+
t.check(!(served.stdout ?? '').split(/\r?\n/).some((l) => l.includes('"event":"gate"')), 'a memgov started without --gate never takes the job port (no gate events)');
|
|
610
722
|
for (const script of ['memory-status.ps1', 'stop-events.ps1']) {
|
|
611
723
|
const text = fs.readFileSync(path.join(t.packageRoot, 'native', 'scripts', script), 'utf8');
|
|
612
724
|
t.check(text.startsWith('#Requires -Version 5.1'), script + ' says its PowerShell version');
|
|
@@ -11,6 +11,7 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
11
11
|
import { pathToFileURL } from 'node:url';
|
|
12
12
|
import { encodeFrame, FrameType } from '../shared/pty/index.js';
|
|
13
13
|
import { SYSTEM } from '../shared/system.js';
|
|
14
|
+
import { GateEventSchema } from '../guest/governor/core.js';
|
|
14
15
|
import { CSC, openViewer, tail, WAIT } from './support/live.js';
|
|
15
16
|
/**
|
|
16
17
|
* The stand-in agent: VT input and SGR mouse tracking on, echoes what it reads ("got <1b>[<0;10;5M"). L prints 500
|
|
@@ -268,6 +269,108 @@ export const liveTerminalSuite = {
|
|
|
268
269
|
Number(set[1]) === Math.max(Math.floor(commit / 2), commit - Math.max(1536, Math.floor((commit * 3) / 100))), 'the ptyhost that creates the job sets the backstop just below the commit limit (auto: 3 %, at least 1.5 GB)', read('job-first.log'));
|
|
269
270
|
t.check(read('job-second.log').includes('joined the existing one'), 'a ptyhost that joins the job never changes its backstop', read('job-second.log'));
|
|
270
271
|
}
|
|
272
|
+
t.section('the memory gate: a burst is held at once and let go in turn');
|
|
273
|
+
{
|
|
274
|
+
// A job of the test's own (never the agents' real one) for both ptyhosts and memgov.
|
|
275
|
+
const jobName = 'Local\\AgentSandboxAgentsJobTest' + process.pid + '-gate-' + Date.now();
|
|
276
|
+
const memgovExe = t.at('memgov-gate.exe');
|
|
277
|
+
const burstExe = t.at('MemoryBurst.exe');
|
|
278
|
+
const gateErrors = compile(path.join(t.packageRoot, 'sandbox', 'csharp', 'MemGov.cs'), memgovExe) ||
|
|
279
|
+
compile(path.join(t.packageRoot, 'src', 'selftest', 'fixtures', 'memory-burst', 'MemoryBurst.cs'), burstExe);
|
|
280
|
+
t.check(!gateErrors, 'memgov and the memory burst program compile', gateErrors);
|
|
281
|
+
if (!gateErrors) {
|
|
282
|
+
const ping = path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'PING.EXE');
|
|
283
|
+
const holder = '"' + SYSTEM.cmd() + '" /c "' + ping + '" -n 60 127.0.0.1 >nul';
|
|
284
|
+
const progress = t.at('burst-progress.txt');
|
|
285
|
+
const startHost = async (command, log) => spawn(ptyhost, [String(await freePort()), 'tok', '80', '24', t.at(log), command], {
|
|
286
|
+
env: { ...process.env, PTYHOST_BIND: 'loopback', PTYHOST_BACKSTOP_MB: 'none', PTYHOST_JOB_NAME: jobName },
|
|
287
|
+
stdio: 'ignore',
|
|
288
|
+
windowsHide: true,
|
|
289
|
+
});
|
|
290
|
+
const read = (file) => (fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '');
|
|
291
|
+
const hosts = [];
|
|
292
|
+
let memgov = null;
|
|
293
|
+
try {
|
|
294
|
+
hosts.push(await startHost(holder, 'gate-a.log'));
|
|
295
|
+
await t.waitFor(() => read(t.at('gate-a.log')).includes('agent job:'), WAIT.screen);
|
|
296
|
+
memgov = spawn(memgovExe, ['serve', '--interval', '1', '--gate', '512:3000:800:4000', '--gate-names', 'memoryburst'], {
|
|
297
|
+
env: { ...process.env, MEMGOV_JOB_NAME: jobName },
|
|
298
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
299
|
+
windowsHide: true,
|
|
300
|
+
});
|
|
301
|
+
let text = '';
|
|
302
|
+
memgov.stdout.on('data', (chunk) => (text += chunk.toString('utf8')));
|
|
303
|
+
const events = () => text
|
|
304
|
+
.split(/\r?\n/)
|
|
305
|
+
.filter((l) => l.includes('"event":"gate"'))
|
|
306
|
+
.flatMap((l) => {
|
|
307
|
+
try {
|
|
308
|
+
const parsed = GateEventSchema.safeParse(JSON.parse(l));
|
|
309
|
+
return parsed.success ? [parsed.data] : [];
|
|
310
|
+
}
|
|
311
|
+
catch {
|
|
312
|
+
return [];
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
const ready = await t.waitFor(() => events().some((e) => e.kind === 'ready'), WAIT.screen);
|
|
316
|
+
const readyEvent = events().find((e) => e.kind === 'ready');
|
|
317
|
+
t.check(Boolean(ready) && readyEvent?.kind === 'ready' && readyEvent.mode === 'notify', "the gate listens to the job's notifications (no polling)", text.slice(-400));
|
|
318
|
+
hosts.push(await startHost('"' + burstExe + '" 1536 "' + progress + '"', 'gate-b.log'));
|
|
319
|
+
const finished = await t.waitFor(() => read(progress).includes('done'), 15000);
|
|
320
|
+
const marks = read(progress)
|
|
321
|
+
.split(/\r?\n/)
|
|
322
|
+
.flatMap((l) => {
|
|
323
|
+
const m = /^(\d+) \d+$/.exec(l.trim());
|
|
324
|
+
return m ? [Number(m[1])] : [];
|
|
325
|
+
});
|
|
326
|
+
const all = events();
|
|
327
|
+
const trips = all.flatMap((e) => (e.kind === 'trip' ? [e] : []));
|
|
328
|
+
const releases = all.flatMap((e) => (e.kind === 'release' ? [e] : []));
|
|
329
|
+
const evidence = text.slice(-1200) + '\nprogress: ' + marks.length + ' lines';
|
|
330
|
+
const firstTrip = trips[0];
|
|
331
|
+
t.check(trips.some((e) => e.held.some((h) => h.name === 'memoryburst')) &&
|
|
332
|
+
firstTrip !== undefined &&
|
|
333
|
+
firstTrip.grewMb >= 512 &&
|
|
334
|
+
firstTrip.grewMb <= 1024, 'a burst past the gate is held at once', evidence);
|
|
335
|
+
const stalls = [];
|
|
336
|
+
for (const trip of trips) {
|
|
337
|
+
if (!trip.held.some((h) => h.name === 'memoryburst'))
|
|
338
|
+
continue;
|
|
339
|
+
const release = releases.find((r) => r.at >= trip.at && r.name === 'memoryburst');
|
|
340
|
+
const until = release ? release.at : Infinity;
|
|
341
|
+
if (marks.some((m) => m > trip.at + 200 && m < until))
|
|
342
|
+
stalls.push('trip at ' + trip.at);
|
|
343
|
+
}
|
|
344
|
+
t.check(stalls.length === 0, 'the held program made no progress more than 200 ms after the trip until it was let go', stalls.join(', ') + '\n' + evidence);
|
|
345
|
+
t.check(releases.length > 0 && releases.every((r) => r.heldMs <= 4500) && releases.some((r) => !r.forced), 'held programs go on in turn, never longer than the most allowed', evidence);
|
|
346
|
+
t.check(Boolean(finished) && trips.length >= 2, 'the burst program reached its end after at least 2 trips', evidence);
|
|
347
|
+
}
|
|
348
|
+
finally {
|
|
349
|
+
if (memgov) {
|
|
350
|
+
memgov.stdin.end();
|
|
351
|
+
const ended = memgov;
|
|
352
|
+
await t.waitFor(() => ended.exitCode !== null, 5000);
|
|
353
|
+
t.check(ended.exitCode === 0, 'memgov with the gate ends cleanly when its input closes');
|
|
354
|
+
try {
|
|
355
|
+
if (ended.exitCode === null)
|
|
356
|
+
ended.kill();
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
/* it ended between the check and the kill: nothing left to stop */
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
for (const c of hosts) {
|
|
363
|
+
try {
|
|
364
|
+
if (c.exitCode === null)
|
|
365
|
+
c.kill();
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
/* it ended between the check and the kill: nothing left to stop */
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
271
374
|
const client = t.at('client.mjs');
|
|
272
375
|
fs.writeFileSync(client, [
|
|
273
376
|
'import { connect, runTerminalClient } from ' +
|
|
@@ -47,7 +47,8 @@ export const personalizationSuite = {
|
|
|
47
47
|
const untouched = fs.readFileSync(settings, 'utf8');
|
|
48
48
|
t.check(untouched.startsWith('// edited') &&
|
|
49
49
|
/"guid": "\{x\}",\s*\}/.test(untouched) &&
|
|
50
|
-
/"reloadEnvironmentVariables":\s*true/.test(untouched)
|
|
50
|
+
/"reloadEnvironmentVariables":\s*true/.test(untouched) &&
|
|
51
|
+
/"copyOnSelect":\s*true/.test(untouched), 'until pwsh is installed only the default switches are added (comments and trailing commas survive)');
|
|
51
52
|
t.write(path.join(data, 'tools', 'pwsh', 'pwsh.exe'), 'MZ');
|
|
52
53
|
syncPwshProfile(data, true);
|
|
53
54
|
const added = read();
|
|
@@ -62,6 +62,7 @@ export const LAUNCH_FILES_GOLDEN = [
|
|
|
62
62
|
'sandbox/csharp/AppActivation.cs',
|
|
63
63
|
'sandbox/csharp/BindMount.cs',
|
|
64
64
|
'sandbox/csharp/CargoShim.cs',
|
|
65
|
+
'sandbox/csharp/ClipboardWatch.cs',
|
|
65
66
|
'sandbox/csharp/Desktop.cs',
|
|
66
67
|
'sandbox/csharp/EnvBroadcast.cs',
|
|
67
68
|
'sandbox/csharp/GitShim.cs',
|
|
@@ -95,6 +96,7 @@ export const LAUNCH_FILES_GOLDEN = [
|
|
|
95
96
|
'sandbox/scripts/set-machine-environment.ps1',
|
|
96
97
|
'sandbox/scripts/set-registry-values.ps1',
|
|
97
98
|
'sandbox/scripts/start-store-app.ps1',
|
|
99
|
+
'sandbox/scripts/watch-clipboard.ps1',
|
|
98
100
|
'sandbox/templates/fivem/README.txt',
|
|
99
101
|
'sandbox/templates/fivem/cs/Client/Client.csproj',
|
|
100
102
|
'sandbox/templates/fivem/cs/Client/ClientMain.cs',
|