windows-development-sandbox 1.11.3 → 1.11.4
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/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 +25 -9
- 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/runtime/daemon.js +14 -1
- package/dist/runtime/main.js +14 -1
- package/dist/selftest/app.test.js +21 -9
- 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-6JF3ASB5.css → main-MU5XWZMS.css} +1 -1
- package/dist/web/{main-Z645W45E.js → main-SLQNJRXD.js} +72 -72
- 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
|
@@ -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,32 @@ export const enableSandbox = defineOperation({
|
|
|
165
165
|
},
|
|
166
166
|
},
|
|
167
167
|
});
|
|
168
|
+
/** A repair row: what it means is the description; what was found and the fix are the detail lines. */
|
|
168
169
|
function checkItems(last) {
|
|
169
170
|
return last.checks.map((r, i) => {
|
|
170
|
-
const
|
|
171
|
+
const found = r.detail + (r.timedOut ? ' (took too long)' : '');
|
|
172
|
+
const rest = resultLines(r)
|
|
171
173
|
.slice(1)
|
|
172
|
-
.map((l) => l.text.trim())
|
|
174
|
+
.map((l) => l.text.trim())
|
|
175
|
+
.filter((l) => !l.startsWith('What it means: '));
|
|
176
|
+
const details = r.means ? [found, ...rest] : rest;
|
|
173
177
|
return {
|
|
174
178
|
id: 'repair:' + i,
|
|
175
179
|
label: r.name,
|
|
176
180
|
hint: BADGE[r.status],
|
|
177
181
|
...(TONE[r.status] ? { tone: TONE[r.status] } : {}),
|
|
178
182
|
badges: [{ text: BADGE[r.status], ...(TONE[r.status] ? { tone: TONE[r.status] } : {}) }],
|
|
179
|
-
description: r.
|
|
180
|
-
...(
|
|
183
|
+
description: r.means ?? found,
|
|
184
|
+
...(details.length ? { details } : {}),
|
|
181
185
|
};
|
|
182
186
|
});
|
|
183
187
|
}
|
|
188
|
+
/** A "Set up this PC" row: the real value is the hint; OK or action needed is the tone and the badge. */
|
|
189
|
+
const SETUP_BADGE = {
|
|
190
|
+
ok: 'OK',
|
|
191
|
+
warn: 'WARNING',
|
|
192
|
+
action: 'NEEDS YOU',
|
|
193
|
+
};
|
|
184
194
|
/** The health page: before any check, the two things to do; after one, what it found. Pure. */
|
|
185
195
|
export function healthPage(o) {
|
|
186
196
|
const last = o.last;
|
|
@@ -223,8 +233,8 @@ export function healthPage(o) {
|
|
|
223
233
|
const sections = [
|
|
224
234
|
{
|
|
225
235
|
id: 'status',
|
|
226
|
-
summary: (last.mode === 'repair' ? '
|
|
227
|
-
|
|
236
|
+
summary: (last.mode === 'repair' ? 'Last repair ' : 'Last check ') + ago(last.at),
|
|
237
|
+
...(last.log ? { notes: [{ text: 'Log: ' + last.log, tone: 'muted' }] } : {}),
|
|
228
238
|
},
|
|
229
239
|
{
|
|
230
240
|
id: 'doctor',
|
|
@@ -232,8 +242,9 @@ export function healthPage(o) {
|
|
|
232
242
|
items: last.details.map(([name, detail], i) => ({
|
|
233
243
|
id: 'doctor:' + i,
|
|
234
244
|
label: name,
|
|
245
|
+
// The hint is the whole value; only a long one is cut, and then the description keeps all of it.
|
|
235
246
|
hint: cut(detail, 50),
|
|
236
|
-
|
|
247
|
+
...(detail.length > 50 ? { description: detail } : {}),
|
|
237
248
|
})),
|
|
238
249
|
},
|
|
239
250
|
{
|
|
@@ -252,9 +263,14 @@ export function healthPage(o) {
|
|
|
252
263
|
: setup.checks.map((c, i) => ({
|
|
253
264
|
id: 'setup:' + i,
|
|
254
265
|
label: c.name,
|
|
255
|
-
hint: c.
|
|
266
|
+
hint: c.detail,
|
|
256
267
|
tone: c.status === 'ok' ? 'ok' : c.status === 'warn' ? 'warn' : 'danger',
|
|
257
|
-
|
|
268
|
+
badges: [
|
|
269
|
+
{
|
|
270
|
+
text: SETUP_BADGE[c.status],
|
|
271
|
+
tone: c.status === 'ok' ? 'ok' : c.status === 'warn' ? 'warn' : 'danger',
|
|
272
|
+
},
|
|
273
|
+
],
|
|
258
274
|
})),
|
|
259
275
|
notes: canEnable
|
|
260
276
|
? [{ text: 'Turn Windows Sandbox on from the menu or with sandbox setup.', tone: 'muted', only: 'web' }]
|
|
@@ -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())
|
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");
|
package/dist/runtime/main.js
CHANGED
|
@@ -29266,6 +29266,17 @@ function ghExePath() {
|
|
|
29266
29266
|
const dir = tool("cli", "gh");
|
|
29267
29267
|
return path34.join(entry?.subdir ? path34.join(dir, entry.subdir) : dir, entry?.exe ?? "gh.exe");
|
|
29268
29268
|
}
|
|
29269
|
+
var GH_NOTE = " (log in once with: gh auth login --web --insecure-storage; it is kept between sandboxes)";
|
|
29270
|
+
function warnKeyringLogin() {
|
|
29271
|
+
try {
|
|
29272
|
+
const hosts = fs28.readFileSync(path34.join(ghConfigDir(), "hosts.yml"), "utf8");
|
|
29273
|
+
if (/^\s*user:/m.test(hosts) && !/oauth_token:/.test(hosts))
|
|
29274
|
+
warn(
|
|
29275
|
+
"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)."
|
|
29276
|
+
);
|
|
29277
|
+
} catch {
|
|
29278
|
+
}
|
|
29279
|
+
}
|
|
29269
29280
|
var toolVars = {
|
|
29270
29281
|
gh: () => ({
|
|
29271
29282
|
GH_CONFIG_DIR: exists(deliveredGhConfigDir()) ? deliveredGhConfigDir() : mkdirp(ghConfigDir()),
|
|
@@ -29306,9 +29317,11 @@ async function setupCli(env, settings, gitAccess, refresh = false) {
|
|
|
29306
29317
|
await setupSingleExe(env, name, entry.repo, entry.asset(arch), entry.exe, tool("cli", name), {
|
|
29307
29318
|
refresh,
|
|
29308
29319
|
subdir: entry.subdir,
|
|
29309
|
-
size: entry.size
|
|
29320
|
+
size: entry.size,
|
|
29321
|
+
...name === "gh" ? { note: GH_NOTE } : {}
|
|
29310
29322
|
});
|
|
29311
29323
|
Object.assign(env.vars, toolVars[name]?.() ?? {});
|
|
29324
|
+
if (name === "gh") warnKeyringLogin();
|
|
29312
29325
|
} catch (e) {
|
|
29313
29326
|
warn(name + " could not be set up and is skipped; the other tools are not affected. Reason: " + errMsg(e));
|
|
29314
29327
|
env.summary.push(entry.name + ": FAILED");
|
|
@@ -629,12 +629,18 @@ export const appSuite = {
|
|
|
629
629
|
const dataSample = dataPage(stored);
|
|
630
630
|
const dataSectionIds = dataSample.sections.map((s) => s.id);
|
|
631
631
|
const dataOps = pageActions(dataSample);
|
|
632
|
-
const
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
(
|
|
637
|
-
|
|
632
|
+
const keptSection = dataSample.sections.find((s) => s.id === 'kept');
|
|
633
|
+
const keptItems = keptSection?.items ?? [];
|
|
634
|
+
const accountsItem = keptItems.find((i) => i.label === 'accounts');
|
|
635
|
+
t.check(dataSectionIds.join() === 'disks,kept' &&
|
|
636
|
+
keptItems.map((i) => i.label).join() === 'rust,state/claude,accounts' &&
|
|
637
|
+
keptSection?.summary === '6.5 GB in all' &&
|
|
638
|
+
keptItems[0]?.primary?.kind === 'operation' &&
|
|
639
|
+
keptItems[0].primary.op === 'data.clean' &&
|
|
640
|
+
JSON.stringify(keptItems[0].primary.input) === '{"targets":["tools/rust"]}' &&
|
|
641
|
+
accountsItem?.primary === undefined &&
|
|
642
|
+
(keptSection?.actions ?? []).some((a) => a.kind === 'operation' && a.op === 'data.prune') &&
|
|
643
|
+
dataOps.some((a) => a.kind === 'operation' && a.op === 'data.clean'), 'data: disks, then one table of every kept folder (total in the summary); delete only where it may be deleted', dataSectionIds.join());
|
|
638
644
|
const shaped = showData.cli.shapeJson(stored);
|
|
639
645
|
const shapedEntries = shaped.entries;
|
|
640
646
|
const shapedDisks = shaped.disks;
|
|
@@ -644,6 +650,10 @@ export const appSuite = {
|
|
|
644
650
|
const dataLines = pageLines(dataSample);
|
|
645
651
|
t.check(dataLines.some((l) => l.startsWith('Disks')) &&
|
|
646
652
|
dataLines.some((l) => /^DRIVE\s+USED\s+FREE\s+SIZE/.test(l)) &&
|
|
653
|
+
dataLines.some((l) => l.startsWith('What the sandbox keeps 6.5 GB in all')) &&
|
|
654
|
+
dataLines.some((l) => /^KIND\s+NAME\s+SIZE\s+FOLDER/.test(l)) &&
|
|
655
|
+
dataLines.some((l) => /^Toolchains\s+rust\s+4\.6 GB\s+C:\\AgentData\\tools\\rust/.test(l)) &&
|
|
656
|
+
dataLines.some((l) => l.startsWith('Kept on this PC on purpose, not deleted here: accounts.')) &&
|
|
647
657
|
dataLines.some((l) => l.startsWith('Persistent data for the sandbox: ')) &&
|
|
648
658
|
dataLines.some((l) => l.startsWith('Free space with:')), 'data on the command line: the disks table, the data folder line and the free space line', dataLines.join('\n'));
|
|
649
659
|
t.check(definitionFor('data', ['clean'])?.id === 'data.clean' &&
|
|
@@ -837,10 +847,12 @@ export const appSuite = {
|
|
|
837
847
|
mountedNow: null,
|
|
838
848
|
};
|
|
839
849
|
const storagePageNow = storagePage(shownStorage, shownContained);
|
|
840
|
-
const
|
|
850
|
+
const folderSection = storagePageNow.sections[0];
|
|
851
|
+
const folderOps = (folderSection?.actions ?? []).map((a) => (a.kind === 'operation' ? a.op : a.kind));
|
|
841
852
|
const containedActions = storagePageNow.sections[1]?.actions ?? [];
|
|
842
|
-
t.check(
|
|
843
|
-
|
|
853
|
+
t.check(folderOps.join() === 'storage.move,storage.use' &&
|
|
854
|
+
(folderSection?.facts ?? []).map((f) => f.label).join() ===
|
|
855
|
+
'Folder,Size,Default folder,Other folder with data' &&
|
|
844
856
|
containedActions.length === 1 &&
|
|
845
857
|
containedActions[0].kind === 'operation' &&
|
|
846
858
|
containedActions[0].op === 'storage.containedOn', 'the storage page: the data folder row changes it, and contained storage offers to turn it on while it is off');
|
package/dist/sync/watcher.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export declare class Watcher {
|
|
|
23
23
|
private views;
|
|
24
24
|
private stops;
|
|
25
25
|
private watchdogTimer;
|
|
26
|
+
/** This PC's memory, read by one long-running HostTools instead of a program per sample. */
|
|
27
|
+
private memoryStream;
|
|
26
28
|
private watchdog;
|
|
27
29
|
private watchdogBusy;
|
|
28
30
|
private lastCpu;
|
package/dist/sync/watcher.js
CHANGED
|
@@ -13,7 +13,7 @@ import { readForwards, sandboxAddress, startPortForwards } from '../host/network
|
|
|
13
13
|
import { AuditCollector, readAuditSettings } from '../host/audit/collect.js';
|
|
14
14
|
import { readMarkedProjects, saveDataSize, watchAndMark } from '../host/guard/index.js';
|
|
15
15
|
import { sizeWithin } from '../core/disk.js';
|
|
16
|
-
import { autoRestartAllowed, causeSince, appendHostMemory, cpuPercentBetween, KeepAwake, LifeRecord, readHostMemoryAsync, readLoadSettings, REQUEST_SECONDS, WATCHDOG_START, watchdogStep, saveCrashReport, SessionLedger, } from '../host/life/index.js';
|
|
16
|
+
import { autoRestartAllowed, causeSince, appendHostMemory, cpuPercentBetween, KeepAwake, LifeRecord, HostMemoryStream, readHostMemoryAsync, readLoadSettings, REQUEST_SECONDS, WATCHDOG_START, watchdogStep, saveCrashReport, SessionLedger, } from '../host/life/index.js';
|
|
17
17
|
import { SandboxChannel } from '../host/channel/index.js';
|
|
18
18
|
import { ProjectRegistry } from '../host/projects/registry.js';
|
|
19
19
|
import { ContainedStorage, setNameOf, storageOf } from '../host/storage/index.js';
|
|
@@ -85,6 +85,8 @@ export class Watcher {
|
|
|
85
85
|
views = [];
|
|
86
86
|
stops = [];
|
|
87
87
|
watchdogTimer = null;
|
|
88
|
+
/** This PC's memory, read by one long-running HostTools instead of a program per sample. */
|
|
89
|
+
memoryStream = null;
|
|
88
90
|
watchdog = WATCHDOG_START;
|
|
89
91
|
watchdogBusy = false;
|
|
90
92
|
lastCpu = os.cpus();
|
|
@@ -208,6 +210,8 @@ export class Watcher {
|
|
|
208
210
|
});
|
|
209
211
|
this.stops = [...viewStops, stopForwards, stopMarking];
|
|
210
212
|
// Memory and CPU of this PC: when low, the sandbox is asked to slow its builds down (host/life/watchdog.ts).
|
|
213
|
+
this.memoryStream = new HostMemoryStream(storageRoot, WATCHDOG_MS / 1000);
|
|
214
|
+
this.memoryStream.start();
|
|
211
215
|
this.watchdogTimer = setInterval(() => void this.watchHost(), WATCHDOG_MS);
|
|
212
216
|
this.watchdogTimer.unref();
|
|
213
217
|
// sandbox.keepAwake, and the life record that tells the next launch how this run ended (host/life).
|
|
@@ -220,7 +224,7 @@ export class Watcher {
|
|
|
220
224
|
this.watchdogBusy = true;
|
|
221
225
|
try {
|
|
222
226
|
const settings = readLoadSettings(this.storageRoot, this.dataPath);
|
|
223
|
-
const memory = await readHostMemoryAsync(this.storageRoot);
|
|
227
|
+
const memory = this.memoryStream?.latest() ?? (await readHostMemoryAsync(this.storageRoot));
|
|
224
228
|
const cpus = os.cpus();
|
|
225
229
|
const cpuPercent = cpuPercentBetween(this.lastCpu, cpus);
|
|
226
230
|
this.lastCpu = cpus;
|
|
@@ -324,6 +328,7 @@ export class Watcher {
|
|
|
324
328
|
this.awake?.stop();
|
|
325
329
|
if (this.watchdogTimer)
|
|
326
330
|
clearInterval(this.watchdogTimer);
|
|
331
|
+
this.memoryStream?.stop();
|
|
327
332
|
this.stops.forEach((stop) => stop());
|
|
328
333
|
if (this.auditTimer)
|
|
329
334
|
clearInterval(this.auditTimer);
|
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-MU5XWZMS.css" />
|
|
9
|
+
<script type="module" src="/main-SLQNJRXD.js"></script>
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|