windows-development-sandbox 1.7.0 → 1.9.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 +10 -0
- package/dist/app/context.d.ts +8 -0
- package/dist/app/context.js +12 -0
- package/dist/app/features/defs.d.ts +322 -0
- package/dist/app/features/defs.js +28 -0
- package/dist/app/features/sandbox/index.d.ts +2 -0
- package/dist/app/features/sandbox/index.js +3 -0
- package/dist/app/features/sandbox/sandbox.def.d.ts +193 -0
- package/dist/app/features/sandbox/sandbox.def.js +245 -0
- package/dist/app/features/sandbox/sandbox.run.d.ts +72 -0
- package/dist/app/features/sandbox/sandbox.run.js +98 -0
- package/dist/app/features/sessions/index.d.ts +2 -0
- package/dist/app/features/sessions/index.js +3 -0
- package/dist/app/features/sessions/sessions.def.d.ts +141 -0
- package/dist/app/features/sessions/sessions.def.js +176 -0
- package/dist/app/features/sessions/sessions.run.d.ts +39 -0
- package/dist/app/features/sessions/sessions.run.js +58 -0
- package/dist/{cli/menu/settings/categories.d.ts → app/features/settings/categories.def.d.ts} +2 -0
- package/dist/{cli/menu/settings/categories.js → app/features/settings/categories.def.js} +24 -5
- package/dist/app/features/settings/index.d.ts +5 -0
- package/dist/app/features/settings/index.js +6 -0
- package/dist/{cli/menu/settings/config-file.d.ts → app/features/settings/settings-file.d.ts} +1 -1
- package/dist/{cli/menu/settings/config-file.js → app/features/settings/settings-file.js} +1 -1
- package/dist/app/features/settings/settings-rows.d.ts +35 -0
- package/dist/app/features/settings/settings-rows.js +94 -0
- package/dist/app/features/settings/settings.def.d.ts +157 -0
- package/dist/app/features/settings/settings.def.js +179 -0
- package/dist/app/features/settings/settings.run.d.ts +67 -0
- package/dist/app/features/settings/settings.run.js +84 -0
- package/dist/app/kernel/define.d.ts +143 -0
- package/dist/app/kernel/define.js +42 -0
- package/dist/app/kernel/errors.d.ts +34 -0
- package/dist/app/kernel/errors.js +45 -0
- package/dist/app/kernel/fields.d.ts +70 -0
- package/dist/app/kernel/fields.js +243 -0
- package/dist/app/kernel/index.d.ts +8 -0
- package/dist/app/kernel/index.js +10 -0
- package/dist/app/kernel/io.d.ts +193 -0
- package/dist/app/kernel/io.js +126 -0
- package/dist/app/kernel/rpc.d.ts +156 -0
- package/dist/app/kernel/rpc.js +47 -0
- package/dist/app/kernel/text.d.ts +6 -0
- package/dist/app/kernel/text.js +57 -0
- package/dist/app/kernel/view.d.ts +146 -0
- package/dist/app/kernel/view.js +28 -0
- package/dist/app/registry.d.ts +10 -0
- package/dist/app/registry.js +50 -0
- package/dist/cli/args.d.ts +5 -0
- package/dist/cli/args.js +6 -0
- package/dist/cli/commands/engine.d.ts +2 -0
- package/dist/cli/commands/engine.js +60 -0
- package/dist/cli/commands/index.d.ts +1 -1
- package/dist/cli/commands/index.js +1 -1
- package/dist/cli/commands/launch/launch.js +47 -7
- package/dist/cli/commands/launch/prepare.js +3 -3
- package/dist/cli/commands/launch/running.js +2 -1
- package/dist/cli/commands/launch/summary.d.ts +2 -0
- package/dist/cli/commands/maintenance/config-share.d.ts +12 -0
- package/dist/cli/commands/maintenance/config-share.js +80 -0
- package/dist/cli/commands/maintenance/config.js +8 -82
- package/dist/cli/commands/maintenance/logs.js +14 -0
- package/dist/cli/commands/registry.js +3 -7
- package/dist/cli/commands/repair/checks/contained.d.ts +2 -0
- package/dist/cli/commands/repair/checks/contained.js +139 -0
- package/dist/cli/commands/repair/checks/hcs.d.ts +7 -0
- package/dist/cli/commands/repair/checks/hcs.js +28 -0
- package/dist/cli/commands/repair/checks/index.js +4 -0
- package/dist/cli/commands/repair/checks/pc.js +32 -1
- package/dist/cli/commands/repair/checks/sandbox.js +16 -1
- package/dist/cli/commands/repair/checks/vpn.js +34 -9
- package/dist/cli/commands/repair/env.js +2 -1
- package/dist/cli/commands/sessions/index.d.ts +0 -4
- package/dist/cli/commands/sessions/index.js +3 -7
- package/dist/cli/commands/shared-use.d.ts +5 -2
- package/dist/cli/commands/shared-use.js +11 -8
- package/dist/cli/commands/storage/contained.d.ts +3 -0
- package/dist/cli/commands/storage/contained.js +179 -0
- package/dist/cli/commands/storage/offer.js +7 -1
- package/dist/cli/commands/storage/storage.js +10 -2
- package/dist/cli/commands/user-config.js +1 -1
- package/dist/cli/commands/vpn.js +19 -3
- package/dist/cli/commands/web.js +2 -0
- package/dist/cli/index.js +2 -1
- package/dist/cli/manifest.d.ts +72 -104
- package/dist/cli/manifest.js +64 -39
- package/dist/cli/menu/add-agent.d.ts +1 -1
- package/dist/cli/menu/add-agent.js +1 -1
- package/dist/cli/menu/apps.d.ts +7 -1
- package/dist/cli/menu/apps.js +36 -0
- package/dist/cli/menu/command-fields.d.ts +1 -1
- package/dist/cli/menu/command-form.js +2 -2
- package/dist/cli/menu/io-menu.d.ts +29 -0
- package/dist/cli/menu/io-menu.js +126 -0
- package/dist/cli/menu/live.d.ts +27 -0
- package/dist/cli/menu/live.js +69 -0
- package/dist/cli/menu/memory.d.ts +19 -0
- package/dist/cli/menu/memory.js +53 -0
- package/dist/cli/menu/menu.d.ts +10 -1
- package/dist/cli/menu/menu.js +27 -9
- package/dist/cli/menu/pages/agents-page.d.ts +3 -1
- package/dist/cli/menu/pages/agents-page.js +12 -5
- package/dist/cli/menu/pages/apps-page.d.ts +3 -1
- package/dist/cli/menu/pages/apps-page.js +19 -2
- package/dist/cli/menu/pages/list-page.d.ts +27 -0
- package/dist/cli/menu/pages/list-page.js +109 -7
- package/dist/cli/menu/pages/maintenance-page.d.ts +0 -5
- package/dist/cli/menu/pages/maintenance-page.js +17 -19
- package/dist/cli/menu/pages/sandbox-page.d.ts +4 -6
- package/dist/cli/menu/pages/sandbox-page.js +26 -118
- package/dist/cli/menu/pages/scopes.d.ts +1 -1
- package/dist/cli/menu/pages/sessions-page.d.ts +5 -11
- package/dist/cli/menu/pages/sessions-page.js +19 -88
- package/dist/cli/menu/pages/settings-page.d.ts +2 -10
- package/dist/cli/menu/pages/settings-page.js +64 -94
- package/dist/cli/menu/render-view.d.ts +28 -0
- package/dist/cli/menu/render-view.js +112 -0
- package/dist/cli/menu/settings/{path-kind.d.ts → ask-path.d.ts} +1 -7
- package/dist/cli/menu/settings/ask-path.js +14 -0
- package/dist/cli/menu/settings/collections/collection-editor.d.ts +1 -1
- package/dist/cli/menu/settings/collections/string-list-editor.d.ts +1 -2
- package/dist/cli/menu/settings/collections/string-list-editor.js +1 -1
- package/dist/cli/menu/settings/draft.d.ts +1 -1
- package/dist/cli/menu/settings/format.d.ts +1 -3
- package/dist/cli/menu/settings/format.js +6 -36
- package/dist/cli/menu/settings/index.d.ts +1 -3
- package/dist/cli/menu/settings/index.js +3 -3
- package/dist/cli/menu/settings/object-editor.d.ts +1 -1
- package/dist/cli/menu/settings/share.d.ts +36 -0
- package/dist/cli/menu/settings/share.js +131 -0
- package/dist/cli/menu/settings/value-editor.d.ts +5 -1
- package/dist/cli/menu/settings/value-editor.js +57 -54
- package/dist/cli/menu/several.js +1 -1
- package/dist/cli/menu/share-settings.d.ts +21 -0
- package/dist/cli/menu/share-settings.js +112 -0
- package/dist/cli/menu/shell.d.ts +21 -5
- package/dist/cli/menu/shell.js +31 -6
- package/dist/cli/menu/views.d.ts +7 -0
- package/dist/cli/menu/views.js +11 -0
- package/dist/cli/menu/vpn-login.js +8 -0
- package/dist/cli/operations/handler.d.ts +6 -0
- package/dist/cli/operations/handler.js +61 -0
- package/dist/cli/operations/index.d.ts +4 -0
- package/dist/cli/operations/index.js +5 -0
- package/dist/cli/operations/input.d.ts +6 -0
- package/dist/cli/operations/input.js +37 -0
- package/dist/cli/operations/io-cli.d.ts +7 -0
- package/dist/cli/operations/io-cli.js +62 -0
- package/dist/cli/operations/spec.d.ts +18 -0
- package/dist/cli/operations/spec.js +108 -0
- package/dist/cli/output/colors.d.ts +2 -2
- package/dist/cli/output/colors.js +2 -11
- package/dist/cli/output/index.d.ts +1 -0
- package/dist/cli/output/index.js +1 -0
- package/dist/cli/output/print.d.ts +3 -1
- package/dist/cli/output/print.js +7 -3
- package/dist/cli/output/progress.d.ts +15 -0
- package/dist/cli/output/progress.js +44 -0
- package/dist/cli/output/render-view.d.ts +4 -0
- package/dist/cli/output/render-view.js +63 -0
- package/dist/cli/services/agent-commands-service.d.ts +2 -0
- package/dist/cli/services/agent-commands-service.js +11 -2
- package/dist/cli/services/logs-service.js +4 -3
- package/dist/cli/tui/app.d.ts +6 -1
- package/dist/cli/tui/app.js +26 -4
- package/dist/cli/tui/list/list-screen.d.ts +14 -0
- package/dist/cli/tui/list/list-screen.js +64 -36
- package/dist/cli/tui/list/list-types.d.ts +2 -0
- package/dist/cli/tui/terminal.d.ts +7 -0
- package/dist/cli/tui/terminal.js +30 -10
- package/dist/core/config/defaults.js +8 -0
- package/dist/core/schema/config.d.ts +13 -0
- package/dist/core/schema/config.js +16 -1
- package/dist/core/schema/harness.d.ts +1 -0
- package/dist/core/schema/harness.js +5 -0
- package/dist/core/schema/sandbox.d.ts +6 -0
- package/dist/core/schema/sandbox.js +20 -3
- package/dist/core/schema/vpn.d.ts +2 -1
- package/dist/core/schema/vpn.js +3 -3
- package/dist/core/types.d.ts +23 -5
- package/dist/core/util/fs.d.ts +2 -0
- package/dist/core/util/fs.js +9 -0
- package/dist/core/util/index.d.ts +1 -1
- package/dist/core/util/index.js +1 -1
- package/dist/guest/agents/agent-files.d.ts +5 -0
- package/dist/guest/agents/agent-files.js +4 -0
- package/dist/guest/agents/local-state.d.ts +2 -0
- package/dist/guest/agents/local-state.js +21 -1
- package/dist/guest/boot/boot.js +2 -1
- package/dist/guest/daemon/daemon.js +4 -0
- package/dist/guest/daemon/sessions.d.ts +5 -0
- package/dist/guest/daemon/sessions.js +17 -3
- package/dist/guest/entries/daemon.js +3 -0
- package/dist/guest/governor/decide.d.ts +61 -0
- package/dist/guest/governor/decide.js +182 -0
- package/dist/guest/governor/governor.d.ts +42 -0
- package/dist/guest/governor/governor.js +214 -0
- package/dist/guest/governor/index.d.ts +2 -0
- package/dist/guest/governor/index.js +2 -0
- package/dist/guest/runtime/csc.d.ts +1 -0
- package/dist/guest/runtime/csc.js +1 -0
- package/dist/guest/sessions/viewer.js +3 -1
- package/dist/guest/toolchains/native/rust-extras.js +4 -2
- package/dist/guest/vpn/vpn.js +69 -21
- package/dist/guest/vpn/wireguard.d.ts +32 -0
- package/dist/guest/vpn/wireguard.js +132 -0
- package/dist/host/agent-commands/commands.d.ts +4 -0
- package/dist/host/agent-commands/commands.js +31 -7
- package/dist/host/audit/log.d.ts +1 -1
- package/dist/host/channel/attach.d.ts +3 -1
- package/dist/host/channel/attach.js +5 -2
- package/dist/host/channel/channel.d.ts +2 -0
- package/dist/host/channel/channel.js +4 -0
- package/dist/host/engines/engine.d.ts +12 -0
- package/dist/host/engines/engine.js +1 -0
- package/dist/host/engines/hcs/check.d.ts +25 -0
- package/dist/host/engines/hcs/check.js +95 -0
- package/dist/host/engines/hcs/documents.d.ts +44 -0
- package/dist/host/engines/hcs/documents.js +155 -0
- package/dist/host/engines/hcs/engine.d.ts +39 -0
- package/dist/host/engines/hcs/engine.js +171 -0
- package/dist/host/engines/hcs/helper.d.ts +43 -0
- package/dist/host/engines/hcs/helper.js +173 -0
- package/dist/host/engines/hcs/images.d.ts +37 -0
- package/dist/host/engines/hcs/images.js +152 -0
- package/dist/host/engines/index.d.ts +7 -0
- package/dist/host/engines/index.js +5 -0
- package/dist/host/engines/registry.d.ts +16 -0
- package/dist/host/engines/registry.js +30 -0
- package/dist/host/engines/types.d.ts +58 -0
- package/dist/host/engines/types.js +1 -0
- package/dist/host/engines/windows-sandbox/engine.d.ts +16 -0
- package/dist/host/engines/windows-sandbox/engine.js +63 -0
- package/dist/host/engines/windows-sandbox/wsb-text.d.ts +2 -0
- package/dist/host/engines/windows-sandbox/wsb-text.js +29 -0
- package/dist/host/launch/generate.d.ts +5 -1
- package/dist/host/launch/generate.js +50 -8
- package/dist/host/launch/index.d.ts +3 -1
- package/dist/host/launch/index.js +3 -1
- package/dist/host/launch/launch-plan.d.ts +13 -0
- package/dist/host/launch/launch-plan.js +26 -0
- package/dist/host/launch/mappings.js +2 -1
- package/dist/host/launch/risk-notes.d.ts +10 -0
- package/dist/host/launch/risk-notes.js +18 -0
- package/dist/host/launch/session-file.d.ts +2 -0
- package/dist/host/launch/session-file.js +7 -1
- package/dist/host/launch/wsb-file.d.ts +28 -4
- package/dist/host/launch/wsb-file.js +61 -42
- package/dist/host/life/crash-report.d.ts +27 -1
- package/dist/host/life/crash-report.js +173 -7
- package/dist/host/life/host-memory.d.ts +40 -0
- package/dist/host/life/host-memory.js +137 -0
- package/dist/host/life/index.d.ts +2 -0
- package/dist/host/life/index.js +2 -0
- package/dist/host/life/stop-events.d.ts +2 -0
- package/dist/host/life/stop-events.js +20 -3
- package/dist/host/life/stop-warning.js +1 -1
- package/dist/host/life/watchdog.d.ts +54 -0
- package/dist/host/life/watchdog.js +108 -0
- package/dist/host/native.d.ts +3 -2
- package/dist/host/native.js +22 -3
- package/dist/host/profile/claude-login.js +3 -2
- package/dist/host/sandbox/contained-deps.d.ts +2 -0
- package/dist/host/sandbox/contained-deps.js +17 -0
- package/dist/host/sandbox/index.d.ts +2 -0
- package/dist/host/sandbox/index.js +2 -0
- package/dist/host/sandbox/platform.d.ts +2 -0
- package/dist/host/sandbox/platform.js +7 -7
- package/dist/host/sandbox/tasklist.d.ts +13 -0
- package/dist/host/sandbox/tasklist.js +47 -0
- package/dist/host/secrets/launch-secrets.js +2 -1
- package/dist/host/sharing/current.js +2 -6
- package/dist/host/storage/contained/check.d.ts +9 -0
- package/dist/host/storage/contained/check.js +37 -0
- package/dist/host/storage/contained/contained-storage.d.ts +66 -0
- package/dist/host/storage/contained/contained-storage.js +168 -0
- package/dist/host/storage/contained/disk-port.d.ts +43 -0
- package/dist/host/storage/contained/disk-port.js +122 -0
- package/dist/host/storage/contained/index.d.ts +9 -0
- package/dist/host/storage/contained/index.js +8 -0
- package/dist/host/storage/contained/layout.d.ts +28 -0
- package/dist/host/storage/contained/layout.js +45 -0
- package/dist/host/storage/contained/resolve.d.ts +4 -0
- package/dist/host/storage/contained/resolve.js +30 -0
- package/dist/host/storage/contained/setting.d.ts +1 -0
- package/dist/host/storage/contained/setting.js +5 -0
- package/dist/host/storage/contained/state.d.ts +89 -0
- package/dist/host/storage/contained/state.js +166 -0
- package/dist/host/storage/contained/transfer.d.ts +24 -0
- package/dist/host/storage/contained/transfer.js +121 -0
- package/dist/host/storage/index.d.ts +2 -0
- package/dist/host/storage/index.js +2 -0
- package/dist/host/storage/providers/contained.d.ts +15 -0
- package/dist/host/storage/providers/contained.js +39 -0
- package/dist/host/storage/providers/folder.d.ts +12 -0
- package/dist/host/storage/providers/folder.js +20 -0
- package/dist/host/storage/providers/index.d.ts +4 -0
- package/dist/host/storage/providers/index.js +3 -0
- package/dist/host/storage/providers/registry.d.ts +9 -0
- package/dist/host/storage/providers/registry.js +14 -0
- package/dist/host/storage/providers/types.d.ts +33 -0
- package/dist/host/storage/providers/types.js +1 -0
- package/dist/host/tools/conpty.js +3 -2
- package/dist/host/tools/windows-terminal.js +8 -7
- package/dist/host/vpn/index.d.ts +1 -1
- package/dist/host/vpn/index.js +1 -1
- package/dist/host/vpn/prepare.d.ts +2 -0
- package/dist/host/vpn/prepare.js +63 -2
- package/dist/host/web/browser.d.ts +2 -0
- package/dist/host/web/browser.js +14 -1
- package/dist/host/web/index.d.ts +3 -2
- package/dist/host/web/index.js +3 -2
- package/dist/host/web/rpc.d.ts +24 -0
- package/dist/host/web/rpc.js +149 -0
- package/dist/host/web/runner.d.ts +2 -0
- package/dist/host/web/runner.js +1 -2
- package/dist/host/web/server.d.ts +7 -0
- package/dist/host/web/server.js +23 -3
- package/dist/runtime/agent.js +69 -8
- package/dist/runtime/daemon.js +1058 -624
- package/dist/runtime/db.js +69 -8
- package/dist/runtime/main.js +624 -182
- package/dist/runtime/netproxy.js +69 -8
- package/dist/runtime/view.js +87 -13
- package/dist/selftest/agent-paths.test.js +11 -1
- package/dist/selftest/app.test.d.ts +2 -0
- package/dist/selftest/app.test.js +324 -0
- package/dist/selftest/channel.test.js +13 -0
- package/dist/selftest/cli.test.js +2 -1
- package/dist/selftest/config.test.js +59 -0
- package/dist/selftest/contained.test.d.ts +2 -0
- package/dist/selftest/contained.test.js +544 -0
- package/dist/selftest/credentials.test.js +1 -1
- package/dist/selftest/engine.test.d.ts +2 -0
- package/dist/selftest/engine.test.js +127 -0
- package/dist/selftest/hcs.test.d.ts +2 -0
- package/dist/selftest/hcs.test.js +482 -0
- package/dist/selftest/heavy-load.test.d.ts +2 -0
- package/dist/selftest/heavy-load.test.js +329 -0
- package/dist/selftest/index.js +12 -0
- package/dist/selftest/layout.test.js +12 -0
- package/dist/selftest/menu-perf.test.d.ts +2 -0
- package/dist/selftest/menu-perf.test.js +248 -0
- package/dist/selftest/menu-screens.js +4 -2
- package/dist/selftest/menu.test.js +36 -8
- package/dist/selftest/support/engine-cases.d.ts +8 -0
- package/dist/selftest/support/engine-cases.js +65 -0
- package/dist/selftest/support/engine-golden.d.ts +3 -0
- package/dist/selftest/support/engine-golden.js +66 -0
- package/dist/selftest/support/fixtures.d.ts +7 -1
- package/dist/selftest/support/hcs-probes.d.ts +17 -0
- package/dist/selftest/support/hcs-probes.js +41 -0
- package/dist/selftest/vpn.test.js +136 -3
- package/dist/selftest/web.test.js +101 -3
- package/dist/shared/channel/run-dir.d.ts +4 -1
- package/dist/shared/channel/run-dir.js +7 -1
- package/dist/shared/channel/schema.d.ts +35 -1
- package/dist/shared/channel/schema.js +27 -0
- package/dist/shared/channel/secrets.d.ts +1 -0
- package/dist/shared/channel/secrets.js +6 -0
- package/dist/shared/memory/index.d.ts +1 -0
- package/dist/shared/memory/index.js +1 -0
- package/dist/shared/memory/policy.d.ts +33 -0
- package/dist/shared/memory/policy.js +55 -0
- package/dist/shared/pty/output-filter.d.ts +14 -1
- package/dist/shared/pty/output-filter.js +22 -4
- package/dist/shared/pty/terminal.d.ts +4 -1
- package/dist/shared/pty/terminal.js +4 -2
- package/dist/shared/system.d.ts +1 -0
- package/dist/shared/system.js +1 -0
- package/dist/shared/vpn/download.d.ts +16 -0
- package/dist/shared/vpn/download.js +16 -0
- package/dist/shared/vpn/wireguard.d.ts +51 -0
- package/dist/shared/vpn/wireguard.js +227 -0
- package/dist/sync/sync-cycle.js +5 -5
- package/dist/sync/watcher.d.ts +13 -0
- package/dist/sync/watcher.js +92 -3
- package/dist/web/index.html +4 -3
- package/dist/web/main-NRRS5O6Z.js +157 -0
- package/dist/web/main-T5DMCHYV.css +1 -0
- package/native/csharp/HcsHost/AssemblyInfo.cs +14 -0
- package/native/csharp/HcsHost/Guard.cs +100 -0
- package/native/csharp/HcsHost/Hcs.cs +256 -0
- package/native/csharp/HcsHost/Json.cs +78 -0
- package/native/csharp/HcsHost/Program.cs +185 -0
- package/native/csharp/HcsHost/VirtualDisk.cs +95 -0
- package/native/csharp/HostTools/AssemblyInfo.cs +14 -0
- package/native/csharp/HostTools/MemoryStatus.cs +107 -0
- package/native/csharp/HostTools/Program.cs +58 -17
- package/native/scripts/check-contained.ps1 +28 -0
- package/native/scripts/contained-common.ps1 +241 -0
- package/native/scripts/dismount-contained.ps1 +66 -0
- package/native/scripts/memory-status.ps1 +34 -0
- package/native/scripts/mount-contained.ps1 +139 -0
- package/native/scripts/run-elevated.ps1 +17 -4
- package/native/scripts/stop-events.ps1 +23 -8
- package/package.json +9 -2
- package/sandbox/csharp/MemGov.cs +398 -0
- package/sandbox/csharp/PtyHost.cs +111 -5
- package/schemas/config.schema.json +60 -6
- package/schemas/sandbox-config.schema.json +3 -3
- package/dist/cli/commands/sessions/control.d.ts +0 -4
- package/dist/cli/commands/sessions/control.js +0 -66
- package/dist/cli/commands/sessions/list.d.ts +0 -2
- package/dist/cli/commands/sessions/list.js +0 -48
- package/dist/cli/commands/sessions/status.d.ts +0 -4
- package/dist/cli/commands/sessions/status.js +0 -114
- package/dist/cli/commands/sessions/window.d.ts +0 -3
- package/dist/cli/commands/sessions/window.js +0 -18
- package/dist/cli/menu/settings/path-kind.js +0 -44
- package/dist/web/main-CXESL347.js +0 -45
- package/dist/web/main-VLR7DVQ4.css +0 -1
- /package/dist/{cli/menu/settings → app/kernel}/schema-node.d.ts +0 -0
- /package/dist/{cli/menu/settings → app/kernel}/schema-node.js +0 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// The only place that touches disks: mounting and dismounting go through native\scripts\mount-contained.ps1 and
|
|
2
|
+
// dismount-contained.ps1 (run elevated through run-elevated.ps1: one Windows permission prompt each), looking goes
|
|
3
|
+
// through check-contained.ps1 (no elevation). Everything above this file works against the `DiskPort` interface, so
|
|
4
|
+
// the tests use a fake and the real scripts are only ever started on a PC.
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import path from 'node:path';
|
|
9
|
+
import { spawn } from 'node:child_process';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { fail } from '../../../core/util/index.js';
|
|
12
|
+
import { readValid } from '../../../shared/io/index.js';
|
|
13
|
+
import { SYSTEM } from '../../../shared/system.js';
|
|
14
|
+
import { nativeFile, nativeScript } from '../../native.js';
|
|
15
|
+
import { refuseInSelfTest } from '../../test-guard.js';
|
|
16
|
+
const OutcomeSchema = z.object({
|
|
17
|
+
ok: z.boolean(),
|
|
18
|
+
message: z.string().max(4000),
|
|
19
|
+
created: z.boolean().optional(),
|
|
20
|
+
alreadyMounted: z.boolean().optional(),
|
|
21
|
+
notes: z
|
|
22
|
+
.union([z.array(z.string().max(2000)).max(100), z.string().max(2000)])
|
|
23
|
+
.optional()
|
|
24
|
+
.transform((v) => (v === undefined ? [] : typeof v === 'string' ? [v] : v)),
|
|
25
|
+
});
|
|
26
|
+
const RealitySchema = z.object({
|
|
27
|
+
imageExists: z.boolean(),
|
|
28
|
+
imageBytes: z.number().nonnegative(),
|
|
29
|
+
mountExists: z.boolean(),
|
|
30
|
+
mountIsMountPoint: z.boolean(),
|
|
31
|
+
attached: z.boolean().nullable(),
|
|
32
|
+
});
|
|
33
|
+
const MOUNT_TIMEOUT_MS = 10 * 60_000;
|
|
34
|
+
const LOOK_TIMEOUT_MS = 30_000;
|
|
35
|
+
export const spawnRunner = (file, args, timeoutMs) => new Promise((resolve) => {
|
|
36
|
+
const child = spawn(file, [...args], { windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
37
|
+
let stdout = '';
|
|
38
|
+
let stderr = '';
|
|
39
|
+
const timer = setTimeout(() => child.kill(), timeoutMs);
|
|
40
|
+
child.stdout.on('data', (d) => (stdout += d.toString('utf8')));
|
|
41
|
+
child.stderr.on('data', (d) => (stderr += d.toString('utf8')));
|
|
42
|
+
child.on('error', (e) => {
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
resolve({ status: null, stdout, stderr: stderr + e.message });
|
|
45
|
+
});
|
|
46
|
+
child.on('close', (code) => {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
resolve({ status: code, stdout, stderr });
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
export class ScriptDiskPort {
|
|
52
|
+
run;
|
|
53
|
+
constructor(run = spawnRunner) {
|
|
54
|
+
this.run = run;
|
|
55
|
+
}
|
|
56
|
+
mount(request) {
|
|
57
|
+
const { paths } = request;
|
|
58
|
+
return this.elevated('mount-contained.ps1', {
|
|
59
|
+
set: paths.set,
|
|
60
|
+
storageRoot: paths.storageRoot,
|
|
61
|
+
image: paths.image,
|
|
62
|
+
mount: paths.mount,
|
|
63
|
+
userSid: request.userSid,
|
|
64
|
+
maxGb: request.maxGb,
|
|
65
|
+
create: request.create,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
dismount(request) {
|
|
69
|
+
const { paths } = request;
|
|
70
|
+
return this.elevated('dismount-contained.ps1', {
|
|
71
|
+
set: paths.set,
|
|
72
|
+
storageRoot: paths.storageRoot,
|
|
73
|
+
image: paths.image,
|
|
74
|
+
mount: paths.mount,
|
|
75
|
+
userSid: request.userSid,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
async reality(paths) {
|
|
79
|
+
const out = await this.run(SYSTEM.powershell(), nativeScript('check-contained.ps1', ['-Image', paths.image, '-Mount', paths.mount]), LOOK_TIMEOUT_MS);
|
|
80
|
+
const parsed = RealitySchema.safeParse(safeJson(out.stdout.trim()));
|
|
81
|
+
if (out.status !== 0 || !parsed.success)
|
|
82
|
+
fail('Could not look at the contained storage: ' + (out.stderr.trim() || out.stdout.trim() || 'no answer') + '.', 'Run sandbox doctor.');
|
|
83
|
+
return parsed.data;
|
|
84
|
+
}
|
|
85
|
+
/** One run of an elevated script: the request goes in a file in a private temp folder, the answer comes back so. */
|
|
86
|
+
async elevated(script, request) {
|
|
87
|
+
// A test hands in a fake runner; only the real one reaches a disk.
|
|
88
|
+
if (this.run === spawnRunner)
|
|
89
|
+
refuseInSelfTest('mount or dismount a disk image');
|
|
90
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'agent-sandbox-contained-'));
|
|
91
|
+
try {
|
|
92
|
+
const file = path.join(dir, 'request.json');
|
|
93
|
+
const text = JSON.stringify(request, null, 2);
|
|
94
|
+
fs.writeFileSync(file, text, 'utf8');
|
|
95
|
+
const sha = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
96
|
+
const out = await this.run(SYSTEM.powershell(), nativeScript('run-elevated.ps1', ['-Script', nativeFile('scripts', script), '-Json', file, '-Sha256', sha]), MOUNT_TIMEOUT_MS);
|
|
97
|
+
const answer = readValid(OutcomeSchema, path.join(dir, 'result.json'));
|
|
98
|
+
if (!answer)
|
|
99
|
+
return fail(out.status === 1
|
|
100
|
+
? 'Windows did not give permission (the prompt was declined or closed).'
|
|
101
|
+
: 'The disk image tool gave no answer' + (out.stderr.trim() ? ': ' + out.stderr.trim() : '.'), 'Run the command again and choose Yes in the Windows prompt.');
|
|
102
|
+
return {
|
|
103
|
+
ok: answer.ok,
|
|
104
|
+
message: answer.message,
|
|
105
|
+
created: answer.created ?? false,
|
|
106
|
+
alreadyMounted: answer.alreadyMounted ?? false,
|
|
107
|
+
notes: answer.notes,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
function safeJson(text) {
|
|
116
|
+
try {
|
|
117
|
+
return JSON.parse(text);
|
|
118
|
+
}
|
|
119
|
+
catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { CONTAINED_FOLDERS, containedMappings, containedPaths, setNameOf, type ContainedFolder } from './layout.js';
|
|
2
|
+
export type { ContainedPaths } from './layout.js';
|
|
3
|
+
export { ContainedStorage, type ContainedDeps, type HolderId, type ReleaseResult } from './contained-storage.js';
|
|
4
|
+
export { ScriptDiskPort, spawnRunner, type DiskPort, type MountRequest, type ScriptOutcome } from './disk-port.js';
|
|
5
|
+
export { containedAvailability, containedFacts, type ContainedFacts } from './check.js';
|
|
6
|
+
export { hostDataFolder, hostDataRoot } from './resolve.js';
|
|
7
|
+
export { writeContainedSetting } from './setting.js';
|
|
8
|
+
export { addHolder, diagnose, markMounted, mountedSets, staleMountNote, pruneHolders, removeHolder, setStateOf, StateFile, type ContainedDiagnosis, type MountReality, } from './state.js';
|
|
9
|
+
export { assertTargetsFree, planTransfer, runTransfer, type TransferPlan, type TransferResult } from './transfer.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { CONTAINED_FOLDERS, containedMappings, containedPaths, setNameOf } from './layout.js';
|
|
2
|
+
export { ContainedStorage } from './contained-storage.js';
|
|
3
|
+
export { ScriptDiskPort, spawnRunner } from './disk-port.js';
|
|
4
|
+
export { containedAvailability, containedFacts } from './check.js';
|
|
5
|
+
export { hostDataFolder, hostDataRoot } from './resolve.js';
|
|
6
|
+
export { writeContainedSetting } from './setting.js';
|
|
7
|
+
export { addHolder, diagnose, markMounted, mountedSets, staleMountNote, pruneHolders, removeHolder, setStateOf, StateFile, } from './state.js';
|
|
8
|
+
export { assertTargetsFree, planTransfer, runTransfer } from './transfer.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { ResolvedMapping } from '../../../core/types.js';
|
|
2
|
+
/** The folders of the data set that move into the image. Everything else of the data set stays on the PC as it is. */
|
|
3
|
+
export declare const CONTAINED_FOLDERS: readonly ["tools", "state", "logs", "databases", "cache"];
|
|
4
|
+
export type ContainedFolder = (typeof CONTAINED_FOLDERS)[number];
|
|
5
|
+
/** A data set name the scripts accept (same as the dataName setting, so every real name passes). */
|
|
6
|
+
export declare const SET_NAME_RULE: RegExp;
|
|
7
|
+
export interface ContainedPaths {
|
|
8
|
+
readonly storageRoot: string;
|
|
9
|
+
readonly set: string;
|
|
10
|
+
readonly imagesDir: string;
|
|
11
|
+
readonly mountsDir: string;
|
|
12
|
+
/** <storage>\images\<set>.vhdx */
|
|
13
|
+
readonly image: string;
|
|
14
|
+
/** <storage>\mounts\<set> */
|
|
15
|
+
readonly mount: string;
|
|
16
|
+
/** <storage>\mounts\state.json: which sets are mounted and who holds them (never inside an image). */
|
|
17
|
+
readonly stateFile: string;
|
|
18
|
+
readonly lockFile: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function containedPaths(storageRoot: string, set: string): ContainedPaths;
|
|
21
|
+
/** The data set's name: the last folder of <storage>\data\<name>. */
|
|
22
|
+
export declare function setNameOf(dataPath: string): string;
|
|
23
|
+
/**
|
|
24
|
+
* The mappings for a data set with contained storage: the run folder from the data folder on the PC, each contained
|
|
25
|
+
* folder from the mounted image. The guest sees C:\AgentData\run, \tools, \state, ... exactly as with one mapping of
|
|
26
|
+
* the whole folder.
|
|
27
|
+
*/
|
|
28
|
+
export declare function containedMappings(dataPath: string, mount: string, guestData: string): ResolvedMapping[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Where contained storage keeps its files, and how the data set splits when it is on: `run\` stays the plain host
|
|
2
|
+
// folder (the host reads it), the folders listed here live in the disk image and are mapped from inside its mount
|
|
3
|
+
// folder to the very same guest paths the whole data folder had.
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { fail } from '../../../core/util/index.js';
|
|
6
|
+
/** The folders of the data set that move into the image. Everything else of the data set stays on the PC as it is. */
|
|
7
|
+
export const CONTAINED_FOLDERS = ['tools', 'state', 'logs', 'databases', 'cache'];
|
|
8
|
+
/** A data set name the scripts accept (same as the dataName setting, so every real name passes). */
|
|
9
|
+
export const SET_NAME_RULE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
10
|
+
export function containedPaths(storageRoot, set) {
|
|
11
|
+
if (!SET_NAME_RULE.test(set))
|
|
12
|
+
fail('The data set name "' + set + '" cannot be used for contained storage.', 'Use letters, numbers, - and _.');
|
|
13
|
+
const root = path.resolve(storageRoot);
|
|
14
|
+
const imagesDir = path.join(root, 'images');
|
|
15
|
+
const mountsDir = path.join(root, 'mounts');
|
|
16
|
+
return {
|
|
17
|
+
storageRoot: root,
|
|
18
|
+
set,
|
|
19
|
+
imagesDir,
|
|
20
|
+
mountsDir,
|
|
21
|
+
image: path.join(imagesDir, set + '.vhdx'),
|
|
22
|
+
mount: path.join(mountsDir, set),
|
|
23
|
+
stateFile: path.join(mountsDir, 'state.json'),
|
|
24
|
+
lockFile: path.join(mountsDir, 'state.lock'),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** The data set's name: the last folder of <storage>\data\<name>. */
|
|
28
|
+
export function setNameOf(dataPath) {
|
|
29
|
+
return path.basename(path.resolve(dataPath));
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The mappings for a data set with contained storage: the run folder from the data folder on the PC, each contained
|
|
33
|
+
* folder from the mounted image. The guest sees C:\AgentData\run, \tools, \state, ... exactly as with one mapping of
|
|
34
|
+
* the whole folder.
|
|
35
|
+
*/
|
|
36
|
+
export function containedMappings(dataPath, mount, guestData) {
|
|
37
|
+
return [
|
|
38
|
+
{ host: path.join(dataPath, 'run'), guest: guestData + '\\run', readOnly: false },
|
|
39
|
+
...CONTAINED_FOLDERS.map((name) => ({
|
|
40
|
+
host: path.join(mount, name),
|
|
41
|
+
guest: guestData + '\\' + name,
|
|
42
|
+
readOnly: false,
|
|
43
|
+
})),
|
|
44
|
+
];
|
|
45
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ContainedFolder } from './layout.js';
|
|
2
|
+
/** The folder that holds tools, state, logs ...: the mount folder while the image is mounted, else the data folder. */
|
|
3
|
+
export declare function hostDataRoot(dataPath: string): string;
|
|
4
|
+
export declare function hostDataFolder(dataPath: string, name: ContainedFolder): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Where the host finds the data set's folders. Plain data folder: <data>\<name>. While contained storage has the image
|
|
2
|
+
// mounted: <storage>\mounts\<set>\<name>, but only when that folder really is another volume (a stale record must
|
|
3
|
+
// never send a write into an empty folder on this PC's own disk).
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { JsonFile } from '../../../shared/io/index.js';
|
|
7
|
+
import { containedPaths } from './layout.js';
|
|
8
|
+
import { ContainedStateSchema } from './state.js';
|
|
9
|
+
/** The folder that holds tools, state, logs ...: the mount folder while the image is mounted, else the data folder. */
|
|
10
|
+
export function hostDataRoot(dataPath) {
|
|
11
|
+
try {
|
|
12
|
+
const set = path.basename(dataPath);
|
|
13
|
+
const paths = containedPaths(path.dirname(path.dirname(dataPath)), set);
|
|
14
|
+
if (!fs.existsSync(paths.stateFile))
|
|
15
|
+
return dataPath;
|
|
16
|
+
const state = new JsonFile(ContainedStateSchema, paths.stateFile).read();
|
|
17
|
+
if (!state?.sets[set]?.mounted)
|
|
18
|
+
return dataPath;
|
|
19
|
+
if (fs.statSync(paths.mount).dev === fs.statSync(paths.mountsDir).dev)
|
|
20
|
+
return dataPath;
|
|
21
|
+
return paths.mount;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// No contained storage here (or an unreadable record): the plain folder is the answer.
|
|
25
|
+
return dataPath;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function hostDataFolder(dataPath, name) {
|
|
29
|
+
return path.join(hostDataRoot(dataPath), name);
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function writeContainedSetting(configFile: string, value: 'off' | 'vhdx'): void;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Writes storage.contained into the settings file (keeping its comments), the way `storage move` pins storage.root.
|
|
2
|
+
import { setValue } from '../../migrate/migrations/config-file.js';
|
|
3
|
+
export function writeContainedSetting(configFile, value) {
|
|
4
|
+
setValue(configFile, ['storage', 'contained'], value);
|
|
5
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { type ContainedPaths } from './layout.js';
|
|
3
|
+
declare const HolderSchema: z.ZodObject<{
|
|
4
|
+
id: z.ZodString;
|
|
5
|
+
pid: z.ZodNumber;
|
|
6
|
+
since: z.ZodString;
|
|
7
|
+
}, z.core.$strict>;
|
|
8
|
+
export type Holder = z.infer<typeof HolderSchema>;
|
|
9
|
+
declare const SetStateSchema: z.ZodObject<{
|
|
10
|
+
mounted: z.ZodBoolean;
|
|
11
|
+
holders: z.ZodArray<z.ZodObject<{
|
|
12
|
+
id: z.ZodString;
|
|
13
|
+
pid: z.ZodNumber;
|
|
14
|
+
since: z.ZodString;
|
|
15
|
+
}, z.core.$strict>>;
|
|
16
|
+
}, z.core.$strict>;
|
|
17
|
+
export type SetState = z.infer<typeof SetStateSchema>;
|
|
18
|
+
export declare const ContainedStateSchema: z.ZodObject<{
|
|
19
|
+
version: z.ZodLiteral<1>;
|
|
20
|
+
sets: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
21
|
+
mounted: z.ZodBoolean;
|
|
22
|
+
holders: z.ZodArray<z.ZodObject<{
|
|
23
|
+
id: z.ZodString;
|
|
24
|
+
pid: z.ZodNumber;
|
|
25
|
+
since: z.ZodString;
|
|
26
|
+
}, z.core.$strict>>;
|
|
27
|
+
}, z.core.$strict>>;
|
|
28
|
+
}, z.core.$strict>;
|
|
29
|
+
export type ContainedState = z.infer<typeof ContainedStateSchema>;
|
|
30
|
+
export declare const EMPTY_STATE: ContainedState;
|
|
31
|
+
export declare function setStateOf(state: ContainedState, set: string): SetState;
|
|
32
|
+
/** Drops the holders whose process is gone (a launch that ended, a watcher that crashed). */
|
|
33
|
+
export declare function pruneHolders(state: ContainedState, set: string, alive: (pid: number) => boolean): ContainedState;
|
|
34
|
+
/** Adds a holder (or refreshes the one with the same id). */
|
|
35
|
+
export declare function addHolder(state: ContainedState, set: string, holder: Holder): ContainedState;
|
|
36
|
+
export declare function removeHolder(state: ContainedState, set: string, id: string): ContainedState;
|
|
37
|
+
export declare function markMounted(state: ContainedState, set: string, mounted: boolean): ContainedState;
|
|
38
|
+
/** What reality looks like, from the status script (check-contained.ps1). */
|
|
39
|
+
export interface MountReality {
|
|
40
|
+
readonly imageExists: boolean;
|
|
41
|
+
readonly imageBytes: number;
|
|
42
|
+
readonly mountExists: boolean;
|
|
43
|
+
readonly mountIsMountPoint: boolean;
|
|
44
|
+
/** null: Windows would not say without administrator rights. */
|
|
45
|
+
readonly attached: boolean | null;
|
|
46
|
+
}
|
|
47
|
+
export type ContainedDiagnosis = {
|
|
48
|
+
readonly kind: 'ok';
|
|
49
|
+
readonly mounted: boolean;
|
|
50
|
+
} | {
|
|
51
|
+
readonly kind: 'in-use';
|
|
52
|
+
} | {
|
|
53
|
+
readonly kind: 'stale-state';
|
|
54
|
+
} | {
|
|
55
|
+
readonly kind: 'stale-mount';
|
|
56
|
+
} | {
|
|
57
|
+
readonly kind: 'image-missing';
|
|
58
|
+
};
|
|
59
|
+
/** Compares the record with what is really mounted. `live` is the number of holders that still run. */
|
|
60
|
+
export declare function diagnose(o: {
|
|
61
|
+
readonly state: SetState;
|
|
62
|
+
readonly reality: MountReality;
|
|
63
|
+
readonly live: number;
|
|
64
|
+
readonly sandboxRunning: boolean;
|
|
65
|
+
}): ContainedDiagnosis;
|
|
66
|
+
/** The best answer for "is a process alive": signal 0 only checks. */
|
|
67
|
+
export declare function processAlive(pid: number): boolean;
|
|
68
|
+
/** The state file with a lock, so two commands at once never lose each other's change. */
|
|
69
|
+
export declare class StateFile {
|
|
70
|
+
private readonly paths;
|
|
71
|
+
private readonly alive;
|
|
72
|
+
private readonly now;
|
|
73
|
+
private readonly file;
|
|
74
|
+
constructor(paths: Pick<ContainedPaths, 'stateFile' | 'lockFile' | 'mountsDir'>, alive?: (pid: number) => boolean, now?: () => number);
|
|
75
|
+
read(): ContainedState;
|
|
76
|
+
exists(): boolean;
|
|
77
|
+
/** Reads, changes and writes under the lock; returns what was written. */
|
|
78
|
+
update(change: (current: ContainedState) => ContainedState): ContainedState;
|
|
79
|
+
private locked;
|
|
80
|
+
private lockIsStale;
|
|
81
|
+
}
|
|
82
|
+
/** The data sets of a storage folder whose image is recorded as mounted (a storage move must wait for these). */
|
|
83
|
+
export declare function mountedSets(storageRoot: string): string[];
|
|
84
|
+
/**
|
|
85
|
+
* A line for the start of a command when the record says an image is mounted that no running process holds and no
|
|
86
|
+
* sandbox uses (nothing is looked up on the disk: this is only the record), or null.
|
|
87
|
+
*/
|
|
88
|
+
export declare function staleMountNote(storageRoot: string, set: string, sandboxRunning: boolean, alive?: (pid: number) => boolean): string | null;
|
|
89
|
+
export {};
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// The record of which data sets are mounted and which processes hold them, kept outside the image
|
|
2
|
+
// (<storage>\mounts\state.json): the first launch mounts, a second one that finds it mounted only adds itself, and the
|
|
3
|
+
// last one to let go dismounts. Pure transitions on a plain object, plus the file (with a lock) around them.
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
import { JsonFile } from '../../../shared/io/index.js';
|
|
8
|
+
import { SET_NAME_RULE } from './layout.js';
|
|
9
|
+
const HolderSchema = z.strictObject({
|
|
10
|
+
/** What holds the mount: "launch" while a launch starts the sandbox, "watcher" for the background process. */
|
|
11
|
+
id: z.string().regex(/^[a-z-]{1,20}$/),
|
|
12
|
+
pid: z.number().int().positive(),
|
|
13
|
+
since: z.string().max(40),
|
|
14
|
+
});
|
|
15
|
+
const SetStateSchema = z.strictObject({
|
|
16
|
+
mounted: z.boolean(),
|
|
17
|
+
holders: z.array(HolderSchema).max(32),
|
|
18
|
+
});
|
|
19
|
+
export const ContainedStateSchema = z.strictObject({
|
|
20
|
+
version: z.literal(1),
|
|
21
|
+
sets: z.record(z.string().regex(SET_NAME_RULE), SetStateSchema),
|
|
22
|
+
});
|
|
23
|
+
export const EMPTY_STATE = { version: 1, sets: {} };
|
|
24
|
+
const NONE = { mounted: false, holders: [] };
|
|
25
|
+
export function setStateOf(state, set) {
|
|
26
|
+
return state.sets[set] ?? NONE;
|
|
27
|
+
}
|
|
28
|
+
function withSet(state, set, next) {
|
|
29
|
+
return { version: 1, sets: { ...state.sets, [set]: next } };
|
|
30
|
+
}
|
|
31
|
+
/** Drops the holders whose process is gone (a launch that ended, a watcher that crashed). */
|
|
32
|
+
export function pruneHolders(state, set, alive) {
|
|
33
|
+
const current = setStateOf(state, set);
|
|
34
|
+
const holders = current.holders.filter((h) => alive(h.pid));
|
|
35
|
+
return holders.length === current.holders.length ? state : withSet(state, set, { ...current, holders });
|
|
36
|
+
}
|
|
37
|
+
/** Adds a holder (or refreshes the one with the same id). */
|
|
38
|
+
export function addHolder(state, set, holder) {
|
|
39
|
+
const current = setStateOf(state, set);
|
|
40
|
+
const holders = [...current.holders.filter((h) => h.id !== holder.id), holder];
|
|
41
|
+
return withSet(state, set, { ...current, holders });
|
|
42
|
+
}
|
|
43
|
+
export function removeHolder(state, set, id) {
|
|
44
|
+
const current = setStateOf(state, set);
|
|
45
|
+
return withSet(state, set, { ...current, holders: current.holders.filter((h) => h.id !== id) });
|
|
46
|
+
}
|
|
47
|
+
export function markMounted(state, set, mounted) {
|
|
48
|
+
return withSet(state, set, { ...setStateOf(state, set), mounted });
|
|
49
|
+
}
|
|
50
|
+
/** Compares the record with what is really mounted. `live` is the number of holders that still run. */
|
|
51
|
+
export function diagnose(o) {
|
|
52
|
+
const { state, reality } = o;
|
|
53
|
+
if (!reality.imageExists)
|
|
54
|
+
return { kind: 'image-missing' };
|
|
55
|
+
if (reality.mountIsMountPoint) {
|
|
56
|
+
if (o.live > 0 || o.sandboxRunning)
|
|
57
|
+
return { kind: 'in-use' };
|
|
58
|
+
return { kind: 'stale-mount' };
|
|
59
|
+
}
|
|
60
|
+
if (state.mounted)
|
|
61
|
+
return { kind: 'stale-state' };
|
|
62
|
+
return { kind: 'ok', mounted: false };
|
|
63
|
+
}
|
|
64
|
+
/** The best answer for "is a process alive": signal 0 only checks. */
|
|
65
|
+
export function processAlive(pid) {
|
|
66
|
+
try {
|
|
67
|
+
process.kill(pid, 0);
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
// EPERM: it exists but belongs to someone else.
|
|
72
|
+
return e.code === 'EPERM';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const LOCK_STALE_MS = 2 * 60_000;
|
|
76
|
+
const LOCK_WAIT_MS = 8_000;
|
|
77
|
+
const pause = (ms) => {
|
|
78
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
79
|
+
};
|
|
80
|
+
/** The state file with a lock, so two commands at once never lose each other's change. */
|
|
81
|
+
export class StateFile {
|
|
82
|
+
paths;
|
|
83
|
+
alive;
|
|
84
|
+
now;
|
|
85
|
+
file;
|
|
86
|
+
constructor(paths, alive = processAlive, now = Date.now) {
|
|
87
|
+
this.paths = paths;
|
|
88
|
+
this.alive = alive;
|
|
89
|
+
this.now = now;
|
|
90
|
+
this.file = new JsonFile(ContainedStateSchema, paths.stateFile);
|
|
91
|
+
}
|
|
92
|
+
read() {
|
|
93
|
+
return this.file.read() ?? EMPTY_STATE;
|
|
94
|
+
}
|
|
95
|
+
exists() {
|
|
96
|
+
return fs.existsSync(this.paths.stateFile);
|
|
97
|
+
}
|
|
98
|
+
/** Reads, changes and writes under the lock; returns what was written. */
|
|
99
|
+
update(change) {
|
|
100
|
+
return this.locked(() => {
|
|
101
|
+
const next = change(this.read());
|
|
102
|
+
this.file.write(next);
|
|
103
|
+
return next;
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
locked(work) {
|
|
107
|
+
fs.mkdirSync(this.paths.mountsDir, { recursive: true });
|
|
108
|
+
const deadline = this.now() + LOCK_WAIT_MS;
|
|
109
|
+
for (;;) {
|
|
110
|
+
try {
|
|
111
|
+
const fd = fs.openSync(this.paths.lockFile, 'wx');
|
|
112
|
+
fs.writeSync(fd, String(process.pid));
|
|
113
|
+
fs.closeSync(fd);
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
if (e.code !== 'EEXIST')
|
|
118
|
+
throw e;
|
|
119
|
+
if (this.lockIsStale()) {
|
|
120
|
+
fs.rmSync(this.paths.lockFile, { force: true });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (this.now() > deadline)
|
|
124
|
+
throw new Error('Another command is changing the contained storage record.');
|
|
125
|
+
pause(50);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
return work();
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
fs.rmSync(this.paths.lockFile, { force: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
lockIsStale() {
|
|
136
|
+
try {
|
|
137
|
+
const age = this.now() - fs.statSync(this.paths.lockFile).mtimeMs;
|
|
138
|
+
const pid = Number.parseInt(fs.readFileSync(this.paths.lockFile, 'utf8'), 10);
|
|
139
|
+
return age > LOCK_STALE_MS || (Number.isInteger(pid) && pid > 0 && !this.alive(pid));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// Gone between the check and the read: the next try finds out.
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/** The data sets of a storage folder whose image is recorded as mounted (a storage move must wait for these). */
|
|
148
|
+
export function mountedSets(storageRoot) {
|
|
149
|
+
const state = new JsonFile(ContainedStateSchema, path.join(path.resolve(storageRoot), 'mounts', 'state.json')).read();
|
|
150
|
+
return state
|
|
151
|
+
? Object.entries(state.sets)
|
|
152
|
+
.filter(([, s]) => s.mounted)
|
|
153
|
+
.map(([name]) => name)
|
|
154
|
+
: [];
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* A line for the start of a command when the record says an image is mounted that no running process holds and no
|
|
158
|
+
* sandbox uses (nothing is looked up on the disk: this is only the record), or null.
|
|
159
|
+
*/
|
|
160
|
+
export function staleMountNote(storageRoot, set, sandboxRunning, alive = processAlive) {
|
|
161
|
+
const record = setStateOf(new JsonFile(ContainedStateSchema, path.join(path.resolve(storageRoot), 'mounts', 'state.json')).read() ??
|
|
162
|
+
EMPTY_STATE, set);
|
|
163
|
+
if (!record.mounted || sandboxRunning || record.holders.some((h) => alive(h.pid)))
|
|
164
|
+
return null;
|
|
165
|
+
return 'Contained storage is still mounted from an earlier sandbox. Run sandbox repair to dismount it (Windows asks for permission).';
|
|
166
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Measured, type MoveEstimate, type MoveProgress } from '../move.js';
|
|
2
|
+
export interface TransferFolder {
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly from: string;
|
|
5
|
+
readonly to: string;
|
|
6
|
+
readonly measured: Measured;
|
|
7
|
+
}
|
|
8
|
+
export interface TransferPlan {
|
|
9
|
+
readonly folders: readonly TransferFolder[];
|
|
10
|
+
readonly total: Measured;
|
|
11
|
+
readonly estimate: MoveEstimate;
|
|
12
|
+
}
|
|
13
|
+
export interface TransferResult {
|
|
14
|
+
readonly files: number;
|
|
15
|
+
readonly bytes: number;
|
|
16
|
+
/** Folders whose original was kept (a link inside, or the folder could not be removed), with the reason. */
|
|
17
|
+
readonly notes: readonly string[];
|
|
18
|
+
}
|
|
19
|
+
/** What would move: each contained folder that exists in `fromRoot` and holds something, with its size and the time. */
|
|
20
|
+
export declare function planTransfer(fromRoot: string, toRoot: string, freeBytesRoot: string, measure?: (dir: string) => Measured): TransferPlan;
|
|
21
|
+
/** Refuses a target folder that already holds files (nothing is ever merged or overwritten). */
|
|
22
|
+
export declare function assertTargetsFree(plan: TransferPlan): void;
|
|
23
|
+
/** Copies every folder of the plan, checks sizes and counts, then removes the originals. `root` bounds the removal. */
|
|
24
|
+
export declare function runTransfer(plan: TransferPlan, root: string, onProgress?: (p: MoveProgress) => void): Promise<TransferResult>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Moving the data set's folders into the image (contained storage on) and back out to plain folders (off). The copy is
|
|
2
|
+
// verified file by file; the originals are removed only after every folder was copied completely, and a link found
|
|
3
|
+
// in a folder is never followed (that folder is then left where it is).
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { pipeline } from 'node:stream/promises';
|
|
7
|
+
import { freeBytesAt } from '../../../core/disk.js';
|
|
8
|
+
import { fail, isLink } from '../../../core/util/index.js';
|
|
9
|
+
import { safeRemove } from '../../../shared/io/index.js';
|
|
10
|
+
import { estimateMove, measureFolder } from '../move.js';
|
|
11
|
+
import { CONTAINED_FOLDERS } from './layout.js';
|
|
12
|
+
const STREAM_FROM_BYTES = 64 * 1024 * 1024;
|
|
13
|
+
const plainFolder = (dir) => {
|
|
14
|
+
try {
|
|
15
|
+
return fs.statSync(dir).isDirectory() && !isLink(dir);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const holdsFiles = (dir) => {
|
|
22
|
+
try {
|
|
23
|
+
return fs.readdirSync(dir).length > 0;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
/** What would move: each contained folder that exists in `fromRoot` and holds something, with its size and the time. */
|
|
30
|
+
export function planTransfer(fromRoot, toRoot, freeBytesRoot, measure = measureFolder) {
|
|
31
|
+
const folders = [];
|
|
32
|
+
for (const name of CONTAINED_FOLDERS) {
|
|
33
|
+
const from = path.join(fromRoot, name);
|
|
34
|
+
if (!plainFolder(from) || !holdsFiles(from))
|
|
35
|
+
continue;
|
|
36
|
+
folders.push({ name, from, to: path.join(toRoot, name), measured: measure(from) });
|
|
37
|
+
}
|
|
38
|
+
const total = {
|
|
39
|
+
bytes: folders.reduce((n, f) => n + f.measured.bytes, 0),
|
|
40
|
+
files: folders.reduce((n, f) => n + f.measured.files, 0),
|
|
41
|
+
partial: folders.some((f) => f.measured.partial),
|
|
42
|
+
};
|
|
43
|
+
return { folders, total, estimate: estimateMove(total, false, freeBytesAt(freeBytesRoot)) };
|
|
44
|
+
}
|
|
45
|
+
/** Refuses a target folder that already holds files (nothing is ever merged or overwritten). */
|
|
46
|
+
export function assertTargetsFree(plan) {
|
|
47
|
+
for (const f of plan.folders)
|
|
48
|
+
if (fs.existsSync(f.to) && holdsFiles(f.to))
|
|
49
|
+
fail(f.to + ' already holds files.', 'Move or delete them first, then run the command again.');
|
|
50
|
+
}
|
|
51
|
+
/** Copies every folder of the plan, checks sizes and counts, then removes the originals. `root` bounds the removal. */
|
|
52
|
+
export async function runTransfer(plan, root, onProgress) {
|
|
53
|
+
assertTargetsFree(plan);
|
|
54
|
+
const done = { bytes: 0, files: 0, links: 0 };
|
|
55
|
+
const linkFolders = new Set();
|
|
56
|
+
let last = 0;
|
|
57
|
+
const report = (force = false) => {
|
|
58
|
+
if (!onProgress || (!force && Date.now() - last < 200))
|
|
59
|
+
return;
|
|
60
|
+
last = Date.now();
|
|
61
|
+
onProgress({ bytes: done.bytes, files: done.files });
|
|
62
|
+
};
|
|
63
|
+
const walk = async (from, to, folder) => {
|
|
64
|
+
await fs.promises.mkdir(to, { recursive: true });
|
|
65
|
+
for (const entry of await fs.promises.readdir(from, { withFileTypes: true })) {
|
|
66
|
+
const a = path.join(from, entry.name);
|
|
67
|
+
const b = path.join(to, entry.name);
|
|
68
|
+
if (entry.isSymbolicLink()) {
|
|
69
|
+
linkFolders.add(folder);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (entry.isDirectory()) {
|
|
73
|
+
await walk(a, b, folder);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
const stat = await fs.promises.stat(a);
|
|
77
|
+
if (stat.size >= STREAM_FROM_BYTES)
|
|
78
|
+
await pipeline(fs.createReadStream(a), fs.createWriteStream(b));
|
|
79
|
+
else
|
|
80
|
+
await fs.promises.copyFile(a, b);
|
|
81
|
+
const copied = await fs.promises.stat(b);
|
|
82
|
+
if (copied.size !== stat.size)
|
|
83
|
+
fail('The copy of ' + a + ' has another size than the original.');
|
|
84
|
+
await fs.promises.utimes(b, stat.atime, stat.mtime);
|
|
85
|
+
done.bytes += stat.size;
|
|
86
|
+
done.files++;
|
|
87
|
+
report();
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
for (const f of plan.folders)
|
|
91
|
+
await walk(f.from, f.to, f.name);
|
|
92
|
+
report(true);
|
|
93
|
+
const notes = [];
|
|
94
|
+
for (const f of plan.folders) {
|
|
95
|
+
const copied = measureFolder(f.to);
|
|
96
|
+
if (!f.measured.partial && copied.files < f.measured.files)
|
|
97
|
+
fail('The copy of ' +
|
|
98
|
+
f.name +
|
|
99
|
+
' has fewer files than the original (' +
|
|
100
|
+
copied.files +
|
|
101
|
+
' of ' +
|
|
102
|
+
f.measured.files +
|
|
103
|
+
').', 'Nothing was removed. Run the command again.');
|
|
104
|
+
}
|
|
105
|
+
// Only now, with everything copied, do the originals go.
|
|
106
|
+
for (const f of plan.folders) {
|
|
107
|
+
if (linkFolders.has(f.name)) {
|
|
108
|
+
notes.push(f.from + ' has links in it, which are not copied; it was left where it is.');
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
try {
|
|
112
|
+
safeRemove(f.from, root);
|
|
113
|
+
if (fs.existsSync(f.from))
|
|
114
|
+
notes.push(f.from + ' could not be removed completely; delete it by hand.');
|
|
115
|
+
}
|
|
116
|
+
catch (e) {
|
|
117
|
+
notes.push(f.from + ' could not be removed (' + (e instanceof Error ? e.message : String(e)) + ').');
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return { files: done.files, bytes: done.bytes, notes };
|
|
121
|
+
}
|