windows-development-sandbox 1.11.3 → 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/data/data.def.d.ts +1 -1
- package/dist/app/features/data/data.def.js +41 -48
- package/dist/app/features/defs.d.ts +7 -0
- package/dist/app/features/defs.js +7 -3
- package/dist/app/features/health/health.def.js +28 -9
- package/dist/app/features/sandbox/sandbox.def.js +4 -4
- package/dist/app/features/storage/storage.def.js +8 -13
- package/dist/app/features/updates/updates.def.js +2 -3
- package/dist/guest/toolchains/tools/cli.js +19 -0
- package/dist/host/life/host-memory.d.ts +18 -0
- package/dist/host/life/host-memory.js +66 -0
- 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 +16 -1
- package/dist/runtime/main.js +16 -1
- package/dist/runtime/view.js +2 -0
- package/dist/selftest/app.test.js +21 -9
- 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/sync/watcher.d.ts +2 -0
- package/dist/sync/watcher.js +7 -2
- package/dist/web/index.html +2 -2
- package/dist/web/{main-Z645W45E.js → main-QL7KHCEE.js} +76 -76
- package/dist/web/{main-6JF3ASB5.css → main-RQFN4WBY.css} +1 -1
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/MemoryStatus.cs +58 -2
- package/native/csharp/HostTools/Program.cs +7 -1
- 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
|
};
|
|
@@ -113,7 +113,7 @@ export declare const pruneStorage: import("../../kernel/define.js").OperationDef
|
|
|
113
113
|
message: z.ZodString;
|
|
114
114
|
removed: z.ZodBoolean;
|
|
115
115
|
}, z.core.$strip>, "data.prune">;
|
|
116
|
-
/** The data page: disks, then one
|
|
116
|
+
/** The data page: the disks, then every kept folder in one table (its total as the summary, clean-up as its action). */
|
|
117
117
|
export declare function dataPage(o: DataShow): Page;
|
|
118
118
|
export declare const dataView: import("../../kernel/define.js").ViewDef<{
|
|
119
119
|
data: import("../../kernel/define.js").QueryDef<z.ZodObject<{}, z.core.$strip>, z.ZodObject<{
|
|
@@ -103,7 +103,7 @@ export const pruneStorage = defineOperation({
|
|
|
103
103
|
},
|
|
104
104
|
});
|
|
105
105
|
const DAYS_FIELD = fieldOf(new SchemaNode(PruneInputSchema).child('days'), ['days']);
|
|
106
|
-
/** The data page: disks, then one
|
|
106
|
+
/** The data page: the disks, then every kept folder in one table (its total as the summary, clean-up as its action). */
|
|
107
107
|
export function dataPage(o) {
|
|
108
108
|
const disks = {
|
|
109
109
|
id: 'disks',
|
|
@@ -132,44 +132,44 @@ export function dataPage(o) {
|
|
|
132
132
|
})),
|
|
133
133
|
notes: [{ text: 'Persistent data for the sandbox: ' + o.dataPath + ' (C:\\AgentData inside)' }],
|
|
134
134
|
};
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
facts: [{ id: 'total', label: 'Total', value: o.total }],
|
|
135
|
+
// Every folder in one table, grouped by kind (the order of DATA_AREAS), biggest first within a kind.
|
|
136
|
+
const kindRank = (area) => DATA_AREAS.indexOf(area);
|
|
137
|
+
const rows = [...o.entries].sort((a, b) => kindRank(a.area) - kindRank(b.area) || b.bytes - a.bytes);
|
|
138
|
+
const kept = {
|
|
139
|
+
id: 'kept',
|
|
140
|
+
title: 'What the sandbox keeps',
|
|
141
|
+
summary: o.total + ' in all',
|
|
142
|
+
columns: [
|
|
143
|
+
{ key: 'kind', label: 'KIND' },
|
|
144
|
+
{ key: 'name', label: 'NAME' },
|
|
145
|
+
{ key: 'size', label: 'SIZE' },
|
|
146
|
+
{ key: 'folder', label: 'FOLDER' },
|
|
147
|
+
],
|
|
148
|
+
items: rows.map((e) => {
|
|
149
|
+
const del = e.target
|
|
150
|
+
? action(cleanData, { targets: [e.target] }, { label: 'Delete', key: 'x', verb: 'delete' })
|
|
151
|
+
: undefined;
|
|
152
|
+
return {
|
|
153
|
+
id: 'entry:' + e.name,
|
|
154
|
+
label: e.name,
|
|
155
|
+
hint: e.size,
|
|
156
|
+
cells: { kind: AREA_LABEL[e.area], name: e.name, size: e.size, folder: e.path },
|
|
157
|
+
...(del ? { primary: del } : {}),
|
|
158
|
+
};
|
|
159
|
+
}),
|
|
160
|
+
actions: [
|
|
161
|
+
action(pruneStorage, {}, {
|
|
162
|
+
label: 'Clean up…',
|
|
163
|
+
verb: 'choose what',
|
|
164
|
+
description: 'Delete caches, old sandboxes and other leftovers (the clean command, with its options).',
|
|
165
|
+
fields: [DAYS_FIELD],
|
|
166
|
+
}),
|
|
167
|
+
],
|
|
169
168
|
notes: [
|
|
170
169
|
...(o.entries.length
|
|
171
170
|
? []
|
|
172
171
|
: [{ text: 'Nothing stored yet. Toolchains, databases and agent logins appear here after the first start.' }]),
|
|
172
|
+
...keptNote(o),
|
|
173
173
|
{
|
|
174
174
|
text: 'Free space with: sandbox data clean <target...> (e.g. tools/go, databases/postgres-18, logs, launches)',
|
|
175
175
|
tone: 'muted',
|
|
@@ -177,19 +177,12 @@ export function dataPage(o) {
|
|
|
177
177
|
},
|
|
178
178
|
],
|
|
179
179
|
};
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
verb: 'choose what',
|
|
187
|
-
description: 'Delete caches, old sandboxes and other leftovers (the clean command, with its options).',
|
|
188
|
-
fields: [DAYS_FIELD],
|
|
189
|
-
}),
|
|
190
|
-
],
|
|
191
|
-
};
|
|
192
|
-
return { title: 'Data', sections: [disks, ...areas, total, clean] };
|
|
180
|
+
return { title: 'Data', sections: [disks, kept] };
|
|
181
|
+
}
|
|
182
|
+
/** The folders kept on purpose (not deleted here), named in one note. */
|
|
183
|
+
function keptNote(o) {
|
|
184
|
+
const names = o.entries.filter((e) => !e.target).map((e) => e.name);
|
|
185
|
+
return names.length ? [{ text: 'Kept on this PC on purpose, not deleted here: ' + names.join(', ') + '.' }] : [];
|
|
193
186
|
}
|
|
194
187
|
export const dataView = defineView({
|
|
195
188
|
id: 'data',
|
|
@@ -1262,9 +1262,16 @@ export interface NavSection {
|
|
|
1262
1262
|
readonly elsewhere?: string;
|
|
1263
1263
|
/** Entries under this one (Sandbox: Status, Projects). */
|
|
1264
1264
|
readonly children?: readonly NavSection[];
|
|
1265
|
+
/** Views shown as more categories inside this entry's page (Settings: VPN, GitHub), not as entries of their own. */
|
|
1266
|
+
readonly embeds?: readonly {
|
|
1267
|
+
readonly label: string;
|
|
1268
|
+
readonly view: ViewId;
|
|
1269
|
+
}[];
|
|
1265
1270
|
}
|
|
1266
1271
|
/** Every entry of the navigation, children included, in order. */
|
|
1267
1272
|
export declare function navSections(nav: readonly NavSection[]): NavSection[];
|
|
1268
1273
|
/** Every view the web's navigation opens. */
|
|
1269
1274
|
export declare function navViews(nav: readonly NavSection[]): ViewId[];
|
|
1275
|
+
/** The entry whose page shows this view as one of its categories (VPN: Settings), if any. */
|
|
1276
|
+
export declare function embeddingOf(nav: readonly NavSection[], view: string): NavSection | undefined;
|
|
1270
1277
|
export declare const NAV: readonly NavSection[];
|
|
@@ -125,7 +125,11 @@ export function navSections(nav) {
|
|
|
125
125
|
}
|
|
126
126
|
/** Every view the web's navigation opens. */
|
|
127
127
|
export function navViews(nav) {
|
|
128
|
-
return navSections(nav).flatMap((n) => (n.view ? [n.view] : []));
|
|
128
|
+
return navSections(nav).flatMap((n) => [...(n.view ? [n.view] : []), ...(n.embeds ?? []).map((e) => e.view)]);
|
|
129
|
+
}
|
|
130
|
+
/** The entry whose page shows this view as one of its categories (VPN: Settings), if any. */
|
|
131
|
+
export function embeddingOf(nav, view) {
|
|
132
|
+
return navSections(nav).find((n) => n.embeds?.some((e) => e.view === view));
|
|
129
133
|
}
|
|
130
134
|
export const NAV = [
|
|
131
135
|
{ label: 'Agents', view: 'agents' },
|
|
@@ -140,8 +144,8 @@ export const NAV = [
|
|
|
140
144
|
{ label: 'Apps', view: 'apps' },
|
|
141
145
|
{
|
|
142
146
|
label: 'Settings',
|
|
143
|
-
|
|
144
|
-
|
|
147
|
+
view: 'settings',
|
|
148
|
+
embeds: [
|
|
145
149
|
{ label: 'VPN', view: 'vpn' },
|
|
146
150
|
{ label: 'GitHub', view: 'github' },
|
|
147
151
|
],
|
|
@@ -165,22 +165,35 @@ export const enableSandbox = defineOperation({
|
|
|
165
165
|
},
|
|
166
166
|
},
|
|
167
167
|
});
|
|
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
|
+
*/
|
|
168
172
|
function checkItems(last) {
|
|
169
173
|
return last.checks.map((r, i) => {
|
|
170
|
-
const
|
|
174
|
+
const found = r.detail + (r.timedOut ? ' (took too long)' : '');
|
|
175
|
+
const rest = resultLines(r)
|
|
171
176
|
.slice(1)
|
|
172
|
-
.map((l) => l.text.trim())
|
|
177
|
+
.map((l) => l.text.trim())
|
|
178
|
+
.filter((l) => !l.startsWith('What it means: '));
|
|
179
|
+
const details = r.status === 'ok' ? [] : ['Found: ' + found, ...rest];
|
|
173
180
|
return {
|
|
174
181
|
id: 'repair:' + i,
|
|
175
182
|
label: r.name,
|
|
176
183
|
hint: BADGE[r.status],
|
|
177
184
|
...(TONE[r.status] ? { tone: TONE[r.status] } : {}),
|
|
178
185
|
badges: [{ text: BADGE[r.status], ...(TONE[r.status] ? { tone: TONE[r.status] } : {}) }],
|
|
179
|
-
description: r.
|
|
180
|
-
...(
|
|
186
|
+
description: r.means ?? (details.length ? undefined : found),
|
|
187
|
+
...(details.length ? { details } : {}),
|
|
181
188
|
};
|
|
182
189
|
});
|
|
183
190
|
}
|
|
191
|
+
/** A "Set up this PC" row: the real value is the hint; OK or action needed is the tone and the badge. */
|
|
192
|
+
const SETUP_BADGE = {
|
|
193
|
+
ok: 'OK',
|
|
194
|
+
warn: 'WARNING',
|
|
195
|
+
action: 'NEEDS YOU',
|
|
196
|
+
};
|
|
184
197
|
/** The health page: before any check, the two things to do; after one, what it found. Pure. */
|
|
185
198
|
export function healthPage(o) {
|
|
186
199
|
const last = o.last;
|
|
@@ -223,8 +236,8 @@ export function healthPage(o) {
|
|
|
223
236
|
const sections = [
|
|
224
237
|
{
|
|
225
238
|
id: 'status',
|
|
226
|
-
summary: (last.mode === 'repair' ? '
|
|
227
|
-
|
|
239
|
+
summary: (last.mode === 'repair' ? 'Last repair ' : 'Last check ') + ago(last.at),
|
|
240
|
+
...(last.log ? { notes: [{ text: 'Log: ' + last.log, tone: 'muted' }] } : {}),
|
|
228
241
|
},
|
|
229
242
|
{
|
|
230
243
|
id: 'doctor',
|
|
@@ -232,8 +245,9 @@ export function healthPage(o) {
|
|
|
232
245
|
items: last.details.map(([name, detail], i) => ({
|
|
233
246
|
id: 'doctor:' + i,
|
|
234
247
|
label: name,
|
|
248
|
+
// The hint is the whole value; only a long one is cut, and then the description keeps all of it.
|
|
235
249
|
hint: cut(detail, 50),
|
|
236
|
-
|
|
250
|
+
...(detail.length > 50 ? { description: detail } : {}),
|
|
237
251
|
})),
|
|
238
252
|
},
|
|
239
253
|
{
|
|
@@ -252,9 +266,14 @@ export function healthPage(o) {
|
|
|
252
266
|
: setup.checks.map((c, i) => ({
|
|
253
267
|
id: 'setup:' + i,
|
|
254
268
|
label: c.name,
|
|
255
|
-
hint: c.
|
|
269
|
+
hint: c.detail,
|
|
256
270
|
tone: c.status === 'ok' ? 'ok' : c.status === 'warn' ? 'warn' : 'danger',
|
|
257
|
-
|
|
271
|
+
badges: [
|
|
272
|
+
{
|
|
273
|
+
text: SETUP_BADGE[c.status],
|
|
274
|
+
tone: c.status === 'ok' ? 'ok' : c.status === 'warn' ? 'warn' : 'danger',
|
|
275
|
+
},
|
|
276
|
+
],
|
|
258
277
|
})),
|
|
259
278
|
notes: canEnable
|
|
260
279
|
? [{ text: 'Turn Windows Sandbox on from the menu or with sandbox setup.', tone: 'muted', only: 'web' }]
|
|
@@ -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
|
}
|
|
@@ -160,22 +160,17 @@ export const containedOff = defineOperation({
|
|
|
160
160
|
});
|
|
161
161
|
/** The Storage page: the data folder (change, move, use another), then contained storage (beta). */
|
|
162
162
|
export function storagePage(o, c) {
|
|
163
|
-
|
|
164
|
-
id: '@data-folder',
|
|
165
|
-
label: 'Data folder',
|
|
166
|
-
hint: o.root,
|
|
167
|
-
description: 'Where your tools, logins, history and logs are kept between sandboxes. Press Enter to move them to another folder or drive, or to use a different folder.',
|
|
168
|
-
details: [
|
|
169
|
-
o.size,
|
|
170
|
-
'Default folder: ' + o.defaultRoot + (o.defaultInUse ? ' (in use)' : ''),
|
|
171
|
-
...o.otherFoldersWithData.map((f) => 'Another folder with data: ' + f),
|
|
172
|
-
],
|
|
173
|
-
primary: action(changeDataFolder, {}, { label: 'Data folder', verb: 'change' }),
|
|
174
|
-
};
|
|
163
|
+
// Move and use another folder are the two ways to change it (the same choice as the Data folder setting).
|
|
175
164
|
const data = {
|
|
176
165
|
id: 'folder',
|
|
177
166
|
title: 'Data folder',
|
|
178
|
-
|
|
167
|
+
description: 'Where your tools, logins, history and logs are kept between sandboxes.',
|
|
168
|
+
facts: [
|
|
169
|
+
{ id: 'folder', label: 'Folder', value: o.root },
|
|
170
|
+
{ id: 'size', label: 'Size', value: o.size },
|
|
171
|
+
{ id: 'default', label: 'Default folder', value: o.defaultRoot + (o.defaultInUse ? ' (in use)' : '') },
|
|
172
|
+
...o.otherFoldersWithData.map((f, i) => ({ id: 'other:' + i, label: 'Other folder with data', value: f })),
|
|
173
|
+
],
|
|
179
174
|
actions: [action(moveStorage, {}), action(useStorage, {})],
|
|
180
175
|
};
|
|
181
176
|
const contained = {
|
|
@@ -159,7 +159,6 @@ export function updatesPage(s, latest) {
|
|
|
159
159
|
hint: 'off',
|
|
160
160
|
tone: 'warn',
|
|
161
161
|
description: s.autoOffText,
|
|
162
|
-
details: [s.autoOffText],
|
|
163
162
|
}),
|
|
164
163
|
]
|
|
165
164
|
: []),
|
|
@@ -178,8 +177,8 @@ export function updatesPage(s, latest) {
|
|
|
178
177
|
row('failed', 'Last failed update', {
|
|
179
178
|
hint: s.failed.version,
|
|
180
179
|
tone: 'danger',
|
|
181
|
-
description:
|
|
182
|
-
details: [s.failed.reason
|
|
180
|
+
description: 'It is not tried again until a newer version is released.',
|
|
181
|
+
details: [s.failed.reason],
|
|
183
182
|
}),
|
|
184
183
|
]
|
|
185
184
|
: []),
|
|
@@ -30,6 +30,22 @@ export function ghExePath() {
|
|
|
30
30
|
const dir = tool('cli', 'gh');
|
|
31
31
|
return path.join(entry?.subdir ? path.join(dir, entry.subdir) : dir, entry?.exe ?? 'gh.exe');
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* gh keeps its token in Windows Credential Manager unless told otherwise, and that is wiped with the sandbox: only a
|
|
35
|
+
* login made with --insecure-storage (the token in GH_CONFIG_DIR, which persists) is still there next time.
|
|
36
|
+
*/
|
|
37
|
+
const GH_NOTE = ' (log in once with: gh auth login --web --insecure-storage; it is kept between sandboxes)';
|
|
38
|
+
/** A saved gh account whose token was in Credential Manager (gone now): say how to log in so that it lasts. */
|
|
39
|
+
function warnKeyringLogin() {
|
|
40
|
+
try {
|
|
41
|
+
const hosts = fs.readFileSync(path.join(ghConfigDir(), 'hosts.yml'), 'utf8');
|
|
42
|
+
if (/^\s*user:/m.test(hosts) && !/oauth_token:/.test(hosts))
|
|
43
|
+
warn('gh: the GitHub login from an earlier sandbox was kept in Windows Credential Manager, which is wiped with each sandbox. Log in again with: gh auth login --web --insecure-storage (then it lasts).');
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// No saved login: nothing to say.
|
|
47
|
+
}
|
|
48
|
+
}
|
|
33
49
|
const toolVars = {
|
|
34
50
|
gh: () => ({
|
|
35
51
|
GH_CONFIG_DIR: exists(deliveredGhConfigDir()) ? deliveredGhConfigDir() : mkdirp(ghConfigDir()),
|
|
@@ -85,8 +101,11 @@ export async function setupCli(env, settings, gitAccess, refresh = false) {
|
|
|
85
101
|
refresh,
|
|
86
102
|
subdir: entry.subdir,
|
|
87
103
|
size: entry.size,
|
|
104
|
+
...(name === 'gh' ? { note: GH_NOTE } : {}),
|
|
88
105
|
});
|
|
89
106
|
Object.assign(env.vars, toolVars[name]?.() ?? {});
|
|
107
|
+
if (name === 'gh')
|
|
108
|
+
warnKeyringLogin();
|
|
90
109
|
}
|
|
91
110
|
catch (e) {
|
|
92
111
|
// One tool failing never takes the others down.
|
|
@@ -17,6 +17,24 @@ export type HostMemory = z.output<typeof HostMemorySchema>;
|
|
|
17
17
|
export declare function parseHostMemory(text: string): HostMemory | null;
|
|
18
18
|
/** This PC's memory now, or null when it cannot be read (and always during the self-test: it never asks Windows). */
|
|
19
19
|
export declare function readHostMemory(storageRoot: string): HostMemory | null;
|
|
20
|
+
/**
|
|
21
|
+
* One long-running `HostTools memory-status --serve` for the watcher: a reading every `seconds`, instead of starting a
|
|
22
|
+
* program (and building a disk counter) for each one. Without HostTools.exe it does nothing and `latest()` stays null.
|
|
23
|
+
*/
|
|
24
|
+
export declare class HostMemoryStream {
|
|
25
|
+
private readonly storageRoot;
|
|
26
|
+
private readonly seconds;
|
|
27
|
+
private child;
|
|
28
|
+
private last;
|
|
29
|
+
private startedAt;
|
|
30
|
+
private stopped;
|
|
31
|
+
constructor(storageRoot: string, seconds: number);
|
|
32
|
+
start(): void;
|
|
33
|
+
/** The newest reading while it is fresh; starts the stream again when it ended (at most once a minute). */
|
|
34
|
+
latest(): HostMemory | null;
|
|
35
|
+
/** Ends it (closing its input ends HostTools). */
|
|
36
|
+
stop(): void;
|
|
37
|
+
}
|
|
20
38
|
/** The same without blocking: for the watcher, which must never wait on it. */
|
|
21
39
|
export declare function readHostMemoryAsync(storageRoot: string): Promise<HostMemory | null>;
|
|
22
40
|
/** How low this PC's memory is: what the host watchdog acts on (pure). */
|
|
@@ -47,6 +47,72 @@ export function readHostMemory(storageRoot) {
|
|
|
47
47
|
const result = spawnSync(file, args, { encoding: 'utf8', windowsHide: true, timeout: TIMEOUT_MS });
|
|
48
48
|
return parseHostMemory(result.stdout || '');
|
|
49
49
|
}
|
|
50
|
+
/** A reading older than this is not used (the stream stopped answering): the watcher asks once instead. */
|
|
51
|
+
const STREAM_FRESH_MS = 30000;
|
|
52
|
+
/** A stream that ended is started again at most this often. */
|
|
53
|
+
const STREAM_RESTART_MS = 60000;
|
|
54
|
+
/**
|
|
55
|
+
* One long-running `HostTools memory-status --serve` for the watcher: a reading every `seconds`, instead of starting a
|
|
56
|
+
* program (and building a disk counter) for each one. Without HostTools.exe it does nothing and `latest()` stays null.
|
|
57
|
+
*/
|
|
58
|
+
export class HostMemoryStream {
|
|
59
|
+
storageRoot;
|
|
60
|
+
seconds;
|
|
61
|
+
child = null;
|
|
62
|
+
last = null;
|
|
63
|
+
startedAt = 0;
|
|
64
|
+
stopped = false;
|
|
65
|
+
constructor(storageRoot, seconds) {
|
|
66
|
+
this.storageRoot = storageRoot;
|
|
67
|
+
this.seconds = seconds;
|
|
68
|
+
}
|
|
69
|
+
start() {
|
|
70
|
+
if (!isWin || selfTestRunning() || this.stopped || this.child)
|
|
71
|
+
return;
|
|
72
|
+
const tools = hostToolsExe(this.storageRoot);
|
|
73
|
+
if (!tools)
|
|
74
|
+
return;
|
|
75
|
+
this.startedAt = Date.now();
|
|
76
|
+
try {
|
|
77
|
+
const child = spawn(tools, ['memory-status', '--serve', String(this.seconds)], {
|
|
78
|
+
windowsHide: true,
|
|
79
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
80
|
+
});
|
|
81
|
+
this.child = child;
|
|
82
|
+
let buffer = '';
|
|
83
|
+
child.stdout.setEncoding('utf8');
|
|
84
|
+
child.stdout.on('data', (chunk) => {
|
|
85
|
+
buffer = (buffer + chunk).slice(-16384);
|
|
86
|
+
let end;
|
|
87
|
+
while ((end = buffer.indexOf('\n')) >= 0) {
|
|
88
|
+
const memory = parseHostMemory(buffer.slice(0, end).trim());
|
|
89
|
+
buffer = buffer.slice(end + 1);
|
|
90
|
+
if (memory)
|
|
91
|
+
this.last = { at: Date.now(), memory };
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
child.stdin.on('error', () => undefined);
|
|
95
|
+
child.on('error', () => (this.child = null));
|
|
96
|
+
child.on('exit', () => (this.child = null));
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// Could not start it: the watcher asks the old way.
|
|
100
|
+
this.child = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/** The newest reading while it is fresh; starts the stream again when it ended (at most once a minute). */
|
|
104
|
+
latest() {
|
|
105
|
+
if (!this.child && Date.now() - this.startedAt >= STREAM_RESTART_MS)
|
|
106
|
+
this.start();
|
|
107
|
+
return this.last && Date.now() - this.last.at < STREAM_FRESH_MS ? this.last.memory : null;
|
|
108
|
+
}
|
|
109
|
+
/** Ends it (closing its input ends HostTools). */
|
|
110
|
+
stop() {
|
|
111
|
+
this.stopped = true;
|
|
112
|
+
this.child?.stdin?.end();
|
|
113
|
+
this.child = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
50
116
|
/** The same without blocking: for the watcher, which must never wait on it. */
|
|
51
117
|
export function readHostMemoryAsync(storageRoot) {
|
|
52
118
|
if (!isWin || selfTestRunning())
|
|
@@ -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
|
@@ -29978,6 +29978,17 @@ function ghConfigDir() {
|
|
|
29978
29978
|
function deliveredGhConfigDir() {
|
|
29979
29979
|
return path45.join(localDir, "gh-token");
|
|
29980
29980
|
}
|
|
29981
|
+
var GH_NOTE = " (log in once with: gh auth login --web --insecure-storage; it is kept between sandboxes)";
|
|
29982
|
+
function warnKeyringLogin() {
|
|
29983
|
+
try {
|
|
29984
|
+
const hosts = fs35.readFileSync(path45.join(ghConfigDir(), "hosts.yml"), "utf8");
|
|
29985
|
+
if (/^\s*user:/m.test(hosts) && !/oauth_token:/.test(hosts))
|
|
29986
|
+
warn(
|
|
29987
|
+
"gh: the GitHub login from an earlier sandbox was kept in Windows Credential Manager, which is wiped with each sandbox. Log in again with: gh auth login --web --insecure-storage (then it lasts)."
|
|
29988
|
+
);
|
|
29989
|
+
} catch {
|
|
29990
|
+
}
|
|
29991
|
+
}
|
|
29981
29992
|
var toolVars = {
|
|
29982
29993
|
gh: () => ({
|
|
29983
29994
|
GH_CONFIG_DIR: exists(deliveredGhConfigDir()) ? deliveredGhConfigDir() : mkdirp(ghConfigDir()),
|
|
@@ -30018,9 +30029,11 @@ async function setupCli(env, settings, gitAccess, refresh = false) {
|
|
|
30018
30029
|
await setupSingleExe(env, name, entry.repo, entry.asset(arch), entry.exe, tool("cli", name), {
|
|
30019
30030
|
refresh,
|
|
30020
30031
|
subdir: entry.subdir,
|
|
30021
|
-
size: entry.size
|
|
30032
|
+
size: entry.size,
|
|
30033
|
+
...name === "gh" ? { note: GH_NOTE } : {}
|
|
30022
30034
|
});
|
|
30023
30035
|
Object.assign(env.vars, toolVars[name]?.() ?? {});
|
|
30036
|
+
if (name === "gh") warnKeyringLogin();
|
|
30024
30037
|
} catch (e) {
|
|
30025
30038
|
warn(name + " could not be set up and is skipped; the other tools are not affected. Reason: " + errMsg(e));
|
|
30026
30039
|
env.summary.push(entry.name + ": FAILED");
|
|
@@ -32050,6 +32063,7 @@ var connectOnce = (host, port, timeoutMs) => {
|
|
|
32050
32063
|
socket.once("connect", () => {
|
|
32051
32064
|
clearTimeout(timer);
|
|
32052
32065
|
socket.setNoDelay(true);
|
|
32066
|
+
socket.setKeepAlive(true, KEEPALIVE_IDLE_MS);
|
|
32053
32067
|
resolve2(socket);
|
|
32054
32068
|
});
|
|
32055
32069
|
socket.once("error", (e) => {
|
|
@@ -32058,6 +32072,7 @@ var connectOnce = (host, port, timeoutMs) => {
|
|
|
32058
32072
|
});
|
|
32059
32073
|
});
|
|
32060
32074
|
};
|
|
32075
|
+
var KEEPALIVE_IDLE_MS = 1e4;
|
|
32061
32076
|
async function connect(endpoint, timeoutMs = 3e3) {
|
|
32062
32077
|
const errors = [];
|
|
32063
32078
|
for (const address of endpoint.addresses) {
|