windows-development-sandbox 1.27.1 → 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.
Files changed (78) hide show
  1. package/dist/app/context.d.ts +5 -0
  2. package/dist/app/context.js +13 -0
  3. package/dist/app/features/accounts/accounts.def.d.ts +16 -0
  4. package/dist/app/features/accounts/accounts.run.d.ts +4 -0
  5. package/dist/app/features/accounts/services.def.d.ts +15 -1
  6. package/dist/app/features/accounts/services.def.js +67 -31
  7. package/dist/app/features/accounts/services.run.d.ts +7 -2
  8. package/dist/app/features/accounts/services.run.js +25 -6
  9. package/dist/app/features/defs.d.ts +17 -0
  10. package/dist/app/registry.d.ts +1 -1
  11. package/dist/app/registry.js +15 -2
  12. package/dist/bin/agent-sandbox.js +2 -2
  13. package/dist/bin/{chunk-QUHNXHVG.js → chunk-4M5SZZ5P.js} +46 -41
  14. package/dist/bin/{chunk-4TLSWPG7.js → chunk-FA5VWLP4.js} +1 -1
  15. package/dist/bin/{chunk-RSIX7C6D.js → chunk-RU7XRN62.js} +249 -101
  16. package/dist/bin/claude-host.js +2 -2
  17. package/dist/bin/claude-sbx.js +2 -2
  18. package/dist/bin/codex-sbx.js +2 -2
  19. package/dist/bin/gemini-sbx.js +2 -2
  20. package/dist/bin/sandbox.js +2 -2
  21. package/dist/bin/sbx-hide.js +2 -2
  22. package/dist/bin/sbx-show.js +2 -2
  23. package/dist/cli/menu/menu.js +6 -1
  24. package/dist/cli/menu/pages/accounts-page.js +9 -5
  25. package/dist/guest/apps/app-installer.js +7 -5
  26. package/dist/guest/apps/comet-unpack.js +2 -2
  27. package/dist/guest/runtime/pools.d.ts +4 -4
  28. package/dist/guest/runtime/pools.js +4 -15
  29. package/dist/guest/runtime/process.d.ts +5 -2
  30. package/dist/guest/runtime/process.js +23 -3
  31. package/dist/guest/toolchains/engines/epic-launcher.d.ts +6 -0
  32. package/dist/guest/toolchains/engines/epic-launcher.js +19 -1
  33. package/dist/guest/toolchains/engines/unreal-engines.js +2 -1
  34. package/dist/guest/toolchains/installers.d.ts +3 -3
  35. package/dist/guest/toolchains/installers.js +5 -5
  36. package/dist/guest/toolchains/native/vcruntime.js +2 -2
  37. package/dist/guest/toolchains/native/vs-persist.d.ts +2 -2
  38. package/dist/guest/toolchains/signature.d.ts +2 -0
  39. package/dist/guest/toolchains/signature.js +9 -2
  40. package/dist/guest/toolchains/step-scheduler.js +1 -1
  41. package/dist/host/accounts/deliver.js +15 -7
  42. package/dist/host/pc-installs/discover.js +5 -2
  43. package/dist/host/pc-installs/unreal.d.ts +3 -1
  44. package/dist/host/pc-installs/unreal.js +27 -1
  45. package/dist/runtime/agent.js +34 -22
  46. package/dist/runtime/daemon.js +96 -51
  47. package/dist/runtime/db.js +32 -16
  48. package/dist/runtime/main.js +98 -53
  49. package/dist/runtime/netproxy.js +26 -12
  50. package/dist/runtime/tool.js +32 -15
  51. package/dist/runtime/view.js +26 -12
  52. package/dist/selftest/app.test.js +20 -2
  53. package/dist/selftest/boot-runner.test.js +15 -1
  54. package/dist/selftest/engines.test.js +22 -1
  55. package/dist/selftest/menu-screens.js +52 -1
  56. package/dist/selftest/menu.test.js +29 -3
  57. package/dist/selftest/service-accounts.test.js +77 -2
  58. package/dist/selftest/ui-text.test.js +4 -0
  59. package/dist/shared/accounts/kinds.js +9 -9
  60. package/dist/shared/apps/catalogue-gamedev.js +1 -0
  61. package/dist/shared/apps/catalogue.d.ts +2 -0
  62. package/dist/shared/concurrency/pools.js +1 -1
  63. package/dist/shared/download/archive.js +4 -2
  64. package/dist/shared/download/index.d.ts +1 -1
  65. package/dist/shared/download/index.js +1 -1
  66. package/dist/shared/download/spawn.d.ts +7 -1
  67. package/dist/shared/download/spawn.js +16 -1
  68. package/dist/shared/paths/pc-installs.d.ts +8 -0
  69. package/dist/shared/unreal/launcher-files.d.ts +7 -0
  70. package/dist/shared/unreal/launcher-files.js +32 -1
  71. package/dist/web/index.html +1 -1
  72. package/dist/web/{main-VPMYTSUL.js → main-NKM7ECSB.js} +59 -59
  73. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  74. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  75. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  76. package/package.json +1 -1
  77. package/schemas/config.schema.json +9 -9
  78. package/schemas/sandbox-config.schema.json +9 -9
