windows-development-sandbox 1.17.3 → 1.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/config.example.json +97 -4
- package/dist/app/features/accounts/accounts.def.d.ts +116 -2
- package/dist/app/features/accounts/accounts.def.js +80 -12
- package/dist/app/features/accounts/accounts.run.d.ts +29 -0
- package/dist/app/features/accounts/accounts.run.js +36 -6
- package/dist/app/features/accounts/index.d.ts +2 -0
- package/dist/app/features/accounts/index.js +4 -1
- package/dist/app/features/accounts/services.def.d.ts +111 -0
- package/dist/app/features/accounts/services.def.js +187 -0
- package/dist/app/features/accounts/services.run.d.ts +65 -0
- package/dist/app/features/accounts/services.run.js +212 -0
- package/dist/app/features/defs.d.ts +112 -1
- package/dist/app/features/defs.js +5 -0
- package/dist/app/features/settings/categories.def.js +21 -4
- package/dist/app/registry.js +5 -1
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-QLGGAG3D.js → chunk-BKF43GJ3.js} +51 -16
- package/dist/bin/{chunk-REE3EMHD.js → chunk-YD3A67RS.js} +3812 -1702
- package/dist/bin/{chunk-XYUVIVJT.js → chunk-Z5D32FN3.js} +1 -1
- package/dist/bin/claude-host.js +2 -2
- package/dist/bin/claude-sbx.js +2 -2
- package/dist/bin/codex-sbx.js +2 -2
- package/dist/bin/gemini-sbx.js +2 -2
- package/dist/bin/sandbox.js +2 -2
- package/dist/bin/sbx-hide.js +2 -2
- package/dist/bin/sbx-show.js +2 -2
- package/dist/cli/commands/launch/launch.js +30 -1
- package/dist/cli/manifest.d.ts +93 -65
- package/dist/cli/manifest.js +23 -5
- package/dist/cli/menu/pages/accounts-page.js +31 -16
- package/dist/cli/services/accounts-service.d.ts +14 -4
- package/dist/cli/services/accounts-service.js +19 -6
- package/dist/cli/services/data-collect.js +26 -1
- package/dist/core/config/defaults.js +47 -4
- package/dist/core/detect/toolchains.d.ts +15 -1
- package/dist/core/detect/toolchains.js +45 -12
- package/dist/core/local-config/overrides.js +6 -0
- package/dist/core/schema/accounts.d.ts +27 -0
- package/dist/core/schema/accounts.js +32 -0
- package/dist/core/schema/config.d.ts +239 -0
- package/dist/core/schema/config.js +6 -0
- package/dist/core/schema/pc-installs.d.ts +7 -0
- package/dist/core/schema/pc-installs.js +17 -0
- package/dist/core/schema/toolchains.d.ts +61 -0
- package/dist/core/schema/toolchains.js +106 -0
- package/dist/core/types.d.ts +18 -0
- package/dist/guest/accounts/adapter.d.ts +25 -0
- package/dist/guest/accounts/adapter.js +21 -0
- package/dist/guest/accounts/adapters.d.ts +5 -0
- package/dist/guest/accounts/adapters.js +18 -0
- package/dist/guest/accounts/apply.d.ts +41 -0
- package/dist/guest/accounts/apply.js +47 -0
- package/dist/guest/accounts/kinds/appstore.d.ts +2 -0
- package/dist/guest/accounts/kinds/appstore.js +7 -0
- package/dist/guest/accounts/kinds/cfx.d.ts +2 -0
- package/dist/guest/accounts/kinds/cfx.js +7 -0
- package/dist/guest/accounts/kinds/epic.d.ts +2 -0
- package/dist/guest/accounts/kinds/epic.js +26 -0
- package/dist/guest/accounts/kinds/expo.d.ts +2 -0
- package/dist/guest/accounts/kinds/expo.js +7 -0
- package/dist/guest/accounts/kinds/keystore.d.ts +2 -0
- package/dist/guest/accounts/kinds/keystore.js +7 -0
- package/dist/guest/accounts/kinds/play.d.ts +2 -0
- package/dist/guest/accounts/kinds/play.js +7 -0
- package/dist/guest/accounts/kinds/steam.d.ts +4 -0
- package/dist/guest/accounts/kinds/steam.js +17 -0
- package/dist/guest/accounts/kinds/unity.d.ts +2 -0
- package/dist/guest/accounts/kinds/unity.js +7 -0
- package/dist/guest/accounts/watch.d.ts +28 -0
- package/dist/guest/accounts/watch.js +78 -0
- package/dist/guest/apps/resolvers-gamedev.d.ts +2 -0
- package/dist/guest/apps/resolvers-gamedev.js +1 -0
- package/dist/guest/apps/resolvers-mobile.d.ts +2 -0
- package/dist/guest/apps/resolvers-mobile.js +1 -0
- package/dist/guest/apps/resolvers.js +4 -0
- package/dist/guest/boot/boot.d.ts +5 -0
- package/dist/guest/boot/boot.js +35 -1
- package/dist/guest/daemon/accounts.d.ts +3 -0
- package/dist/guest/daemon/accounts.js +25 -0
- package/dist/guest/daemon/daemon.js +2 -0
- package/dist/guest/daemon/idle-shutdown.js +3 -1
- package/dist/guest/daemon/sessions.js +2 -0
- package/dist/guest/devtools/android.d.ts +2 -0
- package/dist/guest/devtools/android.js +7 -0
- package/dist/guest/devtools/devtool.d.ts +9 -0
- package/dist/guest/devtools/devtool.js +5 -0
- package/dist/guest/devtools/fivem.d.ts +2 -0
- package/dist/guest/devtools/fivem.js +7 -0
- package/dist/guest/devtools/registry.d.ts +6 -0
- package/dist/guest/devtools/registry.js +12 -0
- package/dist/guest/devtools/steam.d.ts +2 -0
- package/dist/guest/devtools/steam.js +228 -0
- package/dist/guest/devtools/unity.d.ts +2 -0
- package/dist/guest/devtools/unity.js +7 -0
- package/dist/guest/entries/daemon.js +2 -0
- package/dist/guest/entries/tool.d.ts +1 -0
- package/dist/guest/entries/tool.js +17 -0
- package/dist/guest/toolchains/engines/godot.d.ts +4 -0
- package/dist/guest/toolchains/engines/godot.js +4 -0
- package/dist/guest/toolchains/engines/unity-release.d.ts +15 -0
- package/dist/guest/toolchains/engines/unity-release.js +6 -0
- package/dist/guest/toolchains/engines/unity.d.ts +4 -0
- package/dist/guest/toolchains/engines/unity.js +4 -0
- package/dist/guest/toolchains/engines/unreal-engines.d.ts +4 -0
- package/dist/guest/toolchains/engines/unreal-engines.js +4 -0
- package/dist/guest/toolchains/fivem/artifacts.d.ts +6 -0
- package/dist/guest/toolchains/fivem/artifacts.js +7 -0
- package/dist/guest/toolchains/fivem/fivem.d.ts +4 -0
- package/dist/guest/toolchains/fivem/fivem.js +4 -0
- package/dist/guest/toolchains/installers.d.ts +12 -0
- package/dist/guest/toolchains/installers.js +39 -0
- package/dist/guest/toolchains/languages/java.js +1 -1
- package/dist/guest/toolchains/languages/kotlin.d.ts +3 -0
- package/dist/guest/toolchains/languages/kotlin.js +6 -0
- package/dist/guest/toolchains/mobile/android-repo.d.ts +11 -0
- package/dist/guest/toolchains/mobile/android-repo.js +10 -0
- package/dist/guest/toolchains/mobile/android.d.ts +17 -0
- package/dist/guest/toolchains/mobile/android.js +16 -0
- package/dist/guest/toolchains/mobile/dart.d.ts +4 -0
- package/dist/guest/toolchains/mobile/dart.js +4 -0
- package/dist/guest/toolchains/mobile/flutter.d.ts +4 -0
- package/dist/guest/toolchains/mobile/flutter.js +4 -0
- package/dist/guest/toolchains/mobile/react-native.d.ts +4 -0
- package/dist/guest/toolchains/mobile/react-native.js +4 -0
- package/dist/guest/toolchains/native/unreal.d.ts +3 -7
- package/dist/guest/toolchains/native/unreal.js +16 -167
- package/dist/guest/toolchains/native/visual-studio.d.ts +15 -0
- package/dist/guest/toolchains/native/visual-studio.js +159 -0
- package/dist/guest/toolchains/native/vs-request.d.ts +19 -0
- package/dist/guest/toolchains/native/vs-request.js +46 -0
- package/dist/guest/toolchains/not-yet.d.ts +2 -0
- package/dist/guest/toolchains/not-yet.js +6 -0
- package/dist/guest/toolchains/persisted.d.ts +48 -0
- package/dist/guest/toolchains/persisted.js +159 -0
- package/dist/guest/toolchains/plan.js +80 -4
- package/dist/guest/toolchains/steam/apps.d.ts +27 -0
- package/dist/guest/toolchains/steam/apps.js +112 -0
- package/dist/guest/toolchains/steam/client.d.ts +5 -0
- package/dist/guest/toolchains/steam/client.js +67 -0
- package/dist/guest/toolchains/steam/commands.d.ts +58 -0
- package/dist/guest/toolchains/steam/commands.js +155 -0
- package/dist/guest/toolchains/steam/steam.d.ts +4 -0
- package/dist/guest/toolchains/steam/steam.js +61 -0
- package/dist/guest/toolchains/steam/steamcmd-install.d.ts +29 -0
- package/dist/guest/toolchains/steam/steamcmd-install.js +177 -0
- package/dist/guest/toolchains/steam/steamcmd.d.ts +6 -0
- package/dist/guest/toolchains/steam/steamcmd.js +23 -0
- package/dist/guest/toolchains/step.d.ts +10 -0
- package/dist/host/accounts/deliver.d.ts +26 -0
- package/dist/host/accounts/deliver.js +80 -0
- package/dist/host/accounts/handoff.d.ts +17 -0
- package/dist/host/accounts/handoff.js +110 -0
- package/dist/host/accounts/index.d.ts +6 -0
- package/dist/host/accounts/index.js +8 -0
- package/dist/host/accounts/kinds/appstore.d.ts +2 -0
- package/dist/host/accounts/kinds/appstore.js +10 -0
- package/dist/host/accounts/kinds/cfx.d.ts +2 -0
- package/dist/host/accounts/kinds/cfx.js +7 -0
- package/dist/host/accounts/kinds/epic.d.ts +2 -0
- package/dist/host/accounts/kinds/epic.js +15 -0
- package/dist/host/accounts/kinds/expo.d.ts +2 -0
- package/dist/host/accounts/kinds/expo.js +7 -0
- package/dist/host/accounts/kinds/keystore.d.ts +2 -0
- package/dist/host/accounts/kinds/keystore.js +7 -0
- package/dist/host/accounts/kinds/play.d.ts +2 -0
- package/dist/host/accounts/kinds/play.js +7 -0
- package/dist/host/accounts/kinds/steam.d.ts +6 -0
- package/dist/host/accounts/kinds/steam.js +31 -0
- package/dist/host/accounts/kinds/unity.d.ts +2 -0
- package/dist/host/accounts/kinds/unity.js +13 -0
- package/dist/host/accounts/leases.d.ts +40 -0
- package/dist/host/accounts/leases.js +62 -0
- package/dist/host/accounts/source.d.ts +21 -0
- package/dist/host/accounts/source.js +15 -0
- package/dist/host/accounts/sources.d.ts +5 -0
- package/dist/host/accounts/sources.js +18 -0
- package/dist/host/accounts/store.d.ts +23 -0
- package/dist/host/accounts/store.js +74 -0
- package/dist/host/audit/log.d.ts +1 -1
- package/dist/host/launch/generate.d.ts +2 -0
- package/dist/host/launch/generate.js +16 -1
- package/dist/host/launch/hints/android.d.ts +3 -0
- package/dist/host/launch/hints/android.js +3 -0
- package/dist/host/launch/hints/engines.d.ts +3 -0
- package/dist/host/launch/hints/engines.js +3 -0
- package/dist/host/launch/hints/fivem.d.ts +3 -0
- package/dist/host/launch/hints/fivem.js +3 -0
- package/dist/host/launch/hints/hint.d.ts +2 -0
- package/dist/host/launch/hints/hint.js +1 -0
- package/dist/host/launch/hints/index.d.ts +5 -0
- package/dist/host/launch/hints/index.js +29 -0
- package/dist/host/launch/hints/mobile.d.ts +3 -0
- package/dist/host/launch/hints/mobile.js +3 -0
- package/dist/host/launch/hints/steam.d.ts +7 -0
- package/dist/host/launch/hints/steam.js +59 -0
- package/dist/host/launch/index.d.ts +2 -2
- package/dist/host/launch/index.js +2 -2
- package/dist/host/launch/mappings.js +2 -0
- package/dist/host/launch/session-file.js +2 -0
- package/dist/host/launch/stage.js +10 -1
- package/dist/host/launch/toolchains.d.ts +7 -0
- package/dist/host/launch/toolchains.js +11 -0
- package/dist/host/pc-installs/discover.d.ts +19 -0
- package/dist/host/pc-installs/discover.js +59 -0
- package/dist/host/pc-installs/epic.d.ts +2 -0
- package/dist/host/pc-installs/epic.js +7 -0
- package/dist/host/pc-installs/index.d.ts +3 -0
- package/dist/host/pc-installs/index.js +2 -0
- package/dist/host/pc-installs/source.d.ts +9 -0
- package/dist/host/pc-installs/source.js +1 -0
- package/dist/host/pc-installs/steam.d.ts +16 -0
- package/dist/host/pc-installs/steam.js +143 -0
- package/dist/host/pc-installs/unity.d.ts +2 -0
- package/dist/host/pc-installs/unity.js +7 -0
- package/dist/host/pc-installs/unreal.d.ts +2 -0
- package/dist/host/pc-installs/unreal.js +7 -0
- package/dist/host/profile/accounts.d.ts +21 -6
- package/dist/host/profile/accounts.js +108 -28
- package/dist/host/profile/claude-login.d.ts +32 -2
- package/dist/host/profile/claude-login.js +100 -8
- package/dist/host/profile/index.d.ts +1 -1
- package/dist/host/profile/index.js +1 -1
- package/dist/host/secrets/launch-secrets.js +1 -0
- package/dist/runtime/agent.js +321 -9
- package/dist/runtime/daemon.js +1987 -604
- package/dist/runtime/db.js +334 -11
- package/dist/runtime/main.js +2519 -723
- package/dist/runtime/netproxy.js +321 -9
- package/dist/runtime/tool.js +28549 -0
- package/dist/runtime/view.js +321 -9
- package/dist/selftest/accounts.test.js +172 -1
- package/dist/selftest/agent-paths.test.js +1 -0
- package/dist/selftest/android.test.d.ts +2 -0
- package/dist/selftest/android.test.js +7 -0
- package/dist/selftest/app.test.js +1 -1
- package/dist/selftest/engines.test.d.ts +2 -0
- package/dist/selftest/engines.test.js +7 -0
- package/dist/selftest/fivem.test.d.ts +2 -0
- package/dist/selftest/fivem.test.js +7 -0
- package/dist/selftest/generate.test.js +1 -1
- package/dist/selftest/index.js +18 -0
- package/dist/selftest/menu-screens.js +1 -0
- package/dist/selftest/menu.test.js +2 -1
- package/dist/selftest/mobile.test.d.ts +2 -0
- package/dist/selftest/mobile.test.js +7 -0
- package/dist/selftest/pc-installs.test.d.ts +2 -0
- package/dist/selftest/pc-installs.test.js +111 -0
- package/dist/selftest/persisted-installs.test.d.ts +2 -0
- package/dist/selftest/persisted-installs.test.js +107 -0
- package/dist/selftest/projects.test.js +6 -4
- package/dist/selftest/service-accounts.test.d.ts +2 -0
- package/dist/selftest/service-accounts.test.js +320 -0
- package/dist/selftest/steam.test.d.ts +2 -0
- package/dist/selftest/steam.test.js +316 -0
- package/dist/selftest/storage-helper.test.js +31 -8
- package/dist/selftest/support/engine-golden.js +3 -0
- package/dist/selftest/support/fixtures.d.ts +87 -0
- package/dist/selftest/support/fixtures.js +4 -3
- package/dist/selftest/support/pc-memory.d.ts +2 -0
- package/dist/selftest/support/pc-memory.js +17 -0
- package/dist/selftest/support/web-preview.js +20 -1
- package/dist/selftest/unity.test.d.ts +2 -0
- package/dist/selftest/unity.test.js +7 -0
- package/dist/shared/accounts/fingerprint.d.ts +1 -0
- package/dist/shared/accounts/fingerprint.js +20 -0
- package/dist/shared/accounts/index.d.ts +5 -0
- package/dist/shared/accounts/index.js +7 -0
- package/dist/shared/accounts/kinds/epic.d.ts +10 -0
- package/dist/shared/accounts/kinds/epic.js +56 -0
- package/dist/shared/accounts/kinds/steam.d.ts +28 -0
- package/dist/shared/accounts/kinds/steam.js +99 -0
- package/dist/shared/accounts/kinds.d.ts +41 -0
- package/dist/shared/accounts/kinds.js +154 -0
- package/dist/shared/accounts/payloads.d.ts +151 -0
- package/dist/shared/accounts/payloads.js +128 -0
- package/dist/shared/apps/catalogue-gamedev.d.ts +2 -0
- package/dist/shared/apps/catalogue-gamedev.js +1 -0
- package/dist/shared/apps/catalogue-mobile.d.ts +2 -0
- package/dist/shared/apps/catalogue-mobile.js +1 -0
- package/dist/shared/apps/catalogue-modding.d.ts +2 -0
- package/dist/shared/apps/catalogue-modding.js +1 -0
- package/dist/shared/apps/catalogue.d.ts +1 -0
- package/dist/shared/apps/catalogue.js +6 -1
- package/dist/shared/channel/schema.d.ts +183 -0
- package/dist/shared/channel/secrets.d.ts +42 -0
- package/dist/shared/channel/secrets.js +3 -0
- package/dist/shared/download/downloader.d.ts +7 -0
- package/dist/shared/download/downloader.js +10 -3
- package/dist/shared/installs.d.ts +19 -0
- package/dist/shared/installs.js +13 -0
- package/dist/shared/io/bounded.d.ts +6 -0
- package/dist/shared/io/bounded.js +38 -0
- package/dist/shared/io/index.d.ts +2 -0
- package/dist/shared/io/index.js +2 -0
- package/dist/shared/io/text.d.ts +9 -0
- package/dist/shared/io/text.js +18 -0
- package/dist/shared/paths/guest-paths.js +1 -1
- package/dist/shared/paths/index.d.ts +1 -0
- package/dist/shared/paths/index.js +1 -0
- package/dist/shared/paths/pc-installs.d.ts +20 -0
- package/dist/shared/paths/pc-installs.js +19 -0
- package/dist/shared/vdf.d.ts +25 -0
- package/dist/shared/vdf.js +151 -0
- package/dist/sync/sync-cycle.js +9 -0
- package/dist/web/index.html +1 -1
- package/dist/web/{main-JTH3FT4O.js → main-CWIF4VQI.js} +63 -61
- package/native/csharp/HcsHost/AssemblyInfo.cs +3 -3
- package/native/csharp/HostTools/AssemblyInfo.cs +3 -3
- package/native/csharp/StorageHelper/AssemblyInfo.cs +3 -3
- package/package.json +1 -1
- package/sandbox/scripts/add-firewall-port.ps1 +13 -0
- package/sandbox/templates/fivem/README.txt +2 -0
- package/schemas/config.schema.json +485 -0
- package/schemas/sandbox-config.schema.json +416 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
// Steam: the VDF parser and its limits, the SteamCMD sign-in from a fake steamcmd folder (host) and back (sandbox),
|
|
2
|
+
// SteamCMD's command lines (apps, workshop, builds), the PC's libraries from a fixture libraryfolders.vdf, the SteamCMD
|
|
3
|
+
// install (a bad signer, the time limit, nothing private kept on the data disk), room checks with an injected statfs,
|
|
4
|
+
// and the build-script hint. Temp folders and fakes only: nothing is downloaded or run.
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { DEFAULTS } from '../core/config/index.js';
|
|
8
|
+
import { decideToolchains } from '../core/detect/index.js';
|
|
9
|
+
import { HOST_SOURCES } from '../host/accounts/index.js';
|
|
10
|
+
import { steamCmdFolderOf } from '../host/accounts/kinds/steam.js';
|
|
11
|
+
import { steamHints } from '../host/launch/hints/steam.js';
|
|
12
|
+
import { discoverPcInstalls } from '../host/pc-installs/index.js';
|
|
13
|
+
import { findSteamInstalls, steamInstalls, steamLibraries } from '../host/pc-installs/steam.js';
|
|
14
|
+
import { fingerprint, readSteamCmdLogin } from '../shared/accounts/index.js';
|
|
15
|
+
import { encodeText } from '../shared/io/index.js';
|
|
16
|
+
import { InstallRecordsSchema } from '../shared/installs.js';
|
|
17
|
+
import { PC_INSTALLS_GUEST } from '../shared/paths/index.js';
|
|
18
|
+
import { parseVdf, vdfGet, vdfText } from '../shared/vdf.js';
|
|
19
|
+
import { steamAdapterFor } from '../guest/accounts/kinds/steam.js';
|
|
20
|
+
import { assertAppRoom, installedApps, recordApp } from '../guest/toolchains/steam/apps.js';
|
|
21
|
+
import { appBuildArgs, appUpdateArgs, scriptKind, withBuildSettings, workshopGetArgs, workshopPushArgs, } from '../guest/toolchains/steam/commands.js';
|
|
22
|
+
import { fetchSteamCmd, prepareSteamCmd } from '../guest/toolchains/steam/steamcmd-install.js';
|
|
23
|
+
const GB = 1024 ** 3;
|
|
24
|
+
const PC = { localAppData: '', appData: '', programData: '' };
|
|
25
|
+
const roomy = { statfs: () => ({ bavail: 100 * GB, bsize: 1 }) };
|
|
26
|
+
const short = { statfs: () => ({ bavail: GB / 4, bsize: 1 }) };
|
|
27
|
+
/** A VDF string as Steam writes paths: backslashes doubled. */
|
|
28
|
+
const q = (p) => '"' + p.replace(/\\/g, '\\\\') + '"';
|
|
29
|
+
const CONFIG_VDF = `"InstallConfigStore"
|
|
30
|
+
{
|
|
31
|
+
"Software"
|
|
32
|
+
{
|
|
33
|
+
"Valve"
|
|
34
|
+
{
|
|
35
|
+
"Steam"
|
|
36
|
+
{
|
|
37
|
+
"Accounts"
|
|
38
|
+
{
|
|
39
|
+
"builder_bot"
|
|
40
|
+
{
|
|
41
|
+
"SteamID" "76561190000000000"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
"ConnectCache"
|
|
45
|
+
{
|
|
46
|
+
"7a2c1b3d1" "0200000001000000aabbccdd"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
`;
|
|
53
|
+
const APP_BUILD = `"AppBuild"
|
|
54
|
+
{
|
|
55
|
+
"AppID" "480" // Spacewar
|
|
56
|
+
"Desc" "nightly"
|
|
57
|
+
"ContentRoot" "..\\content\\"
|
|
58
|
+
"BuildOutput" "..\\output\\"
|
|
59
|
+
"Depots"
|
|
60
|
+
{
|
|
61
|
+
"481" "depot_build_481.vdf"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
export const steamSuite = {
|
|
66
|
+
name: 'steam',
|
|
67
|
+
title: 'Steam: the VDF parser, the SteamCMD sign-in, sbx-steam command lines, PC libraries, SteamCMD install',
|
|
68
|
+
run: async (t, f) => {
|
|
69
|
+
t.section('the VDF parser');
|
|
70
|
+
const parsed = parseVdf('\uFEFF// a comment\n"libraryfolders"\n{\n\t"0"\n\t{\n\t\t"path"\t\t"C:\\\\Program Files (x86)\\\\Steam"\n' +
|
|
71
|
+
'\t\t"Label" "say \\"hi\\""\n\t}\n\tunquoted value\n\t"cond" "1" [$WIN32]\n}\n');
|
|
72
|
+
t.equal(vdfText(parsed, 'libraryfolders', '0', 'path'), 'C:\\Program Files (x86)\\Steam', 'escaped paths come out');
|
|
73
|
+
t.equal(vdfText(parsed, 'LIBRARYFOLDERS', '0', 'label'), 'say "hi"', 'keys match in any case, quotes unescape');
|
|
74
|
+
t.check(vdfText(parsed, 'libraryfolders', 'unquoted') === 'value' && vdfText(parsed, 'libraryfolders', 'cond') === '1', 'comments, a byte-order mark, unquoted words and [conditions] are handled');
|
|
75
|
+
t.equal(vdfText(parseVdf(APP_BUILD, { escapes: false }), 'AppBuild', 'ContentRoot'), '..\\content\\', 'without escapes a hand-written "..\\content\\" stays as written');
|
|
76
|
+
t.equal(vdfText(parseVdf('"a" { "k" "first" "K" "second" }'), 'a', 'k'), 'first', 'a repeated key keeps the first value');
|
|
77
|
+
const proto = parseVdf('"__proto__" { "polluted" "yes" } "constructor" "x"');
|
|
78
|
+
t.check(proto !== null &&
|
|
79
|
+
{}.polluted === undefined &&
|
|
80
|
+
vdfGet(proto, '__proto__') !== undefined, 'a "__proto__" key is a plain key (nothing is polluted)');
|
|
81
|
+
const refused = [
|
|
82
|
+
['"a" { "b" "c"', 'an unclosed block'],
|
|
83
|
+
['"a" "b" }', 'a stray }'],
|
|
84
|
+
['"a" { "b" "c', 'an unclosed quote'],
|
|
85
|
+
['"a"', 'a key without a value'],
|
|
86
|
+
['{ "a" "b" }', 'a block without a key'],
|
|
87
|
+
['"x" [$WIN32', 'an unclosed condition'],
|
|
88
|
+
['"a" "' + 'x'.repeat(2000) + '"', 'a text over maxBytes', { maxBytes: 1000 }],
|
|
89
|
+
['"a" { "b" { "c" "d" } }', 'nesting over maxDepth', { maxDepth: 1 }],
|
|
90
|
+
['"a" "1" "b" "2" "c" "3"', 'more keys than maxKeys', { maxKeys: 2 }],
|
|
91
|
+
];
|
|
92
|
+
for (const [text, what, limits] of refused)
|
|
93
|
+
t.equal(parseVdf(text, limits), null, 'refused: ' + what);
|
|
94
|
+
const deep = '"a" {'.repeat(50_000);
|
|
95
|
+
const flat = '"r" {' + Array.from({ length: 50_000 }, (_, i) => '"k' + i + '" "v"').join(' ') + '}';
|
|
96
|
+
const started = Date.now();
|
|
97
|
+
const deepResult = parseVdf(deep);
|
|
98
|
+
const flatResult = parseVdf(flat);
|
|
99
|
+
t.check(deepResult === null && flatResult !== null && Date.now() - started < 5000, 'deep nesting is refused and 50000 keys parse quickly (no quadratic work)', Date.now() - started + ' ms');
|
|
100
|
+
t.equal(parseVdf('"a" "b"', { maxBytes: 3 }), null, 'the size limit holds before anything is parsed');
|
|
101
|
+
t.section('the SteamCMD sign-in: from a steamcmd folder on this PC');
|
|
102
|
+
const pcCmd = t.dir('pc-steamcmd');
|
|
103
|
+
t.write(path.join(pcCmd, 'steamcmd.exe'), 'exe');
|
|
104
|
+
t.write(path.join(pcCmd, 'config', 'config.vdf'), CONFIG_VDF);
|
|
105
|
+
for (let i = 1; i <= 5; i++) {
|
|
106
|
+
t.write(path.join(pcCmd, 'ssfn' + i), 'sentry' + i);
|
|
107
|
+
fs.utimesSync(path.join(pcCmd, 'ssfn' + i), new Date(), new Date(Date.now() - (6 - i) * 60_000));
|
|
108
|
+
}
|
|
109
|
+
t.write(path.join(pcCmd, 'ssfn99'), 'x'.repeat(60 * 1024));
|
|
110
|
+
t.write(path.join(pcCmd, 'ssfnabc'), 'not a sentry');
|
|
111
|
+
const source = HOST_SOURCES.steam;
|
|
112
|
+
const login = source.fromPc(PC, pcCmd);
|
|
113
|
+
t.check(login !== null &&
|
|
114
|
+
login.account === 'builder_bot' &&
|
|
115
|
+
login.configVdf === CONFIG_VDF &&
|
|
116
|
+
login.sentries.length === 4 &&
|
|
117
|
+
login.sentries.map((s) => s.name).join() === 'ssfn2,ssfn3,ssfn4,ssfn5' &&
|
|
118
|
+
Buffer.from(login.sentries[3].base64, 'base64').toString() === 'sentry5', 'config.vdf and the newest four sentry files are taken (an oversized or misnamed one is not)', JSON.stringify(login?.sentries.map((s) => s.name)));
|
|
119
|
+
t.equal(login ? source.summary(login) : null, 'builder_bot', 'its summary is the account name, never the token');
|
|
120
|
+
t.check(steamCmdFolderOf(path.join(pcCmd, 'steamcmd.exe')) === pcCmd &&
|
|
121
|
+
steamCmdFolderOf(path.join(pcCmd, 'config', 'config.vdf')) === pcCmd &&
|
|
122
|
+
fingerprint(source.fromPc(PC, path.join(pcCmd, 'config', 'config.vdf'))) === fingerprint(login), '--from may name the folder, its steamcmd.exe or its config.vdf');
|
|
123
|
+
const utf16 = t.dir('pc-steamcmd-utf16');
|
|
124
|
+
fs.mkdirSync(path.join(utf16, 'config'));
|
|
125
|
+
fs.writeFileSync(path.join(utf16, 'config', 'config.vdf'), encodeText(CONFIG_VDF, 'utf16le'));
|
|
126
|
+
t.equal(readSteamCmdLogin(utf16)?.account, 'builder_bot', 'a UTF-16 config.vdf is read too');
|
|
127
|
+
const signedOut = t.dir('pc-steamcmd-out');
|
|
128
|
+
t.write(path.join(signedOut, 'config', 'config.vdf'), CONFIG_VDF.replace(/"ConnectCache"[\s\S]*?\}/, ''));
|
|
129
|
+
t.check(source.fromPc(PC, signedOut) === null && source.fromPc(PC, t.at('nowhere')) === null, 'no cached login (or no folder): nothing is taken');
|
|
130
|
+
const big = t.dir('pc-steamcmd-big');
|
|
131
|
+
t.write(path.join(big, 'config', 'config.vdf'), CONFIG_VDF + ' '.repeat(300 * 1024));
|
|
132
|
+
t.equal(readSteamCmdLogin(big), null, 'a config.vdf over 256 KB is refused');
|
|
133
|
+
t.section('the SteamCMD sign-in: in the sandbox');
|
|
134
|
+
const guestCmd = t.dir('guest-steamcmd');
|
|
135
|
+
const adapter = steamAdapterFor(guestCmd);
|
|
136
|
+
const applied = await adapter.apply(login);
|
|
137
|
+
const back = adapter.current();
|
|
138
|
+
t.check(fs.readFileSync(path.join(guestCmd, 'config', 'config.vdf'), 'utf8') === CONFIG_VDF &&
|
|
139
|
+
fs.readFileSync(path.join(guestCmd, 'ssfn5'), 'utf8') === 'sentry5' &&
|
|
140
|
+
!applied.summary.includes('aabbccdd') &&
|
|
141
|
+
back !== null &&
|
|
142
|
+
fingerprint(back) === fingerprint(login), "applied into SteamCMD's folder and read back the same (the watch hands back nothing new)");
|
|
143
|
+
t.write(path.join(guestCmd, 'config', 'config.vdf'), CONFIG_VDF.replace('aabbccdd', 'eeff0011'));
|
|
144
|
+
t.check(fingerprint(adapter.current()) !== fingerprint(login), 'a renewed login token reads back different (it goes back to the PC)');
|
|
145
|
+
t.section('SteamCMD command lines');
|
|
146
|
+
const dir = 'C:\\AgentData\\tools\\steam-apps\\740';
|
|
147
|
+
t.equal(appUpdateArgs({ appId: '740', dir, login: 'anonymous', beta: 'public-beta', validate: true }), [
|
|
148
|
+
'+force_install_dir',
|
|
149
|
+
dir,
|
|
150
|
+
'+login',
|
|
151
|
+
'anonymous',
|
|
152
|
+
'+app_update',
|
|
153
|
+
'740',
|
|
154
|
+
'-beta',
|
|
155
|
+
'public-beta',
|
|
156
|
+
'validate',
|
|
157
|
+
'+quit',
|
|
158
|
+
], 'app: the folder comes before the login, then the app, its branch and validate');
|
|
159
|
+
t.equal(workshopGetArgs({
|
|
160
|
+
appId: '4000',
|
|
161
|
+
items: ['123', '456'],
|
|
162
|
+
dir: 'C:\\AgentData\\cache\\steam-workshop',
|
|
163
|
+
login: 'me',
|
|
164
|
+
}), [
|
|
165
|
+
'+force_install_dir',
|
|
166
|
+
'C:\\AgentData\\cache\\steam-workshop',
|
|
167
|
+
'+login',
|
|
168
|
+
'me',
|
|
169
|
+
'+workshop_download_item',
|
|
170
|
+
'4000',
|
|
171
|
+
'123',
|
|
172
|
+
'+workshop_download_item',
|
|
173
|
+
'4000',
|
|
174
|
+
'456',
|
|
175
|
+
'+quit',
|
|
176
|
+
], 'workshop get: one download command per item');
|
|
177
|
+
t.equal(workshopPushArgs('me', 'C:\\Workspace\\game\\item.vdf'), ['+login', 'me', '+workshop_build_item', 'C:\\Workspace\\game\\item.vdf', '+quit'], 'workshop push: workshop_build_item');
|
|
178
|
+
t.equal(appBuildArgs('me', 'C:\\Workspace\\game\\app_build_480.vdf'), ['+login', 'me', '+run_app_build', 'C:\\Workspace\\game\\app_build_480.vdf', '+quit'], 'build: run_app_build');
|
|
179
|
+
const bad = [
|
|
180
|
+
[() => appUpdateArgs({ appId: '480 +quit', dir, login: 'me' }), 'an app ID with a command in it'],
|
|
181
|
+
[() => appUpdateArgs({ appId: '0', dir, login: 'me' }), 'app ID 0'],
|
|
182
|
+
[() => appUpdateArgs({ appId: '480', dir, login: 'me +runscript x' }), 'an account with a command in it'],
|
|
183
|
+
[() => appUpdateArgs({ appId: '480', dir, login: 'me', beta: 'a b' }), 'a branch with a space'],
|
|
184
|
+
[() => appUpdateArgs({ appId: '480', dir: 'relative\\dir', login: 'me' }), 'a relative folder'],
|
|
185
|
+
[() => appUpdateArgs({ appId: '480', dir: 'C:\\a "b"', login: 'me' }), 'a folder with a quote'],
|
|
186
|
+
[() => appUpdateArgs({ appId: '480', dir: 'C:\\x +quit', login: 'me' }), 'a folder with " +"'],
|
|
187
|
+
[() => workshopGetArgs({ appId: '480', items: [], dir, login: 'me' }), 'no workshop items'],
|
|
188
|
+
[() => workshopGetArgs({ appId: '480', items: ['12a'], dir, login: 'me' }), 'a bad item ID'],
|
|
189
|
+
[() => workshopPushArgs('anonymous', 'C:\\item.vdf'), 'uploading anonymously'],
|
|
190
|
+
[() => appBuildArgs('me', 'app_build.vdf'), 'a relative script'],
|
|
191
|
+
];
|
|
192
|
+
for (const [fn, what] of bad)
|
|
193
|
+
await t.throws(fn, /not a|cannot be given|no workshop|needs your Steam/, 'refused: ' + what);
|
|
194
|
+
t.check(scriptKind(APP_BUILD) === 'appbuild' &&
|
|
195
|
+
scriptKind('"workshopitem" { "appid" "480" "publishedfileid" "0" }') === 'workshopitem' &&
|
|
196
|
+
scriptKind('"libraryfolders" { }') === null, 'a script is told by its first block');
|
|
197
|
+
const preview = withBuildSettings(APP_BUILD, { preview: true, desc: 'from the sandbox' });
|
|
198
|
+
const previewRoot = preview ? parseVdf(preview, { escapes: false }) : null;
|
|
199
|
+
t.check(vdfText(previewRoot, 'AppBuild', 'Preview') === '1' &&
|
|
200
|
+
vdfText(previewRoot, 'AppBuild', 'Desc') === 'from the sandbox' &&
|
|
201
|
+
vdfText(previewRoot, 'AppBuild', 'ContentRoot') === '..\\content\\', '--preview and --desc set Preview and Desc in a copy of the script, the rest unchanged');
|
|
202
|
+
t.check(vdfText(parseVdf(withBuildSettings(preview, { preview: true }), { escapes: false }), 'AppBuild', 'Preview') ===
|
|
203
|
+
'1' && withBuildSettings('"workshopitem" { }', { preview: true }) === null, 'an existing Preview is replaced; a workshop script is not a build');
|
|
204
|
+
await t.throws(() => withBuildSettings(APP_BUILD, { desc: 'a "quoted" one' }), 'one line', 'a description cannot break the script');
|
|
205
|
+
t.section("this PC's Steam libraries");
|
|
206
|
+
const steamRoot = t.dir('pc-steam');
|
|
207
|
+
const lib2 = t.dir('pc-steam-lib2');
|
|
208
|
+
t.write(path.join(steamRoot, 'steamapps', 'libraryfolders.vdf'), `"libraryfolders"\n{\n\t"0"\n\t{\n\t\t"path"\t\t${q(steamRoot)}\n\t\t"apps"\n\t\t{\n\t\t\t"228980"\t\t"1"\n\t\t}\n\t}\n` +
|
|
209
|
+
`\t"1"\n\t{\n\t\t"path"\t\t${q(lib2)}\n\t\t"apps"\n\t\t{\n\t\t\t"480"\t\t"1"\n\t\t}\n\t}\n}\n`);
|
|
210
|
+
t.write(path.join(lib2, 'steamapps', 'appmanifest_480.acf'), '"AppState"\n{\n\t"appid"\t\t"480"\n\t"name"\t\t"Spacewar"\n\t"installdir"\t\t"Spacewar"\n\t"buildid"\t\t"123"\n}\n');
|
|
211
|
+
t.dir('pc-steam-lib2', 'steamapps', 'common', 'Spacewar');
|
|
212
|
+
t.write(path.join(lib2, 'steamapps', 'appmanifest_10.acf'), '"AppState" { "appid" "10" "name" "Climber" "installdir" "..\\\\..\\\\Windows" }');
|
|
213
|
+
t.equal(steamLibraries(fs.readFileSync(path.join(steamRoot, 'steamapps', 'libraryfolders.vdf'), 'utf8')).map((l) => [
|
|
214
|
+
l.path,
|
|
215
|
+
l.apps,
|
|
216
|
+
]), [
|
|
217
|
+
[steamRoot, ['228980']],
|
|
218
|
+
[lib2, ['480']],
|
|
219
|
+
], 'libraryfolders.vdf: each library and the apps it lists');
|
|
220
|
+
t.equal(steamLibraries('"LibraryFolders" { "TimeNextStatsReport" "1" "ContentStatsID" "2" "1" "D:\\\\Games" }').map((l) => l.path), ['D:\\Games'], 'the old libraryfolders.vdf format too');
|
|
221
|
+
const libraries = findSteamInstalls(steamRoot, true);
|
|
222
|
+
t.check(libraries.map((l) => [l.kind, l.id, l.host].join()).join('|') ===
|
|
223
|
+
[
|
|
224
|
+
'steam-library,library1,' + path.join(steamRoot, 'steamapps'),
|
|
225
|
+
'steam-library,library2,' + path.join(lib2, 'steamapps'),
|
|
226
|
+
].join('|') && libraries.every((l) => !l.name.includes(steamRoot) && !l.name.includes(lib2)), "true: every library's steamapps, named without a path of this PC");
|
|
227
|
+
const games = findSteamInstalls(steamRoot, [480, '10', 'abc', 99]);
|
|
228
|
+
t.equal(games.map((g) => [g.id, g.name, g.version, g.host]), [['480', 'Spacewar', '123', path.join(lib2, 'steamapps', 'common', 'Spacewar')]], 'a list: only those games, from their manifests (an installdir that climbs out, or an unknown ID, is left out)');
|
|
229
|
+
const fixture = { ...steamInstalls, find: (wanted) => findSteamInstalls(steamRoot, wanted) };
|
|
230
|
+
const found = discoverPcInstalls({ ...DEFAULTS.pcInstalls, steamGames: [480] }, { packageRoot: t.packageRoot, storageRoot: f.baseConfig.storage.root, sources: [fixture] }, PC);
|
|
231
|
+
t.check(found.mappings.length === 1 &&
|
|
232
|
+
found.mappings[0].guest === PC_INSTALLS_GUEST + '\\steam\\480' &&
|
|
233
|
+
found.mappings[0].readOnly &&
|
|
234
|
+
!JSON.stringify(found.manifest).includes(lib2), 'mapped read-only at C:\\PcInstalls\\steam\\480; the manifest holds no path of this PC');
|
|
235
|
+
t.section('installing SteamCMD');
|
|
236
|
+
const tools = t.dir('steam-tools');
|
|
237
|
+
const store = path.join(tools, 'steamcmd');
|
|
238
|
+
const work = t.dir('steam-work');
|
|
239
|
+
let downloads = 0;
|
|
240
|
+
const fake = (signed, extra = {}) => ({
|
|
241
|
+
...roomy,
|
|
242
|
+
toolsDir: tools,
|
|
243
|
+
storeDir: store,
|
|
244
|
+
workDir: work,
|
|
245
|
+
signed,
|
|
246
|
+
download: async (_url, file, verify) => {
|
|
247
|
+
downloads++;
|
|
248
|
+
fs.writeFileSync(file + '.part', 'zip');
|
|
249
|
+
try {
|
|
250
|
+
await verify(file + '.part');
|
|
251
|
+
fs.renameSync(file + '.part', file);
|
|
252
|
+
}
|
|
253
|
+
finally {
|
|
254
|
+
fs.rmSync(file + '.part', { force: true });
|
|
255
|
+
}
|
|
256
|
+
},
|
|
257
|
+
extract: async (_archive, destination) => {
|
|
258
|
+
fs.mkdirSync(destination, { recursive: true });
|
|
259
|
+
fs.writeFileSync(path.join(destination, 'steamcmd.exe'), 'steamcmd');
|
|
260
|
+
},
|
|
261
|
+
run: async (_exe, args, cwd) => {
|
|
262
|
+
if (args.join() === '+quit')
|
|
263
|
+
fs.writeFileSync(path.join(cwd, 'steamclient64.dll'), 'updated');
|
|
264
|
+
return { code: 7, timedOut: false };
|
|
265
|
+
},
|
|
266
|
+
...extra,
|
|
267
|
+
});
|
|
268
|
+
await t.throws(() => fetchSteamCmd(store, fake(() => false)), 'not signed by Valve', 'a steamcmd.exe not signed by Valve is refused');
|
|
269
|
+
t.check(!fs.existsSync(store) && !fs.readdirSync(tools).some((n) => n.includes('staging')), 'and nothing of it is kept');
|
|
270
|
+
downloads = 0;
|
|
271
|
+
await t.throws(() => fetchSteamCmd(store, fake(() => true, short)), 'it needs', 'too little room on the data disk: refused before downloading');
|
|
272
|
+
t.equal(downloads, 0, 'nothing was downloaded');
|
|
273
|
+
// The boot applied the sign-in into the working folder before this step (accounts come first).
|
|
274
|
+
t.write(path.join(work, 'config', 'config.vdf'), CONFIG_VDF);
|
|
275
|
+
t.write(path.join(work, 'ssfn5'), 'sentry5');
|
|
276
|
+
const exe = await prepareSteamCmd(fake(() => true));
|
|
277
|
+
t.check(exe === path.join(work, 'steamcmd.exe') &&
|
|
278
|
+
fs.readFileSync(path.join(work, 'config', 'config.vdf'), 'utf8') === CONFIG_VDF &&
|
|
279
|
+
fs.existsSync(path.join(store, 'steamclient64.dll')), 'downloaded once, copied in without touching the sign-in, updated, and the updated binaries kept');
|
|
280
|
+
t.check(!fs.existsSync(path.join(store, 'config')) && !fs.existsSync(path.join(store, 'ssfn5')), 'the kept copy on the data disk never holds config\\ or a sentry file');
|
|
281
|
+
const fresh = t.dir('steam-work-2');
|
|
282
|
+
t.write(path.join(store, 'config', 'config.vdf'), 'planted');
|
|
283
|
+
downloads = 0;
|
|
284
|
+
await prepareSteamCmd(fake(() => true, { workDir: fresh }));
|
|
285
|
+
t.check(downloads === 0 && !fs.existsSync(path.join(store, 'config')) && !fs.existsSync(path.join(fresh, 'config')), 'the next sandbox copies the kept binaries (no download); a config\\ planted there is removed, not copied');
|
|
286
|
+
await t.throws(() => prepareSteamCmd(fake(() => true, { workDir: t.dir('steam-work-3'), run: async () => ({ code: -1, timedOut: true }) })), 'did not finish updating itself', 'its first-run update has a time limit');
|
|
287
|
+
await t.throws(() => prepareSteamCmd(fake((file) => !file.startsWith(t.at('steam-work-4')), { workDir: t.dir('steam-work-4') })), 'not signed by Valve', 'a steamcmd.exe that is not Valve-signed is never run');
|
|
288
|
+
t.check(!fs.existsSync(store), 'and the kept copy goes (downloaded again next boot)');
|
|
289
|
+
t.section('apps and room');
|
|
290
|
+
await t.throws(() => assertAppRoom(path.join(tools, 'steam-apps', '740'), 5 * GB, 'app 740', short), 'it needs 5.0 GB', 'sbx-steam app refuses when the disk is short');
|
|
291
|
+
t.write(path.join(tools, 'steam-apps', '740', 'steamapps', 'appmanifest_740.acf'), '"AppState" { "appid" "740" "name" "Counter-Strike Global Offensive - Dedicated Server" "installdir" "csgo_ds" }');
|
|
292
|
+
recordApp('740', 123_456, { toolsDir: tools });
|
|
293
|
+
const apps = installedApps({ toolsDir: tools });
|
|
294
|
+
t.check(apps.length === 1 &&
|
|
295
|
+
apps[0].name.startsWith('Counter-Strike') &&
|
|
296
|
+
apps[0].sizeBytes === 123_456 &&
|
|
297
|
+
InstallRecordsSchema.safeParse(t.readJson(path.join(tools, 'installs.json'))).success, 'its size is recorded in tools\\installs.json (sandbox data shows it) and sbx-steam games lists it');
|
|
298
|
+
t.section('the build-script hint');
|
|
299
|
+
const project = t.dir('steam-project');
|
|
300
|
+
t.write(path.join(project, 'tools', 'ContentBuilder', 'scripts', 'app_build_480.vdf'), APP_BUILD);
|
|
301
|
+
t.write(path.join(project, 'node_modules', 'x', 'app_build.vdf'), APP_BUILD);
|
|
302
|
+
const tc = structuredClone(DEFAULTS.toolchains);
|
|
303
|
+
const hints = steamHints(project, tc);
|
|
304
|
+
t.check((hints.steam ?? '').includes('tools/ContentBuilder/scripts/app_build_480.vdf') &&
|
|
305
|
+
decideToolchains(tc, [], [], hints).steam.on, 'an app_build .vdf a few folders down turns Steam on');
|
|
306
|
+
const other = t.dir('steam-project-2');
|
|
307
|
+
t.write(path.join(other, 'node_modules', 'x', 'app_build.vdf'), APP_BUILD);
|
|
308
|
+
t.write(path.join(other, 'big.vdf'), '"workshopitem" { "title" "' + 'x'.repeat(70 * 1024) + '" }');
|
|
309
|
+
t.write(path.join(other, 'libraryfolders.vdf'), '"libraryfolders" { }');
|
|
310
|
+
t.write(path.join(other, 'item.txt'), '"workshopitem" { }');
|
|
311
|
+
t.equal(steamHints(other, tc), {}, 'skipped folders, oversized files and other .vdf files give no hint');
|
|
312
|
+
const off = structuredClone(tc);
|
|
313
|
+
off.steam.enabled = false;
|
|
314
|
+
t.equal(steamHints(project, off), {}, 'nothing is read when Steam is not "auto"');
|
|
315
|
+
},
|
|
316
|
+
};
|
|
@@ -59,6 +59,16 @@ function buildHelper(t) {
|
|
|
59
59
|
function own(dir, sid) {
|
|
60
60
|
spawnSync(SYSTEM.icacls(), [dir, '/setowner', '*' + sid, '/T', '/C', '/Q'], { windowsHide: true });
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* A new folder whose permissions are its own (not inherited from %TEMP%, which other tools may have opened up): only
|
|
64
|
+
* `sid`, SYSTEM and Administrators may change it and what is made below it. Null when icacls could not set that.
|
|
65
|
+
*/
|
|
66
|
+
function privateFolder(dir, sid) {
|
|
67
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
68
|
+
const full = '(OI)(CI)F';
|
|
69
|
+
const r = spawnSync(SYSTEM.icacls(), [dir, '/inheritance:r', '/grant:r', '*' + sid + ':' + full, '*S-1-5-18:' + full, '*S-1-5-32-544:' + full, '/Q'], { windowsHide: true });
|
|
70
|
+
return r.status === 0 ? dir : null;
|
|
71
|
+
}
|
|
62
72
|
function units(t) {
|
|
63
73
|
t.section('the request the CLI sends, and what it makes of the answer');
|
|
64
74
|
t.check(HelperRequestSchema.safeParse({ verb: 'mount', set: 'main' }).success, 'a request is a verb and a set');
|
|
@@ -164,7 +174,17 @@ function checkRun(exe, verb, root, set, sid, maxGb) {
|
|
|
164
174
|
}
|
|
165
175
|
function checks(t, exe, sid) {
|
|
166
176
|
t.section('the checks (dry run): what the helper accepts and refuses');
|
|
167
|
-
|
|
177
|
+
// Every folder here starts private, so what is refused below is what the test opened up, never %TEMP%'s own rights.
|
|
178
|
+
const base = privateFolder(t.at('storage-helper', 'checks'), sid);
|
|
179
|
+
t.check(base !== null, 'the test folder is made private (only you, SYSTEM and Administrators may change it)');
|
|
180
|
+
if (!base)
|
|
181
|
+
return;
|
|
182
|
+
const dir = (name) => {
|
|
183
|
+
const p = path.join(base, name);
|
|
184
|
+
fs.mkdirSync(p, { recursive: true });
|
|
185
|
+
return p;
|
|
186
|
+
};
|
|
187
|
+
const root = dir('store');
|
|
168
188
|
const paths = containedPaths(root, 'main');
|
|
169
189
|
for (const dir of [paths.imagesDir, paths.mountsDir, paths.mount])
|
|
170
190
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -221,8 +241,8 @@ function checks(t, exe, sid) {
|
|
|
221
241
|
}
|
|
222
242
|
else
|
|
223
243
|
t.skip('a folder others may change (icacls failed)');
|
|
224
|
-
const linkTarget =
|
|
225
|
-
const junctionRoot =
|
|
244
|
+
const linkTarget = dir('linktarget');
|
|
245
|
+
const junctionRoot = dir('junction');
|
|
226
246
|
const junction = containedPaths(junctionRoot, 'main');
|
|
227
247
|
fs.mkdirSync(junction.mount, { recursive: true });
|
|
228
248
|
const mk = spawnSync(SYSTEM.cmd(), ['/c', 'mklink', '/J', junction.imagesDir, linkTarget], { windowsHide: true });
|
|
@@ -234,8 +254,8 @@ function checks(t, exe, sid) {
|
|
|
234
254
|
}
|
|
235
255
|
else
|
|
236
256
|
t.skip('images folder as a junction');
|
|
237
|
-
const above =
|
|
238
|
-
const realParent =
|
|
257
|
+
const above = dir('above');
|
|
258
|
+
const realParent = dir('above-real');
|
|
239
259
|
const inner = containedPaths(path.join(realParent, 'store'), 'main');
|
|
240
260
|
for (const dir of [inner.imagesDir, inner.mount])
|
|
241
261
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -248,7 +268,7 @@ function checks(t, exe, sid) {
|
|
|
248
268
|
}
|
|
249
269
|
else
|
|
250
270
|
t.skip('a junction above the storage folder');
|
|
251
|
-
const mountRoot =
|
|
271
|
+
const mountRoot = dir('mountlink');
|
|
252
272
|
const mountLinked = containedPaths(mountRoot, 'main');
|
|
253
273
|
fs.mkdirSync(mountLinked.imagesDir, { recursive: true });
|
|
254
274
|
fs.mkdirSync(mountLinked.mountsDir, { recursive: true });
|
|
@@ -261,7 +281,7 @@ function checks(t, exe, sid) {
|
|
|
261
281
|
}
|
|
262
282
|
else
|
|
263
283
|
t.skip('mount folder as a junction');
|
|
264
|
-
const missing =
|
|
284
|
+
const missing = dir('missing');
|
|
265
285
|
own(missing, sid);
|
|
266
286
|
refused('a storage folder without images and mounts', run({ root: missing }), 'missing');
|
|
267
287
|
}
|
|
@@ -347,7 +367,10 @@ async function startConsole(exe, args) {
|
|
|
347
367
|
}
|
|
348
368
|
async function server(t, exe, sid) {
|
|
349
369
|
t.section('the server in its console mode (this user, a pipe of its own, dry run)');
|
|
350
|
-
const root = t.
|
|
370
|
+
const root = privateFolder(t.at('storage-helper', 'served'), sid);
|
|
371
|
+
t.check(root !== null, 'the served storage folder is made private');
|
|
372
|
+
if (!root)
|
|
373
|
+
return;
|
|
351
374
|
const paths = containedPaths(root, 'main');
|
|
352
375
|
for (const dir of [paths.imagesDir, paths.mount])
|
|
353
376
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -29,6 +29,7 @@ export const LAUNCH_FILES_GOLDEN = [
|
|
|
29
29
|
'runtime/gitguard.js',
|
|
30
30
|
'runtime/main.js',
|
|
31
31
|
'runtime/netproxy.js',
|
|
32
|
+
'runtime/tool.js',
|
|
32
33
|
'runtime/view.js',
|
|
33
34
|
// The agent add-ons' lock files (pinned installs), copied with the rest of sandbox\.
|
|
34
35
|
'sandbox/addons/ccusage/package-lock.json',
|
|
@@ -62,6 +63,7 @@ export const LAUNCH_FILES_GOLDEN = [
|
|
|
62
63
|
'sandbox/csharp/SandboxTools/Program.cs',
|
|
63
64
|
'sandbox/csharp/VsWhere.cs',
|
|
64
65
|
'sandbox/csharp/VtInput.cs',
|
|
66
|
+
'sandbox/scripts/add-firewall-port.ps1',
|
|
65
67
|
'sandbox/scripts/enable-vt-input.ps1',
|
|
66
68
|
'sandbox/scripts/expand-archive.ps1',
|
|
67
69
|
'sandbox/scripts/get-gpu-names.ps1',
|
|
@@ -73,5 +75,6 @@ export const LAUNCH_FILES_GOLDEN = [
|
|
|
73
75
|
'sandbox/scripts/set-desktop.ps1',
|
|
74
76
|
'sandbox/scripts/set-dns-servers.ps1',
|
|
75
77
|
'sandbox/scripts/set-machine-environment.ps1',
|
|
78
|
+
'sandbox/templates/fivem/README.txt',
|
|
76
79
|
'session.json',
|
|
77
80
|
];
|
|
@@ -107,6 +107,38 @@ export declare function createFixtures(t: Harness): {
|
|
|
107
107
|
github: {
|
|
108
108
|
shareToken: boolean;
|
|
109
109
|
};
|
|
110
|
+
accounts: {
|
|
111
|
+
epic: {
|
|
112
|
+
share: boolean;
|
|
113
|
+
};
|
|
114
|
+
steam: {
|
|
115
|
+
share: boolean;
|
|
116
|
+
};
|
|
117
|
+
unity: {
|
|
118
|
+
share: boolean;
|
|
119
|
+
};
|
|
120
|
+
cfx: {
|
|
121
|
+
share: boolean;
|
|
122
|
+
};
|
|
123
|
+
keystore: {
|
|
124
|
+
share: boolean;
|
|
125
|
+
};
|
|
126
|
+
play: {
|
|
127
|
+
share: boolean;
|
|
128
|
+
};
|
|
129
|
+
expo: {
|
|
130
|
+
share: boolean;
|
|
131
|
+
};
|
|
132
|
+
appstore: {
|
|
133
|
+
share: boolean;
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
pcInstalls: {
|
|
137
|
+
steamGames: boolean | number[];
|
|
138
|
+
epicGames: boolean | string[];
|
|
139
|
+
unrealEngines: boolean | string[];
|
|
140
|
+
unityEditors: boolean | string[];
|
|
141
|
+
};
|
|
110
142
|
provisioning: {
|
|
111
143
|
copyFromHost: boolean;
|
|
112
144
|
fallbackToInstall: boolean;
|
|
@@ -162,6 +194,59 @@ export declare function createFixtures(t: Harness): {
|
|
|
162
194
|
acceptLicense: boolean;
|
|
163
195
|
components: string[];
|
|
164
196
|
keepInstallFiles: boolean;
|
|
197
|
+
persistEngines: boolean;
|
|
198
|
+
derivedDataCache: "persisted" | "local";
|
|
199
|
+
};
|
|
200
|
+
unity: {
|
|
201
|
+
enabled: boolean | "auto";
|
|
202
|
+
acceptLicense: boolean;
|
|
203
|
+
version: string;
|
|
204
|
+
modules: string[];
|
|
205
|
+
hub: boolean;
|
|
206
|
+
keepVersions: number;
|
|
207
|
+
};
|
|
208
|
+
godot: {
|
|
209
|
+
enabled: boolean | "auto";
|
|
210
|
+
version: string;
|
|
211
|
+
dotnet: boolean | "auto";
|
|
212
|
+
exportTemplates: boolean;
|
|
213
|
+
keepVersions: number;
|
|
214
|
+
};
|
|
215
|
+
steam: {
|
|
216
|
+
enabled: boolean | "auto";
|
|
217
|
+
client: boolean;
|
|
218
|
+
sdkFolder: string;
|
|
219
|
+
};
|
|
220
|
+
fivem: {
|
|
221
|
+
enabled: boolean | "auto";
|
|
222
|
+
acceptLicense: boolean;
|
|
223
|
+
game: "gta5" | "rdr3";
|
|
224
|
+
artifact: string;
|
|
225
|
+
txAdmin: boolean;
|
|
226
|
+
keepVersions: number;
|
|
227
|
+
};
|
|
228
|
+
android: {
|
|
229
|
+
enabled: boolean | "auto";
|
|
230
|
+
acceptLicense: boolean;
|
|
231
|
+
platforms: string[];
|
|
232
|
+
buildTools: string;
|
|
233
|
+
ndk: string | false;
|
|
234
|
+
cmake: string | false;
|
|
235
|
+
packages: string[];
|
|
236
|
+
};
|
|
237
|
+
flutter: {
|
|
238
|
+
enabled: boolean | "auto";
|
|
239
|
+
version: string;
|
|
240
|
+
windowsDesktop: boolean;
|
|
241
|
+
precache: string[];
|
|
242
|
+
};
|
|
243
|
+
dart: {
|
|
244
|
+
enabled: boolean | "auto";
|
|
245
|
+
version: string;
|
|
246
|
+
};
|
|
247
|
+
reactNative: {
|
|
248
|
+
enabled: boolean | "auto";
|
|
249
|
+
easCli: boolean;
|
|
165
250
|
};
|
|
166
251
|
cpp: {
|
|
167
252
|
enabled: boolean | "auto";
|
|
@@ -174,6 +259,7 @@ export declare function createFixtures(t: Harness): {
|
|
|
174
259
|
workloads: string[];
|
|
175
260
|
frameworkTargets: string[];
|
|
176
261
|
netfx35: boolean;
|
|
262
|
+
templates: string[];
|
|
177
263
|
};
|
|
178
264
|
go: {
|
|
179
265
|
enabled: boolean | "auto";
|
|
@@ -185,6 +271,7 @@ export declare function createFixtures(t: Harness): {
|
|
|
185
271
|
version: string;
|
|
186
272
|
maven: boolean;
|
|
187
273
|
gradle: boolean;
|
|
274
|
+
kotlin: boolean;
|
|
188
275
|
};
|
|
189
276
|
ruby: {
|
|
190
277
|
enabled: boolean | "auto";
|
|
@@ -5,6 +5,7 @@ import { spawn } from 'node:child_process';
|
|
|
5
5
|
import { DEFAULTS } from '../../core/config/index.js';
|
|
6
6
|
import { generateSandbox } from '../../host/launch/index.js';
|
|
7
7
|
import { resolveRun } from '../../core/run/index.js';
|
|
8
|
+
import { onFixedPc } from './pc-memory.js';
|
|
8
9
|
export const HOST_NODE_VERSION = 'v22.3.0';
|
|
9
10
|
/** Colour codes are removed from captured output, so tests can match plain text. */
|
|
10
11
|
export function stripAnsi(text) {
|
|
@@ -110,14 +111,14 @@ export function createFixtures(t) {
|
|
|
110
111
|
...over,
|
|
111
112
|
}, HOST_NODE_VERSION);
|
|
112
113
|
const generate = (r, args = [], p = probe) =>
|
|
113
|
-
// A fixed "this PC has" list, so "auto" toolchains decide the same on every machine
|
|
114
|
-
generateSandbox({
|
|
114
|
+
// A fixed "this PC has" list and RAM, so "auto" toolchains and the memory decide the same on every machine.
|
|
115
|
+
onFixedPc(() => generateSandbox({
|
|
115
116
|
packageRoot: t.packageRoot,
|
|
116
117
|
run: r,
|
|
117
118
|
harnessArgs: args,
|
|
118
119
|
probe: p,
|
|
119
120
|
pcLanguages: ['python', 'rust', 'cpp', 'dotnet'],
|
|
120
|
-
});
|
|
121
|
+
}));
|
|
121
122
|
/** Resolves a run and generates its sandbox (.wsb + launch folder + session.json) in one step. */
|
|
122
123
|
const launch = async (over = {}, options = {}) => generate(await run(over, options.config ?? baseConfig), options.args, options.probe);
|
|
123
124
|
/** Writes a project's `.sandbox-config.json` (objects are serialised; strings are written as they are). */
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// A pretend PC's RAM for code that sizes the sandbox from os.totalmem() / os.freemem() (generateSandbox caps a fixed
|
|
2
|
+
// memoryMb at what the PC can keep resident), so launch tests decide the same on every machine they run on.
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
const MB = 1048576;
|
|
5
|
+
/** Runs `fn` (synchronous) as on a PC with this much RAM in total and free; the real os functions come back after. */
|
|
6
|
+
export function onFixedPc(fn, totalMb = 64 * 1024, freeMb = 48 * 1024) {
|
|
7
|
+
const real = { totalmem: os.totalmem, freemem: os.freemem };
|
|
8
|
+
os.totalmem = () => totalMb * MB;
|
|
9
|
+
os.freemem = () => freeMb * MB;
|
|
10
|
+
try {
|
|
11
|
+
return fn();
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
os.totalmem = real.totalmem;
|
|
15
|
+
os.freemem = real.freemem;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -157,8 +157,27 @@ function sampleAccounts() {
|
|
|
157
157
|
isDefault: true,
|
|
158
158
|
isActive: true,
|
|
159
159
|
savedAt: iso(3 * 24 * 60 * MINUTE),
|
|
160
|
+
kind: 'subscription',
|
|
161
|
+
keyHint: null,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'personal',
|
|
165
|
+
email: null,
|
|
166
|
+
isDefault: false,
|
|
167
|
+
isActive: false,
|
|
168
|
+
savedAt: iso(9 * 24 * 60 * MINUTE),
|
|
169
|
+
kind: 'subscription',
|
|
170
|
+
keyHint: null,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'free-credits',
|
|
174
|
+
email: null,
|
|
175
|
+
isDefault: false,
|
|
176
|
+
isActive: false,
|
|
177
|
+
savedAt: iso(1 * 24 * 60 * MINUTE),
|
|
178
|
+
kind: 'api-key',
|
|
179
|
+
keyHint: 'sk-ant-...x7Qa',
|
|
160
180
|
},
|
|
161
|
-
{ name: 'personal', email: null, isDefault: false, isActive: false, savedAt: iso(9 * 24 * 60 * MINUTE) },
|
|
162
181
|
],
|
|
163
182
|
},
|
|
164
183
|
{
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function fingerprint(payload: unknown): string;
|