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.
Files changed (93) 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/features/unreal/unreal.run.js +13 -3
  11. package/dist/app/registry.d.ts +1 -1
  12. package/dist/app/registry.js +15 -2
  13. package/dist/bin/agent-sandbox.js +2 -2
  14. package/dist/bin/{chunk-JXL6ZOO3.js → chunk-4M5SZZ5P.js} +46 -41
  15. package/dist/bin/{chunk-ZXCAVVS4.js → chunk-FA5VWLP4.js} +1 -1
  16. package/dist/bin/{chunk-6WWMBCT2.js → chunk-RU7XRN62.js} +303 -112
  17. package/dist/bin/claude-host.js +2 -2
  18. package/dist/bin/claude-sbx.js +2 -2
  19. package/dist/bin/codex-sbx.js +2 -2
  20. package/dist/bin/gemini-sbx.js +2 -2
  21. package/dist/bin/sandbox.js +2 -2
  22. package/dist/bin/sbx-hide.js +2 -2
  23. package/dist/bin/sbx-show.js +2 -2
  24. package/dist/cli/commands/launch/launch.js +2 -1
  25. package/dist/cli/menu/menu.js +6 -1
  26. package/dist/cli/menu/pages/accounts-page.js +9 -5
  27. package/dist/guest/apps/app-installer.js +7 -5
  28. package/dist/guest/apps/comet-unpack.js +2 -2
  29. package/dist/guest/runtime/pools.d.ts +4 -4
  30. package/dist/guest/runtime/pools.js +4 -15
  31. package/dist/guest/runtime/process.d.ts +5 -2
  32. package/dist/guest/runtime/process.js +23 -3
  33. package/dist/guest/setup/unity-agent.js +4 -1
  34. package/dist/guest/toolchains/engines/epic-launcher.d.ts +7 -1
  35. package/dist/guest/toolchains/engines/epic-launcher.js +30 -8
  36. package/dist/guest/toolchains/engines/unreal-engines.js +2 -1
  37. package/dist/guest/toolchains/engines/unreal-status.js +1 -1
  38. package/dist/guest/toolchains/installers.d.ts +3 -3
  39. package/dist/guest/toolchains/installers.js +5 -5
  40. package/dist/guest/toolchains/native/mesa.js +35 -11
  41. package/dist/guest/toolchains/native/unreal.js +1 -0
  42. package/dist/guest/toolchains/native/vcruntime.js +2 -2
  43. package/dist/guest/toolchains/native/vs-persist.d.ts +2 -2
  44. package/dist/guest/toolchains/signature.d.ts +2 -0
  45. package/dist/guest/toolchains/signature.js +9 -2
  46. package/dist/guest/toolchains/step-scheduler.js +1 -1
  47. package/dist/host/accounts/deliver.d.ts +3 -0
  48. package/dist/host/accounts/deliver.js +27 -0
  49. package/dist/host/accounts/source.d.ts +2 -0
  50. package/dist/host/accounts/source.js +2 -0
  51. package/dist/host/pc-installs/discover.js +8 -4
  52. package/dist/host/pc-installs/epic.js +3 -0
  53. package/dist/host/pc-installs/unity.d.ts +2 -0
  54. package/dist/host/pc-installs/unity.js +12 -3
  55. package/dist/host/pc-installs/unreal.d.ts +5 -1
  56. package/dist/host/pc-installs/unreal.js +51 -9
  57. package/dist/runtime/agent.js +34 -22
  58. package/dist/runtime/daemon.js +134 -65
  59. package/dist/runtime/db.js +32 -16
  60. package/dist/runtime/main.js +136 -67
  61. package/dist/runtime/netproxy.js +26 -12
  62. package/dist/runtime/tool.js +32 -15
  63. package/dist/runtime/view.js +26 -12
  64. package/dist/selftest/app.test.js +20 -2
  65. package/dist/selftest/boot-runner.test.js +15 -1
  66. package/dist/selftest/engines.test.js +52 -2
  67. package/dist/selftest/menu-screens.js +52 -1
  68. package/dist/selftest/menu.test.js +29 -3
  69. package/dist/selftest/mixamo.test.js +1 -1
  70. package/dist/selftest/service-accounts.test.js +88 -2
  71. package/dist/selftest/ui-text.test.js +4 -0
  72. package/dist/shared/accounts/kinds.js +9 -9
  73. package/dist/shared/apps/catalogue-gamedev.js +1 -0
  74. package/dist/shared/apps/catalogue.d.ts +2 -0
  75. package/dist/shared/concurrency/pools.js +1 -1
  76. package/dist/shared/download/archive.js +4 -2
  77. package/dist/shared/download/index.d.ts +1 -1
  78. package/dist/shared/download/index.js +1 -1
  79. package/dist/shared/download/spawn.d.ts +7 -1
  80. package/dist/shared/download/spawn.js +16 -1
  81. package/dist/shared/paths/pc-installs.d.ts +8 -0
  82. package/dist/shared/unreal/launcher-files.d.ts +7 -0
  83. package/dist/shared/unreal/launcher-files.js +32 -1
  84. package/dist/shared/unreal/status.d.ts +1 -0
  85. package/dist/shared/unreal/status.js +2 -0
  86. package/dist/web/index.html +1 -1
  87. package/dist/web/{main-VPMYTSUL.js → main-NKM7ECSB.js} +59 -59
  88. package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
  89. package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
  90. package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
  91. package/package.json +1 -1
  92. package/schemas/config.schema.json +9 -9
  93. package/schemas/sandbox-config.schema.json +9 -9
