windows-development-sandbox 1.27.0 → 1.27.3
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/features/unreal/unreal.run.js +13 -3
- 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-JXL6ZOO3.js → chunk-4M5SZZ5P.js} +46 -41
- package/dist/bin/{chunk-ZXCAVVS4.js → chunk-FA5VWLP4.js} +1 -1
- package/dist/bin/{chunk-6WWMBCT2.js → chunk-RU7XRN62.js} +303 -112
- package/dist/bin/claude-host.js +2 -2
- package/dist/bin/claude-sbx.js +2 -2
- package/dist/bin/codex-sbx.js +2 -2
- package/dist/bin/gemini-sbx.js +2 -2
- package/dist/bin/sandbox.js +2 -2
- package/dist/bin/sbx-hide.js +2 -2
- package/dist/bin/sbx-show.js +2 -2
- package/dist/cli/commands/launch/launch.js +2 -1
- package/dist/cli/menu/menu.js +6 -1
- package/dist/cli/menu/pages/accounts-page.js +9 -5
- package/dist/guest/apps/app-installer.js +7 -5
- package/dist/guest/apps/comet-unpack.js +2 -2
- 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/setup/unity-agent.js +4 -1
- package/dist/guest/toolchains/engines/epic-launcher.d.ts +7 -1
- package/dist/guest/toolchains/engines/epic-launcher.js +30 -8
- package/dist/guest/toolchains/engines/unreal-engines.js +2 -1
- package/dist/guest/toolchains/engines/unreal-status.js +1 -1
- package/dist/guest/toolchains/installers.d.ts +3 -3
- package/dist/guest/toolchains/installers.js +5 -5
- package/dist/guest/toolchains/native/mesa.js +35 -11
- package/dist/guest/toolchains/native/unreal.js +1 -0
- package/dist/guest/toolchains/native/vcruntime.js +2 -2
- package/dist/guest/toolchains/native/vs-persist.d.ts +2 -2
- 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.d.ts +3 -0
- package/dist/host/accounts/deliver.js +27 -0
- package/dist/host/accounts/source.d.ts +2 -0
- package/dist/host/accounts/source.js +2 -0
- package/dist/host/pc-installs/discover.js +8 -4
- package/dist/host/pc-installs/epic.js +3 -0
- package/dist/host/pc-installs/unity.d.ts +2 -0
- package/dist/host/pc-installs/unity.js +12 -3
- package/dist/host/pc-installs/unreal.d.ts +5 -1
- package/dist/host/pc-installs/unreal.js +51 -9
- package/dist/runtime/agent.js +34 -22
- package/dist/runtime/daemon.js +134 -65
- package/dist/runtime/db.js +32 -16
- package/dist/runtime/main.js +136 -67
- package/dist/runtime/netproxy.js +26 -12
- package/dist/runtime/tool.js +32 -15
- package/dist/runtime/view.js +26 -12
- package/dist/selftest/app.test.js +20 -2
- package/dist/selftest/boot-runner.test.js +15 -1
- package/dist/selftest/engines.test.js +52 -2
- package/dist/selftest/menu-screens.js +52 -1
- package/dist/selftest/menu.test.js +29 -3
- package/dist/selftest/mixamo.test.js +1 -1
- package/dist/selftest/service-accounts.test.js +88 -2
- 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/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/paths/pc-installs.d.ts +8 -0
- package/dist/shared/unreal/launcher-files.d.ts +7 -0
- package/dist/shared/unreal/launcher-files.js +32 -1
- package/dist/shared/unreal/status.d.ts +1 -0
- package/dist/shared/unreal/status.js +2 -0
- package/dist/web/index.html +1 -1
- package/dist/web/{main-VPMYTSUL.js → main-NKM7ECSB.js} +59 -59
- 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/schemas/config.schema.json +9 -9
- package/schemas/sandbox-config.schema.json +9 -9
|
@@ -18,9 +18,10 @@ import { clearSecrets, SecretStore } from '../host/secrets/index.js';
|
|
|
18
18
|
import { applyServiceAccounts, readDelivered } from '../guest/accounts/apply.js';
|
|
19
19
|
import { AccountWatch } from '../guest/accounts/watch.js';
|
|
20
20
|
import { epicAdapter } from '../guest/accounts/kinds/epic.js';
|
|
21
|
-
import { epicSettingsFile, fingerprint, readEpicLogin, SERVICE_KINDS,
|
|
21
|
+
import { epicSettingsFile, epicSettingsFiles, fingerprint, readEpicLogin, SERVICE_KINDS, serviceWanted, withEpicLogin, } from '../shared/accounts/index.js';
|
|
22
22
|
import { RunDir } from '../shared/channel/index.js';
|
|
23
23
|
import { decodeText, encodeText, readBoundedFile } from '../shared/io/index.js';
|
|
24
|
+
import { isKindReady } from '../shared/readiness.js';
|
|
24
25
|
const SIGN_IN = 'PlantedEpicSignIn' + 'AbCdEf0123456789+/'.repeat(6) + '==';
|
|
25
26
|
const RENEWED = 'PlantedRenewed' + 'ZyXw9876543210'.repeat(6);
|
|
26
27
|
const CFX = 'cfxk_PlantedServerKey_0123456789';
|
|
@@ -141,6 +142,22 @@ export const serviceAccountsSuite = {
|
|
|
141
142
|
signIns.remove('epic');
|
|
142
143
|
const first = deliver();
|
|
143
144
|
t.check(!first.payloads.epic && first.leased.includes('epic') && launchLeases.holder('epic')?.launchId === 'L-boot', 'nothing saved yet: the lease is claimed anyway, so a first sign-in made there can come back');
|
|
145
|
+
const pcLocal = t.dir('svc-pc-local');
|
|
146
|
+
for (const file of epicSettingsFiles(pcLocal)) {
|
|
147
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
148
|
+
fs.writeFileSync(file, INI);
|
|
149
|
+
}
|
|
150
|
+
const thisPc = { localAppData: pcLocal, appData: t.dir('svc-pc-roaming'), programData: t.dir('svc-pc-data') };
|
|
151
|
+
signIns.remove('epic');
|
|
152
|
+
const fromPc = deliver({ pc: thisPc });
|
|
153
|
+
t.check(fromPc.payloads.epic?.data === SIGN_IN && signIns.info('epic')?.source === 'pc', "shared with nothing saved: this PC's own sign-in is taken and saved, as import would");
|
|
154
|
+
for (const file of epicSettingsFiles(pcLocal))
|
|
155
|
+
fs.writeFileSync(file, INI.replace(SIGN_IN, RENEWED));
|
|
156
|
+
t.check(deliver({ pc: thisPc }).payloads.epic?.data === RENEWED && signIns.read('epic')?.data === RENEWED, "one taken from this PC follows it: the PC's renewed sign-in replaces the stale copy");
|
|
157
|
+
signIns.save('epic', { data: SIGN_IN }, 'sandbox');
|
|
158
|
+
t.check(deliver({ pc: thisPc }).payloads.epic?.data === SIGN_IN && signIns.info('epic')?.source === 'sandbox', 'one a sandbox handed back is newer than the PC’s and is kept');
|
|
159
|
+
signIns.remove('epic');
|
|
160
|
+
t.check(!deliver().payloads.epic, 'without this PC (pc left out) nothing is taken');
|
|
144
161
|
const sharedRun = await f.run({}, config);
|
|
145
162
|
const launched = generateSandbox({
|
|
146
163
|
packageRoot: t.packageRoot,
|
|
@@ -314,8 +331,77 @@ export const serviceAccountsSuite = {
|
|
|
314
331
|
t.section('the accounts page');
|
|
315
332
|
const row = serviceRow('epic', { ...config, accounts: { ...config.accounts, epic: { share: true } } }, signIns, launchLeases);
|
|
316
333
|
const lines = pageLines(servicePage(row)).join('\n');
|
|
317
|
-
t.check(lines.includes('Epic Games') &&
|
|
334
|
+
t.check(lines.includes('Epic Games') &&
|
|
335
|
+
servicePage(row).sections[0].facts?.some((f) => f.description === 'The agent can use it.') === true &&
|
|
336
|
+
!lines.includes(SIGN_IN), 'a service shows its state and one short warning, never the sign-in');
|
|
318
337
|
t.check(Object.values(accountsView.queries).some((q) => q.id === 'accounts.services') &&
|
|
319
338
|
['accounts.service.import', 'accounts.service.login', 'accounts.service.logout', 'settings.set'].every((id) => accountsView.operations.some((o) => o.id === id)), 'the accounts view offers the services and their actions on every surface');
|
|
339
|
+
t.section('every action a service page shows works from the menu');
|
|
340
|
+
const shareOn = { ...config, accounts: { ...config.accounts, perplexity: { share: true } } };
|
|
341
|
+
const pcFound = { localAppData: t.dir('svc-pc-found'), appData: '', programData: '', env: {} };
|
|
342
|
+
t.write(epicSettingsFile(pcFound.localAppData), INI);
|
|
343
|
+
const pcNone = { localAppData: t.dir('svc-pc-none'), appData: '', programData: '', env: {} };
|
|
344
|
+
const pageStore = new ServiceAccountStore(t.dir('svc-page'), SecretStore.memory());
|
|
345
|
+
const pageLeases = new AccountLeases(t.dir('svc-page-leases'));
|
|
346
|
+
const rowOn = (kind, pc, c = config) => serviceRow(kind, c, pageStore, pageLeases, pc);
|
|
347
|
+
const labelsOf = (row) => servicePage(row).sections[0].actions.map((a) => a.label);
|
|
348
|
+
const descriptionsOf = (r) => (servicePage(r).sections[0].facts ?? []).map((f) => ' ' + (f.description ?? '')).join('');
|
|
349
|
+
const epicFound = rowOn('epic', pcFound);
|
|
350
|
+
const epicText = pageLines(servicePage(epicFound)).join('\n') + descriptionsOf(epicFound);
|
|
351
|
+
t.check(epicFound.onPc === true &&
|
|
352
|
+
/On this PC\s+found/.test(epicText) &&
|
|
353
|
+
epicText.includes("takes this PC's") &&
|
|
354
|
+
!epicText.includes(SIGN_IN), 'a kind that comes from this PC says it is found there (never the sign-in), and what sharing does');
|
|
355
|
+
t.check(rowOn('epic', pcNone).onPc === false &&
|
|
356
|
+
/On this PC\s+not found/.test(pageLines(servicePage(rowOn('epic', pcNone))).join('\n')), 'and says not found when this PC has none');
|
|
357
|
+
t.check(labelsOf(epicFound).includes("Take this PC's sign-in") &&
|
|
358
|
+
!labelsOf(epicFound).some((l) => l.startsWith('Import…')), 'taking it from this PC has a plain name');
|
|
359
|
+
const perplexityLabels = labelsOf(rowOn('perplexity', pcFound, shareOn));
|
|
360
|
+
t.check(perplexityLabels.includes("Take this PC's sign-in") &&
|
|
361
|
+
perplexityLabels.includes('Import from a file…') &&
|
|
362
|
+
perplexityLabels.includes('Type the sign-in…'), 'the API key can be taken from this PC, read from a file or typed');
|
|
363
|
+
const cometRow = rowOn('comet', pcFound);
|
|
364
|
+
const cometText = pageLines(servicePage(cometRow)).join('\n') + descriptionsOf(cometRow);
|
|
365
|
+
t.check(cometRow.onPc === null &&
|
|
366
|
+
cometRow.sandboxOnly &&
|
|
367
|
+
labelsOf(cometRow).every((l) => l === 'Share with sandboxes') &&
|
|
368
|
+
cometText.includes('Sign in once inside a sandbox; it is kept for the next ones.'), 'Comet offers only sharing and says plainly to sign in once inside a sandbox');
|
|
369
|
+
// Every action a page shows is one its kind can do (import: a PC or file way; sign in: a typed way).
|
|
370
|
+
for (const kind of SERVICE_KINDS.filter(isKindReady)) {
|
|
371
|
+
const r = rowOn(kind, pcFound);
|
|
372
|
+
for (const a of servicePage(r).sections[0].actions ?? []) {
|
|
373
|
+
if (a.kind !== 'operation')
|
|
374
|
+
continue;
|
|
375
|
+
const src = HOST_SOURCES[kind];
|
|
376
|
+
const ok = a.op === 'accounts.service.import'
|
|
377
|
+
? Boolean(src.fromPc || src.fromFile)
|
|
378
|
+
: a.op === 'accounts.service.login'
|
|
379
|
+
? Boolean(src.fromAnswers)
|
|
380
|
+
: ['accounts.service.logout', 'settings.set'].includes(a.op);
|
|
381
|
+
t.check(ok, kind + ': "' + a.label + '" is something this kind can do');
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
// The paths themselves, through the menu's questions (fakeIO): a file, typed, forgotten.
|
|
385
|
+
const envFile = t.write(t.at('svc-page', 'keys.env'), 'PERPLEXITY_API_KEY=pplx-PlantedKey0123456789\n');
|
|
386
|
+
const fileIO = fakeIO([envFile, true]);
|
|
387
|
+
const fromFile = await importServiceFlow(fileIO.io, 'perplexity', undefined, { store: pageStore, pc: pcNone, share: true }, true);
|
|
388
|
+
t.check(pageStore.info('perplexity')?.source === 'file' &&
|
|
389
|
+
fileIO.seen.questions[0].kind === 'path' &&
|
|
390
|
+
!fromFile.message.includes('PlantedKey'), 'Import from a file asks for the file even when this PC could give one, and saves it');
|
|
391
|
+
await logoutServiceFlow(fakeIO([true]).io, 'perplexity', t.dir('svc-page-root'), pageStore, pageLeases);
|
|
392
|
+
const pcWithKey = { ...pcNone, env: { PERPLEXITY_API_KEY: 'pplx-FromThisPc0123456789' } };
|
|
393
|
+
const keyRow = rowOn('perplexity', pcWithKey);
|
|
394
|
+
t.check(keyRow.onPc === true, 'the API key in this terminal is found on this PC');
|
|
395
|
+
await importServiceFlow(fakeIO([true]).io, 'perplexity', undefined, {
|
|
396
|
+
store: pageStore,
|
|
397
|
+
pc: pcWithKey,
|
|
398
|
+
share: true,
|
|
399
|
+
});
|
|
400
|
+
t.check(pageStore.info('perplexity')?.source === 'pc', "Take this PC's sign-in saves the PC's key");
|
|
401
|
+
await loginServiceFlow(fakeIO(['pplx-Typed0123456789']).io, 'perplexity', { store: pageStore, share: true });
|
|
402
|
+
t.check(pageStore.info('perplexity')?.source === 'typed', 'Type the sign-in saves what was typed');
|
|
403
|
+
const savedRow = rowOn('perplexity', pcWithKey);
|
|
404
|
+
t.check(labelsOf(savedRow).includes('Forget…') && savedRow.state === 'saved', 'a saved sign-in offers Forget');
|
|
405
|
+
await t.throws(() => importServiceFlow(fakeIO([true]).io, 'comet', undefined, { store: pageStore, pc: pcWithKey, share: true }), 'inside a sandbox', 'Comet has nothing to import: the error says to sign in inside a sandbox');
|
|
320
406
|
},
|
|
321
407
|
};
|
|
@@ -55,7 +55,7 @@ export const SERVICE_SPECS = {
|
|
|
55
55
|
},
|
|
56
56
|
steam: {
|
|
57
57
|
kind: 'steam',
|
|
58
|
-
label: 'Steam
|
|
58
|
+
label: 'Steam',
|
|
59
59
|
usedBy: ['steam'],
|
|
60
60
|
sources: ['file', 'sandbox'],
|
|
61
61
|
singleHolder: true,
|
|
@@ -91,7 +91,7 @@ export const SERVICE_SPECS = {
|
|
|
91
91
|
},
|
|
92
92
|
cfx: {
|
|
93
93
|
kind: 'cfx',
|
|
94
|
-
label: '
|
|
94
|
+
label: 'FiveM key',
|
|
95
95
|
usedBy: ['fivem'],
|
|
96
96
|
sources: ['typed', 'file'],
|
|
97
97
|
singleHolder: false,
|
|
@@ -120,7 +120,7 @@ export const SERVICE_SPECS = {
|
|
|
120
120
|
},
|
|
121
121
|
keystore: {
|
|
122
122
|
kind: 'keystore',
|
|
123
|
-
label: 'Android
|
|
123
|
+
label: 'Android keystore',
|
|
124
124
|
usedBy: ['android'],
|
|
125
125
|
sources: ['file'],
|
|
126
126
|
singleHolder: false,
|
|
@@ -135,7 +135,7 @@ export const SERVICE_SPECS = {
|
|
|
135
135
|
},
|
|
136
136
|
play: {
|
|
137
137
|
kind: 'play',
|
|
138
|
-
label: 'Google Play
|
|
138
|
+
label: 'Google Play',
|
|
139
139
|
usedBy: ['android'],
|
|
140
140
|
sources: ['file'],
|
|
141
141
|
singleHolder: false,
|
|
@@ -146,7 +146,7 @@ export const SERVICE_SPECS = {
|
|
|
146
146
|
},
|
|
147
147
|
expo: {
|
|
148
148
|
kind: 'expo',
|
|
149
|
-
label: 'Expo
|
|
149
|
+
label: 'Expo',
|
|
150
150
|
usedBy: ['reactNative'],
|
|
151
151
|
sources: ['typed'],
|
|
152
152
|
singleHolder: false,
|
|
@@ -157,7 +157,7 @@ export const SERVICE_SPECS = {
|
|
|
157
157
|
},
|
|
158
158
|
appstore: {
|
|
159
159
|
kind: 'appstore',
|
|
160
|
-
label: 'App Store Connect
|
|
160
|
+
label: 'App Store Connect',
|
|
161
161
|
usedBy: ['reactNative', 'flutter'],
|
|
162
162
|
sources: ['file', 'typed'],
|
|
163
163
|
singleHolder: false,
|
|
@@ -171,7 +171,7 @@ export const SERVICE_SPECS = {
|
|
|
171
171
|
},
|
|
172
172
|
perplexity: {
|
|
173
173
|
kind: 'perplexity',
|
|
174
|
-
label: 'Perplexity API
|
|
174
|
+
label: 'Perplexity API',
|
|
175
175
|
usedBy: ['perplexity'],
|
|
176
176
|
sources: ['pc', 'file', 'typed'],
|
|
177
177
|
singleHolder: false,
|
|
@@ -195,7 +195,7 @@ export const SERVICE_SPECS = {
|
|
|
195
195
|
},
|
|
196
196
|
comet: {
|
|
197
197
|
kind: 'comet',
|
|
198
|
-
label: 'Comet
|
|
198
|
+
label: 'Comet',
|
|
199
199
|
usedBy: ['comet'],
|
|
200
200
|
sources: ['sandbox'],
|
|
201
201
|
singleHolder: false,
|
|
@@ -212,7 +212,7 @@ export const SERVICE_SPECS = {
|
|
|
212
212
|
},
|
|
213
213
|
perplexityapp: {
|
|
214
214
|
kind: 'perplexityapp',
|
|
215
|
-
label: 'Perplexity
|
|
215
|
+
label: 'Perplexity app',
|
|
216
216
|
usedBy: ['perplexityapp'],
|
|
217
217
|
sources: ['sandbox'],
|
|
218
218
|
singleHolder: false,
|
|
@@ -73,6 +73,8 @@ export interface AppEntry {
|
|
|
73
73
|
exe: string;
|
|
74
74
|
/** A command-line tool: its folder goes on PATH (no desktop shortcut). */
|
|
75
75
|
cli?: boolean;
|
|
76
|
+
/** A desktop program an agent also runs by name (F3D renders previews): its folder goes on PATH, shortcut kept. */
|
|
77
|
+
onPath?: boolean;
|
|
76
78
|
/** Needs these first: 7-Zip to unpack an installer, Mesa for OpenGL without a GPU. */
|
|
77
79
|
needs?: ('7zip' | 'mesa')[];
|
|
78
80
|
/**
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// them to the downloader and extractor (guest/runtime/pools.ts installPools).
|
|
3
3
|
import { Limiter } from './limiter.js';
|
|
4
4
|
/** HTTP bodies. */
|
|
5
|
-
export const net = new Limiter(
|
|
5
|
+
export const net = new Limiter(3, 'net');
|
|
6
6
|
/** extractArchive, 7-Zip/NSIS unpack, npm ci/install, uv/pip, Steam, robocopy of big layouts, rustup/cargo. */
|
|
7
7
|
export const heavy = new Limiter(2, 'heavy');
|
|
8
8
|
/** Every msiexec, vcredist, DXSETUP and the Visual Studio bootstrapper: Windows runs one install at a time. */
|
|
@@ -34,12 +34,14 @@ export function extractArchive(archive, destination, members = []) {
|
|
|
34
34
|
}
|
|
35
35
|
async function extract(archive, destination, members) {
|
|
36
36
|
fs.mkdirSync(destination, { recursive: true });
|
|
37
|
-
let status = await spawnAsync(SYSTEM.tar(), ['-xf', archive, '-C', destination, ...members]
|
|
37
|
+
let status = await spawnAsync(SYSTEM.tar(), ['-xf', archive, '-C', destination, ...members], {
|
|
38
|
+
lowPriority: true,
|
|
39
|
+
});
|
|
38
40
|
if (status !== 0 && /\.(zip|nupkg)$/i.test(archive)) {
|
|
39
41
|
const zip = archive.endsWith('.zip') ? archive : archive + '.zip';
|
|
40
42
|
if (zip !== archive)
|
|
41
43
|
fs.copyFileSync(archive, zip);
|
|
42
|
-
status = await spawnAsync(SYSTEM.powershell(), scriptArgs('expand-archive.ps1', ['-Archive', zip, '-Destination', destination]));
|
|
44
|
+
status = await spawnAsync(SYSTEM.powershell(), scriptArgs('expand-archive.ps1', ['-Archive', zip, '-Destination', destination]), { lowPriority: true });
|
|
43
45
|
}
|
|
44
46
|
if (status !== 0)
|
|
45
47
|
throw new Error('Could not extract ' + archive);
|
|
@@ -2,5 +2,5 @@ export { fetchRetry, fetchJson, fetchText } from './http.js';
|
|
|
2
2
|
export { fetchBounded, fetchBoundedResponse, type BoundedResponse, type Fetcher, type RequestFetcher, } from './bounded-fetch.js';
|
|
3
3
|
export { stagingFolder, moveIntoPlace, extractArchive, findFile, limitExtraction } from './archive.js';
|
|
4
4
|
export { type ArchiveOptions, type Checksums, type DownloadWatcher, Downloader, downloader } from './downloader.js';
|
|
5
|
-
export { spawnAsync } from './spawn.js';
|
|
5
|
+
export { spawnAsync, lowerPriority, type SpawnAsyncOptions } from './spawn.js';
|
|
6
6
|
export { type ReleaseAsset, githubAsset, parseReleasePage, latestGithubTag, parseReleaseTags } from './github.js';
|
|
@@ -2,5 +2,5 @@ export { fetchRetry, fetchJson, fetchText } from './http.js';
|
|
|
2
2
|
export { fetchBounded, fetchBoundedResponse, } from './bounded-fetch.js';
|
|
3
3
|
export { stagingFolder, moveIntoPlace, extractArchive, findFile, limitExtraction } from './archive.js';
|
|
4
4
|
export { Downloader, downloader } from './downloader.js';
|
|
5
|
-
export { spawnAsync } from './spawn.js';
|
|
5
|
+
export { spawnAsync, lowerPriority } from './spawn.js';
|
|
6
6
|
export { githubAsset, parseReleasePage, latestGithubTag, parseReleaseTags } from './github.js';
|
|
@@ -1,3 +1,9 @@
|
|
|
1
1
|
import { type SpawnOptions } from 'node:child_process';
|
|
2
|
+
export interface SpawnAsyncOptions extends SpawnOptions {
|
|
3
|
+
/** Below-normal priority for the program and what it starts: unpacking must not make the desktop sluggish. */
|
|
4
|
+
readonly lowPriority?: boolean;
|
|
5
|
+
}
|
|
6
|
+
/** Lowers a process to below-normal priority; best effort (it may have ended already). */
|
|
7
|
+
export declare function lowerPriority(pid: number | undefined): void;
|
|
2
8
|
/** Runs a program without blocking the event loop (parallel downloads keep going); resolves with its exit code. */
|
|
3
|
-
export declare function spawnAsync(file: string, args: string[], options?:
|
|
9
|
+
export declare function spawnAsync(file: string, args: string[], options?: SpawnAsyncOptions): Promise<number>;
|
|
@@ -1,8 +1,23 @@
|
|
|
1
|
+
import os from 'node:os';
|
|
1
2
|
import { spawn } from 'node:child_process';
|
|
3
|
+
/** Lowers a process to below-normal priority; best effort (it may have ended already). */
|
|
4
|
+
export function lowerPriority(pid) {
|
|
5
|
+
if (pid === undefined)
|
|
6
|
+
return;
|
|
7
|
+
try {
|
|
8
|
+
os.setPriority(pid, os.constants.priority.PRIORITY_BELOW_NORMAL);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
/* a normal priority only makes it a little slower to share the machine */
|
|
12
|
+
}
|
|
13
|
+
}
|
|
2
14
|
/** Runs a program without blocking the event loop (parallel downloads keep going); resolves with its exit code. */
|
|
3
15
|
export function spawnAsync(file, args, options = {}) {
|
|
16
|
+
const { lowPriority, ...spawnOptions } = options;
|
|
4
17
|
return new Promise((resolve, reject) => {
|
|
5
|
-
const child = spawn(file, args, { stdio: 'ignore', windowsHide: true, ...
|
|
18
|
+
const child = spawn(file, args, { stdio: 'ignore', windowsHide: true, ...spawnOptions });
|
|
19
|
+
if (lowPriority)
|
|
20
|
+
lowerPriority(child.pid);
|
|
6
21
|
child.once('error', reject);
|
|
7
22
|
child.once('close', (code) => resolve(code ?? -1));
|
|
8
23
|
});
|
|
@@ -22,6 +22,14 @@ export interface PcInstall {
|
|
|
22
22
|
readonly artifactId: string;
|
|
23
23
|
readonly appVersion: string;
|
|
24
24
|
};
|
|
25
|
+
/**
|
|
26
|
+
* The launcher's manifest of an engine (its Data\Manifests\<name>.item on this PC): without one the sandbox's
|
|
27
|
+
* launcher drops the engine from its library. Its paths name the sandbox's folder by the time it is in session.json.
|
|
28
|
+
*/
|
|
29
|
+
readonly launcherItem?: {
|
|
30
|
+
readonly name: string;
|
|
31
|
+
readonly text: string;
|
|
32
|
+
};
|
|
25
33
|
}
|
|
26
34
|
/** What session.json says about them: no host paths. */
|
|
27
35
|
export interface PcInstallManifest {
|
|
@@ -71,6 +71,13 @@ export declare function itemInfo(text: string): {
|
|
|
71
71
|
export declare function incompleteDirs(...manifestDirs: string[]): Set<string>;
|
|
72
72
|
/** The sizes the saved manifests give their engine folders (lower-case folder -> bytes). */
|
|
73
73
|
export declare function itemSizes(saved: string): Map<string, number>;
|
|
74
|
+
/** A launcher manifest's file name (<hex id>.item). */
|
|
75
|
+
export declare const ITEM_NAME: RegExp;
|
|
76
|
+
/**
|
|
77
|
+
* A launcher manifest moved from folder `from` to `to`: every path in it under `from` (either slash) is under `to`, and
|
|
78
|
+
* a path elsewhere is dropped (it would only name this PC). Null when the text is not a manifest of `from`.
|
|
79
|
+
*/
|
|
80
|
+
export declare function relocateItem(text: string, from: string, to: string): string | null;
|
|
74
81
|
/** Whether the text is a launcher manifest of an app installed under `root`. */
|
|
75
82
|
export declare function isItemUnder(text: string, root: string): boolean;
|
|
76
83
|
/** Copies the launcher's manifests of apps under `root` into `saved` (changed ones only). Returns the file names. */
|
|
@@ -191,6 +191,37 @@ export function itemSizes(saved) {
|
|
|
191
191
|
}
|
|
192
192
|
return sizes;
|
|
193
193
|
}
|
|
194
|
+
/** A launcher manifest's file name (<hex id>.item). */
|
|
195
|
+
export const ITEM_NAME = /^[A-Za-z0-9_-]{1,80}\.item$/;
|
|
196
|
+
/**
|
|
197
|
+
* A launcher manifest moved from folder `from` to `to`: every path in it under `from` (either slash) is under `to`, and
|
|
198
|
+
* a path elsewhere is dropped (it would only name this PC). Null when the text is not a manifest of `from`.
|
|
199
|
+
*/
|
|
200
|
+
export function relocateItem(text, from, to) {
|
|
201
|
+
let item;
|
|
202
|
+
try {
|
|
203
|
+
item = JSON.parse(stripBom(text));
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return null; // Not JSON: not a manifest.
|
|
207
|
+
}
|
|
208
|
+
if (!item || typeof item !== 'object' || Array.isArray(item))
|
|
209
|
+
return null;
|
|
210
|
+
const base = win.resolve(from).toLowerCase();
|
|
211
|
+
const out = {};
|
|
212
|
+
for (const [key, value] of Object.entries(item)) {
|
|
213
|
+
if (typeof value !== 'string' || !win.isAbsolute(value)) {
|
|
214
|
+
out[key] = value;
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
const full = win.resolve(value);
|
|
218
|
+
const low = full.toLowerCase();
|
|
219
|
+
if (low === base || low.startsWith(base + '\\'))
|
|
220
|
+
out[key] = to + full.slice(base.length);
|
|
221
|
+
}
|
|
222
|
+
const at = typeof out.InstallLocation === 'string' ? out.InstallLocation : '';
|
|
223
|
+
return at.toLowerCase() === to.toLowerCase() ? JSON.stringify(out, null, '\t') : null;
|
|
224
|
+
}
|
|
194
225
|
/** Whether the text is a launcher manifest of an app installed under `root`. */
|
|
195
226
|
export function isItemUnder(text, root) {
|
|
196
227
|
const at = itemInstallLocation(text);
|
|
@@ -198,7 +229,7 @@ export function isItemUnder(text, root) {
|
|
|
198
229
|
}
|
|
199
230
|
function itemFiles(dir) {
|
|
200
231
|
try {
|
|
201
|
-
return fs.readdirSync(dir).filter((f) =>
|
|
232
|
+
return fs.readdirSync(dir).filter((f) => ITEM_NAME.test(f));
|
|
202
233
|
}
|
|
203
234
|
catch {
|
|
204
235
|
return [];
|
|
@@ -28,6 +28,7 @@ export declare const UnrealStatusSchema: z.ZodObject<{
|
|
|
28
28
|
}, z.core.$strip>>;
|
|
29
29
|
launcher: z.ZodObject<{
|
|
30
30
|
installed: z.ZodBoolean;
|
|
31
|
+
exe: z.ZodDefault<z.ZodNullable<z.ZodString>>;
|
|
31
32
|
installLocation: z.ZodNullable<z.ZodString>;
|
|
32
33
|
problem: z.ZodNullable<z.ZodString>;
|
|
33
34
|
freeBytes: z.ZodNullable<z.ZodNumber>;
|
|
@@ -30,6 +30,8 @@ export const UnrealStatusSchema = z.object({
|
|
|
30
30
|
.max(64),
|
|
31
31
|
launcher: z.object({
|
|
32
32
|
installed: z.boolean(),
|
|
33
|
+
/** Its program (Program Files or Program Files (x86), by launcher version); null when there is none. */
|
|
34
|
+
exe: Text().nullable().default(null),
|
|
33
35
|
/** Where the launcher installs engines; null when it could not be set. */
|
|
34
36
|
installLocation: Text().nullable(),
|
|
35
37
|
/** Why engines would not go to the data disk (shown loudly), null when they do. */
|
package/dist/web/index.html
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<title>Sandbox</title>
|
|
7
7
|
<link rel="icon" href="data:," />
|
|
8
8
|
<link rel="stylesheet" href="/main-XT7T6ODM.css" />
|
|
9
|
-
<script type="module" src="/main-
|
|
9
|
+
<script type="module" src="/main-NKM7ECSB.js"></script>
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|