windows-development-sandbox 1.11.4 → 1.11.5
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/accounts/accounts.def.js +0 -1
- package/dist/app/features/health/health.def.js +6 -3
- package/dist/app/features/sandbox/sandbox.def.js +4 -4
- package/dist/host/web/runner.d.ts +1 -1
- package/dist/host/web/server.d.ts +12 -3
- package/dist/host/web/server.js +20 -6
- package/dist/runtime/daemon.js +2 -0
- package/dist/runtime/main.js +2 -0
- package/dist/runtime/view.js +2 -0
- package/dist/selftest/health.test.js +35 -0
- package/dist/selftest/live-terminal.test.js +30 -0
- package/dist/selftest/support/web-preview.js +3 -0
- package/dist/selftest/web.test.js +33 -0
- package/dist/shared/pty/client.d.ts +0 -1
- package/dist/shared/pty/client.js +5 -0
- package/dist/web/index.html +2 -2
- package/dist/web/{main-SLQNJRXD.js → main-QL7KHCEE.js} +62 -62
- package/dist/web/{main-MU5XWZMS.css → main-RQFN4WBY.css} +1 -1
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/package.json +1 -1
- package/sandbox/csharp/PtyHost.cs +228 -2
|
@@ -209,7 +209,6 @@ export function agentSection(a) {
|
|
|
209
209
|
id: a.name,
|
|
210
210
|
title: a.name,
|
|
211
211
|
summary: a.accounts.length ? plural(a.accounts.length, 'saved login') : 'no saved login yet',
|
|
212
|
-
description: 'Saved ' + a.name + ' logins.',
|
|
213
212
|
cards: true,
|
|
214
213
|
items: [...saveItems(a), ...a.accounts.map((r) => loginItem(a, r))],
|
|
215
214
|
};
|
|
@@ -165,7 +165,10 @@ export const enableSandbox = defineOperation({
|
|
|
165
165
|
},
|
|
166
166
|
},
|
|
167
167
|
});
|
|
168
|
-
/**
|
|
168
|
+
/**
|
|
169
|
+
* A repair row: what it means is the description; what was found and the fix are label/value detail lines ("Found:",
|
|
170
|
+
* "Fix:"). An ok row has nothing to fix, so what it found is its description.
|
|
171
|
+
*/
|
|
169
172
|
function checkItems(last) {
|
|
170
173
|
return last.checks.map((r, i) => {
|
|
171
174
|
const found = r.detail + (r.timedOut ? ' (took too long)' : '');
|
|
@@ -173,14 +176,14 @@ function checkItems(last) {
|
|
|
173
176
|
.slice(1)
|
|
174
177
|
.map((l) => l.text.trim())
|
|
175
178
|
.filter((l) => !l.startsWith('What it means: '));
|
|
176
|
-
const details = r.
|
|
179
|
+
const details = r.status === 'ok' ? [] : ['Found: ' + found, ...rest];
|
|
177
180
|
return {
|
|
178
181
|
id: 'repair:' + i,
|
|
179
182
|
label: r.name,
|
|
180
183
|
hint: BADGE[r.status],
|
|
181
184
|
...(TONE[r.status] ? { tone: TONE[r.status] } : {}),
|
|
182
185
|
badges: [{ text: BADGE[r.status], ...(TONE[r.status] ? { tone: TONE[r.status] } : {}) }],
|
|
183
|
-
description: r.means ?? found,
|
|
186
|
+
description: r.means ?? (details.length ? undefined : found),
|
|
184
187
|
...(details.length ? { details } : {}),
|
|
185
188
|
};
|
|
186
189
|
});
|
|
@@ -130,12 +130,12 @@ function projectItem(p) {
|
|
|
130
130
|
hint: p.host + ' -> ' + p.guest + ' (' + (MECHANISM_TEXT[p.mechanism] ?? 'wsb share') + ')',
|
|
131
131
|
marked: p.here,
|
|
132
132
|
details: [
|
|
133
|
-
'
|
|
133
|
+
'Setup: ' + p.setup.phase + (p.setup.phase === 'ready' ? '' : ' - ' + p.setup.message),
|
|
134
134
|
...(p.databases.length
|
|
135
|
-
? ['
|
|
135
|
+
? ['Databases: ' + p.databases.map((d) => d.database + ' :' + d.port + ' ' + d.state).join(', ')]
|
|
136
136
|
: []),
|
|
137
|
-
...p.sessions.map((s) => '
|
|
138
|
-
...p.repoGuard.map((f) => '
|
|
137
|
+
...p.sessions.map((s) => 'Session: ' + s.harness + ' ' + s.id + ', ' + s.viewers + ', started ' + ago(s.started)),
|
|
138
|
+
...p.repoGuard.map((f) => 'Guard: ' + f),
|
|
139
139
|
],
|
|
140
140
|
};
|
|
141
141
|
}
|
|
@@ -8,7 +8,7 @@ export interface WebRunOptions {
|
|
|
8
8
|
readonly staticRoot: string;
|
|
9
9
|
/** This program's version, kept in the state file. */
|
|
10
10
|
readonly version: string;
|
|
11
|
-
/** End after this long
|
|
11
|
+
/** End after this long with no browser (sooner with no sandbox): WebServerOptions.idleExitMs. */
|
|
12
12
|
readonly idleExitMs?: number;
|
|
13
13
|
}
|
|
14
14
|
export declare class WebRunner {
|
|
@@ -4,8 +4,13 @@ import type { Endpoint } from '../../shared/pty/index.js';
|
|
|
4
4
|
import type { WebBackend } from './backend.js';
|
|
5
5
|
/** No inline script: the page and its script are files from this server. Inline styles are what xterm.js needs. */
|
|
6
6
|
export declare const CONTENT_SECURITY_POLICY: string;
|
|
7
|
-
/**
|
|
7
|
+
/**
|
|
8
|
+
* The server ends after this long with no browser connected and no request, even while a sandbox runs (sandbox web
|
|
9
|
+
* open and the next launch start it again), so it never sits in memory unused.
|
|
10
|
+
*/
|
|
8
11
|
export declare const IDLE_EXIT_MS: number;
|
|
12
|
+
/** ...and sooner when no sandbox runs either: there is nothing to look at. */
|
|
13
|
+
export declare const IDLE_NO_SANDBOX_MS: number;
|
|
9
14
|
export declare const RPC_PATH = "/api/rpc";
|
|
10
15
|
export interface WebServerOptions {
|
|
11
16
|
/** 0 picks a free port (tests). */
|
|
@@ -20,8 +25,10 @@ export interface WebServerOptions {
|
|
|
20
25
|
readonly dial?: (endpoint: Endpoint) => Promise<net.Socket>;
|
|
21
26
|
/** The shared core's operations and queries the browser may run (none: /api/rpc is not served). */
|
|
22
27
|
readonly rpc?: RpcHost;
|
|
23
|
-
/** End by itself (onStop) after this long
|
|
28
|
+
/** End by itself (onStop) after this long with no browser, even while a sandbox runs; 0 or missing: never (tests). */
|
|
24
29
|
readonly idleExitMs?: number;
|
|
30
|
+
/** ...or after this long when no sandbox runs either (default IDLE_NO_SANDBOX_MS; tests make it short). */
|
|
31
|
+
readonly idleNoSandboxMs?: number;
|
|
25
32
|
/** Heartbeat period (tests make it short). */
|
|
26
33
|
readonly heartbeatMs?: number;
|
|
27
34
|
}
|
|
@@ -54,8 +61,10 @@ export declare class WebServer {
|
|
|
54
61
|
* so they never hold a slot or a session connection.
|
|
55
62
|
*/
|
|
56
63
|
private beat;
|
|
57
|
-
/**
|
|
64
|
+
/** No browser has used the server for a while: it ends (sooner when no sandbox runs). */
|
|
58
65
|
private checkIdle;
|
|
66
|
+
/** How long unused before it ends when no sandbox runs. */
|
|
67
|
+
private quietMs;
|
|
59
68
|
/** Counts a new socket; at the limit, the oldest of its kind makes room (a new tab always works). */
|
|
60
69
|
private track;
|
|
61
70
|
stop(): Promise<void>;
|
package/dist/host/web/server.js
CHANGED
|
@@ -19,8 +19,13 @@ const MAX_TERMINALS = 16;
|
|
|
19
19
|
const MAX_RPC_SOCKETS = 8;
|
|
20
20
|
/** Every socket is pinged this often; one that has not answered the last ping by the next is dropped. */
|
|
21
21
|
const HEARTBEAT_MS = 20_000;
|
|
22
|
-
/**
|
|
23
|
-
|
|
22
|
+
/**
|
|
23
|
+
* The server ends after this long with no browser connected and no request, even while a sandbox runs (sandbox web
|
|
24
|
+
* open and the next launch start it again), so it never sits in memory unused.
|
|
25
|
+
*/
|
|
26
|
+
export const IDLE_EXIT_MS = 20 * 60_000;
|
|
27
|
+
/** ...and sooner when no sandbox runs either: there is nothing to look at. */
|
|
28
|
+
export const IDLE_NO_SANDBOX_MS = 5 * 60_000;
|
|
24
29
|
const SESSION_PATH = /^\/api\/sessions\/([A-Za-z0-9_-]{1,64})\/terminal$/;
|
|
25
30
|
export const RPC_PATH = '/api/rpc';
|
|
26
31
|
class HttpFailure extends Error {
|
|
@@ -72,7 +77,7 @@ export class WebServer {
|
|
|
72
77
|
this.heartbeat = setInterval(() => this.beat(), this.options.heartbeatMs ?? HEARTBEAT_MS);
|
|
73
78
|
this.heartbeat.unref();
|
|
74
79
|
if (this.options.idleExitMs) {
|
|
75
|
-
this.idleCheck = setInterval(() => void this.checkIdle(), Math.min(60_000, this.
|
|
80
|
+
this.idleCheck = setInterval(() => void this.checkIdle(), Math.min(60_000, this.quietMs()));
|
|
76
81
|
this.idleCheck.unref();
|
|
77
82
|
}
|
|
78
83
|
resolve();
|
|
@@ -102,20 +107,29 @@ export class WebServer {
|
|
|
102
107
|
}
|
|
103
108
|
}
|
|
104
109
|
}
|
|
105
|
-
/**
|
|
110
|
+
/** No browser has used the server for a while: it ends (sooner when no sandbox runs). */
|
|
106
111
|
async checkIdle() {
|
|
107
112
|
const limit = this.options.idleExitMs ?? 0;
|
|
108
|
-
if (!limit || this.tracked.size > 0
|
|
113
|
+
if (!limit || this.tracked.size > 0)
|
|
114
|
+
return;
|
|
115
|
+
const idle = Date.now() - this.lastUsed;
|
|
116
|
+
if (idle >= limit)
|
|
117
|
+
return this.options.onStop?.();
|
|
118
|
+
if (idle < this.quietMs())
|
|
109
119
|
return;
|
|
110
120
|
try {
|
|
111
121
|
if ((await this.options.backend.info()).sandbox === 'running')
|
|
112
122
|
return;
|
|
113
123
|
}
|
|
114
124
|
catch {
|
|
115
|
-
return; // cannot tell:
|
|
125
|
+
return; // cannot tell: wait for the longer limit
|
|
116
126
|
}
|
|
117
127
|
this.options.onStop?.();
|
|
118
128
|
}
|
|
129
|
+
/** How long unused before it ends when no sandbox runs. */
|
|
130
|
+
quietMs() {
|
|
131
|
+
return Math.min(this.options.idleExitMs ?? 0, this.options.idleNoSandboxMs ?? IDLE_NO_SANDBOX_MS);
|
|
132
|
+
}
|
|
119
133
|
/** Counts a new socket; at the limit, the oldest of its kind makes room (a new tab always works). */
|
|
120
134
|
track(ws, kind, limit) {
|
|
121
135
|
const same = [...this.tracked.entries()]
|
package/dist/runtime/daemon.js
CHANGED
|
@@ -32063,6 +32063,7 @@ var connectOnce = (host, port, timeoutMs) => {
|
|
|
32063
32063
|
socket.once("connect", () => {
|
|
32064
32064
|
clearTimeout(timer);
|
|
32065
32065
|
socket.setNoDelay(true);
|
|
32066
|
+
socket.setKeepAlive(true, KEEPALIVE_IDLE_MS);
|
|
32066
32067
|
resolve2(socket);
|
|
32067
32068
|
});
|
|
32068
32069
|
socket.once("error", (e) => {
|
|
@@ -32071,6 +32072,7 @@ var connectOnce = (host, port, timeoutMs) => {
|
|
|
32071
32072
|
});
|
|
32072
32073
|
});
|
|
32073
32074
|
};
|
|
32075
|
+
var KEEPALIVE_IDLE_MS = 1e4;
|
|
32074
32076
|
async function connect(endpoint, timeoutMs = 3e3) {
|
|
32075
32077
|
const errors = [];
|
|
32076
32078
|
for (const address of endpoint.addresses) {
|
package/dist/runtime/main.js
CHANGED
|
@@ -34686,6 +34686,7 @@ var connectOnce = (host, port, timeoutMs) => {
|
|
|
34686
34686
|
socket.once("connect", () => {
|
|
34687
34687
|
clearTimeout(timer);
|
|
34688
34688
|
socket.setNoDelay(true);
|
|
34689
|
+
socket.setKeepAlive(true, KEEPALIVE_IDLE_MS);
|
|
34689
34690
|
resolve2(socket);
|
|
34690
34691
|
});
|
|
34691
34692
|
socket.once("error", (e) => {
|
|
@@ -34694,6 +34695,7 @@ var connectOnce = (host, port, timeoutMs) => {
|
|
|
34694
34695
|
});
|
|
34695
34696
|
});
|
|
34696
34697
|
};
|
|
34698
|
+
var KEEPALIVE_IDLE_MS = 1e4;
|
|
34697
34699
|
async function connect(endpoint, timeoutMs = 3e3) {
|
|
34698
34700
|
const errors = [];
|
|
34699
34701
|
for (const address of endpoint.addresses) {
|
package/dist/runtime/view.js
CHANGED
|
@@ -5594,6 +5594,7 @@ var connectOnce = (host, port, timeoutMs) => {
|
|
|
5594
5594
|
socket.once("connect", () => {
|
|
5595
5595
|
clearTimeout(timer);
|
|
5596
5596
|
socket.setNoDelay(true);
|
|
5597
|
+
socket.setKeepAlive(true, KEEPALIVE_IDLE_MS);
|
|
5597
5598
|
resolve(socket);
|
|
5598
5599
|
});
|
|
5599
5600
|
socket.once("error", (e) => {
|
|
@@ -5602,6 +5603,7 @@ var connectOnce = (host, port, timeoutMs) => {
|
|
|
5602
5603
|
});
|
|
5603
5604
|
});
|
|
5604
5605
|
};
|
|
5606
|
+
var KEEPALIVE_IDLE_MS = 1e4;
|
|
5605
5607
|
async function connect(endpoint, timeoutMs = 3e3) {
|
|
5606
5608
|
const errors = [];
|
|
5607
5609
|
for (const address of endpoint.addresses) {
|
|
@@ -524,6 +524,41 @@ async function sharedCore(t) {
|
|
|
524
524
|
t.equal(keys, ['f', 'l'], 'the health keys are f and l (e only when Windows Sandbox can be turned on)');
|
|
525
525
|
const canEnable = healthPage({ last: { ...saved, setup: { ...saved.setup, canEnable: true } } });
|
|
526
526
|
t.equal(menuView(canEnable, { category: 'health', refresh: false }).actions.map((a) => a.key), ['f', 'l', 'e'], 'e turns Windows Sandbox on when the setup says it can be');
|
|
527
|
+
const problems = healthPage({
|
|
528
|
+
last: {
|
|
529
|
+
...saved,
|
|
530
|
+
mode: 'repair',
|
|
531
|
+
checks: [
|
|
532
|
+
{
|
|
533
|
+
id: 'stale',
|
|
534
|
+
group: 'Data folder',
|
|
535
|
+
name: 'Stale run folders',
|
|
536
|
+
status: 'needs-you',
|
|
537
|
+
detail: 'two folders from a sandbox that did not close',
|
|
538
|
+
means: 'they take space',
|
|
539
|
+
fix: 'stop the sandbox',
|
|
540
|
+
lines: ['run-20261012-0931'],
|
|
541
|
+
fixable: false,
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
id: 'net',
|
|
545
|
+
group: 'Network',
|
|
546
|
+
name: 'GitHub',
|
|
547
|
+
status: 'warn',
|
|
548
|
+
detail: 'no answer',
|
|
549
|
+
fix: 'check your VPN',
|
|
550
|
+
fixable: false,
|
|
551
|
+
},
|
|
552
|
+
],
|
|
553
|
+
},
|
|
554
|
+
});
|
|
555
|
+
const repairItems = problems.sections.find((s) => s.id === 'repair')?.items ?? [];
|
|
556
|
+
const stale = repairItems[0];
|
|
557
|
+
t.equal(stale?.details, ['Found: two folders from a sandbox that did not close', 'Fix: stop the sandbox', 'run-20261012-0931'], 'a repair row lists what was found as a Found: line, then the fix and the paths (ids stay plain)');
|
|
558
|
+
t.equal(stale?.description, 'they take space', 'a repair row with a meaning shows it as the description');
|
|
559
|
+
const net = repairItems[1];
|
|
560
|
+
t.equal(net?.details, ['Found: no answer', 'Fix: check your VPN'], 'a repair row without a meaning still says Found:');
|
|
561
|
+
t.equal(net?.description, undefined, 'without a meaning, the found text is not repeated as the description');
|
|
527
562
|
}
|
|
528
563
|
export const healthSuite = {
|
|
529
564
|
name: 'health',
|
|
@@ -90,6 +90,36 @@ export const liveTerminalSuite = {
|
|
|
90
90
|
t.check(!errors, 'ptyhost and the stand-in agent compile', errors);
|
|
91
91
|
if (errors)
|
|
92
92
|
return;
|
|
93
|
+
t.section('what a program leaves running ends with it (no leftover processes)');
|
|
94
|
+
{
|
|
95
|
+
// A copy of PING with a name of its own, started detached and hidden (its own console, so closing the pseudo
|
|
96
|
+
// console does not end it), by a program that then exits.
|
|
97
|
+
const probe = t.at('leftover-probe.exe');
|
|
98
|
+
fs.copyFileSync(path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'PING.EXE'), probe);
|
|
99
|
+
const starter = '"' +
|
|
100
|
+
SYSTEM.powershell() +
|
|
101
|
+
'" -NoProfile -Command "Start-Process -WindowStyle Hidden -FilePath \'' +
|
|
102
|
+
probe +
|
|
103
|
+
"' -ArgumentList '-n','120','127.0.0.1'; Start-Sleep -Seconds 5\"";
|
|
104
|
+
const host = spawn(ptyhost, [String(await freePort()), 'tok', '80', '24', t.at('leftover.log'), starter], {
|
|
105
|
+
env: { ...process.env, PTYHOST_BIND: 'loopback' },
|
|
106
|
+
stdio: 'ignore',
|
|
107
|
+
windowsHide: true,
|
|
108
|
+
});
|
|
109
|
+
const probeRunning = () => spawnSync(SYSTEM.tasklist(), ['/FI', 'IMAGENAME eq leftover-probe.exe', '/NH'], { encoding: 'utf8' })
|
|
110
|
+
.stdout.toLowerCase()
|
|
111
|
+
.includes('leftover-probe.exe');
|
|
112
|
+
await t.waitFor(probeRunning, WAIT.screen);
|
|
113
|
+
t.check(probeRunning(), 'the program started a background process of its own');
|
|
114
|
+
await t.waitFor(() => host.exitCode !== null, 30000);
|
|
115
|
+
await t.waitFor(() => !probeRunning(), 5000);
|
|
116
|
+
const log = fs.existsSync(t.at('leftover.log')) ? fs.readFileSync(t.at('leftover.log'), 'utf8') : '';
|
|
117
|
+
t.check(!probeRunning() && /ended [1-9]\d* process\(es\) the program left running/.test(log), 'when the program exits, the background process it left running is ended too', log.slice(-400));
|
|
118
|
+
if (probeRunning())
|
|
119
|
+
spawnSync(path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'taskkill.exe'), ['/F', '/IM', 'leftover-probe.exe'], {
|
|
120
|
+
stdio: 'ignore',
|
|
121
|
+
});
|
|
122
|
+
}
|
|
93
123
|
t.section("the agents' job: a last-resort backstop, set once");
|
|
94
124
|
{
|
|
95
125
|
// A job of the test's own (PTYHOST_JOB_NAME), never the agents' real one.
|
|
@@ -575,6 +575,8 @@ function previewRpc(ctx, state) {
|
|
|
575
575
|
return { message: 'Done. Restart Windows to finish. (preview: nothing was changed)' };
|
|
576
576
|
case 'engine.status':
|
|
577
577
|
return sampleEngines();
|
|
578
|
+
case 'engine.leftovers':
|
|
579
|
+
return { available: true, message: 'No HCS systems of yours are running.', systems: [] };
|
|
578
580
|
case 'logs.list':
|
|
579
581
|
return sampleLogs();
|
|
580
582
|
case 'logs.tail':
|
|
@@ -789,6 +791,7 @@ function previewRpc(ctx, state) {
|
|
|
789
791
|
'logs.list': 'query',
|
|
790
792
|
'logs.tail': 'query',
|
|
791
793
|
'engine.status': 'query',
|
|
794
|
+
'engine.leftovers': 'query',
|
|
792
795
|
};
|
|
793
796
|
return {
|
|
794
797
|
kind: (id) => kinds[id],
|
|
@@ -245,6 +245,39 @@ export const webSuite = {
|
|
|
245
245
|
await new Promise((r) => setTimeout(r, 300));
|
|
246
246
|
t.check(hub.size === 0 && reads === stopped, 'with nobody watching, nothing is read');
|
|
247
247
|
}
|
|
248
|
+
t.section('an unused server ends by itself (no process left behind)');
|
|
249
|
+
{
|
|
250
|
+
const idleServer = async (sandbox, limits) => {
|
|
251
|
+
let ended = false;
|
|
252
|
+
const idle = new WebServer({
|
|
253
|
+
port: 0,
|
|
254
|
+
token: TOKEN,
|
|
255
|
+
backend: {
|
|
256
|
+
info: () => Promise.resolve({
|
|
257
|
+
sandbox,
|
|
258
|
+
owner: null,
|
|
259
|
+
canUse: true,
|
|
260
|
+
harnesses: [],
|
|
261
|
+
defaultHarness: 'claude',
|
|
262
|
+
projects: [],
|
|
263
|
+
}),
|
|
264
|
+
sessions: () => Promise.resolve([]),
|
|
265
|
+
endpoint: () => null,
|
|
266
|
+
},
|
|
267
|
+
staticRoot: t.dir('idle-static-' + sandbox + limits.exit),
|
|
268
|
+
idleExitMs: limits.exit,
|
|
269
|
+
idleNoSandboxMs: limits.quiet,
|
|
270
|
+
onStop: () => (ended = true),
|
|
271
|
+
});
|
|
272
|
+
await idle.start();
|
|
273
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
274
|
+
await idle.stop();
|
|
275
|
+
return ended;
|
|
276
|
+
};
|
|
277
|
+
t.check(await idleServer('stopped', { exit: 60_000, quiet: 200 }), 'no browser and no sandbox: it ends after the short wait');
|
|
278
|
+
t.check(!(await idleServer('running', { exit: 60_000, quiet: 200 })), 'a sandbox runs: the short wait does not end it');
|
|
279
|
+
t.check(await idleServer('running', { exit: 300, quiet: 200 }), 'no browser for the long wait: it ends even while a sandbox runs');
|
|
280
|
+
}
|
|
248
281
|
t.section('the server');
|
|
249
282
|
const fake = new FakePtyHost();
|
|
250
283
|
await fake.start();
|
|
@@ -4,7 +4,6 @@ export interface Endpoint {
|
|
|
4
4
|
port: number;
|
|
5
5
|
token: string;
|
|
6
6
|
}
|
|
7
|
-
/** Tries every address the sandbox published and returns the first one that answers. */
|
|
8
7
|
export declare function connect(endpoint: Endpoint, timeoutMs?: number): Promise<net.Socket>;
|
|
9
8
|
/** Sends one control frame (e.g. RESTART) without taking over the screen. */
|
|
10
9
|
export declare function sendControl(endpoint: Endpoint, type: number): Promise<void>;
|
|
@@ -10,6 +10,9 @@ const connectOnce = (host, port, timeoutMs) => {
|
|
|
10
10
|
socket.once('connect', () => {
|
|
11
11
|
clearTimeout(timer);
|
|
12
12
|
socket.setNoDelay(true);
|
|
13
|
+
// A sandbox that vanished (closed hard, ended in Task Manager) never closes the connection: keepalive notices
|
|
14
|
+
// within seconds, so an attached terminal or the web's bridge ends instead of hanging on.
|
|
15
|
+
socket.setKeepAlive(true, KEEPALIVE_IDLE_MS);
|
|
13
16
|
resolve(socket);
|
|
14
17
|
});
|
|
15
18
|
socket.once('error', (e) => {
|
|
@@ -19,6 +22,8 @@ const connectOnce = (host, port, timeoutMs) => {
|
|
|
19
22
|
});
|
|
20
23
|
};
|
|
21
24
|
/** Tries every address the sandbox published and returns the first one that answers. */
|
|
25
|
+
/** Idle time before TCP keepalive probes start (the same as ptyhost's own side). */
|
|
26
|
+
const KEEPALIVE_IDLE_MS = 10_000;
|
|
22
27
|
export async function connect(endpoint, timeoutMs = 3000) {
|
|
23
28
|
const errors = [];
|
|
24
29
|
for (const address of endpoint.addresses) {
|
package/dist/web/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
6
|
<title>Sandbox</title>
|
|
7
7
|
<link rel="icon" href="data:," />
|
|
8
|
-
<link rel="stylesheet" href="/main-
|
|
9
|
-
<script type="module" src="/main-
|
|
8
|
+
<link rel="stylesheet" href="/main-RQFN4WBY.css" />
|
|
9
|
+
<script type="module" src="/main-QL7KHCEE.js"></script>
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|