@@ -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 {
@@ -2,6 +2,7 @@ import type { BootWant } from '../../core/local-config/index.js';
2
2
  import type { Config } from '../../core/types.js';
3
3
  import { type Payloads, type ServiceKind, type UsedBy } from '../../shared/accounts/index.js';
4
4
  import { AccountLeases } from './leases.js';
5
+ import type { PcEnv } from './source.js';
5
6
  import { ServiceAccountStore } from './store.js';
6
7
  export interface AccountDelivery {
7
8
  readonly payloads: Payloads;
@@ -22,5 +23,7 @@ export interface DeliveryInput {
22
23
  readonly isRunning: (dataSet: string) => boolean;
23
24
  readonly store?: ServiceAccountStore;
24
25
  readonly leases?: AccountLeases;
26
+ /** This PC's folders: a shared kind with nothing saved takes this PC's own sign-in (left out: never). */
27
+ readonly pc?: PcEnv;
25
28
  }
26
29
  export declare function prepareServiceAccounts(i: DeliveryInput): AccountDelivery;
@@ -2,6 +2,7 @@ import { fingerprint, SERVICE_KINDS, SERVICE_SPECS, serviceWanted, } from '../..
2
2
  import { setNameOf } from '../storage/index.js';
3
3
  import { takeHandoffs } from './handoff.js';
4
4
  import { AccountLeases } from './leases.js';
5
+ import { HOST_SOURCES } from './sources.js';
5
6
  import { ServiceAccountStore } from './store.js';
6
7
  /** How to get a first sign-in of a kind, for the note when none is saved. */
7
8
  function howToSave(spec) {
@@ -21,6 +22,30 @@ function howToSave(spec) {
21
22
  ? 'save one: sandbox account login ' + spec.kind
22
23
  : 'save one: sandbox account import ' + spec.kind + ' --from <file>';
23
24
  }
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) {
31
+ const source = HOST_SOURCES[kind];
32
+ if (!pc || !source.fromPc)
33
+ return saved;
34
+ if (saved && store.info(kind)?.source !== 'pc')
35
+ return saved;
36
+ let payload = null;
37
+ try {
38
+ payload = source.fromPc(pc);
39
+ }
40
+ catch {
41
+ return saved; // This PC's program has no usable sign-in: the saved one (or the note) stands.
42
+ }
43
+ if (!payload)
44
+ return saved;
45
+ if (!saved || fingerprint(payload) !== fingerprint(saved))
46
+ store.save(kind, payload, 'pc', source.summary(payload));
47
+ return payload;
48
+ }
24
49
  export function prepareServiceAccounts(i) {
25
50
  const store = i.store ?? new ServiceAccountStore(i.storageRoot);
26
51
  const leases = i.leases ?? new AccountLeases(i.storageRoot);
@@ -53,6 +78,8 @@ export function prepareServiceAccounts(i) {
53
78
  catch {
54
79
  unreadable = true;
55
80
  }
81
+ if (!unreadable)
82
+ payload = takeFromPc(kind, i.pc, store, payload);
56
83
  if (spec.singleHolder) {
57
84
  const claim = leases.claim(kind, dataSet, i.launchId, payload ? fingerprint(payload) : null, i.isRunning);
58
85
  if (!claim.ok) {
@@ -10,6 +10,8 @@ export interface PcEnv {
10
10
  readonly env?: Readonly<Record<string, string | undefined>>;
11
11
  /** A variable of this Windows user's saved environment (HKCU\Environment), or null when it is not set. */
12
12
  readonly userVariable?: (name: string) => string | null;
13
+ /** This PC's local drives ("D:\\"), asked only by the searches that look on every drive. */
14
+ readonly drives?: () => readonly string[];
13
15
  }
14
16
  /** The value of `name` in `reg query <key> /v <name>` output, or null. */
15
17
  export declare function parseRegValue(text: string, name: string): string | null;
@@ -2,6 +2,7 @@
2
2
  // (fromFile, `sandbox account import <kind> --from <file>`), or typed (fromAnswers, `sandbox account login <kind>`).
3
3
  // One module per kind under kinds/; sources.ts lists them.
4
4
  import { spawnSync } from 'node:child_process';
5
+ import { diskUsage } from '../../core/disk.js';
5
6
  import { fail } from '../../core/util/index.js';
6
7
  import { SYSTEM } from '../../shared/system.js';
7
8
  import { refuseInSelfTest } from '../test-guard.js';
@@ -35,6 +36,7 @@ export function pcEnv() {
35
36
  programFiles: env.ProgramFiles ?? 'C:\\Program Files',
36
37
  env,
37
38
  userVariable: savedUserVariable,
39
+ drives: () => diskUsage(env.ProgramData ?? 'C:\\').map((d) => d.drive),
38
40
  };
39
41
  }
40
42
  /** What a kind whose host side is not written yet says. */
@@ -1,6 +1,7 @@
1
1
  import { assertSafeMapping } from '../../core/safe-paths.js';
2
2
  import { dirExists, errMsg } from '../../core/util/index.js';
3
3
  import { pcInstallGuest } from '../../shared/paths/index.js';
4
+ import { relocateItem } from '../../shared/unreal/index.js';
4
5
  import { pcEnv } from '../accounts/index.js';
5
6
  import { epicInstalls } from './epic.js';
6
7
  import { steamInstalls } from './steam.js';
@@ -10,12 +11,13 @@ import { unrealFolders, unrealInstalls } from './unreal.js';
10
11
  import { unrealVault } from './unreal-vault.js';
11
12
  /** At most this many installs are mapped (each one is a mapping in the .wsb). */
12
13
  export const MAX_PC_INSTALLS = 40;
14
+ // Engines first: a folder is mapped once, as the first source that finds it.
13
15
  export const PC_INSTALL_SOURCES = [
14
- steamInstalls,
15
- epicInstalls,
16
16
  unrealFolders,
17
17
  unrealInstalls,
18
18
  unrealVault,
19
+ steamInstalls,
20
+ epicInstalls,
19
21
  unityInstalls,
20
22
  unityAssets,
21
23
  ];
@@ -73,8 +75,10 @@ export function discoverPcInstalls(s, o, pc = pcEnv()) {
73
75
  readOnly: !writable,
74
76
  label: writable ? 'this PC, writable' : 'this PC, read-only',
75
77
  });
76
- const { host: _host, ...shown } = install;
77
- items.push({ ...shown, guest });
78
+ const { host: _host, launcherItem, ...shown } = install;
79
+ // The manifest names the sandbox's folder, or it is left out: no path of this PC goes to the sandbox.
80
+ const moved = launcherItem ? relocateItem(launcherItem.text, install.host, guest) : null;
81
+ items.push({ ...shown, guest, ...(moved ? { launcherItem: { name: launcherItem.name, text: moved } } : {}) });
78
82
  }
79
83
  catch (e) {
80
84
  notes.push('pcInstalls: ' + install.name + ' is not mapped: ' + errMsg(e));
@@ -11,6 +11,9 @@ export function findEpicGames(text, wanted) {
11
11
  for (const i of text ? launcherInstallations(text) : []) {
12
12
  if (!APP.test(i.appName) || /^\.+$/.test(i.appName) || /^UE_\d/.test(i.appName))
13
13
  continue;
14
+ // Engines and what the launcher puts into one (the Fab plugin lists the engine's folder) are unreal.ts's.
15
+ if (i.ids?.namespaceId.toLowerCase() === 'ue' || /^UE_\d/i.test(path.win32.basename(i.installLocation)))
16
+ continue;
14
17
  if (names && !names.has(i.appName.toLowerCase()))
15
18
  continue;
16
19
  if (found.some((f) => f.id.toLowerCase() === i.appName.toLowerCase()))
@@ -10,6 +10,8 @@ export interface UnityPcFiles {
10
10
  list(dir: string): string[];
11
11
  readonly appData: string;
12
12
  readonly programFiles: string;
13
+ /** Hub's folder on every other drive (D:\Program Files\Unity\Hub\Editor, D:\Unity\Hub\Editor ...). */
14
+ readonly otherHubFolders?: readonly string[];
13
15
  }
14
16
  /** The editors of this PC: all of them (true) or the listed versions. Each version once, each folder once. */
15
17
  export declare function findUnityEditorsOnPc(files: UnityPcFiles, wanted: true | readonly string[]): PcInstall[];
@@ -1,4 +1,5 @@
1
- // Unity editors of this PC (pcInstalls.unityEditors): Unity Hub's default folder (<Program Files>\Unity\Hub\Editor),
1
+ // Unity editors of this PC (pcInstalls.unityEditors): Unity Hub's default folder on every drive
2
+ // (<drive>\Program Files\Unity\Hub\Editor, <drive>\Unity\Hub\Editor),
2
3
  // the folder named by %APPDATA%\UnityHub\secondaryInstallPath.json and the editors in editors-v2.json. An editor counts
3
4
  // only with Editor\Unity.exe; its folder is mapped read-only at C:\PcInstalls\unity\<version>. Every file is read
4
5
  // bounded and every name is checked: what Hub wrote is data, never trusted.
@@ -67,8 +68,12 @@ function listedEditors(files) {
67
68
  /** The editors of this PC: all of them (true) or the listed versions. Each version once, each folder once. */
68
69
  export function findUnityEditorsOnPc(files, wanted) {
69
70
  const candidates = [];
70
- const hubFolders = [win.join(files.programFiles, 'Unity', 'Hub', 'Editor'), secondaryRoot(files)].filter((d) => d !== null);
71
- for (const folder of hubFolders)
71
+ const hubFolders = [
72
+ win.join(files.programFiles, 'Unity', 'Hub', 'Editor'),
73
+ secondaryRoot(files),
74
+ ...(files.otherHubFolders ?? []),
75
+ ].filter((d) => d !== null);
76
+ for (const folder of new Set(hubFolders.map((d) => win.resolve(d))))
72
77
  for (const name of files.list(folder).slice(0, MAX_LISTED).sort())
73
78
  if (UNITY_VERSION.test(name))
74
79
  candidates.push({ version: name, root: win.join(folder, name) });
@@ -125,6 +130,10 @@ export function unityPcFiles(pc) {
125
130
  list: listDir,
126
131
  appData: pc.appData,
127
132
  programFiles: pc.programFiles ?? 'C:\\Program Files',
133
+ otherHubFolders: (pc.drives?.() ?? []).flatMap((d) => [
134
+ win.join(d, 'Program Files', 'Unity', 'Hub', 'Editor'),
135
+ win.join(d, 'Unity', 'Hub', 'Editor'),
136
+ ]),
128
137
  };
129
138
  }
130
139
  /** The cache root the environment names: this terminal's variable, else the saved user one. */
@@ -11,6 +11,10 @@ export interface UnrealPcFiles {
11
11
  readonly builds: string;
12
12
  /** The launcher's default engines folder (<Program Files>\Epic Games). */
13
13
  readonly defaultRoot: string;
14
+ /** More folders that may hold UE_x.y folders: the same places on every drive (D:\Program Files\Epic Games ...). */
15
+ readonly otherRoots?: readonly string[];
16
+ /** The launcher's Data\Manifests folder (its .item files), or none. */
17
+ readonly manifests?: string;
14
18
  readonly list: (dir: string) => string[];
15
19
  readonly read: (file: string, max: number) => Buffer | null;
16
20
  }
@@ -31,7 +35,7 @@ export interface PcSourceBuild {
31
35
  export declare function pcSourceBuilds(files: Pick<UnrealPcFiles, 'read' | 'builds'>): PcSourceBuild[];
32
36
  /** The engine folders you registered (pcInstalls.unrealEngineFolders), mapped writable. A folder that is a source
33
37
  * build of this PC keeps the PC's build id. */
34
- export declare function findEngineFolders(files: Pick<UnrealPcFiles, 'read' | 'builds'>, folders: readonly (string | number)[]): PcInstall[];
38
+ export declare function findEngineFolders(files: Pick<UnrealPcFiles, 'read' | 'builds'> & Partial<Pick<UnrealPcFiles, 'launcherInstalled' | 'manifests' | 'list'>>, folders: readonly (string | number)[]): PcInstall[];
35
39
  /** This PC's files (reg.exe through system32, bounded reads). */
36
40
  export declare function unrealPcFiles(pc: PcEnv): UnrealPcFiles;
37
41
  export declare const unrealInstalls: PcInstallSource;
@@ -1,6 +1,7 @@
1
1
  // Unreal Engine versions of this PC (pcInstalls.unrealEngines): the launcher's LauncherInstalled.dat (engines it
2
- // installed, with its own ids) and the launcher's default folders (<Program Files>\Epic Games\UE_*, for an install
3
- // whose LauncherInstalled.dat is gone). Each must hold Engine\Build\Build.version; it is mapped read-only at
2
+ // installed, with its own ids) and folders named UE_x.y in the launcher's default places on every drive
3
+ // (<drive>\Program Files\Epic Games, <drive>\Epic Games) and next to what it lists (an install whose entry is gone).
4
+ // Each must hold Engine\Build\Build.version; it is mapped read-only at
4
5
  // C:\PcInstalls\unreal\UE_5.4. Source builds (HKCU\Software\Epic Games\Unreal Engine\Builds) are NOT mapped by
5
6
  // themselves, as UBT writes inside an engine. pcInstalls.unrealEngineFolders: folders you registered, mapped writable
6
7
  // (dir-<name>; a source build of this PC keeps its build id). Every file is read bounded.
@@ -9,7 +10,7 @@ import fs from 'node:fs';
9
10
  import path from 'node:path';
10
11
  import { readBoundedFile } from '../../shared/io/index.js';
11
12
  import { SYSTEM } from '../../shared/system.js';
12
- import { BUILDS_KEY, launcherInstallations, launcherInstalledFile, MAX_LAUNCHER_FILE_BYTES, parseBuildVersion, versionText, } from '../../shared/unreal/index.js';
13
+ import { BUILDS_KEY, itemInstallLocation, ITEM_NAME, launcherInstallations, launcherInstalledFile, launcherManifestsDir, MAX_LAUNCHER_FILE_BYTES, parseBuildVersion, versionText, } from '../../shared/unreal/index.js';
13
14
  const win = path.win32;
14
15
  /** At most this many engines of this PC are mapped. */
15
16
  export const MAX_PC_ENGINES = 8;
@@ -28,6 +29,27 @@ const versionOf = (dir, files) => {
28
29
  const bytes = files.read(win.join(dir, 'Engine', 'Build', 'Build.version'), 64 * 1024);
29
30
  return bytes ? parseBuildVersion(bytes.toString('utf8')) : null;
30
31
  };
32
+ /** At most this much of a manifest goes into session.json (an engine's is a few KB). */
33
+ const MAX_ITEM_BYTES = 64 * 1024;
34
+ /** The launcher's manifest of the app in `dir` (with this PC's paths), or nothing. */
35
+ function launcherItemOf(dir, files) {
36
+ if (!files.manifests)
37
+ return {};
38
+ const want = win.resolve(dir).toLowerCase();
39
+ for (const name of files.list(files.manifests).filter((n) => ITEM_NAME.test(n))) {
40
+ const bytes = files.read(win.join(files.manifests, name), MAX_ITEM_BYTES);
41
+ const at = bytes ? itemInstallLocation(bytes.toString('utf8')) : null;
42
+ if (bytes && at && win.resolve(at).toLowerCase() === want)
43
+ return { launcherItem: { name, text: bytes.toString('utf8') } };
44
+ }
45
+ return {};
46
+ }
47
+ /** The launcher's ids for the app in `dir` (from LauncherInstalled.dat), or nothing. */
48
+ function launcherIdsOf(dir, files) {
49
+ const want = win.resolve(dir).toLowerCase();
50
+ const entry = (files.launcherInstalled ? launcherInstallations(files.launcherInstalled) : []).find((i) => APP_NAME.test(i.appName) && i.ids && win.resolve(i.installLocation).toLowerCase() === want);
51
+ return entry?.ids ? { launcher: entry.ids } : {};
52
+ }
31
53
  const isWanted = (wanted, v) => wanted === true || wanted.map((w) => String(w).replace(/^UE_/i, '')).includes(v.major + '.' + v.minor);
32
54
  /** The engines of this PC that `wanted` maps, each folder once, at most MAX_PC_ENGINES. */
33
55
  export function findUnrealInstalls(files, wanted) {
@@ -52,16 +74,28 @@ export function findUnrealInstalls(files, wanted) {
52
74
  host: win.normalize(dir),
53
75
  guest: '',
54
76
  ...(ids ? { launcher: ids } : {}),
77
+ ...launcherItemOf(dir, files),
55
78
  });
56
79
  };
57
- for (const i of files.launcherInstalled ? launcherInstallations(files.launcherInstalled) : [])
80
+ const listed = files.launcherInstalled ? launcherInstallations(files.launcherInstalled) : [];
81
+ for (const i of listed)
58
82
  if (APP_NAME.test(i.appName))
59
83
  installed(i.installLocation, i.ids ?? undefined);
60
- for (const name of files
61
- .list(files.defaultRoot)
62
- .filter((n) => APP_NAME.test(n))
63
- .sort())
64
- installed(win.join(files.defaultRoot, name));
84
+ // Folders named like an engine: the default places, and the folders that hold what the launcher installed (an
85
+ // engine whose entry is gone often sits next to one that is listed, or next to a game).
86
+ const roots = [files.defaultRoot, ...(files.otherRoots ?? []), ...listed.map((i) => win.dirname(i.installLocation))];
87
+ const seen = new Set();
88
+ for (const root of roots) {
89
+ const key = win.resolve(root).toLowerCase();
90
+ if (seen.has(key) || seen.size >= 32)
91
+ continue;
92
+ seen.add(key);
93
+ for (const name of files
94
+ .list(root)
95
+ .filter((n) => APP_NAME.test(n))
96
+ .sort())
97
+ installed(win.join(root, name));
98
+ }
65
99
  return found;
66
100
  }
67
101
  /** Source builds this PC registered (the Builds key). They are not mapped by themselves: UBT writes inside them. */
@@ -105,6 +139,9 @@ export function findEngineFolders(files, folders) {
105
139
  guest: '',
106
140
  writable: true,
107
141
  ...(source ? { buildId: source.buildId } : {}),
142
+ // An engine the launcher installed keeps its ids and manifest, so the sandbox's launcher lists it too.
143
+ ...launcherIdsOf(dir, { launcherInstalled: files.launcherInstalled ?? null }),
144
+ ...(files.list ? launcherItemOf(dir, { manifests: files.manifests, list: files.list, read: files.read }) : {}),
108
145
  });
109
146
  }
110
147
  return found;
@@ -130,6 +167,11 @@ export function unrealPcFiles(pc) {
130
167
  launcherInstalled: dat ? dat.toString('utf8') : null,
131
168
  builds: query.status === 0 ? String(query.stdout ?? '') : '',
132
169
  defaultRoot: win.join(pc.programFiles ?? 'C:\\Program Files', 'Epic Games'),
170
+ otherRoots: (pc.drives?.() ?? []).flatMap((d) => [
171
+ win.join(d, 'Program Files', 'Epic Games'),
172
+ win.join(d, 'Epic Games'),
173
+ ]),
174
+ manifests: launcherManifestsDir(pc.programData),
133
175
  list: listDir,
134
176
  read: readBoundedFile,
135
177
  };
@@ -26093,6 +26093,7 @@ var GAMEDEV_APPS = [
26093
26093
  size: "100 MB",
26094
26094
  source: { github: "f3d-app/f3d", asset: "^F3D-[\\d.]+-Windows-x86_64\\.zip$" },
26095
26095
  exe: "f3d.exe",
26096
+ onPath: true,
26096
26097
  needs: ["mesa"]
26097
26098
  },
26098
26099
  {
@@ -27192,7 +27193,7 @@ var SERVICE_SPECS = {
27192
27193
  },
27193
27194
  steam: {
27194
27195
  kind: "steam",
27195
- label: "Steam (SteamCMD)",
27196
+ label: "Steam",
27196
27197
  usedBy: ["steam"],
27197
27198
  sources: ["file", "sandbox"],
27198
27199
  singleHolder: true,
@@ -27228,7 +27229,7 @@ var SERVICE_SPECS = {
27228
27229
  },
27229
27230
  cfx: {
27230
27231
  kind: "cfx",
27231
- label: "Cfx.re (FiveM / RedM server key)",
27232
+ label: "FiveM key",
27232
27233
  usedBy: ["fivem"],
27233
27234
  sources: ["typed", "file"],
27234
27235
  singleHolder: false,
@@ -27257,7 +27258,7 @@ var SERVICE_SPECS = {
27257
27258
  },
27258
27259
  keystore: {
27259
27260
  kind: "keystore",
27260
- label: "Android upload keystore",
27261
+ label: "Android keystore",
27261
27262
  usedBy: ["android"],
27262
27263
  sources: ["file"],
27263
27264
  singleHolder: false,
@@ -27272,7 +27273,7 @@ var SERVICE_SPECS = {
27272
27273
  },
27273
27274
  play: {
27274
27275
  kind: "play",
27275
- label: "Google Play (service account)",
27276
+ label: "Google Play",
27276
27277
  usedBy: ["android"],
27277
27278
  sources: ["file"],
27278
27279
  singleHolder: false,
@@ -27283,7 +27284,7 @@ var SERVICE_SPECS = {
27283
27284
  },
27284
27285
  expo: {
27285
27286
  kind: "expo",
27286
- label: "Expo (EAS)",
27287
+ label: "Expo",
27287
27288
  usedBy: ["reactNative"],
27288
27289
  sources: ["typed"],
27289
27290
  singleHolder: false,
@@ -27294,7 +27295,7 @@ var SERVICE_SPECS = {
27294
27295
  },
27295
27296
  appstore: {
27296
27297
  kind: "appstore",
27297
- label: "App Store Connect API key",
27298
+ label: "App Store Connect",
27298
27299
  usedBy: ["reactNative", "flutter"],
27299
27300
  sources: ["file", "typed"],
27300
27301
  singleHolder: false,
@@ -27308,7 +27309,7 @@ var SERVICE_SPECS = {
27308
27309
  },
27309
27310
  perplexity: {
27310
27311
  kind: "perplexity",
27311
- label: "Perplexity API key",
27312
+ label: "Perplexity API",
27312
27313
  usedBy: ["perplexity"],
27313
27314
  sources: ["pc", "file", "typed"],
27314
27315
  singleHolder: false,
@@ -27332,7 +27333,7 @@ var SERVICE_SPECS = {
27332
27333
  },
27333
27334
  comet: {
27334
27335
  kind: "comet",
27335
- label: "Comet (Perplexity browser)",
27336
+ label: "Comet",
27336
27337
  usedBy: ["comet"],
27337
27338
  sources: ["sandbox"],
27338
27339
  singleHolder: false,
@@ -27348,7 +27349,7 @@ var SERVICE_SPECS = {
27348
27349
  },
27349
27350
  perplexityapp: {
27350
27351
  kind: "perplexityapp",
27351
- label: "Perplexity desktop app",
27352
+ label: "Perplexity app",
27352
27353
  usedBy: ["perplexityapp"],
27353
27354
  sources: ["sandbox"],
27354
27355
  singleHolder: false,
@@ -28074,7 +28075,7 @@ import path12 from "node:path";
28074
28075
 
28075
28076
  // src/guest/runtime/process.ts
28076
28077
  import fs10 from "node:fs";
28077
- import os3 from "node:os";
28078
+ import os4 from "node:os";
28078
28079
  import path11 from "node:path";
28079
28080
  import { spawn as spawn2, spawnSync } from "node:child_process";
28080
28081
 
@@ -28134,10 +28135,20 @@ function scriptArgs(name, args = []) {
28134
28135
  }
28135
28136
 
28136
28137
  // src/shared/download/spawn.ts
28138
+ import os3 from "node:os";
28137
28139
  import { spawn } from "node:child_process";
28140
+ function lowerPriority(pid) {
28141
+ if (pid === void 0) return;
28142
+ try {
28143
+ os3.setPriority(pid, os3.constants.priority.PRIORITY_BELOW_NORMAL);
28144
+ } catch {
28145
+ }
28146
+ }
28138
28147
  function spawnAsync(file2, args, options = {}) {
28148
+ const { lowPriority, ...spawnOptions } = options;
28139
28149
  return new Promise((resolve, reject) => {
28140
- const child = spawn(file2, args, { stdio: "ignore", windowsHide: true, ...options });
28150
+ const child = spawn(file2, args, { stdio: "ignore", windowsHide: true, ...spawnOptions });
28151
+ if (lowPriority) lowerPriority(child.pid);
28141
28152
  child.once("error", reject);
28142
28153
  child.once("close", (code) => resolve(code ?? -1));
28143
28154
  });
@@ -28158,13 +28169,16 @@ function extractArchive(archive, destination, members2 = []) {
28158
28169
  }
28159
28170
  async function extract(archive, destination, members2) {
28160
28171
  fs8.mkdirSync(destination, { recursive: true });
28161
- let status = await spawnAsync(SYSTEM.tar(), ["-xf", archive, "-C", destination, ...members2]);
28172
+ let status = await spawnAsync(SYSTEM.tar(), ["-xf", archive, "-C", destination, ...members2], {
28173
+ lowPriority: true
28174
+ });
28162
28175
  if (status !== 0 && /\.(zip|nupkg)$/i.test(archive)) {
28163
28176
  const zip = archive.endsWith(".zip") ? archive : archive + ".zip";
28164
28177
  if (zip !== archive) fs8.copyFileSync(archive, zip);
28165
28178
  status = await spawnAsync(
28166
28179
  SYSTEM.powershell(),
28167
- scriptArgs("expand-archive.ps1", ["-Archive", zip, "-Destination", destination])
28180
+ scriptArgs("expand-archive.ps1", ["-Archive", zip, "-Destination", destination]),
28181
+ { lowPriority: true }
28168
28182
  );
28169
28183
  }
28170
28184
  if (status !== 0) throw new Error("Could not extract " + archive);
@@ -28381,13 +28395,15 @@ function logLines(stream) {
28381
28395
  stream.on("end", () => logOutput(partial2));
28382
28396
  }
28383
28397
  async function execAsync(file2, args, options = {}) {
28398
+ const { lowPriority, ...spawnOptions } = options;
28384
28399
  const code = captureOutput() ? await new Promise((resolve, reject) => {
28385
- const child = spawn2(file2, args, { stdio: ["inherit", "pipe", "pipe"], windowsHide: true, ...options });
28400
+ const child = spawn2(file2, args, { stdio: ["inherit", "pipe", "pipe"], windowsHide: true, ...spawnOptions });
28401
+ if (lowPriority) lowerPriority(child.pid);
28386
28402
  logLines(child.stdout);
28387
28403
  logLines(child.stderr);
28388
28404
  child.once("error", reject);
28389
28405
  child.once("close", (c) => resolve(c ?? -1));
28390
- }) : await spawnAsync(file2, args, { stdio: "inherit", ...options });
28406
+ }) : await spawnAsync(file2, args, { stdio: "inherit", ...spawnOptions, ...lowPriority ? { lowPriority } : {} });
28391
28407
  if (code !== 0)
28392
28408
  throw new Error(path11.basename(file2) + " " + args.slice(0, 3).join(" ") + " failed with exit code " + code);
28393
28409
  }
@@ -28416,7 +28432,7 @@ function runScript(name, args = []) {
28416
28432
  throw new Error(name + " failed with exit code " + result.status + (errors ? ": " + errors.slice(0, 400) : ""));
28417
28433
  }
28418
28434
  function runScriptWithJson(name, param, data, args = []) {
28419
- const dir = fs10.mkdtempSync(path11.join(os3.tmpdir(), "sbx-"));
28435
+ const dir = fs10.mkdtempSync(path11.join(os4.tmpdir(), "sbx-"));
28420
28436
  const file2 = path11.join(dir, "input.json");
28421
28437
  try {
28422
28438
  fs10.writeFileSync(file2, JSON.stringify(data), "utf8");
@@ -28492,9 +28508,6 @@ async function setupUv(env) {
28492
28508
  env.prepend.push(mkdirp(tool("uv-bin")));
28493
28509
  }
28494
28510
 
28495
- // src/guest/runtime/pools.ts
28496
- import os4 from "node:os";
28497
-
28498
28511
  // src/shared/concurrency/limiter.ts
28499
28512
  import { AsyncLocalStorage as AsyncLocalStorage2 } from "node:async_hooks";
28500
28513
  var held = new AsyncLocalStorage2();
@@ -28554,14 +28567,13 @@ var Limiter = class {
28554
28567
  };
28555
28568
 
28556
28569
  // src/shared/concurrency/pools.ts
28557
- var net = new Limiter(4, "net");
28570
+ var net = new Limiter(3, "net");
28558
28571
  var heavy = new Limiter(2, "heavy");
28559
28572
  var installer = new Limiter(1, "installer");
28560
28573
 
28561
28574
  // src/guest/runtime/pools.ts
28562
- var BELOW_NORMAL = os4.constants.priority.PRIORITY_BELOW_NORMAL;
28563
28575
  function execHeavy(file2, args, options = {}) {
28564
- return heavy.run(() => execAsync(file2, args, options));
28576
+ return heavy.run(() => execAsync(file2, args, { ...options, lowPriority: true }));
28565
28577
  }
28566
28578
 
28567
28579
  // src/guest/runtime/session.ts