windows-development-sandbox 1.23.2 → 1.25.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/config.example.json +0 -1
- package/dist/bin/agent-sandbox.js +2 -2
- package/dist/bin/{chunk-USTRSHIE.js → chunk-IZRGTSQ5.js} +158 -131
- package/dist/bin/{chunk-QFSJX2KW.js → chunk-KH4MI2U5.js} +1 -1
- package/dist/bin/{chunk-7YPEDAJD.js → chunk-SUYYVIDW.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/core/config/defaults.js +0 -1
- package/dist/core/detect/toolchains.js +0 -6
- package/dist/core/schema/config.d.ts +1 -2
- package/dist/core/schema/toolchains.d.ts +1 -2
- package/dist/core/schema/toolchains.js +3 -6
- package/dist/guest/agents/agent-files.d.ts +11 -7
- package/dist/guest/agents/agent-files.js +37 -11
- package/dist/guest/agents/local-state.d.ts +2 -1
- package/dist/guest/agents/local-state.js +2 -1
- package/dist/guest/boot/boot-console.d.ts +28 -0
- package/dist/guest/boot/boot-console.js +98 -0
- package/dist/guest/boot/boot.d.ts +8 -1
- package/dist/guest/boot/boot.js +66 -42
- package/dist/guest/boot/graph.d.ts +9 -5
- package/dist/guest/boot/graph.js +8 -2
- package/dist/guest/boot/progress-view.d.ts +116 -0
- package/dist/guest/boot/progress-view.js +456 -0
- package/dist/guest/boot/runner.d.ts +20 -10
- package/dist/guest/boot/runner.js +39 -47
- package/dist/guest/runtime/log-line.d.ts +7 -0
- package/dist/guest/runtime/log-line.js +18 -0
- package/dist/guest/runtime/log.d.ts +17 -4
- package/dist/guest/runtime/log.js +40 -17
- package/dist/guest/runtime/process.d.ts +9 -0
- package/dist/guest/runtime/process.js +69 -8
- package/dist/guest/setup/environment.js +9 -4
- package/dist/guest/setup/hooks.d.ts +2 -0
- package/dist/guest/setup/hooks.js +4 -0
- package/dist/guest/setup/path-plan.d.ts +12 -0
- package/dist/guest/setup/path-plan.js +18 -0
- package/dist/guest/toolchains/native/unreal.js +4 -6
- package/dist/guest/toolchains/native/visual-studio.d.ts +14 -14
- package/dist/guest/toolchains/native/visual-studio.js +145 -174
- package/dist/guest/toolchains/native/vs-persist.d.ts +189 -0
- package/dist/guest/toolchains/native/vs-persist.js +256 -0
- package/dist/guest/toolchains/native/vs-request.d.ts +6 -26
- package/dist/guest/toolchains/native/vs-request.js +16 -50
- package/dist/guest/toolchains/plan.js +11 -14
- package/dist/host/migrate/migrations/drop-vs-install-files.d.ts +5 -0
- package/dist/host/migrate/migrations/drop-vs-install-files.js +34 -0
- package/dist/host/migrate/registry.js +7 -1
- package/dist/runtime/agent.js +67 -26
- package/dist/runtime/daemon.js +958 -716
- package/dist/runtime/db.js +67 -26
- package/dist/runtime/main.js +1792 -1038
- package/dist/runtime/netproxy.js +3 -6
- package/dist/runtime/tool.js +44 -24
- package/dist/runtime/view.js +4 -7
- package/dist/selftest/agent-paths.test.js +17 -1
- package/dist/selftest/agent-settings.test.js +36 -5
- package/dist/selftest/boot-console.test.d.ts +2 -0
- package/dist/selftest/boot-console.test.js +291 -0
- package/dist/selftest/boot-runner.test.js +10 -12
- package/dist/selftest/guest-boot.test.js +7 -1
- package/dist/selftest/index.js +2 -0
- package/dist/selftest/layout.test.js +4 -0
- package/dist/selftest/migrate.test.js +21 -0
- package/dist/selftest/persisted-installs.test.js +169 -33
- package/dist/selftest/support/engine-golden.js +2 -0
- package/dist/selftest/support/fixtures.d.ts +1 -2
- package/dist/shared/channel/schema.d.ts +3 -6
- package/dist/shared/pty/terminal.js +2 -1
- package/dist/sync/watcher.js +5 -1
- 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/restore-visual-studio.ps1 +78 -0
- package/sandbox/scripts/save-visual-studio.ps1 +106 -0
- package/schemas/config.schema.json +3 -7
- package/schemas/sandbox-config.schema.json +3 -7
- package/dist/guest/toolchains/native/autosdk.d.ts +0 -3
- package/dist/guest/toolchains/native/autosdk.js +0 -50
|
@@ -34,7 +34,13 @@ export const guestBootSuite = {
|
|
|
34
34
|
];
|
|
35
35
|
const headers = [1, 2, 3, 4, 5, 6, 7, 8, 9].map((n) => boot.out.indexOf('[' + n + '/9] '));
|
|
36
36
|
t.check(headers.every((at, i) => at >= 0 && (i === 0 || at > headers[i - 1])), 'the boot prints its nine stages, [1/9] to [9/9], in order', headers.join(', '));
|
|
37
|
-
t.check(boot.out
|
|
37
|
+
t.check(/^Ready in \d+\.\ds/m.test(boot.out) && /^\[3\/9\] Developer tools +\d+\.\ds$/m.test(boot.out), 'the boot says how long it took and how long each stage took', boot.out);
|
|
38
|
+
const tools = boot.out.indexOf('\nTools\n');
|
|
39
|
+
t.check(!boot.out.includes(': started') &&
|
|
40
|
+
!/^\s*[A-Z][\w -]+ \| /m.test(boot.out) &&
|
|
41
|
+
!boot.out.includes('\x1b[') &&
|
|
42
|
+
tools > headers[8] &&
|
|
43
|
+
/\n {2}gh {2,}off \(not listed in toolchains\.cli\)\n/.test(boot.out.slice(tools)), 'the dry run prints plain lines: no per-task lines, no prefixes, no colours; the tool list once, after the stages', boot.out);
|
|
38
44
|
t.equal(steps.filter((s) => !boot.out.includes(s)), [], 'the guest provisions every enabled toolchain');
|
|
39
45
|
t.check(boot.out.includes('AgentSandbox-Block-Private') &&
|
|
40
46
|
boot.out.includes('remoteport=1-52,54-65535') &&
|
package/dist/selftest/index.js
CHANGED
|
@@ -80,6 +80,7 @@ import { uiTextSuite } from './ui-text.test.js';
|
|
|
80
80
|
import { menuCoverageSuite } from './menu-coverage.test.js';
|
|
81
81
|
import { auditSuite } from './audit.test.js';
|
|
82
82
|
import { bootRunnerSuite } from './boot-runner.test.js';
|
|
83
|
+
import { bootConsoleSuite } from './boot-console.test.js';
|
|
83
84
|
import { layoutSuite } from './layout.test.js';
|
|
84
85
|
import { menuSuite } from './menu.test.js';
|
|
85
86
|
import { menuPerfSuite } from './menu-perf.test.js';
|
|
@@ -133,6 +134,7 @@ export const SUITES = [
|
|
|
133
134
|
// pure helpers and the command line
|
|
134
135
|
layoutSuite,
|
|
135
136
|
bootRunnerSuite,
|
|
137
|
+
bootConsoleSuite,
|
|
136
138
|
toolchainsSuite,
|
|
137
139
|
persistedInstallsSuite,
|
|
138
140
|
steamSuite,
|
|
@@ -19,6 +19,8 @@ const PURE_GUEST = [
|
|
|
19
19
|
'guest/boot/project-env.js',
|
|
20
20
|
'guest/boot/runner.js',
|
|
21
21
|
'guest/boot/graph.js',
|
|
22
|
+
'guest/boot/progress-view.js',
|
|
23
|
+
'guest/runtime/log-line.js',
|
|
22
24
|
'guest/toolchains/split.js',
|
|
23
25
|
'guest/toolchains/step-scheduler.js',
|
|
24
26
|
'guest/audit/parse.js',
|
|
@@ -30,6 +32,8 @@ const PURE_GUEST = [
|
|
|
30
32
|
'guest/governor/decide.js',
|
|
31
33
|
'guest/governor/core.js',
|
|
32
34
|
'guest/daemon/live-folders.js',
|
|
35
|
+
'guest/toolchains/native/vs-request.js',
|
|
36
|
+
'guest/toolchains/native/vs-persist.js',
|
|
33
37
|
];
|
|
34
38
|
const SIDE_EFFECTS = ['guest/runtime/session.js', 'guest/runtime/log.js'];
|
|
35
39
|
const readModules = (dist) => {
|
|
@@ -159,6 +159,27 @@ export const migrateSuite = {
|
|
|
159
159
|
finally {
|
|
160
160
|
fs.rmSync(machineFile);
|
|
161
161
|
}
|
|
162
|
+
t.section('drop-vs-install-files');
|
|
163
|
+
const dropVs = MIGRATIONS.find((m) => m.id === '2026-11-drop-vs-install-files');
|
|
164
|
+
if (!dropVs)
|
|
165
|
+
return t.check(false, 'the drop-vs-install-files migration exists');
|
|
166
|
+
const oldVs = t.write(t.at('vs', 'old.json'), '// mine\n' +
|
|
167
|
+
JSON.stringify({
|
|
168
|
+
toolchains: { unreal: { enabled: true, keepInstallFiles: true, visualStudio: 'autosdk', components: ['X'] } },
|
|
169
|
+
}));
|
|
170
|
+
const fullVs = t.write(t.at('vs', 'full.json'), JSON.stringify({ toolchains: { unreal: { visualStudio: 'full' } } }));
|
|
171
|
+
const vsNotes = [];
|
|
172
|
+
const vsWanted = dropVs.applies(ctx(t.at('vs'), oldVs)) && !dropVs.applies(ctx(t.at('vs'), fullVs));
|
|
173
|
+
await dropVs.run(ctx(t.at('vs'), oldVs), (line) => vsNotes.push(line));
|
|
174
|
+
const vsAfter = parse(readText(oldVs));
|
|
175
|
+
t.check(vsWanted &&
|
|
176
|
+
vsAfter.toolchains.unreal.keepInstallFiles === undefined &&
|
|
177
|
+
vsAfter.toolchains.unreal.visualStudio === undefined &&
|
|
178
|
+
vsAfter.toolchains.unreal.enabled === true &&
|
|
179
|
+
readText(oldVs).includes('// mine') &&
|
|
180
|
+
vsNotes.join().includes('toolchains.unreal.keepInstallFiles') &&
|
|
181
|
+
loadConfig(oldVs).config.toolchains.unreal.visualStudio === 'full' &&
|
|
182
|
+
!dropVs.applies(ctx(t.at('vs'), oldVs)), 'the retired Visual Studio settings are removed (a chosen "full" stays), the file loads again, and it is idempotent');
|
|
162
183
|
t.section('sandbox migrate');
|
|
163
184
|
const cmdUser = t.dir('cmd-user');
|
|
164
185
|
const cmdConfig = t.write(t.at('cmd-user', 'config.json'), JSON.stringify({ ...JSON.parse(JSON.stringify(old.defaults)), sandbox: { memoryMb: 4096 } }, null, 2));
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// Large installs kept between sandboxes (guest/toolchains/persisted.ts): the staging folder and its TEMP, the marker,
|
|
2
2
|
// the refusal when the disk is short (an injected statfs), keeping only keepVersions, the records `sandbox data`
|
|
3
|
-
// reads,
|
|
3
|
+
// reads, which Visual Studio the toolchains ask for (vs-request.ts), and how it is saved on the data disk and put
|
|
4
|
+
// back (vs-persist.ts: folders, marker, instance state, registry). Temp folders only.
|
|
4
5
|
import fs from 'node:fs';
|
|
5
6
|
import path from 'node:path';
|
|
6
7
|
import { DEFAULTS } from '../core/config/index.js';
|
|
7
8
|
import { assertLocalRoom, ensurePersisted, installRecords, InstallRecordsSchema, LOCAL_RESERVE_BYTES, recordUse, } from '../guest/toolchains/persisted.js';
|
|
8
|
-
import { BUILD_TOOLS_COMPONENTS, UNREAL_COMPONENTS, visualStudioWanted, } from '../guest/toolchains/native/vs-request.js';
|
|
9
|
-
import {
|
|
9
|
+
import { BUILD_TOOLS_COMPONENTS, UNREAL_BUILD_TOOLS_COMPONENTS, UNREAL_COMPONENTS, visualStudioWanted, vsExitText, } from '../guest/toolchains/native/vs-request.js';
|
|
10
|
+
import { markerFrom, newestInstance, persistCovers, rebase, registryPlan, restoreInput, rewriteInstanceState, SaveResultSchema, saveInput, SETUP_CLSID, VsPersistMarkerSchema, } from '../guest/toolchains/native/vs-persist.js';
|
|
10
11
|
import { freeBytesAt } from '../shared/io/disk.js';
|
|
11
12
|
import { SIGNERS, signerMatches } from '../shared/signer.js';
|
|
12
13
|
import { decideToolchains } from '../core/detect/toolchains.js';
|
|
@@ -17,7 +18,7 @@ import { collectDataEntries } from '../cli/services/data-collect.js';
|
|
|
17
18
|
const GB = 1024 ** 3;
|
|
18
19
|
export const persistedInstallsSuite = {
|
|
19
20
|
name: 'persisted-installs',
|
|
20
|
-
title: 'large persisted installs: staging, marker, room checks, pruning, records;
|
|
21
|
+
title: 'large persisted installs: staging, marker, room checks, pruning, records; Visual Studio: request, save, restore',
|
|
21
22
|
run: async (t) => {
|
|
22
23
|
t.section('installing a version');
|
|
23
24
|
const tools = t.dir('persisted-tools');
|
|
@@ -110,28 +111,172 @@ export const persistedInstallsSuite = {
|
|
|
110
111
|
all.for.join() === 'unreal,flutter,unity' &&
|
|
111
112
|
UNREAL_COMPONENTS.every((c) => all.components.includes(c)) &&
|
|
112
113
|
all.components.includes('Microsoft.VisualStudio.Component.VC.Llvm.Clang'), 'with Unreal: one Community install serves all of them');
|
|
113
|
-
t.section('Unreal on the
|
|
114
|
-
tc.unreal.visualStudio = '
|
|
115
|
-
const
|
|
116
|
-
t.check(
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
t.
|
|
123
|
-
pairs[0].to === 'T:\\autosdk\\HostWin64\\Win64\\VS2022\\14.44.35207' &&
|
|
124
|
-
pairs[0].from === 'T:\\msvc\\VC\\Tools\\MSVC\\14.44.35207' &&
|
|
125
|
-
pairs[1].from === 'T:\\msvc\\Windows Kits\\10\\Include\\10.0.26100.0' &&
|
|
126
|
-
pairs[1].to === 'T:\\autosdk\\HostWin64\\Win64\\Windows Kits\\10\\Include\\10.0.26100.0', 'the AutoSDK tree is MSVC under VS2022\\<version> and the SDK under Windows Kits\\10 (copies)');
|
|
114
|
+
t.section('Unreal on the Build Tools');
|
|
115
|
+
tc.unreal.visualStudio = 'buildtools';
|
|
116
|
+
const bt = visualStudioWanted(tc);
|
|
117
|
+
t.check(bt?.product === 'buildtools' &&
|
|
118
|
+
bt.for.join() === 'unreal,flutter,unity' &&
|
|
119
|
+
bt.licence === 'toolchains.unreal.acceptLicense' &&
|
|
120
|
+
UNREAL_BUILD_TOOLS_COMPONENTS.every((c) => bt.components.includes(c)) &&
|
|
121
|
+
BUILD_TOOLS_COMPONENTS.every((c) => bt.components.includes(c)) &&
|
|
122
|
+
bt.components.includes('Microsoft.VisualStudio.Component.VC.Llvm.Clang'), '"buildtools": the Build Tools with what UnrealBuildTool needs, shared with Flutter and Unity');
|
|
123
|
+
t.equal(DEFAULTS.toolchains.unreal.visualStudio, 'full', 'Community is the default (the IDE, its debugger and Unreal integration)');
|
|
127
124
|
const on = DEFAULTS.toolchains;
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
125
|
+
const decided = decideToolchains({ ...structuredClone(on), unreal: { ...on.unreal, acceptLicense: true } }, ['Game.uproject'], []);
|
|
126
|
+
t.check(decided.unreal.on && !decided.msvc.on, 'an Unreal project does not turn the portable MSVC on (Visual Studio brings MSVC)');
|
|
127
|
+
t.section('Visual Studio kept on the data disk');
|
|
128
|
+
const places = {
|
|
129
|
+
programFiles: 'C:\\Program Files',
|
|
130
|
+
programFilesX86: 'C:\\Program Files (x86)',
|
|
131
|
+
programData: 'C:\\ProgramData',
|
|
131
132
|
};
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
133
|
+
const root = 'C:\\AgentData\\tools\\visualstudio\\persist';
|
|
134
|
+
const community = 'C:\\Program Files\\Microsoft Visual Studio\\2022\\Community';
|
|
135
|
+
const instances = 'C:\\ProgramData\\Microsoft\\VisualStudio\\Packages\\_Instances';
|
|
136
|
+
const vsAt = { product: 'community', installationPath: community, instanceId: 'a190d188' };
|
|
137
|
+
const save = saveInput(vsAt, root, places, 'T:\\result.json');
|
|
138
|
+
const saved = (name) => save.copies.find((c) => c.name === name);
|
|
139
|
+
t.check(saved('Community')?.from === community &&
|
|
140
|
+
saved('Community')?.to === root + '\\Community' &&
|
|
141
|
+
saved('WindowsKits10')?.from === 'C:\\Program Files (x86)\\Windows Kits\\10' &&
|
|
142
|
+
saved('instance')?.from === instances + '\\a190d188' &&
|
|
143
|
+
saved('instance')?.to === root + '\\instance' &&
|
|
144
|
+
saved('Setup')?.from === 'C:\\ProgramData\\Microsoft\\VisualStudio\\Setup' &&
|
|
145
|
+
saved('Installer')?.optional === false &&
|
|
146
|
+
saved('ReferenceAssemblies')?.optional === true &&
|
|
147
|
+
save.resultFile === 'T:\\result.json', 'the capture mirrors the install, the Windows SDK, the installer, the Setup API and the instance onto the data disk');
|
|
148
|
+
t.check(save.registry.length === 8 &&
|
|
149
|
+
save.registry.filter((r) => r.view === '32').length === 4 &&
|
|
150
|
+
save.registry.some((r) => r.key.endsWith('NETFXSDK') && r.recurse) &&
|
|
151
|
+
save.registry.some((r) => r.key.endsWith('Installed Roots') && r.names?.join() === 'KitsRoot10'), 'it reads the SDK registry values in both views (NETFXSDK with its subkeys, only KitsRoot10 of the kits roots)');
|
|
152
|
+
await t.throws(() => Promise.resolve(saveInput({ ...vsAt, instanceId: '..\\x' }, root, places, 'r')), 'not a Visual Studio instance ID', 'an instance ID is never a path');
|
|
153
|
+
const captured = [
|
|
154
|
+
{
|
|
155
|
+
view: '64',
|
|
156
|
+
key: 'SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots',
|
|
157
|
+
name: 'KitsRoot10',
|
|
158
|
+
kind: 'string',
|
|
159
|
+
value: 'C:\\Program Files (x86)\\Windows Kits\\10\\',
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
view: '32',
|
|
163
|
+
key: 'SOFTWARE\\Microsoft\\Microsoft SDKs\\Windows\\v10.0',
|
|
164
|
+
name: 'ProductVersion',
|
|
165
|
+
kind: 'string',
|
|
166
|
+
value: '10.0.22621',
|
|
167
|
+
},
|
|
168
|
+
{
|
|
169
|
+
view: '32',
|
|
170
|
+
key: 'SOFTWARE\\Microsoft\\Microsoft SDKs\\NETFXSDK\\4.8',
|
|
171
|
+
name: 'KitsInstallationFolder',
|
|
172
|
+
kind: 'string',
|
|
173
|
+
value: 'C:\\Program Files (x86)\\Windows Kits\\NETFXSDK\\4.8\\',
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
view: '32',
|
|
177
|
+
key: 'SOFTWARE\\Microsoft\\Microsoft SDKs\\NETFXSDK\\4.8\\WinSDK-NetFx40Tools-x64',
|
|
178
|
+
name: 'InstallationFolder',
|
|
179
|
+
kind: 'string',
|
|
180
|
+
value: 'c:\\program files (x86)\\Microsoft SDKs\\Windows\\v10.0A\\bin\\NETFX 4.8 Tools\\x64\\',
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
view: '64',
|
|
184
|
+
key: 'SOFTWARE\\Microsoft\\VisualStudio\\Setup',
|
|
185
|
+
name: 'KeepDownloadedPayloads',
|
|
186
|
+
kind: 'dword',
|
|
187
|
+
value: 0,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
view: '64',
|
|
191
|
+
key: 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Run',
|
|
192
|
+
name: 'planted',
|
|
193
|
+
kind: 'string',
|
|
194
|
+
value: 'C:\\planted.exe',
|
|
195
|
+
},
|
|
196
|
+
];
|
|
197
|
+
const result = SaveResultSchema.parse({
|
|
198
|
+
copied: [
|
|
199
|
+
'Community',
|
|
200
|
+
'WindowsKits10',
|
|
201
|
+
'NETFXSDK',
|
|
202
|
+
'MicrosoftSDKs-v10.0A',
|
|
203
|
+
'ReferenceAssemblies',
|
|
204
|
+
'Installer',
|
|
205
|
+
].concat(['Setup', 'instance']),
|
|
206
|
+
registry: captured,
|
|
207
|
+
});
|
|
208
|
+
const marker = VsPersistMarkerSchema.parse(markerFrom({ product: 'community', components: [...UNREAL_COMPONENTS] }, { installationPath: community, instanceId: 'a190d188', version: '17.14.37827.9' }, result, '2026-11-04T00:00:00.000Z'));
|
|
209
|
+
t.check(marker.sdkVersion === '10.0.22621' && marker.instanceId === 'a190d188' && marker.folders.length === 8, 'the marker keeps the instance ID, what was saved and the Windows SDK version from the registry');
|
|
210
|
+
t.check(!VsPersistMarkerSchema.safeParse({ ...marker, instanceId: '..\\..\\Windows' }).success &&
|
|
211
|
+
!VsPersistMarkerSchema.safeParse({ ...marker, folders: ['..'] }).success, 'a marker with a path for its instance ID, or a folder it does not know, is refused');
|
|
212
|
+
const plan = registryPlan(marker, root, places);
|
|
213
|
+
const value = (view, key, name) => plan.find((e) => e.view === view && e.key.toLowerCase() === key.toLowerCase() && e.name === name)?.value;
|
|
214
|
+
const kits = root + '\\WindowsKits10\\';
|
|
215
|
+
const sdk = 'SOFTWARE\\Microsoft\\Microsoft SDKs\\Windows\\v10.0';
|
|
216
|
+
const netfx = 'SOFTWARE\\Microsoft\\Microsoft SDKs\\NETFXSDK\\4.8';
|
|
217
|
+
t.check(['64', '32'].every((v) => value(v, 'SOFTWARE\\Microsoft\\Windows Kits\\Installed Roots', 'KitsRoot10') === kits &&
|
|
218
|
+
value(v, sdk, 'InstallationFolder') === kits &&
|
|
219
|
+
value(v, sdk, 'ProductVersion') === '10.0.22621' &&
|
|
220
|
+
value(v, netfx, 'KitsInstallationFolder') === root + '\\NETFXSDK\\4.8\\' &&
|
|
221
|
+
value(v, netfx + '\\WinSDK-NetFx40Tools-x64', 'InstallationFolder') ===
|
|
222
|
+
root + '\\MicrosoftSDKs-v10.0A\\bin\\NETFX 4.8 Tools\\x64\\' &&
|
|
223
|
+
value(v, 'SOFTWARE\\Microsoft\\VisualStudio\\Setup', 'KeepDownloadedPayloads') === 0), 'both registry views point the Windows SDK and the .NET Framework SDK at the data disk (any case); the rest stays');
|
|
224
|
+
const clsid = 'SOFTWARE\\Classes\\CLSID\\' + SETUP_CLSID + '\\InprocServer32';
|
|
225
|
+
const setupDll = 'Microsoft.VisualStudio.Setup.Configuration.Native.dll';
|
|
226
|
+
t.check(value('64', clsid, '') === 'C:\\ProgramData\\Microsoft\\VisualStudio\\Setup\\x64\\' + setupDll &&
|
|
227
|
+
value('32', clsid, '') === 'C:\\ProgramData\\Microsoft\\VisualStudio\\Setup\\x86\\' + setupDll &&
|
|
228
|
+
value('64', clsid, 'ThreadingModel') === 'Both' &&
|
|
229
|
+
value('32', clsid, 'ThreadingModel') === 'Both', 'the Setup Configuration COM class is registered (x64 and x86), so vswhere and UnrealBuildTool find the instance');
|
|
230
|
+
t.check(!plan.some((e) => /CurrentVersion\\Run/i.test(e.key)), 'a value the capture never reads is never written back (a tampered marker)');
|
|
231
|
+
const back = restoreInput(marker, root, places);
|
|
232
|
+
const copied = (name) => back.copies.find((c) => c.name === name);
|
|
233
|
+
t.check(!copied('Community') &&
|
|
234
|
+
!copied('WindowsKits10') &&
|
|
235
|
+
!copied('NETFXSDK') &&
|
|
236
|
+
!copied('Shared') &&
|
|
237
|
+
copied('Installer')?.to === 'C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer' &&
|
|
238
|
+
copied('Installer')?.mode === 'all' &&
|
|
239
|
+
copied('ReferenceAssemblies')?.mode === 'missing' &&
|
|
240
|
+
copied('instance')?.from === root + '\\instance' &&
|
|
241
|
+
copied('instance')?.to === instances + '\\a190d188' &&
|
|
242
|
+
back.registry.length === plan.length, 'a boot copies back only the small fixed-path folders that were saved (the instance under its ID); the big ones stay');
|
|
243
|
+
t.check(persistCovers(marker, { product: 'community', components: [...UNREAL_COMPONENTS] }) &&
|
|
244
|
+
persistCovers(marker, { product: 'community', components: ['component.unreal'] }) &&
|
|
245
|
+
!persistCovers(marker, {
|
|
246
|
+
product: 'community',
|
|
247
|
+
components: [...UNREAL_COMPONENTS, 'Microsoft.VisualStudio.Component.VC.Llvm.Clang'],
|
|
248
|
+
}) &&
|
|
249
|
+
persistCovers(marker, { product: 'buildtools', components: [...BUILD_TOOLS_COMPONENTS] }) &&
|
|
250
|
+
!persistCovers({ ...marker, product: 'buildtools' }, { product: 'community', components: [] }) &&
|
|
251
|
+
!persistCovers({ ...marker, folders: marker.folders.filter((f) => f !== 'Setup') }, {
|
|
252
|
+
product: 'community',
|
|
253
|
+
components: [],
|
|
254
|
+
}), 'a saved copy serves requests with no more components (Community serves the Build Tools too); one without the Setup API none');
|
|
255
|
+
const state = JSON.stringify({
|
|
256
|
+
installationPath: community,
|
|
257
|
+
resolvedInstallationPath: community.toLowerCase(),
|
|
258
|
+
nested: community + '\\Common7\\IDE',
|
|
259
|
+
longer: community + '2',
|
|
260
|
+
launchParams: { fileName: 'Common7\\IDE\\devenv.exe' },
|
|
261
|
+
enginePath: 'C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\resources\\app',
|
|
262
|
+
});
|
|
263
|
+
const moved = JSON.parse(rewriteInstanceState(state, community, root + '\\Community'));
|
|
264
|
+
t.check(moved.installationPath === root + '\\Community' &&
|
|
265
|
+
moved.resolvedInstallationPath === root + '\\Community' &&
|
|
266
|
+
moved.nested === root + '\\Community\\Common7\\IDE' &&
|
|
267
|
+
moved.longer === community + '2' &&
|
|
268
|
+
moved.enginePath === 'C:\\Program Files (x86)\\Microsoft Visual Studio\\Installer\\resources\\app', "the instance's state names the saved folder (JSON-escaped, any case); a longer name that only starts the same stays");
|
|
269
|
+
const buildTools = 'C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools';
|
|
270
|
+
const movedBt = JSON.parse(rewriteInstanceState(JSON.stringify({ installationPath: buildTools }), buildTools + '\\', root + '\\BuildTools'));
|
|
271
|
+
t.check(movedBt.installationPath === root + '\\BuildTools' &&
|
|
272
|
+
rebase('D:\\Other\\x', [{ from: 'C:\\Program Files', to: 'D:\\y' }]) === 'D:\\Other\\x', 'the Build Tools too (a trailing backslash does not matter); a value outside the saved folders is kept');
|
|
273
|
+
t.equal(newestInstance([
|
|
274
|
+
{ installationVersion: '17.9.1', id: 'a' },
|
|
275
|
+
{ installationVersion: '17.14.37827.9', id: 'b' },
|
|
276
|
+
{ installationVersion: '17.14.3', id: 'c' },
|
|
277
|
+
])?.id, 'b', 'the newest instance is chosen by version (vswhere -latest fails on two instances installed at the same time)');
|
|
278
|
+
t.equal(newestInstance([]), null, 'no instance: none');
|
|
279
|
+
t.section('the Visual Studio installer');
|
|
135
280
|
t.check(vsExitText(3762504530).includes('crashed') &&
|
|
136
281
|
vsExitText(-1).includes('exit 4294967295') &&
|
|
137
282
|
vsExitText(0) === 'installed' &&
|
|
@@ -139,15 +284,6 @@ export const persistedInstallsSuite = {
|
|
|
139
284
|
vsExitText(8006).includes('running') &&
|
|
140
285
|
vsExitText(3010).includes('restart') &&
|
|
141
286
|
vsExitText(5007).includes('refused'), 'installer exit codes are told in words (0xE0434352 is a crash)');
|
|
142
|
-
const reserve = LOCAL_RESERVE_BYTES;
|
|
143
|
-
const room = (free) => freeBytesAt(t.dir('layout-room'), () => ({ bavail: free, bsize: 1 })) ?? 0;
|
|
144
|
-
t.check(layoutNextStep(0, 0, reserve) === 'done' &&
|
|
145
|
-
layoutNextStep(3762504530, room(40 * GB), reserve) === 'stage' &&
|
|
146
|
-
layoutNextStep(3762504530, room(20 * GB), reserve) === 'skip' &&
|
|
147
|
-
layoutNextStep(null, room(40 * GB), reserve) === 'stage', 'a failed layout is built on the sandbox disk only with room for it (8 GB plus the reserve)');
|
|
148
|
-
t.check(layoutNextStep(null, room(14 * GB), STAGING_RESERVE_BYTES) === 'stage' &&
|
|
149
|
-
layoutNextStep(null, room(9.5 * GB), STAGING_RESERVE_BYTES) === 'stage' &&
|
|
150
|
-
layoutNextStep(null, room(9 * GB), STAGING_RESERVE_BYTES) === 'skip', 'before Visual Studio is installed, the ~14 GB free at boot is enough to build the install files (9.5 GB needed)');
|
|
151
287
|
t.equal(freeBytesAt(path.join(t.dir('layout-room'), 'not', 'there'), () => ({ bavail: 3, bsize: 2 })), 6, 'free room of a folder that does not exist asks its parent');
|
|
152
288
|
t.equal(freeBytesAt(t.dir('layout-room'), () => {
|
|
153
289
|
throw new Error('no');
|
|
@@ -80,6 +80,8 @@ export const LAUNCH_FILES_GOLDEN = [
|
|
|
80
80
|
'sandbox/scripts/link-profile-dirs.ps1',
|
|
81
81
|
'sandbox/scripts/new-shortcuts.ps1',
|
|
82
82
|
'sandbox/scripts/repair-wmi-services.ps1',
|
|
83
|
+
'sandbox/scripts/restore-visual-studio.ps1',
|
|
84
|
+
'sandbox/scripts/save-visual-studio.ps1',
|
|
83
85
|
'sandbox/scripts/set-desktop.ps1',
|
|
84
86
|
'sandbox/scripts/set-dns-servers.ps1',
|
|
85
87
|
'sandbox/scripts/set-machine-environment.ps1',
|
|
@@ -199,8 +199,7 @@ export declare function createFixtures(t: Harness): {
|
|
|
199
199
|
enabled: boolean | "auto";
|
|
200
200
|
acceptLicense: boolean;
|
|
201
201
|
components: string[];
|
|
202
|
-
|
|
203
|
-
visualStudio: "full" | "autosdk";
|
|
202
|
+
visualStudio: "full" | "buildtools";
|
|
204
203
|
persistEngines: boolean;
|
|
205
204
|
derivedDataCache: "persisted" | "local";
|
|
206
205
|
};
|
|
@@ -109,10 +109,9 @@ export declare const ProjectSpecSchema: z.ZodObject<{
|
|
|
109
109
|
enabled: z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>;
|
|
110
110
|
acceptLicense: z.ZodBoolean;
|
|
111
111
|
components: z.ZodArray<z.ZodString>;
|
|
112
|
-
keepInstallFiles: z.ZodBoolean;
|
|
113
112
|
visualStudio: z.ZodEnum<{
|
|
114
113
|
full: "full";
|
|
115
|
-
|
|
114
|
+
buildtools: "buildtools";
|
|
116
115
|
}>;
|
|
117
116
|
persistEngines: z.ZodBoolean;
|
|
118
117
|
derivedDataCache: z.ZodEnum<{
|
|
@@ -346,10 +345,9 @@ export declare const GuestRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
346
345
|
enabled: z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>;
|
|
347
346
|
acceptLicense: z.ZodBoolean;
|
|
348
347
|
components: z.ZodArray<z.ZodString>;
|
|
349
|
-
keepInstallFiles: z.ZodBoolean;
|
|
350
348
|
visualStudio: z.ZodEnum<{
|
|
351
349
|
full: "full";
|
|
352
|
-
|
|
350
|
+
buildtools: "buildtools";
|
|
353
351
|
}>;
|
|
354
352
|
persistEngines: z.ZodBoolean;
|
|
355
353
|
derivedDataCache: z.ZodEnum<{
|
|
@@ -603,10 +601,9 @@ export declare const GuestRequestSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
603
601
|
enabled: z.ZodUnion<readonly [z.ZodBoolean, z.ZodLiteral<"auto">]>;
|
|
604
602
|
acceptLicense: z.ZodBoolean;
|
|
605
603
|
components: z.ZodArray<z.ZodString>;
|
|
606
|
-
keepInstallFiles: z.ZodBoolean;
|
|
607
604
|
visualStudio: z.ZodEnum<{
|
|
608
605
|
full: "full";
|
|
609
|
-
|
|
606
|
+
buildtools: "buildtools";
|
|
610
607
|
}>;
|
|
611
608
|
persistEngines: z.ZodBoolean;
|
|
612
609
|
derivedDataCache: z.ZodEnum<{
|
|
@@ -185,7 +185,8 @@ options = {}) {
|
|
|
185
185
|
socket.on('close', () => finish({ reason: 'closed' }));
|
|
186
186
|
socket.on('error', () => finish({ reason: 'closed' }));
|
|
187
187
|
const [c, r] = size();
|
|
188
|
-
|
|
188
|
+
// A clean screen, but the scrollback stays: the boot's summary (warnings, tools, Ready) is just above the session.
|
|
189
|
+
stdout.write('\x1b[2J\x1b[H');
|
|
189
190
|
socket.write(helloFrame(token, c, r));
|
|
190
191
|
if (stdin.isTTY)
|
|
191
192
|
stdin.setRawMode(true);
|
package/dist/sync/watcher.js
CHANGED
|
@@ -68,7 +68,11 @@ export function watcherRunning(dataPath) {
|
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
70
|
/** The ways the sandbox itself ends badly (the PC going down takes the watcher with it, so it never gets here). */
|
|
71
|
-
|
|
71
|
+
/**
|
|
72
|
+
* Ends that start the sandbox again: only those Windows logged. A "vanished" end (busy, no word of leaving, nothing
|
|
73
|
+
* in the log) is also what closing the Windows Sandbox window looks like, so it gets a report but no restart.
|
|
74
|
+
*/
|
|
75
|
+
const RESTARTABLE = ['sandboxCrash', 'memory', 'gpu', 'hyperv'];
|
|
72
76
|
/**
|
|
73
77
|
* The background watcher of one data folder: while the sandbox runs it syncs history back every minute, guards the
|
|
74
78
|
* projects' git folders, keeps filtered workspaces in step, forwards ports, marks downloaded files, records the audit
|
|
@@ -8,9 +8,9 @@ using System.Runtime.InteropServices;
|
|
|
8
8
|
[assembly: AssemblyCompany("windows-development-sandbox (open source)")]
|
|
9
9
|
[assembly: AssemblyProduct("Windows Development Sandbox")]
|
|
10
10
|
[assembly: AssemblyCopyright("MIT License, windows-development-sandbox contributors")]
|
|
11
|
-
[assembly: AssemblyVersion("1.
|
|
12
|
-
[assembly: AssemblyFileVersion("1.
|
|
13
|
-
[assembly: AssemblyInformationalVersion("1.
|
|
11
|
+
[assembly: AssemblyVersion("1.25.0.0")]
|
|
12
|
+
[assembly: AssemblyFileVersion("1.25.0.0")]
|
|
13
|
+
[assembly: AssemblyInformationalVersion("1.25.0")]
|
|
14
14
|
[assembly: ComVisible(false)]
|
|
15
15
|
// computecore.dll and computestorage.dll are not KnownDLLs: load them from System32 only, never from the exe's folder.
|
|
16
16
|
[assembly: DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
|
|
@@ -8,7 +8,7 @@ using System.Runtime.InteropServices;
|
|
|
8
8
|
[assembly: AssemblyCompany("windows-development-sandbox (open source)")]
|
|
9
9
|
[assembly: AssemblyProduct("Windows Development Sandbox")]
|
|
10
10
|
[assembly: AssemblyCopyright("MIT License, windows-development-sandbox contributors")]
|
|
11
|
-
[assembly: AssemblyVersion("1.
|
|
12
|
-
[assembly: AssemblyFileVersion("1.
|
|
13
|
-
[assembly: AssemblyInformationalVersion("1.
|
|
11
|
+
[assembly: AssemblyVersion("1.25.0.0")]
|
|
12
|
+
[assembly: AssemblyFileVersion("1.25.0.0")]
|
|
13
|
+
[assembly: AssemblyInformationalVersion("1.25.0")]
|
|
14
14
|
[assembly: ComVisible(false)]
|
|
@@ -9,7 +9,7 @@ using System.Runtime.InteropServices;
|
|
|
9
9
|
[assembly: AssemblyCompany("windows-development-sandbox (open source)")]
|
|
10
10
|
[assembly: AssemblyProduct("Windows Development Sandbox")]
|
|
11
11
|
[assembly: AssemblyCopyright("MIT License, windows-development-sandbox contributors")]
|
|
12
|
-
[assembly: AssemblyVersion("1.
|
|
13
|
-
[assembly: AssemblyFileVersion("1.
|
|
14
|
-
[assembly: AssemblyInformationalVersion("1.
|
|
12
|
+
[assembly: AssemblyVersion("1.25.0.0")]
|
|
13
|
+
[assembly: AssemblyFileVersion("1.25.0.0")]
|
|
14
|
+
[assembly: AssemblyInformationalVersion("1.25.0")]
|
|
15
15
|
[assembly: ComVisible(false)]
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "windows-development-sandbox",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.25.0",
|
|
4
4
|
"description": "A disposable Windows Sandbox for development, with or without an AI agent (Claude Code, Codex, Gemini, opencode, ...): the folder you run it from is the workspace, toolchains and databases for your project install themselves, and nothing else on your PC is reachable.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
# Puts a saved Visual Studio back at boot, in seconds (guest/toolchains/native/visual-studio.ts; what goes where is
|
|
3
|
+
# decided in vs-persist.ts): the small folders programs look for at fixed paths are copied back, then the registry
|
|
4
|
+
# values that point at the big folders on the data disk and the Setup Configuration COM class are written. -PlanFile is
|
|
5
|
+
# JSON written by the runtime:
|
|
6
|
+
# copies: [{ name, from, to, mode: 'all' | 'missing' }] 'all': robocopy /E (only what differs is copied);
|
|
7
|
+
# 'missing': each subfolder only when the target has none
|
|
8
|
+
# registry: [{ view: '64'|'32', key, name, kind: 'string'|'expand'|'dword', value }] under HKLM ('' = default)
|
|
9
|
+
# Safe to run again. Exit 0 when everything is in place, 1 when a copy failed, 2 when a registry value could not be set.
|
|
10
|
+
param([Parameter(Mandatory = $true)][string]$PlanFile)
|
|
11
|
+
$ErrorActionPreference = 'Stop'
|
|
12
|
+
|
|
13
|
+
$plan = Get-Content -LiteralPath $PlanFile -Raw -Encoding UTF8 | ConvertFrom-Json
|
|
14
|
+
$robocopy = Join-Path $env:SystemRoot 'System32\Robocopy.exe'
|
|
15
|
+
|
|
16
|
+
function Copy-Folder([string]$From, [string]$To) {
|
|
17
|
+
New-Item -ItemType Directory -Force -Path $To | Out-Null
|
|
18
|
+
& $robocopy $From $To /E /COPY:DAT /XJ /R:1 /W:1 /MT:16 /NP /NFL /NDL /NJH /NJS | Where-Object { $_ -and $_.Trim() } | Out-Host
|
|
19
|
+
return $LASTEXITCODE
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
$copied = 0
|
|
23
|
+
$failed = New-Object System.Collections.ArrayList
|
|
24
|
+
foreach ($copy in @($plan.copies)) {
|
|
25
|
+
if (-not (Test-Path -LiteralPath $copy.from -PathType Container)) {
|
|
26
|
+
[void]$failed.Add($copy.name + ' (' + $copy.from + ' is missing)')
|
|
27
|
+
continue
|
|
28
|
+
}
|
|
29
|
+
if ($copy.mode -eq 'missing') {
|
|
30
|
+
foreach ($child in Get-ChildItem -LiteralPath $copy.from -Directory) {
|
|
31
|
+
$target = Join-Path $copy.to $child.Name
|
|
32
|
+
if (Test-Path -LiteralPath $target) { continue }
|
|
33
|
+
$code = Copy-Folder -From $child.FullName -To $target
|
|
34
|
+
if ($code -ge 8) { [void]$failed.Add($copy.name + '\' + $child.Name + ' (robocopy ' + $code + ')') }
|
|
35
|
+
}
|
|
36
|
+
$copied++
|
|
37
|
+
continue
|
|
38
|
+
}
|
|
39
|
+
$code = Copy-Folder -From $copy.from -To $copy.to
|
|
40
|
+
if ($code -ge 8) { [void]$failed.Add($copy.name + ' (robocopy ' + $code + ')') } else { $copied++ }
|
|
41
|
+
}
|
|
42
|
+
if ($failed.Count) {
|
|
43
|
+
Write-Error ('restore-visual-studio: could not copy ' + ($failed -join ', '))
|
|
44
|
+
exit 1
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
$set = 0
|
|
48
|
+
$bad = New-Object System.Collections.ArrayList
|
|
49
|
+
foreach ($entry in @($plan.registry)) {
|
|
50
|
+
$view = [Microsoft.Win32.RegistryView]::Registry64
|
|
51
|
+
if ($entry.view -eq '32') { $view = [Microsoft.Win32.RegistryView]::Registry32 }
|
|
52
|
+
$base = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, $view)
|
|
53
|
+
try {
|
|
54
|
+
$key = $base.CreateSubKey([string]$entry.key)
|
|
55
|
+
try {
|
|
56
|
+
if ($entry.kind -eq 'dword') {
|
|
57
|
+
$key.SetValue([string]$entry.name, [int]$entry.value, [Microsoft.Win32.RegistryValueKind]::DWord)
|
|
58
|
+
} elseif ($entry.kind -eq 'expand') {
|
|
59
|
+
$key.SetValue([string]$entry.name, [string]$entry.value, [Microsoft.Win32.RegistryValueKind]::ExpandString)
|
|
60
|
+
} else {
|
|
61
|
+
$key.SetValue([string]$entry.name, [string]$entry.value, [Microsoft.Win32.RegistryValueKind]::String)
|
|
62
|
+
}
|
|
63
|
+
$set++
|
|
64
|
+
} finally {
|
|
65
|
+
$key.Close()
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
[void]$bad.Add($entry.view + ':' + $entry.key + '\' + $entry.name + ' (' + $_.Exception.Message + ')')
|
|
69
|
+
} finally {
|
|
70
|
+
$base.Close()
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if ($bad.Count) {
|
|
74
|
+
Write-Error ('restore-visual-studio: could not set ' + ($bad -join '; '))
|
|
75
|
+
exit 2
|
|
76
|
+
}
|
|
77
|
+
Write-Output ('Restored Visual Studio: ' + $copied + ' folders in place, ' + $set + ' registry values set')
|
|
78
|
+
exit 0
|