windows-development-sandbox 1.13.2 → 1.14.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/app/features/storage/storage.run.js +1 -1
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-VENWSGX2.js → chunk-BQ23GBLC.js} +411 -58
- package/dist/bin/{chunk-K4SU3MHJ.js → chunk-F7IMME5G.js} +1 -1
- package/dist/bin/{chunk-IL7BXJF2.js → chunk-LQHEUN2A.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/repair/checks/contained.js +1 -1
- package/dist/guest/daemon/sessions.d.ts +7 -0
- package/dist/guest/daemon/sessions.js +23 -2
- package/dist/guest/entries/daemon.js +1 -0
- 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/install/install-record.d.ts +1 -0
- package/dist/host/install/install-record.js +2 -0
- package/dist/host/sandbox/contained-deps.d.ts +2 -1
- package/dist/host/sandbox/contained-deps.js +6 -4
- 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 +9 -0
- package/dist/runtime/daemon.js +57 -3
- package/dist/runtime/db.js +9 -0
- package/dist/runtime/main.js +195 -12
- package/dist/runtime/netproxy.js +9 -0
- package/dist/runtime/view.js +195 -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 +2 -0
- package/dist/selftest/install.test.js +11 -0
- package/dist/selftest/live-daemon.test.js +51 -1
- package/dist/selftest/live-terminal.test.js +112 -4
- package/dist/selftest/storage-helper.test.d.ts +2 -0
- package/dist/selftest/storage-helper.test.js +493 -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/run-dir.d.ts +6 -0
- package/dist/shared/channel/run-dir.js +9 -0
- 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 +1 -1
- package/dist/web/index.html +1 -1
- package/dist/web/{main-NBQ3LF4E.js → main-6XNBTJLT.js} +33 -33
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- 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/set-storage-helper.ps1 +203 -0
- package/package.json +1 -1
- package/sandbox/csharp/PtyHost.cs +236 -1
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** Mixed-endian GUID bytes, as VHDX (and .NET's Guid) store them. */
|
|
2
|
+
export declare function guidBytes(text: string): Buffer;
|
|
3
|
+
/** CRC-32C (Castagnoli), the checksum of a VHDX header. */
|
|
4
|
+
export declare function crc32c(data: Buffer): number;
|
|
5
|
+
/**
|
|
6
|
+
* `gb`: the virtual size; `parent`: a parent locator item (a differencing disk); `log`: the current header names a log
|
|
7
|
+
* to replay; `staleLog`: only the older header names one (fine).
|
|
8
|
+
*/
|
|
9
|
+
export declare function fakeVhdx(file: string, o: {
|
|
10
|
+
gb: number;
|
|
11
|
+
parent?: boolean;
|
|
12
|
+
log?: boolean;
|
|
13
|
+
staleLog?: boolean;
|
|
14
|
+
}): void;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// A file with the parts of a VHDX the contained-storage checks read (the signature, the two headers with their CRC-32C,
|
|
2
|
+
// the region table and the metadata table), shared by the script tests and the storage helper tests. Nothing here is a
|
|
3
|
+
// usable disk.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
/** Mixed-endian GUID bytes, as VHDX (and .NET's Guid) store them. */
|
|
6
|
+
export function guidBytes(text) {
|
|
7
|
+
const hex = text.replace(/-/g, '');
|
|
8
|
+
const raw = Buffer.from(hex, 'hex');
|
|
9
|
+
return Buffer.concat([
|
|
10
|
+
Buffer.from(raw.subarray(0, 4)).reverse(),
|
|
11
|
+
Buffer.from(raw.subarray(4, 6)).reverse(),
|
|
12
|
+
Buffer.from(raw.subarray(6, 8)).reverse(),
|
|
13
|
+
raw.subarray(8, 16),
|
|
14
|
+
]);
|
|
15
|
+
}
|
|
16
|
+
const CRC_TABLE = (() => {
|
|
17
|
+
const table = new Uint32Array(256);
|
|
18
|
+
for (let i = 0; i < 256; i++) {
|
|
19
|
+
let c = i;
|
|
20
|
+
for (let k = 0; k < 8; k++)
|
|
21
|
+
c = c & 1 ? 0x82f63b78 ^ (c >>> 1) : c >>> 1;
|
|
22
|
+
table[i] = c >>> 0;
|
|
23
|
+
}
|
|
24
|
+
return table;
|
|
25
|
+
})();
|
|
26
|
+
/** CRC-32C (Castagnoli), the checksum of a VHDX header. */
|
|
27
|
+
export function crc32c(data) {
|
|
28
|
+
let crc = 0xffffffff;
|
|
29
|
+
for (const byte of data)
|
|
30
|
+
crc = CRC_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
31
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
32
|
+
}
|
|
33
|
+
function header(sequence, log) {
|
|
34
|
+
const h = Buffer.alloc(4096);
|
|
35
|
+
h.write('head', 0, 'ascii');
|
|
36
|
+
h.writeBigUInt64LE(BigInt(sequence), 8);
|
|
37
|
+
if (log)
|
|
38
|
+
guidBytes('11111111-2222-3333-4444-555555555555').copy(h, 48);
|
|
39
|
+
h.writeUInt32LE(crc32c(h), 4);
|
|
40
|
+
return h;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* `gb`: the virtual size; `parent`: a parent locator item (a differencing disk); `log`: the current header names a log
|
|
44
|
+
* to replay; `staleLog`: only the older header names one (fine).
|
|
45
|
+
*/
|
|
46
|
+
export function fakeVhdx(file, o) {
|
|
47
|
+
const buf = Buffer.alloc(2 * 1024 * 1024);
|
|
48
|
+
buf.write('vhdxfile', 0, 'ascii');
|
|
49
|
+
header(2, o.log === true).copy(buf, 65536);
|
|
50
|
+
header(1, o.staleLog === true).copy(buf, 131072);
|
|
51
|
+
const table = 196608;
|
|
52
|
+
buf.write('regi', table, 'ascii');
|
|
53
|
+
buf.writeUInt32LE(1, table + 8);
|
|
54
|
+
guidBytes('8b7ca206-4790-4b9a-b8fe-575f050f886e').copy(buf, table + 16);
|
|
55
|
+
buf.writeBigUInt64LE(BigInt(1048576), table + 32);
|
|
56
|
+
buf.writeUInt32LE(65536, table + 40);
|
|
57
|
+
const md = 1048576;
|
|
58
|
+
buf.write('metadata', md, 'ascii');
|
|
59
|
+
const items = [
|
|
60
|
+
{ guid: '2fa54224-cd1b-4876-b211-5dbed83bf4b8', offset: 65536 },
|
|
61
|
+
...(o.parent ? [{ guid: 'a8d35f2d-b30b-454d-abf7-d3d84834ab0c', offset: 65600 }] : []),
|
|
62
|
+
];
|
|
63
|
+
buf.writeUInt16LE(items.length, md + 10);
|
|
64
|
+
items.forEach((item, i) => {
|
|
65
|
+
const at = md + 32 + 32 * i;
|
|
66
|
+
guidBytes(item.guid).copy(buf, at);
|
|
67
|
+
buf.writeUInt32LE(item.offset, at + 16);
|
|
68
|
+
buf.writeUInt32LE(8, at + 20);
|
|
69
|
+
});
|
|
70
|
+
buf.writeBigUInt64LE(BigInt(o.gb) * 1024n ** 3n, md + 65536);
|
|
71
|
+
fs.writeFileSync(file, buf);
|
|
72
|
+
}
|
|
@@ -9,7 +9,7 @@ import { cancelled } from '../app/kernel/index.js';
|
|
|
9
9
|
import { webSession } from '../cli/services/index.js';
|
|
10
10
|
import { fail } from '../core/util/index.js';
|
|
11
11
|
import { cookieValue, csrfOk, hostAllowed, LiveHub, originAllowed, sameSecret, StaticFiles, WebAuth, WebServer, WebStore, } from '../host/web/index.js';
|
|
12
|
-
import { encodeFrame, FrameReader, FrameType } from '../shared/pty/index.js';
|
|
12
|
+
import { encodeFrame, FrameReader, FrameType, MOUSE_OFF } from '../shared/pty/index.js';
|
|
13
13
|
import { CSRF_HEADER, CSRF_VALUE, parseClientMessage } from '../shared/web/index.js';
|
|
14
14
|
const TOKEN = 'a'.repeat(64);
|
|
15
15
|
const ATTACH_TOKEN = 'SECRET-ATTACH-TOKEN';
|
|
@@ -249,7 +249,7 @@ export const webSuite = {
|
|
|
249
249
|
}
|
|
250
250
|
t.section('an unused server ends by itself (no process left behind)');
|
|
251
251
|
{
|
|
252
|
-
const idleServer = async (sandbox, limits) => {
|
|
252
|
+
const idleServer = async (sandbox, limits, sleepMs = 0) => {
|
|
253
253
|
let ended = false;
|
|
254
254
|
const idle = new WebServer({
|
|
255
255
|
port: 0,
|
|
@@ -272,6 +272,16 @@ export const webSuite = {
|
|
|
272
272
|
onStop: () => (ended = true),
|
|
273
273
|
});
|
|
274
274
|
await idle.start();
|
|
275
|
+
if (sleepMs) {
|
|
276
|
+
// The PC "sleeps": no timer runs, then the overdue check fires at once on waking.
|
|
277
|
+
const until = Date.now() + sleepMs;
|
|
278
|
+
while (Date.now() < until)
|
|
279
|
+
;
|
|
280
|
+
await new Promise((r) => setTimeout(r, 20));
|
|
281
|
+
const endedOnWaking = ended;
|
|
282
|
+
await idle.stop();
|
|
283
|
+
return endedOnWaking;
|
|
284
|
+
}
|
|
275
285
|
await new Promise((r) => setTimeout(r, 1500));
|
|
276
286
|
await idle.stop();
|
|
277
287
|
return ended;
|
|
@@ -279,6 +289,7 @@ export const webSuite = {
|
|
|
279
289
|
t.check(await idleServer('stopped', { exit: 60_000, quiet: 200 }), 'no browser and no sandbox: it ends after the short wait');
|
|
280
290
|
t.check(!(await idleServer('running', { exit: 60_000, quiet: 200 })), 'a sandbox runs: the short wait does not end it');
|
|
281
291
|
t.check(await idleServer('running', { exit: 300, quiet: 200 }), 'no browser for the long wait: it ends even while a sandbox runs');
|
|
292
|
+
t.check(!(await idleServer('stopped', { exit: 60_000, quiet: 200 }, 1200)), 'after the PC slept, it does not end on waking: open pages get time to reconnect');
|
|
282
293
|
}
|
|
283
294
|
t.section('the server');
|
|
284
295
|
const fake = new FakePtyHost();
|
|
@@ -373,6 +384,7 @@ export const webSuite = {
|
|
|
373
384
|
tab.socket.send(Buffer.from('x'));
|
|
374
385
|
await t.waitFor(() => tab.closeCode !== null, 5000);
|
|
375
386
|
t.check(tab.texts.includes('{"t":"exit","code":3}'), 'EXIT reaches the browser with its code');
|
|
387
|
+
t.check(Buffer.concat(tab.binary).toString('latin1').endsWith(MOUSE_OFF), "before it, the browser's terminal is told to stop tracking the mouse (a drag selects text again)");
|
|
376
388
|
t.equal(tab.closeCode, 1000, 'and the socket closes cleanly');
|
|
377
389
|
const rude = new Tab(url, { Origin: origin, Cookie });
|
|
378
390
|
await t.waitFor(() => rude.binary.length > 0, 5000);
|
|
@@ -17,12 +17,18 @@ export declare class RunDir {
|
|
|
17
17
|
readonly projectsDir: string;
|
|
18
18
|
/** audit.processes: programs the sandbox started, in small .jsonl files the host takes in and deletes. */
|
|
19
19
|
readonly auditDir: string;
|
|
20
|
+
/**
|
|
21
|
+
* Each running session's process tree, recorded by its ptyhost (guest only: the daemon ends what a ptyhost that was
|
|
22
|
+
* itself ended left running).
|
|
23
|
+
*/
|
|
24
|
+
readonly treesDir: string;
|
|
20
25
|
/** One-shot secrets the host hands over (see secrets.ts). Next to run, not in it: shared-use users can change run. */
|
|
21
26
|
readonly secretsDir: string;
|
|
22
27
|
constructor(dataDir: string);
|
|
23
28
|
requestFile(id: string): string;
|
|
24
29
|
responseFile(id: string): string;
|
|
25
30
|
sessionFile(id: string): string;
|
|
31
|
+
treeFile(id: string): string;
|
|
26
32
|
projectFile(name: string): string;
|
|
27
33
|
secretFile(id: string): string;
|
|
28
34
|
readSandboxStatus(): SandboxStatus | null;
|
|
@@ -44,6 +44,11 @@ export class RunDir {
|
|
|
44
44
|
projectsDir;
|
|
45
45
|
/** audit.processes: programs the sandbox started, in small .jsonl files the host takes in and deletes. */
|
|
46
46
|
auditDir;
|
|
47
|
+
/**
|
|
48
|
+
* Each running session's process tree, recorded by its ptyhost (guest only: the daemon ends what a ptyhost that was
|
|
49
|
+
* itself ended left running).
|
|
50
|
+
*/
|
|
51
|
+
treesDir;
|
|
47
52
|
/** One-shot secrets the host hands over (see secrets.ts). Next to run, not in it: shared-use users can change run. */
|
|
48
53
|
secretsDir;
|
|
49
54
|
constructor(dataDir) {
|
|
@@ -57,6 +62,7 @@ export class RunDir {
|
|
|
57
62
|
this.sessionsDir = path.join(this.root, 'sessions');
|
|
58
63
|
this.projectsDir = path.join(this.root, 'projects');
|
|
59
64
|
this.auditDir = path.join(this.root, 'audit');
|
|
65
|
+
this.treesDir = path.join(this.root, 'trees');
|
|
60
66
|
this.secretsDir = path.join(dataDir, 'secrets');
|
|
61
67
|
}
|
|
62
68
|
requestFile(id) {
|
|
@@ -68,6 +74,9 @@ export class RunDir {
|
|
|
68
74
|
sessionFile(id) {
|
|
69
75
|
return path.join(this.sessionsDir, safeId(id) + '.json');
|
|
70
76
|
}
|
|
77
|
+
treeFile(id) {
|
|
78
|
+
return path.join(this.treesDir, safeId(id) + '.json');
|
|
79
|
+
}
|
|
71
80
|
projectFile(name) {
|
|
72
81
|
return path.join(this.projectsDir, safeId(name) + '.json');
|
|
73
82
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { FrameType, HelloFlag, PING_INTERVAL_MS, type Frame, DETACH_KEY, encodeFrame, helloFrame, resizeFrame, MAX_FRAME_BYTES, FrameReader, } from './protocol.js';
|
|
2
2
|
export { type Endpoint, connect, sendControl, type Viewers, queryViewers } from './client.js';
|
|
3
|
-
export { type ClientResult, MOUSE_OFF,
|
|
3
|
+
export { type ClientResult, MOUSE_OFF, runTerminalClient } from './terminal.js';
|
|
4
|
+
export { fixWindowsKeys, KeyGate } from './key-gate.js';
|
|
5
|
+
export { MouseModes } from './mouse-modes.js';
|
|
4
6
|
export { JOIN_MS, InputJoiner } from './input-joiner.js';
|
|
5
7
|
export { CTRL_C_CONFIRM_MS, CTRL_C_HINT, CtrlCGuard } from './ctrl-c.js';
|
|
6
8
|
export { type VtInput, enableVtInput } from './console-input.js';
|
package/dist/shared/pty/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export { FrameType, HelloFlag, PING_INTERVAL_MS, DETACH_KEY, encodeFrame, helloFrame, resizeFrame, MAX_FRAME_BYTES, FrameReader, } from './protocol.js';
|
|
2
2
|
export { connect, sendControl, queryViewers } from './client.js';
|
|
3
|
-
export { MOUSE_OFF,
|
|
3
|
+
export { MOUSE_OFF, runTerminalClient } from './terminal.js';
|
|
4
|
+
export { fixWindowsKeys, KeyGate } from './key-gate.js';
|
|
5
|
+
export { MouseModes } from './mouse-modes.js';
|
|
4
6
|
export { JOIN_MS, InputJoiner } from './input-joiner.js';
|
|
5
7
|
export { CTRL_C_CONFIRM_MS, CTRL_C_HINT, CtrlCGuard } from './ctrl-c.js';
|
|
6
8
|
export { enableVtInput } from './console-input.js';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node on Windows reads keys through the console API, where Backspace is ^H and Ctrl+Backspace is DEL: the reverse of
|
|
3
|
+
* what terminals send. ConPTY reads ^H as Ctrl+Backspace (delete a word), so swap them back.
|
|
4
|
+
*/
|
|
5
|
+
export declare function fixWindowsKeys(chunk: Buffer): Buffer;
|
|
6
|
+
/**
|
|
7
|
+
* Typed input before the console's mode is settled. Once ENABLE_VIRTUAL_TERMINAL_INPUT is on (enableVtInput), the
|
|
8
|
+
* console hands over the terminal's own bytes (DEL for Backspace, ^H for Ctrl+Backspace) and nothing may be swapped;
|
|
9
|
+
* only when it could not be turned on are keys read as console key records and swapped (fixWindowsKeys). Turning it
|
|
10
|
+
* on takes a moment (a PowerShell helper), and input read meanwhile used to be swapped although it was the terminal's
|
|
11
|
+
* own: a Backspace pressed then reached the agent as ^H and deleted a word. So input waits here until the mode is
|
|
12
|
+
* known.
|
|
13
|
+
*/
|
|
14
|
+
export declare class KeyGate {
|
|
15
|
+
private pending;
|
|
16
|
+
private decided;
|
|
17
|
+
private swap;
|
|
18
|
+
/** windowsConsole: input comes from a Windows console in raw mode (its mode is decided later); else it goes as is. */
|
|
19
|
+
constructor(windowsConsole: boolean);
|
|
20
|
+
/** The bytes to send now (empty while the mode is not known yet). */
|
|
21
|
+
push(data: Buffer): Buffer;
|
|
22
|
+
/** The console's mode is known: what was held, ready to send. Later calls only change how new input is read. */
|
|
23
|
+
decide(vtInput: boolean): Buffer;
|
|
24
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Windows: holds what you type until the client knows how the console hands keys over, so Backspace stays Backspace.
|
|
2
|
+
/**
|
|
3
|
+
* Node on Windows reads keys through the console API, where Backspace is ^H and Ctrl+Backspace is DEL: the reverse of
|
|
4
|
+
* what terminals send. ConPTY reads ^H as Ctrl+Backspace (delete a word), so swap them back.
|
|
5
|
+
*/
|
|
6
|
+
export function fixWindowsKeys(chunk) {
|
|
7
|
+
if (!chunk.includes(0x08) && !chunk.includes(0x7f))
|
|
8
|
+
return chunk;
|
|
9
|
+
return Buffer.from(chunk.map((b) => (b === 0x08 ? 0x7f : b === 0x7f ? 0x08 : b)));
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Typed input before the console's mode is settled. Once ENABLE_VIRTUAL_TERMINAL_INPUT is on (enableVtInput), the
|
|
13
|
+
* console hands over the terminal's own bytes (DEL for Backspace, ^H for Ctrl+Backspace) and nothing may be swapped;
|
|
14
|
+
* only when it could not be turned on are keys read as console key records and swapped (fixWindowsKeys). Turning it
|
|
15
|
+
* on takes a moment (a PowerShell helper), and input read meanwhile used to be swapped although it was the terminal's
|
|
16
|
+
* own: a Backspace pressed then reached the agent as ^H and deleted a word. So input waits here until the mode is
|
|
17
|
+
* known.
|
|
18
|
+
*/
|
|
19
|
+
export class KeyGate {
|
|
20
|
+
pending = [];
|
|
21
|
+
decided;
|
|
22
|
+
swap = false;
|
|
23
|
+
/** windowsConsole: input comes from a Windows console in raw mode (its mode is decided later); else it goes as is. */
|
|
24
|
+
constructor(windowsConsole) {
|
|
25
|
+
this.decided = !windowsConsole;
|
|
26
|
+
}
|
|
27
|
+
/** The bytes to send now (empty while the mode is not known yet). */
|
|
28
|
+
push(data) {
|
|
29
|
+
if (!data.length)
|
|
30
|
+
return data;
|
|
31
|
+
if (!this.decided) {
|
|
32
|
+
this.pending.push(data);
|
|
33
|
+
return Buffer.alloc(0);
|
|
34
|
+
}
|
|
35
|
+
return this.swap ? fixWindowsKeys(data) : data;
|
|
36
|
+
}
|
|
37
|
+
/** The console's mode is known: what was held, ready to send. Later calls only change how new input is read. */
|
|
38
|
+
decide(vtInput) {
|
|
39
|
+
this.swap = !vtInput;
|
|
40
|
+
this.decided = true;
|
|
41
|
+
const held = Buffer.concat(this.pending);
|
|
42
|
+
this.pending = [];
|
|
43
|
+
return this.swap ? fixWindowsKeys(held) : held;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Follows the private modes a program turns on and off in its output (DECSET / DECRST, kept across chunks: mouse
|
|
3
|
+
* tracking 9, 1000, 1002, 1003, the encodings 1005 and 1015, bracketed paste 2004 ...) and drops the mouse reports it
|
|
4
|
+
* did not ask for: none without tracking, motion only with 1002 (while a button is held: a drag) or 1003 (every move).
|
|
5
|
+
* Your terminal reports what the program's modes say; what is dropped is what was still on its way when the program
|
|
6
|
+
* turned tracking off, exited or restarted, and would reach it as text.
|
|
7
|
+
*/
|
|
8
|
+
export declare class MouseModes {
|
|
9
|
+
private readonly on;
|
|
10
|
+
/** 0 text, 1 after ESC, 2 after ESC[, 3 in ESC[? parameters, 4 after ESC[! */
|
|
11
|
+
private state;
|
|
12
|
+
private params;
|
|
13
|
+
/** Reads the program's output (after OutputFilter): the mode changes in it. */
|
|
14
|
+
watch(output: Buffer): void;
|
|
15
|
+
/** The program is gone (exited or restarting): its modes went with it. */
|
|
16
|
+
reset(): void;
|
|
17
|
+
/** True while the program tracks the mouse in any way. */
|
|
18
|
+
tracking(): boolean;
|
|
19
|
+
/** True while the program has a mode on (2004: bracketed paste). */
|
|
20
|
+
has(mode: number): boolean;
|
|
21
|
+
/** The mode numbers on now (for the report and tests). */
|
|
22
|
+
modes(): number[];
|
|
23
|
+
/** `input` without the mouse reports the program has not asked for. Sequences must be whole (InputJoiner). */
|
|
24
|
+
filter(input: Buffer): Buffer;
|
|
25
|
+
private allows;
|
|
26
|
+
private apply;
|
|
27
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// The mouse tracking a program asked for, read from its output, and the mouse reports it may receive.
|
|
2
|
+
/** X10 compatibility (press only), normal (press/release/wheel), button-event (+ drag), any-event (+ every motion). */
|
|
3
|
+
const TRACKING = [9, 1000, 1002, 1003];
|
|
4
|
+
/** Longest DECSET / DECRST parameter list followed; anything longer is not a mode change. */
|
|
5
|
+
const MAX_PARAMS = 64;
|
|
6
|
+
/** Button code bits of a mouse report: motion (drag or move), and "no button" in the low two bits. */
|
|
7
|
+
const MOTION = 32;
|
|
8
|
+
const NO_BUTTON = 3;
|
|
9
|
+
/**
|
|
10
|
+
* Follows the private modes a program turns on and off in its output (DECSET / DECRST, kept across chunks: mouse
|
|
11
|
+
* tracking 9, 1000, 1002, 1003, the encodings 1005 and 1015, bracketed paste 2004 ...) and drops the mouse reports it
|
|
12
|
+
* did not ask for: none without tracking, motion only with 1002 (while a button is held: a drag) or 1003 (every move).
|
|
13
|
+
* Your terminal reports what the program's modes say; what is dropped is what was still on its way when the program
|
|
14
|
+
* turned tracking off, exited or restarted, and would reach it as text.
|
|
15
|
+
*/
|
|
16
|
+
export class MouseModes {
|
|
17
|
+
on = new Set();
|
|
18
|
+
/** 0 text, 1 after ESC, 2 after ESC[, 3 in ESC[? parameters, 4 after ESC[! */
|
|
19
|
+
state = 0;
|
|
20
|
+
params = '';
|
|
21
|
+
/** Reads the program's output (after OutputFilter): the mode changes in it. */
|
|
22
|
+
watch(output) {
|
|
23
|
+
for (const b of output) {
|
|
24
|
+
if (b === 0x1b) {
|
|
25
|
+
this.state = 1;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (this.state === 1) {
|
|
29
|
+
// RIS (ESC c), a full reset, turns every mode off.
|
|
30
|
+
if (b === 0x63)
|
|
31
|
+
this.on.clear();
|
|
32
|
+
this.state = b === 0x5b ? 2 : 0;
|
|
33
|
+
}
|
|
34
|
+
else if (this.state === 2) {
|
|
35
|
+
// DECSTR (ESC[!p), a soft reset, turns mouse tracking off too.
|
|
36
|
+
this.state = b === 0x3f ? 3 : b === 0x21 ? 4 : 0;
|
|
37
|
+
this.params = '';
|
|
38
|
+
}
|
|
39
|
+
else if (this.state === 4) {
|
|
40
|
+
if (b === 0x70)
|
|
41
|
+
this.on.clear();
|
|
42
|
+
this.state = 0;
|
|
43
|
+
}
|
|
44
|
+
else if (this.state === 3) {
|
|
45
|
+
if ((b >= 0x30 && b <= 0x39) || b === 0x3b) {
|
|
46
|
+
this.params += String.fromCharCode(b);
|
|
47
|
+
if (this.params.length > MAX_PARAMS)
|
|
48
|
+
this.state = 0;
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
if (b === 0x68 || b === 0x6c)
|
|
52
|
+
this.apply(this.params, b === 0x68);
|
|
53
|
+
this.state = 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** The program is gone (exited or restarting): its modes went with it. */
|
|
59
|
+
reset() {
|
|
60
|
+
this.on.clear();
|
|
61
|
+
this.state = 0;
|
|
62
|
+
this.params = '';
|
|
63
|
+
}
|
|
64
|
+
/** True while the program tracks the mouse in any way. */
|
|
65
|
+
tracking() {
|
|
66
|
+
return TRACKING.some((m) => this.on.has(m));
|
|
67
|
+
}
|
|
68
|
+
/** True while the program has a mode on (2004: bracketed paste). */
|
|
69
|
+
has(mode) {
|
|
70
|
+
return this.on.has(mode);
|
|
71
|
+
}
|
|
72
|
+
/** The mode numbers on now (for the report and tests). */
|
|
73
|
+
modes() {
|
|
74
|
+
return [...this.on].sort((a, b) => a - b);
|
|
75
|
+
}
|
|
76
|
+
/** `input` without the mouse reports the program has not asked for. Sequences must be whole (InputJoiner). */
|
|
77
|
+
filter(input) {
|
|
78
|
+
if (!input.includes(0x1b))
|
|
79
|
+
return input;
|
|
80
|
+
const parts = [];
|
|
81
|
+
let from = 0;
|
|
82
|
+
let i = input.indexOf(0x1b);
|
|
83
|
+
while (i !== -1) {
|
|
84
|
+
const report = mouseReport(input, i, this.on.has(1005), this.on.has(1015));
|
|
85
|
+
if (report && !this.allows(report.button)) {
|
|
86
|
+
parts.push(input.subarray(from, i));
|
|
87
|
+
from = i + report.length;
|
|
88
|
+
i = input.indexOf(0x1b, from);
|
|
89
|
+
}
|
|
90
|
+
else
|
|
91
|
+
i = input.indexOf(0x1b, i + 1);
|
|
92
|
+
}
|
|
93
|
+
if (from === 0)
|
|
94
|
+
return input;
|
|
95
|
+
parts.push(input.subarray(from));
|
|
96
|
+
return Buffer.concat(parts);
|
|
97
|
+
}
|
|
98
|
+
allows(button) {
|
|
99
|
+
if (!this.tracking())
|
|
100
|
+
return false;
|
|
101
|
+
if ((button & MOTION) === 0)
|
|
102
|
+
return true;
|
|
103
|
+
if (this.on.has(1003))
|
|
104
|
+
return true;
|
|
105
|
+
return this.on.has(1002) && (button & NO_BUTTON) !== NO_BUTTON;
|
|
106
|
+
}
|
|
107
|
+
apply(params, set) {
|
|
108
|
+
for (const p of params.split(';')) {
|
|
109
|
+
const mode = Number(p);
|
|
110
|
+
if (!p || !Number.isSafeInteger(mode))
|
|
111
|
+
continue;
|
|
112
|
+
if (set)
|
|
113
|
+
this.on.add(mode);
|
|
114
|
+
else
|
|
115
|
+
this.on.delete(mode);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The mouse report starting at `at`: SGR (ESC[<b;x;yM or m, 1006), urxvt (ESC[b;x;yM, 1015, b offset by 32; only while
|
|
121
|
+
* 1015 is on) or X10
|
|
122
|
+
* (ESC[M and three bytes; not with UTF-8 coordinates, 1005, where its length cannot be told). Null when there is none.
|
|
123
|
+
*/
|
|
124
|
+
function mouseReport(data, at, utf8, urxvt) {
|
|
125
|
+
if (data[at + 1] !== 0x5b)
|
|
126
|
+
return null;
|
|
127
|
+
const sgr = data[at + 2] === 0x3c;
|
|
128
|
+
if (sgr || (urxvt && data[at + 2] >= 0x30 && data[at + 2] <= 0x39)) {
|
|
129
|
+
let i = at + (sgr ? 3 : 2);
|
|
130
|
+
let digits = '';
|
|
131
|
+
let fields = 1;
|
|
132
|
+
for (; i < data.length && i - at < 32; i++) {
|
|
133
|
+
const b = data[i];
|
|
134
|
+
if (b >= 0x30 && b <= 0x39) {
|
|
135
|
+
if (fields === 1)
|
|
136
|
+
digits += String.fromCharCode(b);
|
|
137
|
+
}
|
|
138
|
+
else if (b === 0x3b)
|
|
139
|
+
fields++;
|
|
140
|
+
else
|
|
141
|
+
break;
|
|
142
|
+
}
|
|
143
|
+
if (i >= data.length || fields !== 3 || !digits)
|
|
144
|
+
return null;
|
|
145
|
+
if (sgr ? data[i] !== 0x4d && data[i] !== 0x6d : data[i] !== 0x4d)
|
|
146
|
+
return null;
|
|
147
|
+
const button = Number(digits) - (sgr ? 0 : 32);
|
|
148
|
+
return button >= 0 ? { button, length: i - at + 1 } : null;
|
|
149
|
+
}
|
|
150
|
+
if (data[at + 2] === 0x4d && !utf8 && data.length - at >= 6) {
|
|
151
|
+
const button = data[at + 3] - 32;
|
|
152
|
+
return button >= 0 ? { button, length: 6 } : null;
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
@@ -13,11 +13,6 @@ export type ClientResult = {
|
|
|
13
13
|
* urxvt).
|
|
14
14
|
*/
|
|
15
15
|
export declare const MOUSE_OFF = "\u001B[?1000l\u001B[?1002l\u001B[?1003l\u001B[?1005l\u001B[?1006l\u001B[?1015l";
|
|
16
|
-
/**
|
|
17
|
-
* Node on Windows reads keys through the console API, where Backspace is ^H and Ctrl+Backspace is DEL: the reverse of
|
|
18
|
-
* what terminals send. ConPTY reads ^H as Ctrl+Backspace (delete a word), so swap them back.
|
|
19
|
-
*/
|
|
20
|
-
export declare function fixWindowsKeys(chunk: Buffer): Buffer;
|
|
21
16
|
/**
|
|
22
17
|
* Bridges this process's terminal to the remote program until it exits, the connection drops, or the user presses
|
|
23
18
|
* the detach key. Restores the terminal before resolving.
|
|
@@ -3,20 +3,37 @@ import { OutputFilter } from './output-filter.js';
|
|
|
3
3
|
import { enableVtInput } from './console-input.js';
|
|
4
4
|
import { InputJoiner, JOIN_MS } from './input-joiner.js';
|
|
5
5
|
import { CTRL_C_CONFIRM_MS, CtrlCGuard } from './ctrl-c.js';
|
|
6
|
+
import { KeyGate } from './key-gate.js';
|
|
7
|
+
import { MouseModes } from './mouse-modes.js';
|
|
6
8
|
/**
|
|
7
9
|
* Turns off every mouse tracking mode a program may have turned on in your terminal (normal, button, any, UTF-8, SGR,
|
|
8
10
|
* urxvt).
|
|
9
11
|
*/
|
|
10
12
|
export const MOUSE_OFF = '\x1b[?1000l\x1b[?1002l\x1b[?1003l\x1b[?1005l\x1b[?1006l\x1b[?1015l';
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
/** After leaving: mouse reports still on their way are dropped until input is this quiet (at most SETTLE_MAX_MS). */
|
|
14
|
+
const SETTLE_QUIET_MS = 50;
|
|
15
|
+
const SETTLE_MAX_MS = 300;
|
|
16
|
+
/** Waits until `stream` has been quiet for SETTLE_QUIET_MS (or SETTLE_MAX_MS passed), dropping what it reads. */
|
|
17
|
+
function drainInput(stream) {
|
|
18
|
+
return new Promise((done) => {
|
|
19
|
+
let quiet;
|
|
20
|
+
const end = () => {
|
|
21
|
+
clearTimeout(quiet);
|
|
22
|
+
clearTimeout(cap);
|
|
23
|
+
stream.off('data', onData);
|
|
24
|
+
done();
|
|
25
|
+
};
|
|
26
|
+
const onData = () => {
|
|
27
|
+
clearTimeout(quiet);
|
|
28
|
+
quiet = setTimeout(end, SETTLE_QUIET_MS);
|
|
29
|
+
};
|
|
30
|
+
const cap = setTimeout(end, SETTLE_MAX_MS);
|
|
31
|
+
stream.on('data', onData);
|
|
32
|
+
onData();
|
|
33
|
+
});
|
|
19
34
|
}
|
|
35
|
+
/** Longest wait for the console's input mode before typed keys are sent anyway (it normally takes about 2 s). */
|
|
36
|
+
const VT_WAIT_MS = 10000;
|
|
20
37
|
/**
|
|
21
38
|
* Bridges this process's terminal to the remote program until it exits, the connection drops, or the user presses
|
|
22
39
|
* the detach key. Restores the terminal before resolving.
|
|
@@ -32,8 +49,8 @@ options = {}) {
|
|
|
32
49
|
const [c, r] = stdout.isTTY ? stdout.getWindowSize() : [0, 0];
|
|
33
50
|
return [c || 120, r || 30];
|
|
34
51
|
};
|
|
35
|
-
// Until the console
|
|
36
|
-
|
|
52
|
+
// Until the console's input mode is known (enableVtInput), what is typed waits: see KeyGate.
|
|
53
|
+
const keys = new KeyGate(process.platform === 'win32' && Boolean(stdin.isTTY));
|
|
37
54
|
return new Promise((resolve) => {
|
|
38
55
|
let done = false;
|
|
39
56
|
const reader = new FrameReader();
|
|
@@ -42,7 +59,10 @@ options = {}) {
|
|
|
42
59
|
let resizeTimer;
|
|
43
60
|
// Set once raw mode is on (below); finish() may run before that.
|
|
44
61
|
// eslint-disable-next-line prefer-const
|
|
45
|
-
let vtInput;
|
|
62
|
+
let vtInput, vtTimer;
|
|
63
|
+
// The agent's mouse modes, read from its output; only known when it is passed through (conpty.dll, see below).
|
|
64
|
+
const mouse = new MouseModes();
|
|
65
|
+
let passthrough = false;
|
|
46
66
|
const finish = (result) => {
|
|
47
67
|
if (done)
|
|
48
68
|
return;
|
|
@@ -54,11 +74,16 @@ options = {}) {
|
|
|
54
74
|
clearInterval(sizeWatch);
|
|
55
75
|
clearTimeout(joinTimer);
|
|
56
76
|
clearTimeout(hintTimer);
|
|
77
|
+
clearTimeout(vtTimer);
|
|
57
78
|
socket.destroy();
|
|
58
|
-
// The agent's mouse tracking ends with it: your terminal's wheel scrolls again (and clicks select text).
|
|
59
|
-
|
|
79
|
+
// The agent's mouse tracking ends with it: your terminal's wheel scrolls again (and clicks select text). Reports
|
|
80
|
+
// the terminal sent before it read that are dropped here, or they would reach your shell as text.
|
|
81
|
+
const tracked = mouse.tracking();
|
|
82
|
+
const reset = '\x1b[0m\x1b[?25h' + MOUSE_OFF + (mouse.has(2004) ? '\x1b[?2004l' : '') + '\r\n';
|
|
83
|
+
const flushed = new Promise((written) => stdout.write(reset, () => written()));
|
|
84
|
+
const settled = tracked ? flushed.then(() => drainInput(stdin)) : Promise.resolve();
|
|
60
85
|
// Wait for a console-mode change still in progress, then give the console back in its normal mode.
|
|
61
|
-
void (vtInput?.stop()
|
|
86
|
+
void Promise.all([settled, vtInput?.stop()]).then(() => {
|
|
62
87
|
if (stdin.isTTY)
|
|
63
88
|
stdin.setRawMode(false);
|
|
64
89
|
stdin.pause();
|
|
@@ -67,10 +92,12 @@ options = {}) {
|
|
|
67
92
|
};
|
|
68
93
|
const joiner = new InputJoiner();
|
|
69
94
|
let joinTimer;
|
|
70
|
-
const
|
|
95
|
+
const write = (data) => {
|
|
71
96
|
if (data.length && !done)
|
|
72
|
-
socket.write(encodeFrame(FrameType.Input,
|
|
97
|
+
socket.write(encodeFrame(FrameType.Input, data));
|
|
73
98
|
};
|
|
99
|
+
// Mouse reports the agent has not asked for (motion without 1002/1003 ...) are dropped: see MouseModes.
|
|
100
|
+
const sendInput = (data) => write(keys.push(passthrough ? mouse.filter(data) : data));
|
|
74
101
|
const guard = options.confirmCtrlC ? new CtrlCGuard() : null;
|
|
75
102
|
let hintTimer;
|
|
76
103
|
const onInput = (chunk) => {
|
|
@@ -99,7 +126,6 @@ options = {}) {
|
|
|
99
126
|
// repaints racing each other left the screen garbled. Wait until the size settles, clear what this terminal
|
|
100
127
|
// reflowed on its own, then let ptyhost repaint everything once at the new size. With conpty.dll (passthrough)
|
|
101
128
|
// nothing is repainted: this terminal's own reflow and scrollback stay, and the program redraws what it needs.
|
|
102
|
-
let passthrough = false;
|
|
103
129
|
const sendSize = () => {
|
|
104
130
|
const [c, r] = size();
|
|
105
131
|
if (c === sent[0] && r === sent[1])
|
|
@@ -134,13 +160,16 @@ options = {}) {
|
|
|
134
160
|
for (const frame of frames) {
|
|
135
161
|
if (frame.type === FrameType.Output) {
|
|
136
162
|
const out = filter.push(frame.payload);
|
|
163
|
+
mouse.watch(out);
|
|
137
164
|
guard?.watchOutput(out.toString('latin1'));
|
|
138
165
|
stdout.write(out);
|
|
139
166
|
}
|
|
140
167
|
else if (frame.type === FrameType.Console)
|
|
141
168
|
passthrough = frame.payload.toString('latin1') === 'conpty.dll';
|
|
142
|
-
else if (frame.type === FrameType.Restarting)
|
|
169
|
+
else if (frame.type === FrameType.Restarting) {
|
|
170
|
+
mouse.reset();
|
|
143
171
|
stdout.write(MOUSE_OFF + '\x1b[2J\x1b[3J\x1b[H');
|
|
172
|
+
}
|
|
144
173
|
else if (frame.type === FrameType.Exit)
|
|
145
174
|
finish({ reason: 'exit', exitCode: frame.payload.length >= 4 ? frame.payload.readInt32LE(0) : 1 });
|
|
146
175
|
else if (frame.type === FrameType.Error)
|
|
@@ -163,9 +192,11 @@ options = {}) {
|
|
|
163
192
|
// After setRawMode (which sets the console mode): mouse reports, focus and keys then arrive as the terminal sent
|
|
164
193
|
// them.
|
|
165
194
|
vtInput = enableVtInput();
|
|
195
|
+
// A helper that hangs must not hold the keyboard: after VT_WAIT_MS keys go as console keys until it is done.
|
|
196
|
+
vtTimer = setTimeout(() => write(keys.decide(false)), VT_WAIT_MS);
|
|
166
197
|
void vtInput.ready.then((on) => {
|
|
167
|
-
|
|
168
|
-
|
|
198
|
+
clearTimeout(vtTimer);
|
|
199
|
+
write(keys.decide(on));
|
|
169
200
|
});
|
|
170
201
|
stdin.on('data', onInput);
|
|
171
202
|
stdin.resume();
|
package/dist/sync/watcher.js
CHANGED
|
@@ -137,7 +137,7 @@ export class Watcher {
|
|
|
137
137
|
/** Contained storage of this data set (only when a mounted image is on record), else null. */
|
|
138
138
|
contained() {
|
|
139
139
|
try {
|
|
140
|
-
const storage = new ContainedStorage(realContainedDeps(this.storageRoot, 64), setNameOf(this.dataPath));
|
|
140
|
+
const storage = new ContainedStorage(realContainedDeps(this.storageRoot, 64, Boolean(this.resume.config)), setNameOf(this.dataPath));
|
|
141
141
|
return storage.isRecorded() ? storage : null;
|
|
142
142
|
}
|
|
143
143
|
catch {
|
package/dist/web/index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>Sandbox</title>
|
|
7
7
|
<link rel="icon" href="data:," />
|
|
8
8
|
<link rel="stylesheet" href="/main-UAQPI5XD.css" />
|
|
9
|
-
<script type="module" src="/main-
|
|
9
|
+
<script type="module" src="/main-6XNBTJLT.js"></script>
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|