@@ -2,8 +2,8 @@
2
2
  import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
3
  import {
4
4
  run
5
- } from "./chunk-QUHNXHVG.js";
6
- import "./chunk-RSIX7C6D.js";
5
+ } from "./chunk-4M5SZZ5P.js";
6
+ import "./chunk-RU7XRN62.js";
7
7
 
8
8
  // src/bin/claude-host.ts
9
9
  var args = process.argv.slice(2);
@@ -2,8 +2,8 @@
2
2
  import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
3
  import {
4
4
  run
5
- } from "./chunk-QUHNXHVG.js";
6
- import "./chunk-RSIX7C6D.js";
5
+ } from "./chunk-4M5SZZ5P.js";
6
+ import "./chunk-RU7XRN62.js";
7
7
 
8
8
  // src/bin/claude-sbx.ts
9
9
  run(process.argv.slice(2), { harness: "claude" });
@@ -2,8 +2,8 @@
2
2
  import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
3
  import {
4
4
  run
5
- } from "./chunk-QUHNXHVG.js";
6
- import "./chunk-RSIX7C6D.js";
5
+ } from "./chunk-4M5SZZ5P.js";
6
+ import "./chunk-RU7XRN62.js";
7
7
 
8
8
  // src/bin/codex-sbx.ts
9
9
  run(process.argv.slice(2), { harness: "codex" });
@@ -2,8 +2,8 @@
2
2
  import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
3
  import {
4
4
  run
5
- } from "./chunk-QUHNXHVG.js";
6
- import "./chunk-RSIX7C6D.js";
5
+ } from "./chunk-4M5SZZ5P.js";
6
+ import "./chunk-RU7XRN62.js";
7
7
 
8
8
  // src/bin/gemini-sbx.ts
9
9
  run(process.argv.slice(2), { harness: "gemini" });
@@ -2,8 +2,8 @@
2
2
  import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
3
  import {
4
4
  run
5
- } from "./chunk-QUHNXHVG.js";
6
- import "./chunk-RSIX7C6D.js";
5
+ } from "./chunk-4M5SZZ5P.js";
6
+ import "./chunk-RU7XRN62.js";
7
7
 
8
8
  // src/bin/sandbox.ts
9
9
  run(process.argv.slice(2));
@@ -2,8 +2,8 @@
2
2
  import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
3
  import {
4
4
  run
5
- } from "./chunk-QUHNXHVG.js";
6
- import "./chunk-RSIX7C6D.js";
5
+ } from "./chunk-4M5SZZ5P.js";
6
+ import "./chunk-RU7XRN62.js";
7
7
 
8
8
  // src/bin/sbx-hide.ts
9
9
  run(process.argv.slice(2), { command: "hide" });
@@ -2,8 +2,8 @@
2
2
  import { createRequire as __createRequire } from 'node:module'; const require = __createRequire(import.meta.url);
3
3
  import {
4
4
  run
5
- } from "./chunk-QUHNXHVG.js";
6
- import "./chunk-RSIX7C6D.js";
5
+ } from "./chunk-4M5SZZ5P.js";
6
+ import "./chunk-RU7XRN62.js";
7
7
 
8
8
  // src/bin/sbx-show.ts
9
9
  run(process.argv.slice(2), { command: "show" });
@@ -8,6 +8,7 @@ import { errMsg } from '../../core/util/index.js';
8
8
  import { SandboxChannel } from '../../host/channel/index.js';
9
9
  import { RunDir } from '../../shared/channel/index.js';
10
10
  import { isSandboxRunning } from '../../host/sandbox/index.js';
11
+ import { reloadSettings } from '../../app/context.js';
11
12
  import { lookAtSandbox } from '../commands/shared-use.js';
12
13
  import { commandLine } from '../history.js';
13
14
  import { commandSpec } from '../manifest.js';
@@ -98,7 +99,11 @@ export class Menu {
98
99
  new SettingsPage(app, files, draft, ctx),
99
100
  new AccountsPage(app, ctx),
100
101
  new MaintenancePage(app, ctx),
101
- ], draft, (saved) => this.applyAgentCommands(saved));
102
+ ], draft, (saved) => {
103
+ // Ctrl+S wrote the file: every page and operation reads the new settings from here on.
104
+ reloadSettings(this.ctx);
105
+ return this.applyAgentCommands(saved);
106
+ });
102
107
  }
