windows-development-sandbox 1.27.1 → 1.28.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/context.d.ts +5 -0
- package/dist/app/context.js +13 -0
- package/dist/app/features/accounts/accounts.def.d.ts +16 -0
- package/dist/app/features/accounts/accounts.run.d.ts +4 -0
- package/dist/app/features/accounts/services.def.d.ts +15 -1
- package/dist/app/features/accounts/services.def.js +67 -31
- package/dist/app/features/accounts/services.run.d.ts +7 -2
- package/dist/app/features/accounts/services.run.js +25 -6
- package/dist/app/features/defs.d.ts +17 -0
- package/dist/app/registry.d.ts +1 -1
- package/dist/app/registry.js +15 -2
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-RSIX7C6D.js → chunk-CB44HTTN.js} +444 -175
- package/dist/bin/{chunk-4TLSWPG7.js → chunk-UZ5S3EMB.js} +1 -1
- package/dist/bin/{chunk-QUHNXHVG.js → chunk-ZMZI3625.js} +46 -41
- 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/menu/menu.js +6 -1
- package/dist/cli/menu/pages/accounts-page.js +9 -5
- package/dist/core/schema/sandbox.js +1 -1
- package/dist/guest/apps/app-installer.js +7 -5
- package/dist/guest/apps/comet-unpack.js +2 -2
- package/dist/guest/daemon/heartbeat.js +1 -0
- package/dist/guest/governor/core.d.ts +53 -1
- package/dist/guest/governor/core.js +95 -1
- package/dist/guest/governor/decide.d.ts +4 -0
- package/dist/guest/governor/decide.js +2 -0
- package/dist/guest/governor/governor.js +23 -9
- package/dist/guest/runtime/pools.d.ts +4 -4
- package/dist/guest/runtime/pools.js +4 -15
- package/dist/guest/runtime/process.d.ts +5 -2
- package/dist/guest/runtime/process.js +23 -3
- package/dist/guest/toolchains/engines/epic-launcher.d.ts +6 -0
- package/dist/guest/toolchains/engines/epic-launcher.js +19 -1
- package/dist/guest/toolchains/engines/unreal-engines.js +2 -1
- package/dist/guest/toolchains/installers.d.ts +3 -3
- package/dist/guest/toolchains/installers.js +5 -5
- package/dist/guest/toolchains/native/unreal.js +22 -3
- package/dist/guest/toolchains/native/vcruntime.js +2 -2
- package/dist/guest/toolchains/native/vs-persist.d.ts +2 -2
- package/dist/guest/toolchains/runtimes.js +7 -6
- package/dist/guest/toolchains/signature.d.ts +2 -0
- package/dist/guest/toolchains/signature.js +9 -2
- package/dist/guest/toolchains/step-scheduler.js +1 -1
- package/dist/host/accounts/deliver.js +15 -7
- package/dist/host/life/crash-report.d.ts +4 -0
- package/dist/host/life/crash-report.js +30 -1
- package/dist/host/life/last-end.js +1 -0
- package/dist/host/pc-installs/discover.js +5 -2
- package/dist/host/pc-installs/unreal.d.ts +3 -1
- package/dist/host/pc-installs/unreal.js +27 -1
- package/dist/host/tools/windows-terminal.d.ts +4 -1
- package/dist/host/tools/windows-terminal.js +25 -9
- package/dist/runtime/agent.js +39 -23
- package/dist/runtime/daemon.js +279 -121
- package/dist/runtime/db.js +37 -17
- package/dist/runtime/main.js +340 -109
- package/dist/runtime/netproxy.js +31 -13
- package/dist/runtime/tool.js +37 -16
- package/dist/runtime/view.js +150 -38
- package/dist/selftest/agent-paths.test.js +3 -1
- package/dist/selftest/app.test.js +20 -2
- package/dist/selftest/boot-runner.test.js +15 -1
- package/dist/selftest/channel.test.js +33 -0
- package/dist/selftest/engines.test.js +22 -1
- package/dist/selftest/heavy-load.test.js +116 -4
- package/dist/selftest/live-terminal.test.js +103 -0
- package/dist/selftest/menu-screens.js +52 -1
- package/dist/selftest/menu.test.js +29 -3
- package/dist/selftest/personalization.test.js +2 -1
- package/dist/selftest/service-accounts.test.js +77 -2
- package/dist/selftest/support/engine-golden.js +2 -0
- package/dist/selftest/ui-text.test.js +4 -0
- package/dist/shared/accounts/kinds.js +9 -9
- package/dist/shared/apps/catalogue-gamedev.js +1 -0
- package/dist/shared/apps/catalogue.d.ts +2 -0
- package/dist/shared/channel/schema.d.ts +2 -0
- package/dist/shared/channel/schema.js +4 -0
- package/dist/shared/concurrency/pools.js +1 -1
- package/dist/shared/download/archive.js +4 -2
- package/dist/shared/download/index.d.ts +1 -1
- package/dist/shared/download/index.js +1 -1
- package/dist/shared/download/spawn.d.ts +7 -1
- package/dist/shared/download/spawn.js +16 -1
- package/dist/shared/memory/index.d.ts +1 -0
- package/dist/shared/memory/index.js +1 -0
- package/dist/shared/memory/policy.d.ts +12 -0
- package/dist/shared/memory/policy.js +4 -0
- package/dist/shared/memory/unreal-build.d.ts +2 -0
- package/dist/shared/memory/unreal-build.js +21 -0
- package/dist/shared/paths/pc-installs.d.ts +8 -0
- package/dist/shared/pty/clipboard-watch.d.ts +4 -0
- package/dist/shared/pty/clipboard-watch.js +40 -0
- package/dist/shared/pty/ctrl-c.d.ts +13 -7
- package/dist/shared/pty/ctrl-c.js +35 -19
- package/dist/shared/pty/index.d.ts +1 -0
- package/dist/shared/pty/index.js +1 -0
- package/dist/shared/pty/terminal.d.ts +1 -1
- package/dist/shared/pty/terminal.js +16 -13
- package/dist/shared/unreal/launcher-files.d.ts +7 -0
- package/dist/shared/unreal/launcher-files.js +32 -1
- package/dist/shared/web/ctrl-c.d.ts +26 -0
- package/dist/shared/web/ctrl-c.js +64 -0
- package/dist/shared/web/index.d.ts +1 -0
- package/dist/shared/web/index.js +1 -0
- package/dist/web/index.html +1 -1
- package/dist/web/{main-VPMYTSUL.js → main-7NY4X6YU.js} +67 -67
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
- package/package.json +1 -1
- package/sandbox/csharp/ClipboardWatch.cs +59 -0
- package/sandbox/csharp/MemGov.cs +752 -7
- package/sandbox/scripts/watch-clipboard.ps1 +8 -0
- package/schemas/config.schema.json +10 -10
- package/schemas/sandbox-config.schema.json +9 -9
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
// A file's Authenticode signature (sandbox\scripts\get-signature.ps1) checked against the signer a download names.
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { signerMatches } from '../../shared/signer.js';
|
|
4
|
-
import { scriptOutput } from '../runtime/process.js';
|
|
4
|
+
import { scriptOutput, scriptOutputAsync } from '../runtime/process.js';
|
|
5
5
|
const SignatureSchema = z.object({ subject: z.string().max(4096), issuer: z.string().max(4096) });
|
|
6
6
|
/** The names on a file's valid signature, or null when it is unsigned, tampered with, or cannot be read. */
|
|
7
7
|
export function signatureOf(file) {
|
|
8
|
-
|
|
8
|
+
return parseSignature(scriptOutput('get-signature.ps1', ['-Path', file]));
|
|
9
|
+
}
|
|
10
|
+
function parseSignature(output) {
|
|
11
|
+
const text = output.trim();
|
|
9
12
|
if (!text)
|
|
10
13
|
return null;
|
|
11
14
|
try {
|
|
@@ -20,3 +23,7 @@ export function signatureOf(file) {
|
|
|
20
23
|
export function signedBy(file, signer) {
|
|
21
24
|
return signerMatches(signatureOf(file), signer);
|
|
22
25
|
}
|
|
26
|
+
/** signedBy without blocking the event loop: a boot check of a big download must not stall the other tasks. */
|
|
27
|
+
export async function signedByAsync(file, signer) {
|
|
28
|
+
return signerMatches(parseSignature(await scriptOutputAsync('get-signature.ps1', ['-Path', file])), signer);
|
|
29
|
+
}
|
|
@@ -5,7 +5,7 @@ import { downloader } from '../../shared/download/index.js';
|
|
|
5
5
|
import { errMsg } from '../../core/util/errors.js';
|
|
6
6
|
import { emptyEnv } from './tool-env.js';
|
|
7
7
|
/** How many installers run at once: enough to overlap downloads with installs, few enough not to thrash the disk. */
|
|
8
|
-
const PARALLEL =
|
|
8
|
+
const PARALLEL = 3;
|
|
9
9
|
/** Runs one installer; a failure is reported and skipped so it never blocks the other toolchains. */
|
|
10
10
|
async function runReported(sink, name, env, work) {
|
|
11
11
|
try {
|
|
@@ -22,19 +22,27 @@ function howToSave(spec) {
|
|
|
22
22
|
? 'save one: sandbox account login ' + spec.kind
|
|
23
23
|
: 'save one: sandbox account import ' + spec.kind + ' --from <file>';
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
26
|
-
|
|
25
|
+
/**
|
|
26
|
+
* This PC's own sign-in, saved like `sandbox account import`: when nothing is saved, and when the saved one was taken
|
|
27
|
+
* from this PC and the PC's has changed since (Epic replaces its token whenever the launcher here renews it, so the
|
|
28
|
+
* old copy no longer signs in). One a sandbox handed back is newer than the PC's and is kept.
|
|
29
|
+
*/
|
|
30
|
+
function takeFromPc(kind, pc, store, saved) {
|
|
27
31
|
const source = HOST_SOURCES[kind];
|
|
28
32
|
if (!pc || !source.fromPc)
|
|
29
|
-
return
|
|
33
|
+
return saved;
|
|
34
|
+
if (saved && store.info(kind)?.source !== 'pc')
|
|
35
|
+
return saved;
|
|
30
36
|
let payload = null;
|
|
31
37
|
try {
|
|
32
38
|
payload = source.fromPc(pc);
|
|
33
39
|
}
|
|
34
40
|
catch {
|
|
35
|
-
return
|
|
41
|
+
return saved; // This PC's program has no usable sign-in: the saved one (or the note) stands.
|
|
36
42
|
}
|
|
37
|
-
if (payload)
|
|
43
|
+
if (!payload)
|
|
44
|
+
return saved;
|
|
45
|
+
if (!saved || fingerprint(payload) !== fingerprint(saved))
|
|
38
46
|
store.save(kind, payload, 'pc', source.summary(payload));
|
|
39
47
|
return payload;
|
|
40
48
|
}
|
|
@@ -70,8 +78,8 @@ export function prepareServiceAccounts(i) {
|
|
|
70
78
|
catch {
|
|
71
79
|
unreadable = true;
|
|
72
80
|
}
|
|
73
|
-
if (!
|
|
74
|
-
payload = takeFromPc(kind, i.pc, store);
|
|
81
|
+
if (!unreadable)
|
|
82
|
+
payload = takeFromPc(kind, i.pc, store, payload);
|
|
75
83
|
if (spec.singleHolder) {
|
|
76
84
|
const claim = leases.claim(kind, dataSet, i.launchId, payload ? fingerprint(payload) : null, i.isRunning);
|
|
77
85
|
if (!claim.ok) {
|
|
@@ -22,6 +22,10 @@ export declare function findingLines(events: readonly StopEvent[]): string[];
|
|
|
22
22
|
export declare function findDumps(dirs: readonly string[], since: Date): DumpFile[];
|
|
23
23
|
/** Whether the newest launch started at or before `lastSeen` had vGPU on (host-only .wsb in <storage>\launches). */
|
|
24
24
|
export declare function launchHadVgpu(storageRoot: string, lastSeen: string): boolean;
|
|
25
|
+
/**
|
|
26
|
+
* "This PC still had ..." from the last line of its memory log, when RAM and commit were both 4 GB or more free (pure).
|
|
27
|
+
*/
|
|
28
|
+
export declare function memoryWasFineLine(hostMemoryLog: string): string | null;
|
|
25
29
|
/**
|
|
26
30
|
* The report text (pure): what happened (the sandbox's or this PC's stop code, where the dump is) and the memory log's
|
|
27
31
|
* last lines. `memoryLog` is the sandbox's own file, so it is shown through safeText.
|
|
@@ -54,7 +54,10 @@ export function findingLines(events) {
|
|
|
54
54
|
lines.push('WHEA-Logger: Windows reported a hardware error (CPU, memory, a bus or a disk): heat or failing hardware.');
|
|
55
55
|
if (has((e) => lower(e).startsWith('hyper-v')))
|
|
56
56
|
lines.push("Hyper-V events: the sandbox's virtual machine (or the service that runs it) reported a problem.");
|
|
57
|
-
|
|
57
|
+
const isAppCrash = (e) => /^(application error|windows error reporting)$/i.test(e.provider);
|
|
58
|
+
if (has((e) => isAppCrash(e) && /vmwp\.exe/i.test(e.process)))
|
|
59
|
+
lines.push("vmwp.exe, the Windows program that runs the sandbox's VM, crashed. That is a fault in Windows' Hyper-V code or a driver it uses (with vGPU on, usually the graphics driver), not a program inside the sandbox running out of memory.");
|
|
60
|
+
else if (has(isAppCrash))
|
|
58
61
|
lines.push("A crash report names vmwp.exe or vmmem, the programs that hold the sandbox's VM.");
|
|
59
62
|
return lines;
|
|
60
63
|
}
|
|
@@ -122,6 +125,30 @@ function hostMemoryTail(storageRoot, dataPath) {
|
|
|
122
125
|
return '';
|
|
123
126
|
}
|
|
124
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* "This PC still had ..." from the last line of its memory log, when RAM and commit were both 4 GB or more free (pure).
|
|
130
|
+
*/
|
|
131
|
+
export function memoryWasFineLine(hostMemoryLog) {
|
|
132
|
+
const last = hostMemoryLog
|
|
133
|
+
.split(/\r?\n/)
|
|
134
|
+
.filter((l) => l.trim() !== '')
|
|
135
|
+
.pop();
|
|
136
|
+
if (!last)
|
|
137
|
+
return null;
|
|
138
|
+
const ram = /RAM free\/total MB (\d{1,9})\/(\d{1,9})/.exec(last);
|
|
139
|
+
const commit = /commit free\/limit MB (\d{1,9})\/(\d{1,9})/.exec(last);
|
|
140
|
+
if (!ram || !commit)
|
|
141
|
+
return null;
|
|
142
|
+
const freeRam = Number(ram[1]);
|
|
143
|
+
const freeCommit = Number(commit[1]);
|
|
144
|
+
if (freeRam < 4096 || freeCommit < 4096)
|
|
145
|
+
return null;
|
|
146
|
+
return ('This PC still had ' +
|
|
147
|
+
Math.floor(freeRam / 1024) +
|
|
148
|
+
' GB of RAM and ' +
|
|
149
|
+
Math.floor(freeCommit / 1024) +
|
|
150
|
+
' GB of memory (RAM plus paging file) free just before, so it did not run out of memory.');
|
|
151
|
+
}
|
|
125
152
|
/**
|
|
126
153
|
* The report text (pure): what happened (the sandbox's or this PC's stop code, where the dump is) and the memory log's
|
|
127
154
|
* last lines. `memoryLog` is the sandbox's own file, so it is shown through safeText.
|
|
@@ -152,6 +179,7 @@ export function crashReportText(cause, events, memoryLog, lastSeen, extra = {})
|
|
|
152
179
|
.map((l) => ' ' + safeText(l));
|
|
153
180
|
const freeze = freezeLine(cause, lastSeen);
|
|
154
181
|
const meaning = findingLines(unique);
|
|
182
|
+
const fine = memoryWasFineLine(extra.hostMemoryLog ?? '');
|
|
155
183
|
const dumps = extra.dumps ?? [];
|
|
156
184
|
const vgpu = extra.vgpu ? ['vGPU was on for this sandbox. ' + vgpuWarning('its launch settings'), ''] : [];
|
|
157
185
|
return [
|
|
@@ -165,6 +193,7 @@ export function crashReportText(cause, events, memoryLog, lastSeen, extra = {})
|
|
|
165
193
|
...vgpu,
|
|
166
194
|
'What that usually means:',
|
|
167
195
|
...(meaning.length ? meaning.map((l) => ' ' + l) : [' (no memory, blue screen or hardware event was logged)']),
|
|
196
|
+
...(fine ? [' ' + fine] : []),
|
|
168
197
|
'',
|
|
169
198
|
'Crash dumps Windows left (C:\\Windows\\Minidump, MEMORY.DMP):',
|
|
170
199
|
...(dumps.length ? dumps.map((d) => ' ' + d.name + ' ' + d.at) : [' (none from that time)']),
|
|
@@ -39,6 +39,7 @@ export function heartbeatLine(hb) {
|
|
|
39
39
|
hb.step +
|
|
40
40
|
(hb.hostStep > 0 ? ' (this PC asked for step ' + hb.hostStep + ')' : '') +
|
|
41
41
|
(hb.diskQueueReported !== undefined ? ', this PC disk queue ' + hb.diskQueueReported : '') +
|
|
42
|
+
(hb.held ? ', ' + hb.held + ' builds held by the memory gate' : '') +
|
|
42
43
|
(top ? '; biggest: ' + top : ''));
|
|
43
44
|
}
|
|
44
45
|
/** The likeliest reason, from the last heartbeat (pure). */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { assertSafeMapping } from '../../core/safe-paths.js';
|
|
2
2
|
import { dirExists, errMsg } from '../../core/util/index.js';
|
|
3
3
|
import { pcInstallGuest } from '../../shared/paths/index.js';
|
|
4
|
+
import { relocateItem } from '../../shared/unreal/index.js';
|
|
4
5
|
import { pcEnv } from '../accounts/index.js';
|
|
5
6
|
import { epicInstalls } from './epic.js';
|
|
6
7
|
import { steamInstalls } from './steam.js';
|
|
@@ -74,8 +75,10 @@ export function discoverPcInstalls(s, o, pc = pcEnv()) {
|
|
|
74
75
|
readOnly: !writable,
|
|
75
76
|
label: writable ? 'this PC, writable' : 'this PC, read-only',
|
|
76
77
|
});
|
|
77
|
-
const { host: _host, ...shown } = install;
|
|
78
|
-
|
|
78
|
+
const { host: _host, launcherItem, ...shown } = install;
|
|
79
|
+
// The manifest names the sandbox's folder, or it is left out: no path of this PC goes to the sandbox.
|
|
80
|
+
const moved = launcherItem ? relocateItem(launcherItem.text, install.host, guest) : null;
|
|
81
|
+
items.push({ ...shown, guest, ...(moved ? { launcherItem: { name: launcherItem.name, text: moved } } : {}) });
|
|
79
82
|
}
|
|
80
83
|
catch (e) {
|
|
81
84
|
notes.push('pcInstalls: ' + install.name + ' is not mapped: ' + errMsg(e));
|
|
@@ -13,6 +13,8 @@ export interface UnrealPcFiles {
|
|
|
13
13
|
readonly defaultRoot: string;
|
|
14
14
|
/** More folders that may hold UE_x.y folders: the same places on every drive (D:\Program Files\Epic Games ...). */
|
|
15
15
|
readonly otherRoots?: readonly string[];
|
|
16
|
+
/** The launcher's Data\Manifests folder (its .item files), or none. */
|
|
17
|
+
readonly manifests?: string;
|
|
16
18
|
readonly list: (dir: string) => string[];
|
|
17
19
|
readonly read: (file: string, max: number) => Buffer | null;
|
|
18
20
|
}
|
|
@@ -33,7 +35,7 @@ export interface PcSourceBuild {
|
|
|
33
35
|
export declare function pcSourceBuilds(files: Pick<UnrealPcFiles, 'read' | 'builds'>): PcSourceBuild[];
|
|
34
36
|
/** The engine folders you registered (pcInstalls.unrealEngineFolders), mapped writable. A folder that is a source
|
|
35
37
|
* build of this PC keeps the PC's build id. */
|
|
36
|
-
export declare function findEngineFolders(files: Pick<UnrealPcFiles, 'read' | 'builds'
|
|
38
|
+
export declare function findEngineFolders(files: Pick<UnrealPcFiles, 'read' | 'builds'> & Partial<Pick<UnrealPcFiles, 'launcherInstalled' | 'manifests' | 'list'>>, folders: readonly (string | number)[]): PcInstall[];
|
|
37
39
|
/** This PC's files (reg.exe through system32, bounded reads). */
|
|
38
40
|
export declare function unrealPcFiles(pc: PcEnv): UnrealPcFiles;
|
|
39
41
|
export declare const unrealInstalls: PcInstallSource;
|
|
@@ -10,7 +10,7 @@ import fs from 'node:fs';
|
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { readBoundedFile } from '../../shared/io/index.js';
|
|
12
12
|
import { SYSTEM } from '../../shared/system.js';
|
|
13
|
-
import { BUILDS_KEY, launcherInstallations, launcherInstalledFile, MAX_LAUNCHER_FILE_BYTES, parseBuildVersion, versionText, } from '../../shared/unreal/index.js';
|
|
13
|
+
import { BUILDS_KEY, itemInstallLocation, ITEM_NAME, launcherInstallations, launcherInstalledFile, launcherManifestsDir, MAX_LAUNCHER_FILE_BYTES, parseBuildVersion, versionText, } from '../../shared/unreal/index.js';
|
|
14
14
|
const win = path.win32;
|
|
15
15
|
/** At most this many engines of this PC are mapped. */
|
|
16
16
|
export const MAX_PC_ENGINES = 8;
|
|
@@ -29,6 +29,27 @@ const versionOf = (dir, files) => {
|
|
|
29
29
|
const bytes = files.read(win.join(dir, 'Engine', 'Build', 'Build.version'), 64 * 1024);
|
|
30
30
|
return bytes ? parseBuildVersion(bytes.toString('utf8')) : null;
|
|
31
31
|
};
|
|
32
|
+
/** At most this much of a manifest goes into session.json (an engine's is a few KB). */
|
|
33
|
+
const MAX_ITEM_BYTES = 64 * 1024;
|
|
34
|
+
/** The launcher's manifest of the app in `dir` (with this PC's paths), or nothing. */
|
|
35
|
+
function launcherItemOf(dir, files) {
|
|
36
|
+
if (!files.manifests)
|
|
37
|
+
return {};
|
|
38
|
+
const want = win.resolve(dir).toLowerCase();
|
|
39
|
+
for (const name of files.list(files.manifests).filter((n) => ITEM_NAME.test(n))) {
|
|
40
|
+
const bytes = files.read(win.join(files.manifests, name), MAX_ITEM_BYTES);
|
|
41
|
+
const at = bytes ? itemInstallLocation(bytes.toString('utf8')) : null;
|
|
42
|
+
if (bytes && at && win.resolve(at).toLowerCase() === want)
|
|
43
|
+
return { launcherItem: { name, text: bytes.toString('utf8') } };
|
|
44
|
+
}
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
/** The launcher's ids for the app in `dir` (from LauncherInstalled.dat), or nothing. */
|
|
48
|
+
function launcherIdsOf(dir, files) {
|
|
49
|
+
const want = win.resolve(dir).toLowerCase();
|
|
50
|
+
const entry = (files.launcherInstalled ? launcherInstallations(files.launcherInstalled) : []).find((i) => APP_NAME.test(i.appName) && i.ids && win.resolve(i.installLocation).toLowerCase() === want);
|
|
51
|
+
return entry?.ids ? { launcher: entry.ids } : {};
|
|
52
|
+
}
|
|
32
53
|
const isWanted = (wanted, v) => wanted === true || wanted.map((w) => String(w).replace(/^UE_/i, '')).includes(v.major + '.' + v.minor);
|
|
33
54
|
/** The engines of this PC that `wanted` maps, each folder once, at most MAX_PC_ENGINES. */
|
|
34
55
|
export function findUnrealInstalls(files, wanted) {
|
|
@@ -53,6 +74,7 @@ export function findUnrealInstalls(files, wanted) {
|
|
|
53
74
|
host: win.normalize(dir),
|
|
54
75
|
guest: '',
|
|
55
76
|
...(ids ? { launcher: ids } : {}),
|
|
77
|
+
...launcherItemOf(dir, files),
|
|
56
78
|
});
|
|
57
79
|
};
|
|
58
80
|
const listed = files.launcherInstalled ? launcherInstallations(files.launcherInstalled) : [];
|
|
@@ -117,6 +139,9 @@ export function findEngineFolders(files, folders) {
|
|
|
117
139
|
guest: '',
|
|
118
140
|
writable: true,
|
|
119
141
|
...(source ? { buildId: source.buildId } : {}),
|
|
142
|
+
// An engine the launcher installed keeps its ids and manifest, so the sandbox's launcher lists it too.
|
|
143
|
+
...launcherIdsOf(dir, { launcherInstalled: files.launcherInstalled ?? null }),
|
|
144
|
+
...(files.list ? launcherItemOf(dir, { manifests: files.manifests, list: files.list, read: files.read }) : {}),
|
|
120
145
|
});
|
|
121
146
|
}
|
|
122
147
|
return found;
|
|
@@ -146,6 +171,7 @@ export function unrealPcFiles(pc) {
|
|
|
146
171
|
win.join(d, 'Program Files', 'Epic Games'),
|
|
147
172
|
win.join(d, 'Epic Games'),
|
|
148
173
|
]),
|
|
174
|
+
manifests: launcherManifestsDir(pc.programData),
|
|
149
175
|
list: listDir,
|
|
150
176
|
read: readBoundedFile,
|
|
151
177
|
};
|
|
@@ -19,10 +19,13 @@ declare const TerminalSettingsSchema: z.ZodObject<{
|
|
|
19
19
|
export declare function withPwshProfile(settings: z.output<typeof TerminalSettingsSchema>, visible: boolean): z.output<typeof TerminalSettingsSchema> | null;
|
|
20
20
|
/** settings.json that makes new tabs read the current environment; null when it already does. Pure. */
|
|
21
21
|
export declare function withReloadEnvironment(settings: z.output<typeof TerminalSettingsSchema>): z.output<typeof TerminalSettingsSchema> | null;
|
|
22
|
+
/** settings.json that copies on select; null when the user already chose (true or false). Pure. */
|
|
23
|
+
export declare function withCopyOnSelect(settings: z.output<typeof TerminalSettingsSchema>): z.output<typeof TerminalSettingsSchema> | null;
|
|
22
24
|
/**
|
|
23
25
|
* Shows the PowerShell 7 profile once pwsh is installed in this data folder (and `allowed`, i.e. toolchains.pwsh is
|
|
24
26
|
* not off), hides it otherwise. settings.json is written by the sandbox too: read with a size cap, through no links,
|
|
25
|
-
* shape-checked, only rewritten when the profile changes
|
|
27
|
+
* shape-checked, only rewritten when the profile changes or a default switch is missing (reload the environment,
|
|
28
|
+
* copy on select; a value the user set stays); a file that does not parse is left alone.
|
|
26
29
|
*/
|
|
27
30
|
export declare function syncPwshProfile(dataPath: string, allowed: boolean): void;
|
|
28
31
|
export declare function ensureTerminal(dataPath: string, refresh: boolean, options?: {
|
|
@@ -24,6 +24,9 @@ const SETTINGS = {
|
|
|
24
24
|
theme: 'dark',
|
|
25
25
|
firstWindowPreference: 'defaultProfile',
|
|
26
26
|
confirmCloseAllTabs: false,
|
|
27
|
+
// Copied the moment the mouse is released, so an agent's redraw cannot clear the selection first.
|
|
28
|
+
copyOnSelect: true,
|
|
29
|
+
copyFormatting: 'none',
|
|
27
30
|
// New tabs pick up the PATH and variables the runner writes machine-wide after the window opened.
|
|
28
31
|
compatibility: { reloadEnvironmentVariables: true },
|
|
29
32
|
warning: { confirmCloseAllTabs: false, largePaste: false, multiLinePaste: false },
|
|
@@ -75,10 +78,17 @@ export function withReloadEnvironment(settings) {
|
|
|
75
78
|
return null;
|
|
76
79
|
return { ...settings, compatibility: { ...compatibility, reloadEnvironmentVariables: true } };
|
|
77
80
|
}
|
|
81
|
+
/** settings.json that copies on select; null when the user already chose (true or false). Pure. */
|
|
82
|
+
export function withCopyOnSelect(settings) {
|
|
83
|
+
if ('copyOnSelect' in settings)
|
|
84
|
+
return null;
|
|
85
|
+
return { ...settings, copyOnSelect: true };
|
|
86
|
+
}
|
|
78
87
|
/**
|
|
79
88
|
* Shows the PowerShell 7 profile once pwsh is installed in this data folder (and `allowed`, i.e. toolchains.pwsh is
|
|
80
89
|
* not off), hides it otherwise. settings.json is written by the sandbox too: read with a size cap, through no links,
|
|
81
|
-
* shape-checked, only rewritten when the profile changes
|
|
90
|
+
* shape-checked, only rewritten when the profile changes or a default switch is missing (reload the environment,
|
|
91
|
+
* copy on select; a value the user set stays); a file that does not parse is left alone.
|
|
82
92
|
*/
|
|
83
93
|
export function syncPwshProfile(dataPath, allowed) {
|
|
84
94
|
const file = path.join(terminalDir(dataPath), 'settings', 'settings.json');
|
|
@@ -100,16 +110,22 @@ export function syncPwshProfile(dataPath, allowed) {
|
|
|
100
110
|
}
|
|
101
111
|
const withProfile = withPwshProfile(parsed, visible);
|
|
102
112
|
if (withProfile) {
|
|
103
|
-
|
|
113
|
+
const reloaded = withReloadEnvironment(withProfile) ?? withProfile;
|
|
114
|
+
writeJson(file, withCopyOnSelect(reloaded) ?? reloaded);
|
|
104
115
|
return;
|
|
105
116
|
}
|
|
106
|
-
// Only
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
// Only default switches are missing: set those values in the text, so comments and the layout stay.
|
|
118
|
+
const missing = [];
|
|
119
|
+
if (withReloadEnvironment(parsed))
|
|
120
|
+
missing.push([['compatibility', 'reloadEnvironmentVariables'], true]);
|
|
121
|
+
if (withCopyOnSelect(parsed))
|
|
122
|
+
missing.push([['copyOnSelect'], true]);
|
|
123
|
+
if (missing.length === 0)
|
|
124
|
+
return;
|
|
125
|
+
let edited = text;
|
|
126
|
+
for (const [at, value] of missing)
|
|
127
|
+
edited = applyEdits(edited, modify(edited, at, value, { formattingOptions: { insertSpaces: true, tabSize: 4 } }));
|
|
128
|
+
writeFileAtomic(file, edited);
|
|
113
129
|
}
|
|
114
130
|
export async function ensureTerminal(dataPath, refresh, options = {}) {
|
|
115
131
|
const result = await installTerminal(dataPath, refresh);
|
package/dist/runtime/agent.js
CHANGED
|
@@ -25502,7 +25502,7 @@ var SandboxSchema = external_exports.strictObject({
|
|
|
25502
25502
|
'The last resort that keeps the sandbox up, in MB below its commit limit (RAM plus paging file): builds are slowed down long before (sandbox.pressureResponse); only if everything the agents start together would take the sandbox to the very edge of its memory, where Windows inside it would start failing, is a build refused memory instead of Windows. "auto" = the larger of 1536 MB and 3 % of the commit limit; "none" = no backstop. Yours only: a project file cannot change it.'
|
|
25503
25503
|
),
|
|
25504
25504
|
pressureResponse: external_exports.boolean().describe(
|
|
25505
|
-
"When the sandbox (or this PC) runs low on memory, slow builds down in steps, like a PC that gets slower instead of failing: build programs get lower CPU and memory priority, then less CPU, then compilers take turns (the waiting ones briefly suspended and trimmed to the paging file). Nothing is stopped or killed, and the agents themselves are never touched. Yours only: a project file cannot change it."
|
|
25505
|
+
"When the sandbox (or this PC) runs low on memory, slow builds down in steps, like a PC that gets slower instead of failing: build programs get lower CPU and memory priority, then less CPU, then compilers take turns (the waiting ones briefly suspended and trimmed to the paging file). Nothing is stopped or killed, and the agents themselves are never touched. A memory gate also holds new compilers and linkers for a moment when the memory of the build programs grows faster than 4 GB in 3 seconds (3 GB with vGPU), and lets them go one at a time. Yours only: a project file cannot change it."
|
|
25506
25506
|
),
|
|
25507
25507
|
vGpu: external_exports.boolean().describe(
|
|
25508
25508
|
"Virtual GPU. Off by default: Microsoft notes it widens the sandbox attack surface. The sandbox then uses this PC's graphics card, and with some graphics drivers heavy use can freeze the whole PC: turn it on only when a program in the sandbox needs the GPU (the mesa toolchain gives software OpenGL without it). Browsers in the sandbox never use it (they draw in software)."
|
|
@@ -26093,6 +26093,7 @@ var GAMEDEV_APPS = [
|
|
|
26093
26093
|
size: "100 MB",
|
|
26094
26094
|
source: { github: "f3d-app/f3d", asset: "^F3D-[\\d.]+-Windows-x86_64\\.zip$" },
|
|
26095
26095
|
exe: "f3d.exe",
|
|
26096
|
+
onPath: true,
|
|
26096
26097
|
needs: ["mesa"]
|
|
26097
26098
|
},
|
|
26098
26099
|
{
|
|
@@ -27192,7 +27193,7 @@ var SERVICE_SPECS = {
|
|
|
27192
27193
|
},
|
|
27193
27194
|
steam: {
|
|
27194
27195
|
kind: "steam",
|
|
27195
|
-
label: "Steam
|
|
27196
|
+
label: "Steam",
|
|
27196
27197
|
usedBy: ["steam"],
|
|
27197
27198
|
sources: ["file", "sandbox"],
|
|
27198
27199
|
singleHolder: true,
|
|
@@ -27228,7 +27229,7 @@ var SERVICE_SPECS = {
|
|
|
27228
27229
|
},
|
|
27229
27230
|
cfx: {
|
|
27230
27231
|
kind: "cfx",
|
|
27231
|
-
label: "
|
|
27232
|
+
label: "FiveM key",
|
|
27232
27233
|
usedBy: ["fivem"],
|
|
27233
27234
|
sources: ["typed", "file"],
|
|
27234
27235
|
singleHolder: false,
|
|
@@ -27257,7 +27258,7 @@ var SERVICE_SPECS = {
|
|
|
27257
27258
|
},
|
|
27258
27259
|
keystore: {
|
|
27259
27260
|
kind: "keystore",
|
|
27260
|
-
label: "Android
|
|
27261
|
+
label: "Android keystore",
|
|
27261
27262
|
usedBy: ["android"],
|
|
27262
27263
|
sources: ["file"],
|
|
27263
27264
|
singleHolder: false,
|
|
@@ -27272,7 +27273,7 @@ var SERVICE_SPECS = {
|
|
|
27272
27273
|
},
|
|
27273
27274
|
play: {
|
|
27274
27275
|
kind: "play",
|
|
27275
|
-
label: "Google Play
|
|
27276
|
+
label: "Google Play",
|
|
27276
27277
|
usedBy: ["android"],
|
|
27277
27278
|
sources: ["file"],
|
|
27278
27279
|
singleHolder: false,
|
|
@@ -27283,7 +27284,7 @@ var SERVICE_SPECS = {
|
|
|
27283
27284
|
},
|
|
27284
27285
|
expo: {
|
|
27285
27286
|
kind: "expo",
|
|
27286
|
-
label: "Expo
|
|
27287
|
+
label: "Expo",
|
|
27287
27288
|
usedBy: ["reactNative"],
|
|
27288
27289
|
sources: ["typed"],
|
|
27289
27290
|
singleHolder: false,
|
|
@@ -27294,7 +27295,7 @@ var SERVICE_SPECS = {
|
|
|
27294
27295
|
},
|
|
27295
27296
|
appstore: {
|
|
27296
27297
|
kind: "appstore",
|
|
27297
|
-
label: "App Store Connect
|
|
27298
|
+
label: "App Store Connect",
|
|
27298
27299
|
usedBy: ["reactNative", "flutter"],
|
|
27299
27300
|
sources: ["file", "typed"],
|
|
27300
27301
|
singleHolder: false,
|
|
@@ -27308,7 +27309,7 @@ var SERVICE_SPECS = {
|
|
|
27308
27309
|
},
|
|
27309
27310
|
perplexity: {
|
|
27310
27311
|
kind: "perplexity",
|
|
27311
|
-
label: "Perplexity API
|
|
27312
|
+
label: "Perplexity API",
|
|
27312
27313
|
usedBy: ["perplexity"],
|
|
27313
27314
|
sources: ["pc", "file", "typed"],
|
|
27314
27315
|
singleHolder: false,
|
|
@@ -27332,7 +27333,7 @@ var SERVICE_SPECS = {
|
|
|
27332
27333
|
},
|
|
27333
27334
|
comet: {
|
|
27334
27335
|
kind: "comet",
|
|
27335
|
-
label: "Comet
|
|
27336
|
+
label: "Comet",
|
|
27336
27337
|
usedBy: ["comet"],
|
|
27337
27338
|
sources: ["sandbox"],
|
|
27338
27339
|
singleHolder: false,
|
|
@@ -27348,7 +27349,7 @@ var SERVICE_SPECS = {
|
|
|
27348
27349
|
},
|
|
27349
27350
|
perplexityapp: {
|
|
27350
27351
|
kind: "perplexityapp",
|
|
27351
|
-
label: "Perplexity
|
|
27352
|
+
label: "Perplexity app",
|
|
27352
27353
|
usedBy: ["perplexityapp"],
|
|
27353
27354
|
sources: ["sandbox"],
|
|
27354
27355
|
singleHolder: false,
|
|
@@ -27699,6 +27700,8 @@ var PressureStatusSchema = external_exports.object({
|
|
|
27699
27700
|
lowered: external_exports.number().int().min(0).max(1024),
|
|
27700
27701
|
/** Compilers waiting their turn right now (name, process id, seconds until they may go on). */
|
|
27701
27702
|
paused: external_exports.array(external_exports.object({ name: Text(64), pid: external_exports.number().int(), seconds: external_exports.number().int().min(0).max(60) })).max(8),
|
|
27703
|
+
/** Build programs the memory gate holds right now (let go one at a time). */
|
|
27704
|
+
held: external_exports.number().int().min(0).max(1024).optional(),
|
|
27702
27705
|
/** The CPU share the compilers get together (percent; 100 = no cap). */
|
|
27703
27706
|
cpuPercent: external_exports.number().int().min(0).max(100).optional(),
|
|
27704
27707
|
/** The PC itself asked for slowing down (its memory or CPU is low). */
|
|
@@ -27719,6 +27722,8 @@ var HeartbeatSchema = external_exports.object({
|
|
|
27719
27722
|
topProcesses: external_exports.array(external_exports.object({ name: Text(64), privateMb: external_exports.number().int().min(0).max(1e7) })).max(5),
|
|
27720
27723
|
/** The disk queue this PC last reported while asking the sandbox to slow down. */
|
|
27721
27724
|
diskQueueReported: external_exports.number().min(0).max(1e5).optional(),
|
|
27725
|
+
/** Build programs the memory gate holds right now (let go one at a time). */
|
|
27726
|
+
held: external_exports.number().int().min(0).max(1024).optional(),
|
|
27722
27727
|
/** "ending": the sandbox is shutting down in order (idle shutdown, sandbox stop, Windows shutting down). */
|
|
27723
27728
|
phase: external_exports.enum(["ready", "working", "ending"])
|
|
27724
27729
|
});
|
|
@@ -28074,7 +28079,7 @@ import path12 from "node:path";
|
|
|
28074
28079
|
|
|
28075
28080
|
// src/guest/runtime/process.ts
|
|
28076
28081
|
import fs10 from "node:fs";
|
|
28077
|
-
import
|
|
28082
|
+
import os4 from "node:os";
|
|
28078
28083
|
import path11 from "node:path";
|
|
28079
28084
|
import { spawn as spawn2, spawnSync } from "node:child_process";
|
|
28080
28085
|
|
|
@@ -28134,10 +28139,20 @@ function scriptArgs(name, args = []) {
|
|
|
28134
28139
|
}
|
|
28135
28140
|
|
|
28136
28141
|
// src/shared/download/spawn.ts
|
|
28142
|
+
import os3 from "node:os";
|
|
28137
28143
|
import { spawn } from "node:child_process";
|
|
28144
|
+
function lowerPriority(pid) {
|
|
28145
|
+
if (pid === void 0) return;
|
|
28146
|
+
try {
|
|
28147
|
+
os3.setPriority(pid, os3.constants.priority.PRIORITY_BELOW_NORMAL);
|
|
28148
|
+
} catch {
|
|
28149
|
+
}
|
|
28150
|
+
}
|
|
28138
28151
|
function spawnAsync(file2, args, options = {}) {
|
|
28152
|
+
const { lowPriority, ...spawnOptions } = options;
|
|
28139
28153
|
return new Promise((resolve, reject) => {
|
|
28140
|
-
const child = spawn(file2, args, { stdio: "ignore", windowsHide: true, ...
|
|
28154
|
+
const child = spawn(file2, args, { stdio: "ignore", windowsHide: true, ...spawnOptions });
|
|
28155
|
+
if (lowPriority) lowerPriority(child.pid);
|
|
28141
28156
|
child.once("error", reject);
|
|
28142
28157
|
child.once("close", (code) => resolve(code ?? -1));
|
|
28143
28158
|
});
|
|
@@ -28158,13 +28173,16 @@ function extractArchive(archive, destination, members2 = []) {
|
|
|
28158
28173
|
}
|
|
28159
28174
|
async function extract(archive, destination, members2) {
|
|
28160
28175
|
fs8.mkdirSync(destination, { recursive: true });
|
|
28161
|
-
let status = await spawnAsync(SYSTEM.tar(), ["-xf", archive, "-C", destination, ...members2]
|
|
28176
|
+
let status = await spawnAsync(SYSTEM.tar(), ["-xf", archive, "-C", destination, ...members2], {
|
|
28177
|
+
lowPriority: true
|
|
28178
|
+
});
|
|
28162
28179
|
if (status !== 0 && /\.(zip|nupkg)$/i.test(archive)) {
|
|
28163
28180
|
const zip = archive.endsWith(".zip") ? archive : archive + ".zip";
|
|
28164
28181
|
if (zip !== archive) fs8.copyFileSync(archive, zip);
|
|
28165
28182
|
status = await spawnAsync(
|
|
28166
28183
|
SYSTEM.powershell(),
|
|
28167
|
-
scriptArgs("expand-archive.ps1", ["-Archive", zip, "-Destination", destination])
|
|
28184
|
+
scriptArgs("expand-archive.ps1", ["-Archive", zip, "-Destination", destination]),
|
|
28185
|
+
{ lowPriority: true }
|
|
28168
28186
|
);
|
|
28169
28187
|
}
|
|
28170
28188
|
if (status !== 0) throw new Error("Could not extract " + archive);
|
|
@@ -28381,13 +28399,15 @@ function logLines(stream) {
|
|
|
28381
28399
|
stream.on("end", () => logOutput(partial2));
|
|
28382
28400
|
}
|
|
28383
28401
|
async function execAsync(file2, args, options = {}) {
|
|
28402
|
+
const { lowPriority, ...spawnOptions } = options;
|
|
28384
28403
|
const code = captureOutput() ? await new Promise((resolve, reject) => {
|
|
28385
|
-
const child = spawn2(file2, args, { stdio: ["inherit", "pipe", "pipe"], windowsHide: true, ...
|
|
28404
|
+
const child = spawn2(file2, args, { stdio: ["inherit", "pipe", "pipe"], windowsHide: true, ...spawnOptions });
|
|
28405
|
+
if (lowPriority) lowerPriority(child.pid);
|
|
28386
28406
|
logLines(child.stdout);
|
|
28387
28407
|
logLines(child.stderr);
|
|
28388
28408
|
child.once("error", reject);
|
|
28389
28409
|
child.once("close", (c) => resolve(c ?? -1));
|
|
28390
|
-
}) : await spawnAsync(file2, args, { stdio: "inherit", ...
|
|
28410
|
+
}) : await spawnAsync(file2, args, { stdio: "inherit", ...spawnOptions, ...lowPriority ? { lowPriority } : {} });
|
|
28391
28411
|
if (code !== 0)
|
|
28392
28412
|
throw new Error(path11.basename(file2) + " " + args.slice(0, 3).join(" ") + " failed with exit code " + code);
|
|
28393
28413
|
}
|
|
@@ -28416,7 +28436,7 @@ function runScript(name, args = []) {
|
|
|
28416
28436
|
throw new Error(name + " failed with exit code " + result.status + (errors ? ": " + errors.slice(0, 400) : ""));
|
|
28417
28437
|
}
|
|
28418
28438
|
function runScriptWithJson(name, param, data, args = []) {
|
|
28419
|
-
const dir = fs10.mkdtempSync(path11.join(
|
|
28439
|
+
const dir = fs10.mkdtempSync(path11.join(os4.tmpdir(), "sbx-"));
|
|
28420
28440
|
const file2 = path11.join(dir, "input.json");
|
|
28421
28441
|
try {
|
|
28422
28442
|
fs10.writeFileSync(file2, JSON.stringify(data), "utf8");
|
|
@@ -28492,9 +28512,6 @@ async function setupUv(env) {
|
|
|
28492
28512
|
env.prepend.push(mkdirp(tool("uv-bin")));
|
|
28493
28513
|
}
|
|
28494
28514
|
|
|
28495
|
-
// src/guest/runtime/pools.ts
|
|
28496
|
-
import os4 from "node:os";
|
|
28497
|
-
|
|
28498
28515
|
// src/shared/concurrency/limiter.ts
|
|
28499
28516
|
import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
|
|
28500
28517
|
var held = new AsyncLocalStorage2();
|
|
@@ -28554,14 +28571,13 @@ var Limiter = class {
|
|
|
28554
28571
|
};
|
|
28555
28572
|
|
|
28556
28573
|
// src/shared/concurrency/pools.ts
|
|
28557
|
-
var net = new Limiter(
|
|
28574
|
+
var net = new Limiter(3, "net");
|
|
28558
28575
|
var heavy = new Limiter(2, "heavy");
|
|
28559
28576
|
var installer = new Limiter(1, "installer");
|
|
28560
28577
|
|
|
28561
28578
|
// src/guest/runtime/pools.ts
|
|
28562
|
-
var BELOW_NORMAL = os4.constants.priority.PRIORITY_BELOW_NORMAL;
|
|
28563
28579
|
function execHeavy(file2, args, options = {}) {
|
|
28564
|
-
return heavy.run(() => execAsync(file2, args, options));
|
|
28580
|
+
return heavy.run(() => execAsync(file2, args, { ...options, lowPriority: true }));
|
|
28565
28581
|
}
|
|
28566
28582
|
|
|
28567
28583
|
// src/guest/runtime/session.ts
|