windows-development-sandbox 1.13.2 → 1.15.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/config.example.json +2 -1
- package/dist/app/features/defs.d.ts +1 -1
- package/dist/app/features/projects/projects.def.d.ts +1 -1
- package/dist/app/features/projects/projects.run.d.ts +1 -1
- package/dist/app/features/projects/projects.run.js +10 -2
- package/dist/app/features/sandbox/sandbox.def.js +3 -1
- package/dist/app/features/settings/categories.def.js +1 -0
- package/dist/app/features/storage/storage.run.js +1 -1
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-VENWSGX2.js → chunk-D7CM63BS.js} +2303 -1462
- package/dist/bin/{chunk-K4SU3MHJ.js → chunk-HTQT5SBY.js} +1 -1
- package/dist/bin/{chunk-IL7BXJF2.js → chunk-RQ4BWBYX.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 +1 -1
- package/dist/cli/commands/projects/here.js +2 -0
- package/dist/cli/commands/repair/checks/contained.js +1 -1
- package/dist/cli/services/health-service.js +15 -3
- package/dist/cli/services/projects-service.d.ts +8 -3
- package/dist/cli/services/projects-service.js +21 -4
- package/dist/core/config/defaults.js +1 -0
- package/dist/core/schema/config.d.ts +4 -0
- package/dist/core/schema/projects.d.ts +4 -0
- package/dist/core/schema/projects.js +3 -0
- package/dist/guest/boot/boot.js +8 -2
- package/dist/guest/daemon/daemon.js +3 -0
- package/dist/guest/daemon/live-folders.d.ts +48 -0
- package/dist/guest/daemon/live-folders.js +245 -0
- package/dist/guest/daemon/live.d.ts +4 -0
- package/dist/guest/daemon/live.js +57 -0
- package/dist/guest/daemon/sessions.d.ts +7 -0
- package/dist/guest/daemon/sessions.js +23 -2
- package/dist/guest/entries/daemon.js +5 -2
- package/dist/guest/runtime/csc.d.ts +1 -0
- package/dist/guest/runtime/csc.js +1 -0
- package/dist/guest/runtime/secrets.d.ts +5 -2
- package/dist/guest/runtime/secrets.js +12 -5
- package/dist/guest/sessions/pty-host.d.ts +10 -0
- package/dist/guest/sessions/pty-host.js +48 -3
- package/dist/host/channel/channel.js +1 -1
- package/dist/host/engines/hcs/live-folders.d.ts +117 -0
- package/dist/host/engines/hcs/live-folders.js +347 -0
- package/dist/host/engines/index.d.ts +1 -0
- package/dist/host/engines/index.js +1 -0
- package/dist/host/install/install-record.d.ts +1 -0
- package/dist/host/install/install-record.js +2 -0
- package/dist/host/launch/generate.js +8 -1
- package/dist/host/projects/join.d.ts +4 -0
- package/dist/host/projects/join.js +11 -3
- package/dist/host/projects/mapper.d.ts +34 -5
- package/dist/host/projects/mapper.js +88 -20
- package/dist/host/projects/registry.d.ts +2 -0
- package/dist/host/projects/registry.js +1 -1
- package/dist/host/sandbox/contained-deps.d.ts +2 -1
- package/dist/host/sandbox/contained-deps.js +6 -4
- package/dist/host/secrets/launch-secrets.js +2 -1
- package/dist/host/sharing/lifecycle.js +9 -1
- package/dist/host/storage/contained/contained-storage.js +13 -0
- package/dist/host/storage/contained/disk-port.d.ts +5 -0
- package/dist/host/storage/contained/helper-port.d.ts +72 -0
- package/dist/host/storage/contained/helper-port.js +162 -0
- package/dist/host/storage/contained/index.d.ts +1 -0
- package/dist/host/storage/contained/index.js +1 -0
- package/dist/host/web/bridge.d.ts +5 -0
- package/dist/host/web/bridge.js +11 -1
- package/dist/host/web/server.d.ts +2 -0
- package/dist/host/web/server.js +11 -1
- package/dist/runtime/agent.js +23 -0
- package/dist/runtime/daemon.js +523 -135
- package/dist/runtime/db.js +23 -0
- package/dist/runtime/main.js +221 -15
- package/dist/runtime/netproxy.js +23 -0
- package/dist/runtime/view.js +209 -12
- package/dist/selftest/channel.test.js +72 -1
- package/dist/selftest/contained.test.js +1 -37
- package/dist/selftest/hcs.test.js +5 -2
- package/dist/selftest/index.js +4 -0
- package/dist/selftest/install.test.js +11 -0
- package/dist/selftest/layout.test.js +1 -0
- package/dist/selftest/live-daemon.test.js +51 -1
- package/dist/selftest/live-folders.test.d.ts +2 -0
- package/dist/selftest/live-folders.test.js +588 -0
- package/dist/selftest/live-terminal.test.js +112 -4
- package/dist/selftest/projects.test.js +7 -7
- package/dist/selftest/storage-helper.test.d.ts +2 -0
- package/dist/selftest/storage-helper.test.js +493 -0
- package/dist/selftest/support/engine-golden.js +1 -0
- package/dist/selftest/support/fixtures.d.ts +1 -0
- package/dist/selftest/support/live.js +2 -0
- package/dist/selftest/support/vhdx.d.ts +14 -0
- package/dist/selftest/support/vhdx.js +72 -0
- package/dist/selftest/web.test.js +14 -2
- package/dist/shared/channel/index.d.ts +1 -0
- package/dist/shared/channel/index.js +1 -0
- package/dist/shared/channel/live.d.ts +53 -0
- package/dist/shared/channel/live.js +92 -0
- package/dist/shared/channel/run-dir.d.ts +11 -0
- package/dist/shared/channel/run-dir.js +20 -0
- package/dist/shared/channel/secrets.d.ts +1 -0
- package/dist/shared/channel/secrets.js +5 -0
- package/dist/shared/paths/projects.d.ts +6 -1
- package/dist/shared/paths/projects.js +21 -1
- package/dist/shared/pty/index.d.ts +3 -1
- package/dist/shared/pty/index.js +3 -1
- package/dist/shared/pty/key-gate.d.ts +24 -0
- package/dist/shared/pty/key-gate.js +45 -0
- package/dist/shared/pty/mouse-modes.d.ts +27 -0
- package/dist/shared/pty/mouse-modes.js +155 -0
- package/dist/shared/pty/terminal.d.ts +0 -5
- package/dist/shared/pty/terminal.js +51 -20
- package/dist/sync/watcher.js +10 -1
- package/dist/web/index.html +1 -1
- package/dist/web/{main-NBQ3LF4E.js → main-7DJZ2M27.js} +34 -34
- package/native/csharp/HcsHost/AssemblyInfo.cs +4 -4
- package/native/csharp/HcsHost/FolderAccess.cs +361 -0
- package/native/csharp/HcsHost/Hcs.cs +31 -3
- package/native/csharp/HcsHost/Program.cs +54 -6
- package/native/csharp/HcsHost/Share.cs +64 -11
- package/native/csharp/HcsHost/WsbGuard.cs +130 -0
- package/native/csharp/HcsHost/WsbShare.cs +295 -0
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/StorageHelper/AssemblyInfo.cs +15 -0
- package/native/csharp/StorageHelper/Client.cs +297 -0
- package/native/csharp/StorageHelper/Log.cs +177 -0
- package/native/csharp/StorageHelper/MountRecords.cs +160 -0
- package/native/csharp/StorageHelper/Operations.cs +596 -0
- package/native/csharp/StorageHelper/Pins.cs +270 -0
- package/native/csharp/StorageHelper/Privileges.cs +78 -0
- package/native/csharp/StorageHelper/Program.cs +349 -0
- package/native/csharp/StorageHelper/Protocol.cs +107 -0
- package/native/csharp/StorageHelper/RateLimiter.cs +97 -0
- package/native/csharp/StorageHelper/Rules.cs +156 -0
- package/native/csharp/StorageHelper/Server.cs +656 -0
- package/native/csharp/StorageHelper/Settings.cs +297 -0
- package/native/csharp/StorageHelper/StorageService.cs +53 -0
- package/native/csharp/StorageHelper/UserSettings.cs +206 -0
- package/native/csharp/StorageHelper/Vhdx.cs +230 -0
- package/native/csharp/StorageHelper/VirtualDisk.cs +241 -0
- package/native/csharp/StorageHelper/Volumes.cs +379 -0
- package/native/csharp/StorageHelper/Watchdog.cs +128 -0
- package/native/csharp/StorageHelper/Win32.cs +250 -0
- package/native/scripts/contained-common.ps1 +4 -0
- package/native/scripts/hcs-live-share.ps1 +191 -0
- package/native/scripts/set-storage-helper.ps1 +203 -0
- package/package.json +1 -1
- package/sandbox/csharp/BindMount.cs +120 -0
- package/sandbox/csharp/PtyHost.cs +236 -1
- package/schemas/config.schema.json +8 -0
package/dist/runtime/view.js
CHANGED
|
@@ -5892,12 +5892,175 @@ var CtrlCGuard = class {
|
|
|
5892
5892
|
}
|
|
5893
5893
|
};
|
|
5894
5894
|
|
|
5895
|
-
// src/shared/pty/
|
|
5896
|
-
var MOUSE_OFF = "\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1005l\x1B[?1006l\x1B[?1015l";
|
|
5895
|
+
// src/shared/pty/key-gate.ts
|
|
5897
5896
|
function fixWindowsKeys(chunk) {
|
|
5898
5897
|
if (!chunk.includes(8) && !chunk.includes(127)) return chunk;
|
|
5899
5898
|
return Buffer.from(chunk.map((b) => b === 8 ? 127 : b === 127 ? 8 : b));
|
|
5900
5899
|
}
|
|
5900
|
+
var KeyGate = class {
|
|
5901
|
+
pending = [];
|
|
5902
|
+
decided;
|
|
5903
|
+
swap = false;
|
|
5904
|
+
/** windowsConsole: input comes from a Windows console in raw mode (its mode is decided later); else it goes as is. */
|
|
5905
|
+
constructor(windowsConsole) {
|
|
5906
|
+
this.decided = !windowsConsole;
|
|
5907
|
+
}
|
|
5908
|
+
/** The bytes to send now (empty while the mode is not known yet). */
|
|
5909
|
+
push(data) {
|
|
5910
|
+
if (!data.length) return data;
|
|
5911
|
+
if (!this.decided) {
|
|
5912
|
+
this.pending.push(data);
|
|
5913
|
+
return Buffer.alloc(0);
|
|
5914
|
+
}
|
|
5915
|
+
return this.swap ? fixWindowsKeys(data) : data;
|
|
5916
|
+
}
|
|
5917
|
+
/** The console's mode is known: what was held, ready to send. Later calls only change how new input is read. */
|
|
5918
|
+
decide(vtInput) {
|
|
5919
|
+
this.swap = !vtInput;
|
|
5920
|
+
this.decided = true;
|
|
5921
|
+
const held = Buffer.concat(this.pending);
|
|
5922
|
+
this.pending = [];
|
|
5923
|
+
return this.swap ? fixWindowsKeys(held) : held;
|
|
5924
|
+
}
|
|
5925
|
+
};
|
|
5926
|
+
|
|
5927
|
+
// src/shared/pty/mouse-modes.ts
|
|
5928
|
+
var TRACKING = [9, 1e3, 1002, 1003];
|
|
5929
|
+
var MAX_PARAMS = 64;
|
|
5930
|
+
var MOTION = 32;
|
|
5931
|
+
var NO_BUTTON = 3;
|
|
5932
|
+
var MouseModes = class {
|
|
5933
|
+
on = /* @__PURE__ */ new Set();
|
|
5934
|
+
/** 0 text, 1 after ESC, 2 after ESC[, 3 in ESC[? parameters, 4 after ESC[! */
|
|
5935
|
+
state = 0;
|
|
5936
|
+
params = "";
|
|
5937
|
+
/** Reads the program's output (after OutputFilter): the mode changes in it. */
|
|
5938
|
+
watch(output2) {
|
|
5939
|
+
for (const b of output2) {
|
|
5940
|
+
if (b === 27) {
|
|
5941
|
+
this.state = 1;
|
|
5942
|
+
continue;
|
|
5943
|
+
}
|
|
5944
|
+
if (this.state === 1) {
|
|
5945
|
+
if (b === 99) this.on.clear();
|
|
5946
|
+
this.state = b === 91 ? 2 : 0;
|
|
5947
|
+
} else if (this.state === 2) {
|
|
5948
|
+
this.state = b === 63 ? 3 : b === 33 ? 4 : 0;
|
|
5949
|
+
this.params = "";
|
|
5950
|
+
} else if (this.state === 4) {
|
|
5951
|
+
if (b === 112) this.on.clear();
|
|
5952
|
+
this.state = 0;
|
|
5953
|
+
} else if (this.state === 3) {
|
|
5954
|
+
if (b >= 48 && b <= 57 || b === 59) {
|
|
5955
|
+
this.params += String.fromCharCode(b);
|
|
5956
|
+
if (this.params.length > MAX_PARAMS) this.state = 0;
|
|
5957
|
+
} else {
|
|
5958
|
+
if (b === 104 || b === 108) this.apply(this.params, b === 104);
|
|
5959
|
+
this.state = 0;
|
|
5960
|
+
}
|
|
5961
|
+
}
|
|
5962
|
+
}
|
|
5963
|
+
}
|
|
5964
|
+
/** The program is gone (exited or restarting): its modes went with it. */
|
|
5965
|
+
reset() {
|
|
5966
|
+
this.on.clear();
|
|
5967
|
+
this.state = 0;
|
|
5968
|
+
this.params = "";
|
|
5969
|
+
}
|
|
5970
|
+
/** True while the program tracks the mouse in any way. */
|
|
5971
|
+
tracking() {
|
|
5972
|
+
return TRACKING.some((m) => this.on.has(m));
|
|
5973
|
+
}
|
|
5974
|
+
/** True while the program has a mode on (2004: bracketed paste). */
|
|
5975
|
+
has(mode) {
|
|
5976
|
+
return this.on.has(mode);
|
|
5977
|
+
}
|
|
5978
|
+
/** The mode numbers on now (for the report and tests). */
|
|
5979
|
+
modes() {
|
|
5980
|
+
return [...this.on].sort((a, b) => a - b);
|
|
5981
|
+
}
|
|
5982
|
+
/** `input` without the mouse reports the program has not asked for. Sequences must be whole (InputJoiner). */
|
|
5983
|
+
filter(input2) {
|
|
5984
|
+
if (!input2.includes(27)) return input2;
|
|
5985
|
+
const parts = [];
|
|
5986
|
+
let from = 0;
|
|
5987
|
+
let i = input2.indexOf(27);
|
|
5988
|
+
while (i !== -1) {
|
|
5989
|
+
const report = mouseReport(input2, i, this.on.has(1005), this.on.has(1015));
|
|
5990
|
+
if (report && !this.allows(report.button)) {
|
|
5991
|
+
parts.push(input2.subarray(from, i));
|
|
5992
|
+
from = i + report.length;
|
|
5993
|
+
i = input2.indexOf(27, from);
|
|
5994
|
+
} else i = input2.indexOf(27, i + 1);
|
|
5995
|
+
}
|
|
5996
|
+
if (from === 0) return input2;
|
|
5997
|
+
parts.push(input2.subarray(from));
|
|
5998
|
+
return Buffer.concat(parts);
|
|
5999
|
+
}
|
|
6000
|
+
allows(button) {
|
|
6001
|
+
if (!this.tracking()) return false;
|
|
6002
|
+
if ((button & MOTION) === 0) return true;
|
|
6003
|
+
if (this.on.has(1003)) return true;
|
|
6004
|
+
return this.on.has(1002) && (button & NO_BUTTON) !== NO_BUTTON;
|
|
6005
|
+
}
|
|
6006
|
+
apply(params, set2) {
|
|
6007
|
+
for (const p of params.split(";")) {
|
|
6008
|
+
const mode = Number(p);
|
|
6009
|
+
if (!p || !Number.isSafeInteger(mode)) continue;
|
|
6010
|
+
if (set2) this.on.add(mode);
|
|
6011
|
+
else this.on.delete(mode);
|
|
6012
|
+
}
|
|
6013
|
+
}
|
|
6014
|
+
};
|
|
6015
|
+
function mouseReport(data, at, utf8, urxvt) {
|
|
6016
|
+
if (data[at + 1] !== 91) return null;
|
|
6017
|
+
const sgr = data[at + 2] === 60;
|
|
6018
|
+
if (sgr || urxvt && data[at + 2] >= 48 && data[at + 2] <= 57) {
|
|
6019
|
+
let i = at + (sgr ? 3 : 2);
|
|
6020
|
+
let digits = "";
|
|
6021
|
+
let fields = 1;
|
|
6022
|
+
for (; i < data.length && i - at < 32; i++) {
|
|
6023
|
+
const b = data[i];
|
|
6024
|
+
if (b >= 48 && b <= 57) {
|
|
6025
|
+
if (fields === 1) digits += String.fromCharCode(b);
|
|
6026
|
+
} else if (b === 59) fields++;
|
|
6027
|
+
else break;
|
|
6028
|
+
}
|
|
6029
|
+
if (i >= data.length || fields !== 3 || !digits) return null;
|
|
6030
|
+
if (sgr ? data[i] !== 77 && data[i] !== 109 : data[i] !== 77) return null;
|
|
6031
|
+
const button = Number(digits) - (sgr ? 0 : 32);
|
|
6032
|
+
return button >= 0 ? { button, length: i - at + 1 } : null;
|
|
6033
|
+
}
|
|
6034
|
+
if (data[at + 2] === 77 && !utf8 && data.length - at >= 6) {
|
|
6035
|
+
const button = data[at + 3] - 32;
|
|
6036
|
+
return button >= 0 ? { button, length: 6 } : null;
|
|
6037
|
+
}
|
|
6038
|
+
return null;
|
|
6039
|
+
}
|
|
6040
|
+
|
|
6041
|
+
// src/shared/pty/terminal.ts
|
|
6042
|
+
var MOUSE_OFF = "\x1B[?1000l\x1B[?1002l\x1B[?1003l\x1B[?1005l\x1B[?1006l\x1B[?1015l";
|
|
6043
|
+
var SETTLE_QUIET_MS = 50;
|
|
6044
|
+
var SETTLE_MAX_MS = 300;
|
|
6045
|
+
function drainInput(stream) {
|
|
6046
|
+
return new Promise((done) => {
|
|
6047
|
+
let quiet;
|
|
6048
|
+
const end = () => {
|
|
6049
|
+
clearTimeout(quiet);
|
|
6050
|
+
clearTimeout(cap);
|
|
6051
|
+
stream.off("data", onData);
|
|
6052
|
+
done();
|
|
6053
|
+
};
|
|
6054
|
+
const onData = () => {
|
|
6055
|
+
clearTimeout(quiet);
|
|
6056
|
+
quiet = setTimeout(end, SETTLE_QUIET_MS);
|
|
6057
|
+
};
|
|
6058
|
+
const cap = setTimeout(end, SETTLE_MAX_MS);
|
|
6059
|
+
stream.on("data", onData);
|
|
6060
|
+
onData();
|
|
6061
|
+
});
|
|
6062
|
+
}
|
|
6063
|
+
var VT_WAIT_MS = 1e4;
|
|
5901
6064
|
function runTerminalClient(socket, token, options = {}) {
|
|
5902
6065
|
const stdin = process.stdin;
|
|
5903
6066
|
const stdout = process.stdout;
|
|
@@ -5905,14 +6068,16 @@ function runTerminalClient(socket, token, options = {}) {
|
|
|
5905
6068
|
const [c, r] = stdout.isTTY ? stdout.getWindowSize() : [0, 0];
|
|
5906
6069
|
return [c || 120, r || 30];
|
|
5907
6070
|
};
|
|
5908
|
-
|
|
6071
|
+
const keys = new KeyGate(process.platform === "win32" && Boolean(stdin.isTTY));
|
|
5909
6072
|
return new Promise((resolve) => {
|
|
5910
6073
|
let done = false;
|
|
5911
6074
|
const reader = new FrameReader();
|
|
5912
6075
|
const filter = new OutputFilter({ clipboardWrites: options.clipboardWrites });
|
|
5913
6076
|
let sent = size();
|
|
5914
6077
|
let resizeTimer;
|
|
5915
|
-
let vtInput;
|
|
6078
|
+
let vtInput, vtTimer;
|
|
6079
|
+
const mouse = new MouseModes();
|
|
6080
|
+
let passthrough = false;
|
|
5916
6081
|
const finish = (result) => {
|
|
5917
6082
|
if (done) return;
|
|
5918
6083
|
done = true;
|
|
@@ -5923,9 +6088,13 @@ function runTerminalClient(socket, token, options = {}) {
|
|
|
5923
6088
|
clearInterval(sizeWatch);
|
|
5924
6089
|
clearTimeout(joinTimer);
|
|
5925
6090
|
clearTimeout(hintTimer);
|
|
6091
|
+
clearTimeout(vtTimer);
|
|
5926
6092
|
socket.destroy();
|
|
5927
|
-
|
|
5928
|
-
|
|
6093
|
+
const tracked = mouse.tracking();
|
|
6094
|
+
const reset = "\x1B[0m\x1B[?25h" + MOUSE_OFF + (mouse.has(2004) ? "\x1B[?2004l" : "") + "\r\n";
|
|
6095
|
+
const flushed = new Promise((written) => stdout.write(reset, () => written()));
|
|
6096
|
+
const settled = tracked ? flushed.then(() => drainInput(stdin)) : Promise.resolve();
|
|
6097
|
+
void Promise.all([settled, vtInput?.stop()]).then(() => {
|
|
5929
6098
|
if (stdin.isTTY) stdin.setRawMode(false);
|
|
5930
6099
|
stdin.pause();
|
|
5931
6100
|
resolve(result);
|
|
@@ -5933,9 +6102,10 @@ function runTerminalClient(socket, token, options = {}) {
|
|
|
5933
6102
|
};
|
|
5934
6103
|
const joiner = new InputJoiner();
|
|
5935
6104
|
let joinTimer;
|
|
5936
|
-
const
|
|
5937
|
-
if (data.length && !done) socket.write(encodeFrame(FrameType.Input,
|
|
6105
|
+
const write = (data) => {
|
|
6106
|
+
if (data.length && !done) socket.write(encodeFrame(FrameType.Input, data));
|
|
5938
6107
|
};
|
|
6108
|
+
const sendInput = (data) => write(keys.push(passthrough ? mouse.filter(data) : data));
|
|
5939
6109
|
const guard = options.confirmCtrlC ? new CtrlCGuard() : null;
|
|
5940
6110
|
let hintTimer;
|
|
5941
6111
|
const onInput = (chunk) => {
|
|
@@ -5957,7 +6127,6 @@ function runTerminalClient(socket, token, options = {}) {
|
|
|
5957
6127
|
sendInput(joiner.push(chunk));
|
|
5958
6128
|
if (joiner.holding()) joinTimer = setTimeout(() => sendInput(joiner.flush()), JOIN_MS);
|
|
5959
6129
|
};
|
|
5960
|
-
let passthrough = false;
|
|
5961
6130
|
const sendSize = () => {
|
|
5962
6131
|
const [c2, r2] = size();
|
|
5963
6132
|
if (c2 === sent[0] && r2 === sent[1]) return;
|
|
@@ -5987,11 +6156,14 @@ function runTerminalClient(socket, token, options = {}) {
|
|
|
5987
6156
|
for (const frame of frames) {
|
|
5988
6157
|
if (frame.type === FrameType.Output) {
|
|
5989
6158
|
const out = filter.push(frame.payload);
|
|
6159
|
+
mouse.watch(out);
|
|
5990
6160
|
guard?.watchOutput(out.toString("latin1"));
|
|
5991
6161
|
stdout.write(out);
|
|
5992
6162
|
} else if (frame.type === FrameType.Console) passthrough = frame.payload.toString("latin1") === "conpty.dll";
|
|
5993
|
-
else if (frame.type === FrameType.Restarting)
|
|
5994
|
-
|
|
6163
|
+
else if (frame.type === FrameType.Restarting) {
|
|
6164
|
+
mouse.reset();
|
|
6165
|
+
stdout.write(MOUSE_OFF + "\x1B[2J\x1B[3J\x1B[H");
|
|
6166
|
+
} else if (frame.type === FrameType.Exit)
|
|
5995
6167
|
finish({ reason: "exit", exitCode: frame.payload.length >= 4 ? frame.payload.readInt32LE(0) : 1 });
|
|
5996
6168
|
else if (frame.type === FrameType.Error)
|
|
5997
6169
|
finish({
|
|
@@ -6007,8 +6179,10 @@ function runTerminalClient(socket, token, options = {}) {
|
|
|
6007
6179
|
socket.write(helloFrame(token, c, r));
|
|
6008
6180
|
if (stdin.isTTY) stdin.setRawMode(true);
|
|
6009
6181
|
vtInput = enableVtInput();
|
|
6182
|
+
vtTimer = setTimeout(() => write(keys.decide(false)), VT_WAIT_MS);
|
|
6010
6183
|
void vtInput.ready.then((on) => {
|
|
6011
|
-
|
|
6184
|
+
clearTimeout(vtTimer);
|
|
6185
|
+
write(keys.decide(on));
|
|
6012
6186
|
});
|
|
6013
6187
|
stdin.on("data", onInput);
|
|
6014
6188
|
stdin.resume();
|
|
@@ -27075,6 +27249,9 @@ var ProjectsSchema = external_exports.strictObject({
|
|
|
27075
27249
|
),
|
|
27076
27250
|
wsbShare: external_exports.boolean().describe(
|
|
27077
27251
|
"On Windows 11 24H2+ with the Windows Sandbox command line (wsb), add other folders to the running sandbox with `wsb share` (untested). Otherwise starting a project outside every root offers a restart."
|
|
27252
|
+
),
|
|
27253
|
+
liveFolders: external_exports.enum(["off", "hcs"]).describe(
|
|
27254
|
+
'ALPHA, UNTESTED ON A REAL PC: "hcs" adds a project folder to the running sandbox through the Host Compute Service where `wsb share` is missing (one administrator prompt per add or remove; always read-only in this alpha). If it fails, the sandbox may close: save your work first. Turn it on before the sandbox starts. "off" (default) offers a restart instead. The real-PC tests still to pass are in docs/design/hcs-live-folders.md ("Test plan").'
|
|
27078
27255
|
)
|
|
27079
27256
|
});
|
|
27080
27257
|
|
|
@@ -27603,8 +27780,16 @@ var RunDir = class {
|
|
|
27603
27780
|
projectsDir;
|
|
27604
27781
|
/** audit.processes: programs the sandbox started, in small .jsonl files the host takes in and deletes. */
|
|
27605
27782
|
auditDir;
|
|
27783
|
+
/**
|
|
27784
|
+
* Each running session's process tree, recorded by its ptyhost (guest only: the daemon ends what a ptyhost that was
|
|
27785
|
+
* itself ended left running).
|
|
27786
|
+
*/
|
|
27787
|
+
treesDir;
|
|
27606
27788
|
/** One-shot secrets the host hands over (see secrets.ts). Next to run, not in it: shared-use users can change run. */
|
|
27607
27789
|
secretsDir;
|
|
27790
|
+
/** Live folders (live.ts): signed host requests, and the daemon's replies. */
|
|
27791
|
+
liveDir;
|
|
27792
|
+
liveRepliesDir;
|
|
27608
27793
|
constructor(dataDir2) {
|
|
27609
27794
|
this.root = path5.join(dataDir2, "run");
|
|
27610
27795
|
this.sandboxFile = path5.join(this.root, "sandbox.json");
|
|
@@ -27616,7 +27801,16 @@ var RunDir = class {
|
|
|
27616
27801
|
this.sessionsDir = path5.join(this.root, "sessions");
|
|
27617
27802
|
this.projectsDir = path5.join(this.root, "projects");
|
|
27618
27803
|
this.auditDir = path5.join(this.root, "audit");
|
|
27804
|
+
this.treesDir = path5.join(this.root, "trees");
|
|
27619
27805
|
this.secretsDir = path5.join(dataDir2, "secrets");
|
|
27806
|
+
this.liveDir = path5.join(this.root, "live");
|
|
27807
|
+
this.liveRepliesDir = path5.join(this.root, "live-replies");
|
|
27808
|
+
}
|
|
27809
|
+
liveFile(id) {
|
|
27810
|
+
return path5.join(this.liveDir, safeId(id) + ".json");
|
|
27811
|
+
}
|
|
27812
|
+
liveReplyFile(id) {
|
|
27813
|
+
return path5.join(this.liveRepliesDir, safeId(id) + ".json");
|
|
27620
27814
|
}
|
|
27621
27815
|
requestFile(id) {
|
|
27622
27816
|
return path5.join(this.requestsDir, safeId(id) + ".json");
|
|
@@ -27627,6 +27821,9 @@ var RunDir = class {
|
|
|
27627
27821
|
sessionFile(id) {
|
|
27628
27822
|
return path5.join(this.sessionsDir, safeId(id) + ".json");
|
|
27629
27823
|
}
|
|
27824
|
+
treeFile(id) {
|
|
27825
|
+
return path5.join(this.treesDir, safeId(id) + ".json");
|
|
27826
|
+
}
|
|
27630
27827
|
projectFile(name) {
|
|
27631
27828
|
return path5.join(this.projectsDir, safeId(name) + ".json");
|
|
27632
27829
|
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
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
|
-
import { CTRL_C_CONFIRM_MS, CTRL_C_HINT, CtrlCGuard, encodeFrame, FrameReader, FrameType, fixWindowsKeys, helloFrame, InputJoiner, MAX_FRAME_BYTES, MOUSE_OFF, OutputFilter, } from '../shared/pty/index.js';
|
|
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
7
|
import { bootSilent, FIRST_STATUS_MS } from '../host/channel/index.js';
|
|
8
8
|
export const channelSuite = {
|
|
9
9
|
name: 'channel',
|
|
@@ -148,6 +148,77 @@ export const channelSuite = {
|
|
|
148
148
|
hello.payload[4] === 2 &&
|
|
149
149
|
hello.payload.subarray(5).toString() === 'tok', 'a frame of exactly the maximum size is accepted; a hello frame carries size, flags and token');
|
|
150
150
|
t.equal(fixWindowsKeys(Buffer.from([0x61, 0x08, 0x7f, 0x1b])), Buffer.from([0x61, 0x7f, 0x08, 0x1b]), 'Windows console Backspace (^H) reaches the agent as DEL, so it deletes one character, not a word');
|
|
151
|
+
{
|
|
152
|
+
// Keys typed while the client is still turning VT input on are the terminal's own bytes once it is on: they wait
|
|
153
|
+
// and are never swapped (a Backspace sent as DEL used to reach the agent as ^H, deleting a word).
|
|
154
|
+
const keys = new KeyGate(true);
|
|
155
|
+
const early = keys.push(Buffer.from([0x7f, 0x08]));
|
|
156
|
+
const held = keys.decide(true);
|
|
157
|
+
const later = keys.push(Buffer.from([0x7f]));
|
|
158
|
+
const fallback = new KeyGate(true);
|
|
159
|
+
fallback.push(Buffer.from([0x08]));
|
|
160
|
+
const off = fallback.decide(false);
|
|
161
|
+
const plain = new KeyGate(false).push(Buffer.from([0x7f, 0x08]));
|
|
162
|
+
t.check(early.length === 0 &&
|
|
163
|
+
held.equals(Buffer.from([0x7f, 0x08])) &&
|
|
164
|
+
later.equals(Buffer.from([0x7f])) &&
|
|
165
|
+
off.equals(Buffer.from([0x7f])) &&
|
|
166
|
+
fallback.push(Buffer.from([0x7f])).equals(Buffer.from([0x08])) &&
|
|
167
|
+
plain.equals(Buffer.from([0x7f, 0x08])), 'keys typed before the console mode is known wait, then go unswapped with VT input (swapped only without it)');
|
|
168
|
+
}
|
|
169
|
+
t.section('raw mouse input: motion and drag only while the agent asked for them');
|
|
170
|
+
{
|
|
171
|
+
const m = new MouseModes();
|
|
172
|
+
const b = (text) => Buffer.from(text, 'latin1');
|
|
173
|
+
const press = '\x1b[<0;10;5M';
|
|
174
|
+
const release = '\x1b[<0;10;5m';
|
|
175
|
+
const wheel = '\x1b[<64;10;5M';
|
|
176
|
+
const drag = '\x1b[<32;11;5M';
|
|
177
|
+
const ctrlDrag = '\x1b[<48;11;5M';
|
|
178
|
+
const move = '\x1b[<35;12;6M';
|
|
179
|
+
const all = 'a' + press + drag + move + release + wheel + 'b';
|
|
180
|
+
const pass = () => m.filter(b(all)).toString('latin1');
|
|
181
|
+
const none = pass();
|
|
182
|
+
m.watch(b('\x1b[?10'));
|
|
183
|
+
m.watch(b('00h\x1b[?1006h'));
|
|
184
|
+
const normal = pass();
|
|
185
|
+
m.watch(b('\x1b[?1002h'));
|
|
186
|
+
const button = pass();
|
|
187
|
+
const ctrl = m.filter(b(ctrlDrag)).toString('latin1');
|
|
188
|
+
m.watch(b('\x1b[?1003;1015h'));
|
|
189
|
+
const any = pass();
|
|
190
|
+
const urxvt = m.filter(b('\x1b[67;3;4M\x1b[1;5C')).toString('latin1');
|
|
191
|
+
t.check(none === 'ab' &&
|
|
192
|
+
normal === 'a' + press + release + wheel + 'b' &&
|
|
193
|
+
button === 'a' + press + drag + release + wheel + 'b' &&
|
|
194
|
+
ctrl === ctrlDrag &&
|
|
195
|
+
any === all &&
|
|
196
|
+
urxvt === '\x1b[67;3;4M\x1b[1;5C' &&
|
|
197
|
+
m.modes().join(',') === '1000,1002,1003,1006,1015', 'no tracking: no reports; 1000: clicks and wheel; 1002: + drags (modifiers kept); 1003: + every move; mode ' +
|
|
198
|
+
'changes split across chunks and lists (1003;1015) are followed; keys are never touched', JSON.stringify({ none, normal, button, ctrl, any, urxvt, modes: m.modes() }));
|
|
199
|
+
m.watch(b('\x1b[?1003l'));
|
|
200
|
+
const afterOff = pass();
|
|
201
|
+
m.watch(b('\x1b[?1015l\x1b[?1000l\x1b[?1002l'));
|
|
202
|
+
const x10 = m.filter(b('k\x1b[M #%')).toString('latin1');
|
|
203
|
+
m.watch(b('\x1b[?1000h'));
|
|
204
|
+
const x10On = m.filter(b('k\x1b[M #%\x1b[MC#%')).toString('latin1');
|
|
205
|
+
m.watch(b('\x1bc'));
|
|
206
|
+
const ris = m.tracking();
|
|
207
|
+
m.watch(b('\x1b[?1003h\x1b[!p'));
|
|
208
|
+
const decstr = m.tracking();
|
|
209
|
+
m.watch(b('\x1b[?1002h\x1b[?2004h'));
|
|
210
|
+
const paste = m.has(2004);
|
|
211
|
+
m.reset();
|
|
212
|
+
t.check(afterOff === 'a' + press + drag + release + wheel + 'b' &&
|
|
213
|
+
x10 === 'k' &&
|
|
214
|
+
x10On === 'k\x1b[M #%' &&
|
|
215
|
+
!ris &&
|
|
216
|
+
!decstr &&
|
|
217
|
+
paste &&
|
|
218
|
+
!m.tracking() &&
|
|
219
|
+
!m.has(2004), 'turning 1003 off drops moves again; X10 reports follow the same rules; a full or soft reset (ESC c, ' +
|
|
220
|
+
'ESC[!p) and a restart turn tracking off', JSON.stringify({ afterOff, x10, x10On, ris, decstr, paste }));
|
|
221
|
+
}
|
|
151
222
|
t.section('Ctrl+C meant as "copy" does not reach the agent');
|
|
152
223
|
const ctrlC = Buffer.from([3]);
|
|
153
224
|
const g = new CtrlCGuard();
|
|
@@ -15,44 +15,8 @@ import { currentUserSid } from '../host/sharing/index.js';
|
|
|
15
15
|
import { addHolder, CONTAINED_FOLDERS, containedAvailability, ContainedStorage, containedMappings, containedPaths, diagnose, hostDataFolder, markMounted, mountedSets, planTransfer, providerFor, pruneHolders, removeHolder, runTransfer, ScriptDiskPort, setNameOf, setStateOf, staleMountNote, StateFile, } from '../host/storage/index.js';
|
|
16
16
|
import { EMPTY_STATE } from '../host/storage/contained/state.js';
|
|
17
17
|
import { SYSTEM } from '../shared/system.js';
|
|
18
|
+
import { fakeVhdx } from './support/vhdx.js';
|
|
18
19
|
const SCRIPTS = ['contained-common', 'mount-contained', 'dismount-contained', 'check-contained', 'run-elevated'];
|
|
19
|
-
/** Mixed-endian GUID bytes, as VHDX (and .NET's Guid) store them. */
|
|
20
|
-
function guidBytes(text) {
|
|
21
|
-
const hex = text.replace(/-/g, '');
|
|
22
|
-
const raw = Buffer.from(hex, 'hex');
|
|
23
|
-
return Buffer.concat([
|
|
24
|
-
Buffer.from(raw.subarray(0, 4)).reverse(),
|
|
25
|
-
Buffer.from(raw.subarray(4, 6)).reverse(),
|
|
26
|
-
Buffer.from(raw.subarray(6, 8)).reverse(),
|
|
27
|
-
raw.subarray(8, 16),
|
|
28
|
-
]);
|
|
29
|
-
}
|
|
30
|
-
/** A file with the parts of a VHDX header the scripts read (signature, region table, metadata table). */
|
|
31
|
-
function fakeVhdx(file, o) {
|
|
32
|
-
const buf = Buffer.alloc(2 * 1024 * 1024);
|
|
33
|
-
buf.write('vhdxfile', 0, 'ascii');
|
|
34
|
-
const table = 196608;
|
|
35
|
-
buf.write('regi', table, 'ascii');
|
|
36
|
-
buf.writeUInt32LE(1, table + 8);
|
|
37
|
-
guidBytes('8b7ca206-4790-4b9a-b8fe-575f050f886e').copy(buf, table + 16);
|
|
38
|
-
buf.writeBigUInt64LE(BigInt(1048576), table + 32);
|
|
39
|
-
buf.writeUInt32LE(65536, table + 40);
|
|
40
|
-
const md = 1048576;
|
|
41
|
-
buf.write('metadata', md, 'ascii');
|
|
42
|
-
const items = [
|
|
43
|
-
{ guid: '2fa54224-cd1b-4876-b211-5dbed83bf4b8', offset: 65536 },
|
|
44
|
-
...(o.parent ? [{ guid: 'a8d35f2d-b30b-454d-abf7-d3d84834ab0c', offset: 65600 }] : []),
|
|
45
|
-
];
|
|
46
|
-
buf.writeUInt16LE(items.length, md + 10);
|
|
47
|
-
items.forEach((item, i) => {
|
|
48
|
-
const at = md + 32 + 32 * i;
|
|
49
|
-
guidBytes(item.guid).copy(buf, at);
|
|
50
|
-
buf.writeUInt32LE(item.offset, at + 16);
|
|
51
|
-
buf.writeUInt32LE(8, at + 20);
|
|
52
|
-
});
|
|
53
|
-
buf.writeBigUInt64LE(BigInt(o.gb) * 1024n ** 3n, md + 65536);
|
|
54
|
-
fs.writeFileSync(file, buf);
|
|
55
|
-
}
|
|
56
20
|
class FakePort {
|
|
57
21
|
paths;
|
|
58
22
|
calls = [];
|
|
@@ -537,10 +537,13 @@ export const hcsSuite = {
|
|
|
537
537
|
}
|
|
538
538
|
t.section('the helpers say the version they ship with (file details)');
|
|
539
539
|
const version = JSON.parse(fs.readFileSync(path.join(t.packageRoot, 'package.json'), 'utf8')).version;
|
|
540
|
-
for (const helper of ['HostTools', 'HcsHost']) {
|
|
540
|
+
for (const helper of ['HostTools', 'HcsHost', 'StorageHelper']) {
|
|
541
541
|
const info = fs.readFileSync(path.join(t.packageRoot, 'native', 'csharp', helper, 'AssemblyInfo.cs'), 'utf8');
|
|
542
542
|
t.check(info.includes('AssemblyInformationalVersion("' + version + '")') &&
|
|
543
|
-
info.includes('AssemblyFileVersion("' + version + '.0")'), helper
|
|
543
|
+
info.includes('AssemblyFileVersion("' + version + '.0")'), (helper === 'StorageHelper' ? 'AgentSandboxStorage' : helper) +
|
|
544
|
+
'.exe file version is ' +
|
|
545
|
+
version +
|
|
546
|
+
' (update AssemblyInfo.cs with package.json)');
|
|
544
547
|
}
|
|
545
548
|
t.section('HcsHost.exe compiles (C# 5, the csc that ships with Windows)');
|
|
546
549
|
if (!isWin || !fs.existsSync(SYSTEM.csc())) {
|
package/dist/selftest/index.js
CHANGED
|
@@ -34,7 +34,9 @@ import { detectSuite } from './detect.test.js';
|
|
|
34
34
|
import { generateSuite } from './generate.test.js';
|
|
35
35
|
import { engineSuite } from './engine.test.js';
|
|
36
36
|
import { containedSuite } from './contained.test.js';
|
|
37
|
+
import { storageHelperSuite } from './storage-helper.test.js';
|
|
37
38
|
import { hcsSuite } from './hcs.test.js';
|
|
39
|
+
import { liveFoldersSuite } from './live-folders.test.js';
|
|
38
40
|
import { gitAccessSuite } from './git-access.test.js';
|
|
39
41
|
import { guestBootSuite } from './guest-boot.test.js';
|
|
40
42
|
import { hostileInputSuite } from './hostile-input.test.js';
|
|
@@ -86,7 +88,9 @@ export const SUITES = [
|
|
|
86
88
|
generateSuite,
|
|
87
89
|
engineSuite,
|
|
88
90
|
containedSuite,
|
|
91
|
+
storageHelperSuite,
|
|
89
92
|
hcsSuite,
|
|
93
|
+
liveFoldersSuite,
|
|
90
94
|
guestBootSuite,
|
|
91
95
|
mirrorSuite,
|
|
92
96
|
projectsSuite,
|
|
@@ -24,6 +24,17 @@ function installerText(t, script) {
|
|
|
24
24
|
timeout: 60000,
|
|
25
25
|
});
|
|
26
26
|
t.equal(parse.status, 0, 'it parses under the PowerShell 5.1 parser' + (parse.stderr ? ': ' + parse.stderr : ''));
|
|
27
|
+
t.section('install.ps1: the storage helper (installs for everyone only)');
|
|
28
|
+
t.check(/\[switch\]\$ContainedStorageHelper,/.test(source) &&
|
|
29
|
+
/\[switch\]\$NoContainedStorageHelper,/.test(source) &&
|
|
30
|
+
source.includes("$env:WDS_STORAGEHELPER -eq '1'"), 'it has -ContainedStorageHelper / -NoContainedStorageHelper (and WDS_STORAGEHELPER)');
|
|
31
|
+
const helperStep = source.slice(source.indexOf('# 8b.'), source.indexOf('# 9. Automatic updates'));
|
|
32
|
+
t.check(helperStep.includes('if ($machine) {') &&
|
|
33
|
+
helperStep.includes('set-storage-helper.ps1') &&
|
|
34
|
+
helperStep.includes('-Install -GroupSid $groupSid -InstallRoot $toolFolder'), 'the helper is installed by its own script, only for an install for everyone, for the group');
|
|
35
|
+
t.check(helperStep.includes("Ask 'Install a small Windows service") && helperStep.includes('$record.storageHelper'), 'asked once (default no), then the answer in install.json is kept');
|
|
36
|
+
t.check(source.includes('storageHelper = [bool]$helperOn') && /'staging', 'helper', 'active-version'/.test(source), 'install.json records it, and uninstall removes the helper folder');
|
|
37
|
+
t.check(/if \(\$machine\) \{ Remove-StorageHelper/.test(source), 'uninstall stops and deletes the service (and dismounts what it mounted) first');
|
|
27
38
|
t.section('install.ps1: the shims equal shimText()');
|
|
28
39
|
const cases = [
|
|
29
40
|
['sandbox', undefined],
|
|
@@ -24,6 +24,7 @@ const PURE_GUEST = [
|
|
|
24
24
|
'guest/toolchains/tools/vscode-profile.js',
|
|
25
25
|
'guest/governor/decide.js',
|
|
26
26
|
'guest/governor/core.js',
|
|
27
|
+
'guest/daemon/live-folders.js',
|
|
27
28
|
];
|
|
28
29
|
const SIDE_EFFECTS = ['guest/runtime/session.js', 'guest/runtime/log.js'];
|
|
29
30
|
const readModules = (dist) => {
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
// with cmd.exe standing in for the agent. One daemon serves all the checks below, in order (compiling ptyhost is slow).
|
|
3
3
|
import fs from 'node:fs';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
+
import { spawnSync } from 'node:child_process';
|
|
5
6
|
import { queryViewers } from '../shared/pty/index.js';
|
|
7
|
+
import { SYSTEM } from '../shared/system.js';
|
|
6
8
|
import { copyCmdAsFakeAgent, openViewer, sees, startDaemon, tail, viewersWhen, WAIT } from './support/live.js';
|
|
7
9
|
export const liveDaemonSuite = {
|
|
8
10
|
name: 'live-daemon',
|
|
9
|
-
title: 'live: session daemon + ptyhost (start, attach, viewers, exec, restart, exit)',
|
|
11
|
+
title: 'live: session daemon + ptyhost (start, attach, viewers, exec, a killed ptyhost, restart, exit)',
|
|
10
12
|
live: true,
|
|
11
13
|
timeoutMs: 360000,
|
|
12
14
|
run: async (t, f) => {
|
|
@@ -107,6 +109,54 @@ export const liveDaemonSuite = {
|
|
|
107
109
|
const again = await daemon.response('close2');
|
|
108
110
|
t.check(again?.ok === false, 'closing a session that has ended says it is not running', JSON.stringify(again));
|
|
109
111
|
}
|
|
112
|
+
t.section('live: a session host ended from outside (Task Manager)');
|
|
113
|
+
{
|
|
114
|
+
// A copy of PING with a name of its own, started hidden in a console of its own (closing the pseudo console
|
|
115
|
+
// does not end it) by a PowerShell the shell runs; then the session's ptyhost is killed.
|
|
116
|
+
const probe = t.at('tree-probe.exe');
|
|
117
|
+
fs.copyFileSync(path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'PING.EXE'), probe);
|
|
118
|
+
const probeRunning = () => spawnSync(SYSTEM.tasklist(), ['/FI', 'IMAGENAME eq tree-probe.exe', '/NH'], { encoding: 'utf8' })
|
|
119
|
+
.stdout.toLowerCase()
|
|
120
|
+
.includes('tree-probe.exe');
|
|
121
|
+
daemon.request({ kind: 'session', id: 'shell2', harness: 'shell', args: [], cwd: workspace, shell: 'cmd' });
|
|
122
|
+
const tree = await daemon.started('shell2');
|
|
123
|
+
const record = path.join(daemon.run, 'trees', 'shell2.json');
|
|
124
|
+
const readRecord = () => {
|
|
125
|
+
try {
|
|
126
|
+
return JSON.parse(fs.readFileSync(record, 'utf8'));
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
if (tree?.attach) {
|
|
133
|
+
const treeView = await openViewer(tree.attach);
|
|
134
|
+
viewers.push(treeView);
|
|
135
|
+
await sees(t, treeView, '>');
|
|
136
|
+
treeView.type('"' +
|
|
137
|
+
SYSTEM.powershell() +
|
|
138
|
+
'" -NoProfile -Command "Start-Process -WindowStyle Hidden -FilePath \'' +
|
|
139
|
+
probe +
|
|
140
|
+
"' -ArgumentList '-n','120','127.0.0.1'; Start-Sleep -Seconds 8\"");
|
|
141
|
+
// cmd, the PowerShell and the probe, once ptyhost has followed the tree (every 3 s).
|
|
142
|
+
const recorded = await t.waitFor(() => {
|
|
143
|
+
const r = readRecord();
|
|
144
|
+
return probeRunning() && r && r.processes.length >= 3 ? r : null;
|
|
145
|
+
}, WAIT.screen);
|
|
146
|
+
t.check(recorded !== null, "the session's ptyhost records what its program started in the run folder", JSON.stringify(readRecord()) + ' probe running: ' + probeRunning() + '\n' + daemon.diagnostics());
|
|
147
|
+
if (recorded) {
|
|
148
|
+
spawnSync(path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'taskkill.exe'), ['/F', '/PID', String(recorded.ptyhost.pid)], { stdio: 'ignore' });
|
|
149
|
+
const gone = await t.waitFor(() => !probeRunning() && !fs.existsSync(record), 30000);
|
|
150
|
+
t.check(Boolean(gone) &&
|
|
151
|
+
daemon.status('shell2')?.phase === 'exited' &&
|
|
152
|
+
/session host was ended from outside; ended [1-9]\d* process\(es\)/.test(daemon.diagnostics()), 'when its ptyhost is killed, the daemon ends what the session left running (and forgets the record)', 'probe running: ' + probeRunning() + ', record: ' + fs.existsSync(record) + '\n' + daemon.diagnostics());
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
else
|
|
156
|
+
t.check(false, 'a second shell session starts', JSON.stringify(tree) + '\n' + daemon.diagnostics());
|
|
157
|
+
if (probeRunning())
|
|
158
|
+
spawnSync(path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'taskkill.exe'), ['/F', '/IM', 'tree-probe.exe'], { stdio: 'ignore' });
|
|
159
|
+
}
|
|
110
160
|
t.section('live: restart and exit');
|
|
111
161
|
daemon.request({ kind: 'restart', id: 'restart1', harness: 'claude' });
|
|
112
162
|
const sawResumed = await sees(t, viewer, 'LIVE-AGENT-RESUMED');
|