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.
Files changed (121) 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-RSIX7C6D.js → chunk-CB44HTTN.js} +444 -175
  14. package/dist/bin/{chunk-4TLSWPG7.js → chunk-UZ5S3EMB.js} +1 -1
  15. package/dist/bin/{chunk-QUHNXHVG.js → chunk-ZMZI3625.js} +46 -41
  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/core/schema/sandbox.js +1 -1
  26. package/dist/guest/apps/app-installer.js +7 -5
  27. package/dist/guest/apps/comet-unpack.js +2 -2
  28. package/dist/guest/daemon/heartbeat.js +1 -0
  29. package/dist/guest/governor/core.d.ts +53 -1
  30. package/dist/guest/governor/core.js +95 -1
  31. package/dist/guest/governor/decide.d.ts +4 -0
  32. package/dist/guest/governor/decide.js +2 -0
  33. package/dist/guest/governor/governor.js +23 -9
  34. package/dist/guest/runtime/pools.d.ts +4 -4
  35. package/dist/guest/runtime/pools.js +4 -15
  36. package/dist/guest/runtime/process.d.ts +5 -2
  37. package/dist/guest/runtime/process.js +23 -3
  38. package/dist/guest/toolchains/engines/epic-launcher.d.ts +6 -0
  39. package/dist/guest/toolchains/engines/epic-launcher.js +19 -1
  40. package/dist/guest/toolchains/engines/unreal-engines.js +2 -1
  41. package/dist/guest/toolchains/installers.d.ts +3 -3
  42. package/dist/guest/toolchains/installers.js +5 -5
  43. package/dist/guest/toolchains/native/unreal.js +22 -3
  44. package/dist/guest/toolchains/native/vcruntime.js +2 -2
  45. package/dist/guest/toolchains/native/vs-persist.d.ts +2 -2
  46. package/dist/guest/toolchains/runtimes.js +7 -6
  47. package/dist/guest/toolchains/signature.d.ts +2 -0
  48. package/dist/guest/toolchains/signature.js +9 -2
  49. package/dist/guest/toolchains/step-scheduler.js +1 -1
  50. package/dist/host/accounts/deliver.js +15 -7
  51. package/dist/host/life/crash-report.d.ts +4 -0
  52. package/dist/host/life/crash-report.js +30 -1
  53. package/dist/host/life/last-end.js +1 -0
  54. package/dist/host/pc-installs/discover.js +5 -2
  55. package/dist/host/pc-installs/unreal.d.ts +3 -1
  56. package/dist/host/pc-installs/unreal.js +27 -1
  57. package/dist/host/tools/windows-terminal.d.ts +4 -1
  58. package/dist/host/tools/windows-terminal.js +25 -9
  59. package/dist/runtime/agent.js +39 -23
  60. package/dist/runtime/daemon.js +279 -121
  61. package/dist/runtime/db.js +37 -17
  62. package/dist/runtime/main.js +340 -109
  63. package/dist/runtime/netproxy.js +31 -13
  64. package/dist/runtime/tool.js +37 -16
  65. package/dist/runtime/view.js +150 -38
  66. package/dist/selftest/agent-paths.test.js +3 -1
  67. package/dist/selftest/app.test.js +20 -2
  68. package/dist/selftest/boot-runner.test.js +15 -1
  69. package/dist/selftest/channel.test.js +33 -0
  70. package/dist/selftest/engines.test.js +22 -1
  71. package/dist/selftest/heavy-load.test.js +116 -4
  72. package/dist/selftest/live-terminal.test.js +103 -0
  73. package/dist/selftest/menu-screens.js +52 -1
  74. package/dist/selftest/menu.test.js +29 -3
  75. package/dist/selftest/personalization.test.js +2 -1
  76. package/dist/selftest/service-accounts.test.js +77 -2
  77. package/dist/selftest/support/engine-golden.js +2 -0
  78. package/dist/selftest/ui-text.test.js +4 -0
  79. package/dist/shared/accounts/kinds.js +9 -9
  80. package/dist/shared/apps/catalogue-gamedev.js +1 -0
  81. package/dist/shared/apps/catalogue.d.ts +2 -0
  82. package/dist/shared/channel/schema.d.ts +2 -0
  83. package/dist/shared/channel/schema.js +4 -0
  84. package/dist/shared/concurrency/pools.js +1 -1
  85. package/dist/shared/download/archive.js +4 -2
  86. package/dist/shared/download/index.d.ts +1 -1
  87. package/dist/shared/download/index.js +1 -1
  88. package/dist/shared/download/spawn.d.ts +7 -1
  89. package/dist/shared/download/spawn.js +16 -1
  90. package/dist/shared/memory/index.d.ts +1 -0
  91. package/dist/shared/memory/index.js +1 -0
  92. package/dist/shared/memory/policy.d.ts +12 -0
  93. package/dist/shared/memory/policy.js +4 -0
  94. package/dist/shared/memory/unreal-build.d.ts +2 -0
  95. package/dist/shared/memory/unreal-build.js +21 -0
  96. package/dist/shared/paths/pc-installs.d.ts +8 -0
  97. package/dist/shared/pty/clipboard-watch.d.ts +4 -0
  98. package/dist/shared/pty/clipboard-watch.js +40 -0
  99. package/dist/shared/pty/ctrl-c.d.ts +13 -7
  100. package/dist/shared/pty/ctrl-c.js +35 -19
  101. package/dist/shared/pty/index.d.ts +1 -0
  102. package/dist/shared/pty/index.js +1 -0
  103. package/dist/shared/pty/terminal.d.ts +1 -1
  104. package/dist/shared/pty/terminal.js +16 -13
  105. package/dist/shared/unreal/launcher-files.d.ts +7 -0
  106. package/dist/shared/unreal/launcher-files.js +32 -1
  107. package/dist/shared/web/ctrl-c.d.ts +26 -0
  108. package/dist/shared/web/ctrl-c.js +64 -0
  109. package/dist/shared/web/index.d.ts +1 -0
  110. package/dist/shared/web/index.js +1 -0
  111. package/dist/web/index.html +1 -1
  112. package/dist/web/{main-VPMYTSUL.js → main-7NY4X6YU.js} +67 -67
  113. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  114. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  115. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  116. package/package.json +1 -1
  117. package/sandbox/csharp/ClipboardWatch.cs +59 -0
  118. package/sandbox/csharp/MemGov.cs +752 -7
  119. package/sandbox/scripts/watch-clipboard.ps1 +8 -0
  120. package/schemas/config.schema.json +10 -10
  121. package/schemas/sandbox-config.schema.json +9 -9