103
108
  /** Runs the menu until you leave it, or until a command that takes over the terminal (attach) has run. */
104
109
  async start() {
@@ -7,12 +7,16 @@ import { readQuery } from '../../../app/registry.js';
7
7
  import { MenuOperations } from '../io-menu.js';
8
8
  import { menuView } from '../render-view.js';
9
9
  import { ListPage } from './list-page.js';
10
+ /** The services the menu lists: those whose sign-in is written (the others would offer actions that cannot work). */
11
+ function shown(m) {
12
+ return m.services.filter((s) => s.ready);
13
+ }
10
14
  /** The menu's rows, keys and Enter labels of one agent's category. */
11
15
  function viewOf(m, category) {
12
16
  const agent = m.agents.find((a) => a.name === category);
13
17
  if (agent)
14
18
  return menuView(agentPage(agent), { category: agent.name, refresh: false });
15
- const service = m.services.find((s) => serviceCategory(s.kind) === category);
19
+ const service = shown(m).find((s) => serviceCategory(s.kind) === category);
16
20
  if (service)
17
21
  return menuView(servicePage(service), { category: serviceCategory(service.kind), refresh: false });
18
22
  return menuView({ title: 'Accounts', sections: [] }, { refresh: false });
@@ -21,7 +25,7 @@ function viewOf(m, category) {
21
25
  export function accountsOptions(m) {
22
26
  return {
23
27
  path: [],
24
- intro: 'Logins saved on this PC, per agent (Use puts one into the sandbox; new launches start with the default), and the service sign-ins sandboxes may get.',
28
+ intro: 'Logins saved on this PC, per agent, and the service sign-ins sandboxes may get.',
25
29
  categories: [
26
30
  ...m.agents.map((a) => ({
27
31
  id: a.name,
@@ -30,15 +34,15 @@ export function accountsOptions(m) {
30
34
  ? 'Saved ' + a.name + ' logins.'
31
35
  : "This agent's login is not kept between sandboxes: it cannot be pointed at a folder we control.",
32
36
  })),
33
- ...m.services.map((s) => ({
37
+ ...shown(m).map((s) => ({
34
38
  id: serviceCategory(s.kind),
35
39
  label: s.label,
36
- description: 'Your ' + s.label + ' sign-in for sandboxes that use ' + s.usedFor + '.',
40
+ description: 'For ' + s.usedFor + '.',
37
41
  })),
38
42
  ],
39
43
  items: [
40
44
  ...m.agents.flatMap((a) => menuView(agentPage(a), { category: a.name, refresh: false }).items),
41
- ...m.services.flatMap((s) => menuView(servicePage(s), { category: serviceCategory(s.kind), refresh: false }).items),
45
+ ...shown(m).flatMap((s) => menuView(servicePage(s), { category: serviceCategory(s.kind), refresh: false }).items),
42
46
  ],
43
47
  empty: 'No agents.',
44
48
  actions: (category) => viewOf(m, category).actions,
@@ -3,6 +3,7 @@
3
3
  import fs from 'node:fs';
4
4
  import path from 'node:path';
5
5
  import { appsToInstall, previewGuideLine } from '../../shared/apps/index.js';
6
+ import { asBackground } from '../../shared/concurrency/index.js';
6
7
  import { downloader, moveIntoPlace, stagingFolder } from '../../shared/download/index.js';
7
8
  import { safeRemove } from '../../shared/io/safe-remove.js';
8
9
  import { dryRun, launchDir } from '../runtime/paths.js';
@@ -28,8 +29,8 @@ const MARKER = '.agent-sandbox-app';
28
29
  function prefetchedFile(entry, fileName) {
29
30
  return path.join(tool('apps', ...entry.id.split('/')) + '.download', fileName.replace(/[\\/:*?"<>|]/g, '_'));
30
31
  }
31
- /** How many apps download and unpack at once (they are big: a few at a time keeps the disk and network busy). */
32
- const PARALLEL = 3;
32
+ /** How many apps unpack at once (as many as the heavy pool holds: more would only queue up behind it). */
33
+ const PARALLEL = 2;
33
34
  export class AppInstaller {
34
35
  env;
35
36
  options;
@@ -53,7 +54,8 @@ export class AppInstaller {
53
54
  !('npm' in e.source) &&
54
55
  e.id !== '7zip' &&
55
56
  (this.options.refresh || !exists(path.join(tool('apps', ...e.id.split('/')), MARKER))));
56
- await Promise.all(todo.map(async (entry) => {
57
+ // Behind the downloads other steps need now (the runtimes, Visual Studio): these only save time for the install.
58
+ await asBackground(() => Promise.all(todo.map(async (entry) => {
57
59
  try {
58
60
  const d = await resolve(entry);
59
61
  const file = prefetchedFile(entry, d.fileName);
@@ -67,7 +69,7 @@ export class AppInstaller {
67
69
  catch {
68
70
  /* the install resolves and downloads again, and reports the reason if it fails too */
69
71
  }
70
- }));
72
+ })));
71
73
  }
72
74
  /** Installs the entries (7-Zip first: the NSIS apps need it to unpack), then their shortcuts. */
73
75
  async installAll(entries) {
@@ -124,7 +126,7 @@ export class AppInstaller {
124
126
  throw new Error(entry.exe + ' not found in the download');
125
127
  await afterInstall(entry, exe);
126
128
  // A proxy or MCP server (agent add-ons) is started by the sandbox or the agent: no PATH entry, no shortcut.
127
- if (!entry.service && (entry.cli || entry.id === 'sysinternals')) {
129
+ if (!entry.service && (entry.cli || entry.onPath || entry.id === 'sysinternals')) {
128
130
  // rustup's rust-analyzer proxy (in cargo's bin folder) only errors until its component is added.
129
131
  if (entry.id === 'rust-analyzer')
130
132
  this.env.prepend.unshift(path.dirname(exe));
@@ -9,7 +9,7 @@ import { installedCometExe } from '../browser/comet.js';
9
9
  import { execAsync } from '../runtime/process.js';
10
10
  import { heavy } from '../../shared/concurrency/index.js';
11
11
  import { mkdirp } from '../runtime/fs.js';
12
- import { signedBy } from '../toolchains/signature.js';
12
+ import { signedByAsync } from '../toolchains/signature.js';
13
13
  /** The archives inside, last stage first: the first one found is opened next. */
14
14
  const INNER = [
15
15
  /^chrome\.7z$/i,
@@ -118,7 +118,7 @@ export async function installFromKept(dest, signer, what) {
118
118
  const installed = installedCometExe();
119
119
  if (fs.existsSync(installed))
120
120
  return installed;
121
- if (!signedBy(installer, signer)) {
121
+ if (!(await signedByAsync(installer, signer))) {
122
122
  fs.rmSync(path.join(dest, '.agent-sandbox-app'), { force: true });
123
123
  fs.rmSync(installer, { force: true });
124
124
  throw new Error(what + "'s installer is not signed by its vendor; deleted it (it is downloaded again next boot)");
@@ -1,10 +1,10 @@
1
- import { type SpawnOptions } from 'node:child_process';
2
1
  import { heavy, installer, net } from '../../shared/concurrency/index.js';
2
+ import { lowerPriority, type SpawnAsyncOptions } from '../../shared/download/index.js';
3
3
  export { net, heavy, installer };
4
4
  /** Called at the start of the boot and of the daemon: the downloader and the extractor use the pools from then on. */
5
5
  export declare function installPools(): void;
6
6
  /** Lowers a process (and the children it starts from now on) to below-normal priority; best effort. */
7
- export declare function belowNormal(pid: number | undefined): void;
7
+ export declare const belowNormal: typeof lowerPriority;
8
8
  export interface InstallerOptions {
9
9
  /** Below-normal priority for the installer and what it starts (the background Visual Studio). */
10
10
  readonly lowPriority?: boolean;
@@ -20,5 +20,5 @@ export interface InstallerOptions {
20
20
  export declare function runInstaller(file: string, args: string[], o?: InstallerOptions): Promise<number>;
21
21
  /** runInstaller that throws like execAsync when the installer fails. */
22
22
  export declare function runInstallerChecked(file: string, args: string[], o?: InstallerOptions): Promise<void>;
23
- /** execAsync for heavy local work (npm, uv, rustup ...): one of the two heavy slots. */
24
- export declare function execHeavy(file: string, args: string[], options?: SpawnOptions): Promise<void>;
23
+ /** execAsync for heavy local work (npm, uv, rustup ...): one of the two heavy slots, at below-normal priority. */
24
+ export declare function execHeavy(file: string, args: string[], options?: SpawnAsyncOptions): Promise<void>;
@@ -1,10 +1,9 @@
1
1
  // The guest's work pools: network bodies, heavy local work and installers. A task takes one slot at the leaf (the
2
2
  // call that downloads, unpacks or installs), never while it holds another: the limiter refuses nested pools.
3
- import os from 'node:os';
4
3
  import { spawn } from 'node:child_process';
5
4
  import path from 'node:path';
6
5
  import { heavy, installer, net, INSTALLER_WAIT_MS, installerRetry } from '../../shared/concurrency/index.js';
7
- import { downloader, limitExtraction } from '../../shared/download/index.js';
6
+ import { downloader, limitExtraction, lowerPriority } from '../../shared/download/index.js';
8
7
  import { log } from './log.js';
9
8
  import { execAsync } from './process.js';
10
9
  export { net, heavy, installer };
@@ -13,18 +12,8 @@ export function installPools() {
13
12
  downloader.limit = net;
14
13
  limitExtraction(heavy);
15
14
  }
16
- const BELOW_NORMAL = os.constants.priority.PRIORITY_BELOW_NORMAL;
17
15
  /** Lowers a process (and the children it starts from now on) to below-normal priority; best effort. */
18
- export function belowNormal(pid) {
19
- if (pid === undefined)
20
- return;
21
- try {
22
- os.setPriority(pid, BELOW_NORMAL);
23
- }
24
- catch {
25
- /* the process may have ended already; a normal priority only makes it a little slower to share the machine */
26
- }
27
- }
16
+ export const belowNormal = lowerPriority;
28
17
  /**
29
18
  * Runs an installer in the one installer slot and starts it again while another process holds the Windows Installer
30
19
  * lock (exit 1618). Resolves with the exit code of the last try.
@@ -54,7 +43,7 @@ export async function runInstallerChecked(file, args, o = {}) {
54
43
  if (code !== 0)
55
44
  throw new Error(path.basename(file) + ' ' + args.slice(0, 3).join(' ') + ' failed with exit code ' + code);
56
45
  }
57
- /** execAsync for heavy local work (npm, uv, rustup ...): one of the two heavy slots. */
46
+ /** execAsync for heavy local work (npm, uv, rustup ...): one of the two heavy slots, at below-normal priority. */
58
47
  export function execHeavy(file, args, options = {}) {
59
- return heavy.run(() => execAsync(file, args, options));
48
+ return heavy.run(() => execAsync(file, args, { ...options, lowPriority: true }));
60
49
  }
@@ -1,4 +1,5 @@
1
- import { type SpawnOptions, type SpawnSyncOptions } from 'node:child_process';
1
+ import { type SpawnSyncOptions } from 'node:child_process';
2
+ import { type SpawnAsyncOptions } from '../../shared/download/index.js';
2
3
  /** Takes the output of the programs started while the boot draws its progress (boot/boot-console.ts). */
3
4
  export interface OutputSink {
4
5
  /** Whether programs started now hand their output over instead of writing to the console. */
@@ -11,7 +12,7 @@ export declare function routeOutput(sink: OutputSink | null): void;
11
12
  /** Runs a program, streaming output, and throws with a readable message when it fails. */
12
13
  export declare function exec(file: string, args: string[], options?: SpawnSyncOptions): void;
13
14
  /** exec without blocking: other toolchains keep downloading while this one runs an installer. */
14
- export declare function execAsync(file: string, args: string[], options?: SpawnOptions): Promise<void>;
15
+ export declare function execAsync(file: string, args: string[], options?: SpawnAsyncOptions): Promise<void>;
15
16
  export declare function execOutput(file: string, args: string[], options?: SpawnSyncOptions): string;
16
17
  /** Runs sandbox\scripts\<name> (a .ps1 in the launch folder) with `args`; throws when it fails. */
17
18
  export declare function runScript(name: string, args?: readonly string[]): void;
@@ -19,6 +20,8 @@ export declare function runScript(name: string, args?: readonly string[]): void;
19
20
  export declare function runScriptAsync(name: string, args?: readonly string[]): Promise<void>;
20
21
  /** runScript for what it prints ('' when it fails). */
21
22
  export declare function scriptOutput(name: string, args?: readonly string[], options?: SpawnSyncOptions): string;
23
+ /** scriptOutput without blocking the event loop ('' when the script fails). */
24
+ export declare function scriptOutputAsync(name: string, args?: readonly string[]): Promise<string>;
22
25
  /** Runs sandbox\scripts\<name> with a JSON file (-<param> path) holding `data`, removed afterwards. */
23
26
  export declare function runScriptWithJson(name: string, param: string, data: unknown, args?: readonly string[]): void;
24
27
  /** runScriptWithJson without blocking the event loop. */
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { spawn, spawnSync } from 'node:child_process';
5
- import { moveIntoPlace, spawnAsync, stagingFolder } from '../../shared/download/index.js';
5
+ import { lowerPriority, moveIntoPlace, spawnAsync, stagingFolder, } from '../../shared/download/index.js';
6
6
  import { safeRemove } from '../../shared/io/safe-remove.js';
7
7
  import { robocopyErrors, robocopyFailed, robocopyMeaning } from '../../shared/io/robocopy-result.js';
8
8
  import { SYSTEM } from '../../shared/system.js';
@@ -57,15 +57,18 @@ export function exec(file, args, options = {}) {
57
57
  }
58
58
  /** exec without blocking: other toolchains keep downloading while this one runs an installer. */
59
59
  export async function execAsync(file, args, options = {}) {
60
+ const { lowPriority, ...spawnOptions } = options;
60
61
  const code = captureOutput()
61
62
  ? await new Promise((resolve, reject) => {
62
- const child = spawn(file, args, { stdio: ['inherit', 'pipe', 'pipe'], windowsHide: true, ...options });
63
+ const child = spawn(file, args, { stdio: ['inherit', 'pipe', 'pipe'], windowsHide: true, ...spawnOptions });
64
+ if (lowPriority)
65
+ lowerPriority(child.pid);
63
66
  logLines(child.stdout);
64
67
  logLines(child.stderr);
65
68
  child.once('error', reject);
66
69
  child.once('close', (c) => resolve(c ?? -1));
67
70
  })
68
- : await spawnAsync(file, args, { stdio: 'inherit', ...options });
71
+ : await spawnAsync(file, args, { stdio: 'inherit', ...spawnOptions, ...(lowPriority ? { lowPriority } : {}) });
69
72
  if (code !== 0)
70
73
  throw new Error(path.basename(file) + ' ' + args.slice(0, 3).join(' ') + ' failed with exit code ' + code);
71
74
  }
@@ -131,6 +134,23 @@ export function runScriptAsync(name, args = []) {
131
134
  export function scriptOutput(name, args = [], options = {}) {
132
135
  return execOutput(SYSTEM.powershell(), scriptArgs(name, args), options);
133
136
  }
137
+ /** scriptOutput without blocking the event loop ('' when the script fails). */
138
+ export function scriptOutputAsync(name, args = []) {
139
+ return new Promise((resolve) => {
140
+ const child = spawn(SYSTEM.powershell(), scriptArgs(name, args), {
141
+ stdio: ['ignore', 'pipe', 'ignore'],
142
+ windowsHide: true,
143
+ });
144
+ let out = '';
145
+ child.stdout?.setEncoding('utf8');
146
+ child.stdout?.on('data', (chunk) => {
147
+ if (out.length < CAPTURE_BYTES)
148
+ out += chunk;
149
+ });
150
+ child.once('error', () => resolve(''));
151
+ child.once('close', (code) => resolve(code === 0 ? out.trim() : ''));
152
+ });
153
+ }
134
154
  /** Runs sandbox\scripts\<name> with a JSON file (-<param> path) holding `data`, removed afterwards. */
135
155
  export function runScriptWithJson(name, param, data, args = []) {
136
156
  const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sbx-'));
@@ -1,3 +1,4 @@
1
+ import type { PcInstall } from '../../../shared/paths/pc-installs.js';
1
2
  import { type LauncherEntry } from '../../../shared/unreal/index.js';
2
3
  /** The launcher's program: newer MSIs install into Program Files, older ones into Program Files (x86). */
3
4
  export declare function launcherExe(): string | null;
@@ -17,6 +18,11 @@ export declare function placeEngines(persist: boolean): LauncherPlaceResult;
17
18
  export declare function savedEntries(): LauncherEntry[];
18
19
  /** Puts the saved manifests back where the launcher reads them; the engine folders they name. */
19
20
  export declare function restoreLauncherItems(): string[];
21
+ /**
22
+ * The manifests of this PC's engines (session.json) where the launcher reads them: without one it drops the engine
23
+ * from LauncherInstalled.dat when it starts. Only a manifest of the engine's own folder; one of ours is replaced.
24
+ */
25
+ export declare function writePcLauncherItems(items: readonly Pick<PcInstall, 'kind' | 'guest' | 'launcherItem'>[]): number;
20
26
  /** Merges these entries into the launcher's LauncherInstalled.dat (foreign entries kept). */
21
27
  export declare function writeLauncherEntries(entries: readonly LauncherEntry[]): void;
22
28
  /**
@@ -9,7 +9,7 @@ import { z } from 'zod';
9
9
  import { readBoundedFile } from '../../../shared/io/bounded.js';
10
10
  import { readValid, writeJsonAtomic } from '../../../shared/io/json.js';
11
11
  import { SYSTEM } from '../../../shared/system.js';
12
- import { enginePlaceProblem, ENGINE_NEED_BYTES, entriesUnder, launcherInstalledFile, launcherManifestsDir, launcherSettingsFiles, MAX_LAUNCHER_FILE_BYTES, mergeLauncherInstalled, restoreItems, saveItems, setLauncherInstallLocation, } from '../../../shared/unreal/index.js';
12
+ import { enginePlaceProblem, ENGINE_NEED_BYTES, entriesUnder, itemInstallLocation, ITEM_NAME, launcherInstalledFile, launcherManifestsDir, launcherSettingsFiles, MAX_LAUNCHER_FILE_BYTES, mergeLauncherInstalled, restoreItems, saveItems, setLauncherInstallLocation, } from '../../../shared/unreal/index.js';
13
13
  import { errMsg } from '../../../core/util/errors.js';
14
14
  import { runInstaller } from '../../runtime/pools.js';
15
15
  import { exists, mkdirp } from '../../runtime/fs.js';
@@ -93,6 +93,24 @@ export function savedEntries() {
93
93
  export function restoreLauncherItems() {
94
94
  return restoreItems(SAVED_MANIFESTS, launcherManifestsDir(programData()), ENGINES_DIR, exists);
95
95
  }
96
+ /**
97
+ * The manifests of this PC's engines (session.json) where the launcher reads them: without one it drops the engine
98
+ * from LauncherInstalled.dat when it starts. Only a manifest of the engine's own folder; one of ours is replaced.
99
+ */
100
+ export function writePcLauncherItems(items) {
101
+ const dir = launcherManifestsDir(programData());
102
+ let written = 0;
103
+ for (const i of items) {
104
+ if (i.kind !== 'unreal' || !i.launcherItem || !ITEM_NAME.test(i.launcherItem.name))
105
+ continue;
106
+ const at = itemInstallLocation(i.launcherItem.text);
107
+ if (!at || path.win32.resolve(at).toLowerCase() !== path.win32.resolve(i.guest).toLowerCase())
108
+ continue;
109
+ fs.writeFileSync(path.join(mkdirp(dir), i.launcherItem.name), i.launcherItem.text, 'utf8');
110
+ written++;
111
+ }
112
+ return written;
113
+ }
96
114
  /** Merges these entries into the launcher's LauncherInstalled.dat (foreign entries kept). */
97
115
  export function writeLauncherEntries(entries) {
98
116
  if (!entries.length)
@@ -16,7 +16,7 @@ import { runScriptWithJsonAsync } from '../../runtime/process.js';
16
16
  import { projectWorkspace } from '../../runtime/project-context.js';
17
17
  import { session } from '../../runtime/session.js';
18
18
  import { setUnrealMcp, writeAddonMcpServers } from '../../setup/addon-mcp.js';
19
- import { saveLauncherState, savedEntries, writeLauncherEntries } from './epic-launcher.js';
19
+ import { saveLauncherState, savedEntries, writeLauncherEntries, writePcLauncherItems } from './epic-launcher.js';
20
20
  import { DDC_DIR, ENGINES_DIR, SAVED_DIR, updateUnrealStatus } from './unreal-status.js';
21
21
  const SAVED_MANIFESTS = path.join(SAVED_DIR, 'Manifests');
22
22
  /** The real disk, read bounded. */
@@ -76,6 +76,7 @@ export async function registerEngines(engines, selectorFrom, ctx, aliases = [])
76
76
  if (values.length)
77
77
  await runScriptWithJsonAsync('set-registry-values.ps1', 'PlanFile', { values });
78
78
  try {
79
+ writePcLauncherItems(ctx.pcInstalls.items);
79
80
  writeLauncherEntries(launcherEntries(engines, ctx));
80
81
  }
81
82
  catch (e) {
@@ -1,8 +1,8 @@
1
1
  import { SIGNERS, type AppSigner } from '../../shared/signer.js';
2
- import { signedBy } from './signature.js';
2
+ import { signedBy, signedByAsync } from './signature.js';
3
3
  export { SIGNERS };
4
- export { signedBy };
4
+ export { signedBy, signedByAsync };
5
5
  /** A downloader `verify` hook: refuses a file not signed by `signer` (it counts as integrity for requireChecksum). */
6
- export declare function signatureCheck(signer: AppSigner, what: string): (file: string) => void;
6
+ export declare function signatureCheck(signer: AppSigner, what: string): (file: string) => Promise<void>;
7
7
  /** The installer at `file` (downloaded once from `url`), checked against its signer and, when given, its SHA-256. */
8
8
  export declare function signedInstaller(url: string, file: string, what: string, signer: AppSigner, sha256?: string | null): Promise<string>;
@@ -6,13 +6,13 @@ import { downloader } from '../../shared/download/index.js';
6
6
  import { exists, mkdirp } from '../runtime/fs.js';
7
7
  import { log } from '../runtime/log.js';
8
8
  import { SIGNERS } from '../../shared/signer.js';
9
- import { signedBy } from './signature.js';
9
+ import { signedBy, signedByAsync } from './signature.js';
10
10
  export { SIGNERS };
11
- export { signedBy };
11
+ export { signedBy, signedByAsync };
12
12
  /** A downloader `verify` hook: refuses a file not signed by `signer` (it counts as integrity for requireChecksum). */
13
13
  export function signatureCheck(signer, what) {
14
- return (file) => {
15
- if (!signedBy(file, signer))
14
+ return async (file) => {
15
+ if (!(await signedByAsync(file, signer)))
16
16
  throw new Error(what + ' is not signed by its vendor');
17
17
  };
18
18
  }
@@ -23,7 +23,7 @@ export async function signedInstaller(url, file, what, signer, sha256 = null) {
23
23
  mkdirp(path.dirname(file));
24
24
  await downloader.file(url, file, { sha256, verify: signatureCheck(signer, path.basename(file)) });
25
25
  }
26
- if (!signedBy(file, signer)) {
26
+ if (!(await signedByAsync(file, signer))) {
27
27
  fs.rmSync(file, { force: true });
28
28
  throw new Error(path.basename(file) + ' is not signed by its vendor; deleted it (it is downloaded again next boot)');
29
29
  }
@@ -6,7 +6,7 @@ import { dryRun } from '../../runtime/paths.js';
6
6
  import { exists } from '../../runtime/fs.js';
7
7
  import { log } from '../../runtime/log.js';
8
8
  import { downloadFile } from '../install.js';
9
- import { SIGNERS, signedBy } from '../installers.js';
9
+ import { SIGNERS, signedByAsync } from '../installers.js';
10
10
  import { tool } from '../tool-env.js';
11
11
  const VC_REDIST = 'https://aka.ms/vs/17/release/vc_redist.x64.exe';
12
12
  /** 1638: a newer version is already installed; 3010: installed, a restart would finish it (not needed for the DLLs). */
@@ -37,7 +37,7 @@ export async function setupVcRuntime(env, summary = true) {
37
37
  log('Downloading the Visual C++ runtime (~25 MB once) ...');
38
38
  await downloadFile(VC_REDIST, installer);
39
39
  }
40
- if (!signedBy(installer, SIGNERS.microsoft)) {
40
+ if (!(await signedByAsync(installer, SIGNERS.microsoft))) {
41
41
  fs.rmSync(installer, { force: true });
42
42
  throw new Error('vc_redist.x64.exe is not signed by Microsoft; deleted it (it is downloaded again next boot)');
43
43
  }
@@ -71,6 +71,7 @@ export declare const VsPersistMarkerSchema: z.ZodObject<{
71
71
  installationPath: z.ZodString;
72
72
  sdkVersion: z.ZodString;
73
73
  folders: z.ZodArray<z.ZodEnum<{
74
+ Shared: "Shared";
74
75
  Community: "Community";
75
76
  BuildTools: "BuildTools";
76
77
  WindowsKits10: "WindowsKits10";
@@ -78,7 +79,6 @@ export declare const VsPersistMarkerSchema: z.ZodObject<{
78
79
  "MicrosoftSDKs-v10.0A": "MicrosoftSDKs-v10.0A";
79
80
  ReferenceAssemblies: "ReferenceAssemblies";
80
81
  Installer: "Installer";
81
- Shared: "Shared";
82
82
  Setup: "Setup";
83
83
  instance: "instance";
84
84
  }>>;
@@ -102,6 +102,7 @@ export type VsPersistMarker = z.infer<typeof VsPersistMarkerSchema>;
102
102
  /** What save-visual-studio.ps1 writes to its result file. */
103
103
  export declare const SaveResultSchema: z.ZodObject<{
104
104
  copied: z.ZodArray<z.ZodEnum<{
105
+ Shared: "Shared";
105
106
  Community: "Community";
106
107
  BuildTools: "BuildTools";
107
108
  WindowsKits10: "WindowsKits10";
@@ -109,7 +110,6 @@ export declare const SaveResultSchema: z.ZodObject<{
109
110
  "MicrosoftSDKs-v10.0A": "MicrosoftSDKs-v10.0A";
110
111
  ReferenceAssemblies: "ReferenceAssemblies";
111
112
  Installer: "Installer";
112
- Shared: "Shared";
113
113
  Setup: "Setup";
114
114
  instance: "instance";
115
115
  }>>;
@@ -3,3 +3,5 @@ import { type AppSigner, type Signature } from '../../shared/signer.js';
3
3
  export declare function signatureOf(file: string): Signature | null;
4
4
  /** Whether a file's valid signature is the expected signer's. */
5
5
  export declare function signedBy(file: string, signer: AppSigner): boolean;
6
+ /** signedBy without blocking the event loop: a boot check of a big download must not stall the other tasks. */
7
+ export declare function signedByAsync(file: string, signer: AppSigner): Promise<boolean>;
@@ -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
- const text = scriptOutput('get-signature.ps1', ['-Path', file]).trim();
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 = 4;
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
- /** The first time a shared kind has nothing saved: this PC's own sign-in, saved like `sandbox account import`. */
26
- function takeFromPc(kind, pc, store) {
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 null;
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 null; // This PC's program has no usable sign-in: the note says how to save one.
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 (!payload && !unreadable)
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) {