@@ -1,6 +1,94 @@
1
1
  import { now } from '../../shared/channel/run-dir.js';
2
+ import { z } from 'zod';
3
+ import { BURST_GATE } from '../../shared/memory/index.js';
2
4
  import { errMsg } from '../../core/util/errors.js';
3
- import { decide, HOLD_MS, INITIAL_STATE } from './decide.js';
5
+ import { decide, GATE_NAMES, HOLD_MS, INITIAL_STATE, } from './decide.js';
6
+ /** memgov.exe's arguments: samples every 2 s and the memory gate for this profile. */
7
+ export function memgovArgs(profile) {
8
+ const g = BURST_GATE[profile];
9
+ return [
10
+ 'serve',
11
+ '--interval',
12
+ '2',
13
+ '--gate',
14
+ [g.growthMb, g.windowMs, g.releaseMs, g.maxHoldMs].join(':'),
15
+ '--gate-names',
16
+ GATE_NAMES.join(','),
17
+ ];
18
+ }
19
+ const GateBase = { event: z.literal('gate'), at: z.number() };
20
+ const GateName = z.string().max(128);
21
+ const GateCount = z.number().int().min(0);
22
+ /** What the memory gate in memgov reports on its stdout (one JSON line each). */
23
+ export const GateEventSchema = z.discriminatedUnion('kind', [
24
+ z.strictObject({
25
+ ...GateBase,
26
+ kind: z.literal('ready'),
27
+ mode: z.enum(['notify', 'poll']),
28
+ growthMb: GateCount,
29
+ windowMs: GateCount,
30
+ }),
31
+ z.strictObject({ ...GateBase, kind: z.literal('off'), reason: z.string().max(200) }),
32
+ z.strictObject({
33
+ ...GateBase,
34
+ kind: z.literal('trip'),
35
+ grewMb: GateCount,
36
+ ms: GateCount,
37
+ jobMb: GateCount,
38
+ held: z.array(z.strictObject({ pid: z.number().int(), name: GateName })).max(64),
39
+ }),
40
+ z.strictObject({ ...GateBase, kind: z.literal('hold'), pid: z.number().int(), name: GateName }),
41
+ z.strictObject({
42
+ ...GateBase,
43
+ kind: z.literal('release'),
44
+ pid: z.number().int(),
45
+ name: GateName,
46
+ heldMs: GateCount,
47
+ forced: z.boolean(),
48
+ }),
49
+ ]);
50
+ /** One memory-log line for a gate event (names cut to 64 characters). */
51
+ export function gateLine(e) {
52
+ const cut = (s) => s.slice(0, 64);
53
+ switch (e.kind) {
54
+ case 'ready':
55
+ return ('gate: on (' +
56
+ e.mode +
57
+ "): new compilers wait when the agents' programs grow by more than " +
58
+ e.growthMb +
59
+ ' MB within ' +
60
+ e.windowMs / 1000 +
61
+ ' s');
62
+ case 'off':
63
+ return 'gate: off (' + e.reason + '): builds are only slowed down by the dial';
64
+ case 'trip':
65
+ return ("gate: the agents' programs grew " +
66
+ e.grewMb +
67
+ ' MB in ' +
68
+ (e.ms / 1000).toFixed(1) +
69
+ ' s (now ' +
70
+ e.jobMb +
71
+ ' MB); holding ' +
72
+ e.held.length +
73
+ ': ' +
74
+ e.held
75
+ .slice(0, 8)
76
+ .map((p) => cut(p.name) + ' (' + p.pid + ')')
77
+ .join(', ') +
78
+ (e.held.length > 8 ? ' …' : ''));
79
+ case 'hold':
80
+ return 'gate: ' + cut(e.name) + ' (' + e.pid + ') waits at start while a build burst goes through in turn';
81
+ case 'release':
82
+ return ('gate: ' +
83
+ cut(e.name) +
84
+ ' (' +
85
+ e.pid +
86
+ ') goes on after ' +
87
+ (e.heldMs / 1000).toFixed(1) +
88
+ ' s' +
89
+ (e.forced ? ' (the longest it may wait)' : ''));
90
+ }
91
+ }
4
92
  const STEP_TEXT = [
5
93
  'builds back to normal',
6
94
  'build programs at lower priority',
@@ -55,6 +143,7 @@ export class GovernorCore {
55
143
  commitPercent: d.commitPercent,
56
144
  step: d.state.step,
57
145
  hostStep: nowMs < d.state.hostUntil ? d.state.hostStep : 0,
146
+ held: sample.gateHeld ?? 0,
58
147
  diskQueue: null,
59
148
  top: [...sample.procs]
60
149
  .sort((a, b) => b.mb - a.mb)
@@ -89,6 +178,7 @@ export class GovernorCore {
89
178
  paused: [...this.held]
90
179
  .slice(0, 8)
91
180
  .map(([pid, name]) => ({ name: name.slice(0, 64), pid, seconds: HOLD_MS / 1000 })),
181
+ held: sample.gateHeld ?? 0,
92
182
  cpuPercent: d.state.cpuRate,
93
183
  pcLow: nowMs < d.state.hostUntil,
94
184
  lastAction: this.lastAction.slice(0, 300),
@@ -102,6 +192,10 @@ export class GovernorCore {
102
192
  this.deps.saveStatus(status);
103
193
  }
104
194
  }
195
+ /** An event from the memory gate in memgov: goes to the memory log. */
196
+ onGate(event) {
197
+ this.note(gateLine(event));
198
+ }
105
199
  apply(action) {
106
200
  const port = this.deps.port;
107
201
  try {
@@ -16,6 +16,8 @@ export interface MemSample {
16
16
  readonly commitAvailMb: number;
17
17
  /** Only the processes in the agents' job. */
18
18
  readonly procs: readonly ProcSample[];
19
+ /** Build programs the memory gate holds now (memgov). */
20
+ readonly gateHeld?: number;
19
21
  }
20
22
  export declare const isNeverTouched: (name: string) => boolean;
21
23
  export declare const canLower: (name: string) => boolean;
@@ -24,6 +26,8 @@ export declare const isLinker: (name: string) => boolean;
24
26
  export declare function linkerSlots(totalMb: number, cpus: number): number;
25
27
  export declare const isHeavyCompiler: (name: string) => boolean;
26
28
  export declare const canPause: (name: string) => boolean;
29
+ /** What the memory gate in memgov may freeze (compilers, linkers, Unreal's shader workers); passed as --gate-names. */
30
+ export declare const GATE_NAMES: readonly string[];
27
31
  export type Level = 'ok' | 'throttled' | 'suspending';
28
32
  export interface GovernorState {
29
33
  /** The dial: 0 nothing wrong, 1 lower priority, 2 less CPU and fewer compilers, 3 and 4 harder. */
@@ -109,6 +109,8 @@ export function linkerSlots(totalMb, cpus) {
109
109
  }
110
110
  export const isHeavyCompiler = (name) => !isNeverTouched(name) && HEAVY.has(key(name));
111
111
  export const canPause = (name) => !isNeverTouched(name) && COMPILERS.has(key(name));
112
+ /** What the memory gate in memgov may freeze (compilers, linkers, Unreal's shader workers); passed as --gate-names. */
113
+ export const GATE_NAMES = [...COMPILERS, 'shadercompileworker'].filter((n) => !NEVER.has(n)).sort();
112
114
  export const INITIAL_STATE = {
113
115
  step: 0,
114
116
  calm: 0,
@@ -1,6 +1,7 @@
1
1
  // The memory governor: samples the sandbox's memory every 2 seconds (memgov.exe, sandbox/csharp/MemGov.cs) and turns
2
2
  // a dial as it fills: the build programs the agents started get lower priority, lower memory priority, less CPU, and
3
- // take turns (decide.ts). Builds slow down; nothing fails. What it does goes to the memory log and run\pressure.json.
3
+ // take turns (decide.ts). A memory gate in memgov also holds new compilers when memory grows faster than a few GB in
4
+ // seconds. Builds slow down; nothing fails. What it does goes to the memory log and run\pressure.json.
4
5
  import fs from 'node:fs';
5
6
  import os from 'node:os';
6
7
  import path from 'node:path';
@@ -8,14 +9,14 @@ import { spawn } from 'node:child_process';
8
9
  import { createInterface } from 'node:readline';
9
10
  import { z } from 'zod';
10
11
  import { writeJsonAtomic } from '../../shared/io/json.js';
11
- import { DIAL_STEPS, maxCompilers } from '../../shared/memory/index.js';
12
+ import { BURST_GATE, DIAL_STEPS, maxCompilers } from '../../shared/memory/index.js';
12
13
  import { ensureCompiled } from '../runtime/csc.js';
13
14
  import { dataDir } from '../runtime/paths.js';
14
15
  import { warn } from '../runtime/log.js';
15
16
  import { run } from '../runtime/paths.js';
16
17
  import { session } from '../runtime/session.js';
17
18
  import { errMsg } from '../../core/util/errors.js';
18
- import { GovernorCore } from './core.js';
19
+ import { GateEventSchema, GovernorCore, memgovArgs, } from './core.js';
19
20
  const SampleSchema = z.object({
20
21
  ramTotalMb: z.number(),
21
22
  ramAvailMb: z.number(),
@@ -29,27 +30,36 @@ const SampleSchema = z.object({
29
30
  prio: z.enum(['idle', 'below', 'normal']).catch('normal'),
30
31
  }))
31
32
  .max(1024),
33
+ gateHeld: z.number().int().min(0).max(1024).optional().catch(undefined),
32
34
  });
33
35
  /** memgov.exe as a child process: samples come out of its stdout, commands go into its stdin. */
34
36
  class MemGovProcess {
37
+ args;
35
38
  onSample;
39
+ onGate;
36
40
  child = null;
37
- constructor(onSample) {
41
+ constructor(args, onSample, onGate) {
42
+ this.args = args;
38
43
  this.onSample = onSample;
44
+ this.onGate = onGate;
39
45
  }
40
46
  start() {
41
47
  try {
42
- const child = spawn(ensureCompiled('memgov'), ['serve', '--interval', '2'], {
48
+ const child = spawn(ensureCompiled('memgov'), [...this.args], {
43
49
  stdio: ['pipe', 'pipe', 'ignore'],
44
50
  windowsHide: true,
45
51
  });
46
52
  this.child = child;
47
53
  createInterface({ input: child.stdout }).on('line', (line) => {
48
54
  try {
49
- this.onSample(SampleSchema.parse(JSON.parse(line)));
55
+ const raw = JSON.parse(line);
56
+ if (typeof raw === 'object' && raw !== null && raw.event === 'gate')
57
+ this.onGate(GateEventSchema.parse(raw));
58
+ else
59
+ this.onSample(SampleSchema.parse(raw));
50
60
  }
51
61
  catch (e) {
52
- warn('governor: bad sample: ' + errMsg(e));
62
+ warn('governor: bad line from memgov: ' + errMsg(e));
53
63
  }
54
64
  });
55
65
  child.stdin.on('error', () => undefined);
@@ -120,13 +130,17 @@ export class Governor {
120
130
  first.availablePercent +
121
131
  '% free RAM or ' +
122
132
  first.commitPercent +
123
- '% commit, in steps';
133
+ '% commit, in steps; holds new compilers when memory grows faster than ' +
134
+ BURST_GATE[profile].growthMb +
135
+ ' MB in ' +
136
+ BURST_GATE[profile].windowMs / 1000 +
137
+ ' s';
124
138
  }
125
139
  start() {
126
140
  if (session.pressureResponse === false)
127
141
  return;
128
142
  const profile = session.loadProfile === 'gentle' ? 'gentle' : 'normal';
129
- const proc = new MemGovProcess((s) => this.core?.onSample(s));
143
+ const proc = new MemGovProcess(memgovArgs(profile), (s) => this.core?.onSample(s), (e) => this.core?.onGate(e));
130
144
  this.core = new GovernorCore({
131
145
  port: proc,
132
146
  profile,
@@ -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
  }
@@ -1,20 +1,23 @@
1
1
  // Unreal Engine development, machine-wide (toolchains.unreal, once per boot): the DirectX End-User Runtime the engine
2
2
  // lists and the Epic Games Launcher (real installers: they run every boot), the launcher pointed at the data disk for
3
- // engines and given back the engines it installed there, the Windows SDK debuggers for crash dumps, and a check that
4
- // UnrealBuildTool finds Visual Studio the way it looks (the Setup Configuration API). Visual Studio itself is its own
5
- // step (visual-studio.ts), which the plan runs first; each project's engine is set up by engines/unreal-engines.ts.
3
+ // engines and given back the engines it installed there, the Windows SDK debuggers for crash dumps, a check that
4
+ // UnrealBuildTool finds Visual Studio the way it looks (the Setup Configuration API), and its parallel compiles capped.
5
+ // Visual Studio itself is its own step (visual-studio.ts), which the plan runs first; each project's engine is set up
6
+ // by engines/unreal-engines.ts.
6
7
  import fs from 'node:fs';
7
8
  import os from 'node:os';
8
9
  import path from 'node:path';
9
10
  import { errMsg } from '../../../core/util/errors.js';
10
11
  import { readValid } from '../../../shared/io/json.js';
11
12
  import { safeRemove } from '../../../shared/io/safe-remove.js';
13
+ import { buildJobs, ubtBuildConfiguration, usableCpus } from '../../../shared/memory/index.js';
12
14
  import { VsSetupReportSchema, vsVerdict } from '../../../shared/unreal/index.js';
13
15
  import { runInstaller } from '../../runtime/pools.js';
14
16
  import { dryRun } from '../../runtime/paths.js';
15
17
  import { exists, mkdirp } from '../../runtime/fs.js';
16
18
  import { log, warn } from '../../runtime/log.js';
17
19
  import { runScriptAsync } from '../../runtime/process.js';
20
+ import { session } from '../../runtime/session.js';
18
21
  import { ensureLauncher, fetchLauncher, placeEngines, restoreLauncherItems } from '../engines/epic-launcher.js';
19
22
  import { resetUnrealStatus, updateUnrealStatus } from '../engines/unreal-status.js';
20
23
  import { SIGNERS, signedInstaller } from '../installers.js';
@@ -68,6 +71,18 @@ async function checkVisualStudio() {
68
71
  fs.rmSync(dir, { recursive: true, force: true });
69
72
  }
70
73
  }
74
+ /**
75
+ * UnrealBuildTool's per-user BuildConfiguration.xml (%APPDATA%, wiped with the sandbox): compiles at once from the
76
+ * sandbox's build budget, so a build start cannot outrun the memory Windows backs the sandbox with. Returns the cap.
77
+ */
78
+ function capUnrealBuilds() {
79
+ const cpus = usableCpus(os.cpus().length, session.cpuLimitPercent ?? null);
80
+ const jobs = buildJobs(cpus, os.totalmem() / 1024 ** 3);
81
+ const appData = process.env.APPDATA ?? 'C:\\Users\\WDAGUtilityAccount\\AppData\\Roaming';
82
+ const dir = mkdirp(path.join(appData, 'Unreal Engine', 'UnrealBuildTool'));
83
+ fs.writeFileSync(path.join(dir, 'BuildConfiguration.xml'), ubtBuildConfiguration(jobs), 'utf8');
84
+ return jobs;
85
+ }
71
86
  /** Runs one part, warning (not failing the step) when it does not work. */
72
87
  async function part(what, work, fallback) {
73
88
  try {
@@ -86,6 +101,9 @@ export async function setupUnreal(env, config, _ctx) {
86
101
  return;
87
102
  }
88
103
  resetUnrealStatus();
104
+ const jobs = await part('capping parallel compiles', async () => capUnrealBuilds(), 0);
105
+ if (jobs)
106
+ log('Unreal: UnrealBuildTool runs at most ' + jobs + ' compiles at once (the build budget)');
89
107
  await part('the DirectX End-User Runtime', installDirectX, undefined);
90
108
  const launcher = await part('the Epic Games Launcher', ensureLauncher, null);
91
109
  // Before anyone opens the launcher: engines go to the data disk, and it knows the ones it installed there before.
@@ -131,6 +149,7 @@ export async function setupUnreal(env, config, _ctx) {
131
149
  vs.version +
132
150
  (check.ok ? ' (UnrealBuildTool finds it)' : ' (UnrealBuildTool would NOT find it)')
133
151
  : 'no Visual Studio') +
152
+ (jobs ? ', at most ' + jobs + ' compiles at once' : '') +
134
153
  ', DirectX End-User Runtime' +
135
154
  (launcher ? ', Epic Games Launcher' : '') +
136
155
  (debuggers ? ', cdb/windbg' : ''));
@@ -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
  }>>;
@@ -28,12 +28,13 @@ export async function setupSingleExe(env, name, repo, pattern, exeName, dir, opt
28
28
  }
29
29
  export async function setupBun(env) {
30
30
  await setupSingleExe(env, 'bun', 'oven-sh/bun', arch === 'arm64' ? /^bun-windows-aarch64\.zip$/ : /^bun-windows-x64\.zip$/, 'bun.exe', tool('bun', 'bin'), { size: '35 MB' });
31
- // Bun cannot keep its cache or temp files on a mapped folder (EINVAL); they go to the sandbox's own disk.
32
- Object.assign(env.vars, {
33
- BUN_INSTALL: tool('bun'),
34
- BUN_INSTALL_CACHE_DIR: path.join(localDir, 'bun-cache'),
35
- BUN_TMPDIR: path.join(localDir, 'bun-tmp'),
36
- });
31
+ // Bun cannot keep its cache or temp files on a mapped folder (EINVAL); they go to the sandbox's own disk. The temp
32
+ // folder must exist: programs compiled with Bun (opencode) unpack their DLLs there and fail when it is missing.
33
+ const cache = path.join(localDir, 'bun-cache');
34
+ const temp = path.join(localDir, 'bun-tmp');
35
+ if (!dryRun)
36
+ [cache, temp].forEach((dir) => mkdirp(dir));
37
+ Object.assign(env.vars, { BUN_INSTALL: tool('bun'), BUN_INSTALL_CACHE_DIR: cache, BUN_TMPDIR: temp });
37
38
  }
38
39
  export async function setupDeno(env) {
39
40
  await setupSingleExe(env, 'deno', 'denoland/deno', arch === 'arm64' ? /^deno-aarch64-pc-windows-msvc[.]zip$/ : /^deno-x86_64-pc-windows-msvc[.]zip$/, 'deno.exe', tool('deno', 'bin'), { size: '40 MB' });
@@ -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>;