rightsize 0.7.8 → 0.7.10
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/dist/backend-docker/backend.d.ts +4 -1
- package/dist/backend-docker/backend.d.ts.map +1 -1
- package/dist/backend-docker/backend.js +4 -0
- package/dist/backend-docker/backend.js.map +1 -1
- package/dist/backend-msb/backend.d.ts +618 -68
- package/dist/backend-msb/backend.d.ts.map +1 -1
- package/dist/backend-msb/backend.js +1236 -133
- package/dist/backend-msb/backend.js.map +1 -1
- package/dist/backend-msb/commands.d.ts +160 -16
- package/dist/backend-msb/commands.d.ts.map +1 -1
- package/dist/backend-msb/commands.js +197 -31
- package/dist/backend-msb/commands.js.map +1 -1
- package/dist/backend-msb/provisioner.d.ts +1 -1
- package/dist/backend-msb/provisioner.d.ts.map +1 -1
- package/dist/backend-msb/provisioner.js +1 -1
- package/dist/backend-msb/provisioner.js.map +1 -1
- package/dist/backend-msb/restore-access-denied.d.ts +50 -0
- package/dist/backend-msb/restore-access-denied.d.ts.map +1 -0
- package/dist/backend-msb/restore-access-denied.js +52 -0
- package/dist/backend-msb/restore-access-denied.js.map +1 -0
- package/dist/backend-msb/restore-broker.d.ts +181 -0
- package/dist/backend-msb/restore-broker.d.ts.map +1 -0
- package/dist/backend-msb/restore-broker.js +264 -0
- package/dist/backend-msb/restore-broker.js.map +1 -0
- package/dist/backend-msb/sandbox-already-exists.d.ts +32 -0
- package/dist/backend-msb/sandbox-already-exists.d.ts.map +1 -0
- package/dist/backend-msb/sandbox-already-exists.js +35 -0
- package/dist/backend-msb/sandbox-already-exists.js.map +1 -0
- package/dist/backend-msb/snapshot-create.d.ts +22 -0
- package/dist/backend-msb/snapshot-create.d.ts.map +1 -0
- package/dist/backend-msb/snapshot-create.js +33 -0
- package/dist/backend-msb/snapshot-create.js.map +1 -0
- package/dist/backend-msb/snapshot-import.d.ts +30 -14
- package/dist/backend-msb/snapshot-import.d.ts.map +1 -1
- package/dist/backend-msb/snapshot-import.js +35 -17
- package/dist/backend-msb/snapshot-import.js.map +1 -1
- package/dist/backend-msb/snapshot-rm.d.ts +28 -0
- package/dist/backend-msb/snapshot-rm.d.ts.map +1 -0
- package/dist/backend-msb/snapshot-rm.js +30 -0
- package/dist/backend-msb/snapshot-rm.js.map +1 -0
- package/dist/core/backend.d.ts +53 -22
- package/dist/core/backend.d.ts.map +1 -1
- package/dist/core/checkpoint/api.d.ts +2 -2
- package/dist/core/checkpoint/api.js +2 -2
- package/dist/core/checkpoint/registry.d.ts +34 -0
- package/dist/core/checkpoint/registry.d.ts.map +1 -1
- package/dist/core/checkpoint/registry.js +20 -1
- package/dist/core/checkpoint/registry.js.map +1 -1
- package/dist/core/errors.d.ts +49 -0
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +61 -0
- package/dist/core/errors.js.map +1 -1
- package/dist/core/generic-container.d.ts +58 -10
- package/dist/core/generic-container.d.ts.map +1 -1
- package/dist/core/generic-container.js +222 -27
- package/dist/core/generic-container.js.map +1 -1
- package/dist/core/model.d.ts +3 -2
- package/dist/core/model.d.ts.map +1 -1
- package/dist/core/sandbox-name.d.ts +3 -0
- package/dist/core/sandbox-name.d.ts.map +1 -0
- package/dist/core/sandbox-name.js +19 -0
- package/dist/core/sandbox-name.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/modules/minio.d.ts +5 -3
- package/dist/modules/minio.d.ts.map +1 -1
- package/dist/modules/minio.js +14 -4
- package/dist/modules/minio.js.map +1 -1
- package/package.json +1 -1
- package/dist/backend-msb/snapshot-list.d.ts +0 -43
- package/dist/backend-msb/snapshot-list.d.ts.map +0 -1
- package/dist/backend-msb/snapshot-list.js +0 -53
- package/dist/backend-msb/snapshot-list.js.map +0 -1
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Windows-only job-object-breakaway problem this whole module exists to
|
|
3
|
+
* route around. EMPIRICALLY VERIFIED across a four-round live diagnostic
|
|
4
|
+
* campaign on Windows CI (2026-09-18): msb's detached spawn on Windows
|
|
5
|
+
* (`sdk/rust/lib/runtime/spawn.rs`, msb 0.7.1/0.7.2) always passes
|
|
6
|
+
* `DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP | CREATE_BREAKAWAY_FROM_JOB`
|
|
7
|
+
* when it launches the new `msb.exe` that supervises a restored sandbox —
|
|
8
|
+
* `msb restore` is detached-by-design (see `MsbCliBackend.bootRestoreOnce`'s
|
|
9
|
+
* own doc), so it must spawn that new process every time. When the CALLING
|
|
10
|
+
* `msb.exe` itself sits inside a Windows job object that was never granted
|
|
11
|
+
* `JOB_OBJECT_LIMIT_BREAKAWAY_OK`, `CreateProcess` fails outright with
|
|
12
|
+
* `ERROR_ACCESS_DENIED`, and msb surfaces the bare `error: io error: Access
|
|
13
|
+
* is denied. (os error 5)` — the exact signature `isRestoreAccessDeniedFailure`
|
|
14
|
+
* matches, previously attributed entirely to a Windows file-handle release
|
|
15
|
+
* lag. Direct probes at failure time ruled the lag theory out: no msb.exe
|
|
16
|
+
* processes alive, the artifact readable/writable/hardlinkable from another
|
|
17
|
+
* process, nothing locked — the denial is the spawn itself, not the file.
|
|
18
|
+
*
|
|
19
|
+
* Gradle test workers and cargo-test binaries run inside exactly such
|
|
20
|
+
* breakaway-denying job objects (`inJob=True, limitFlags=0x0`, live-probed),
|
|
21
|
+
* which is why the sibling rust/kotlin libraries hit this deterministically
|
|
22
|
+
* on their own Windows CI while this repository's node test workers —
|
|
23
|
+
* confirmed to add no such job object — never have.
|
|
24
|
+
*
|
|
25
|
+
* THE MITIGATION IS LIVE-VALIDATED: the identical `msb restore <artifact>
|
|
26
|
+
* --name X` launched through WMI (`Invoke-CimMethod -ClassName Win32_Process
|
|
27
|
+
* -MethodName Create`), whose children run under `WmiPrvSE` OUTSIDE the
|
|
28
|
+
* caller's job hierarchy, returns `ReturnValue=0` and the restored sandbox
|
|
29
|
+
* shows Running in `msb ls` — reproduced in two separate cycles in the exact
|
|
30
|
+
* environment where a direct spawn is denied every time.
|
|
31
|
+
*
|
|
32
|
+
* This module is the resulting escape hatch: `realRestoreBroker` shells the
|
|
33
|
+
* exact same `msb restore ...` invocation through a WMI-created process
|
|
34
|
+
* instead of a direct child, for the callers in `backend.ts`
|
|
35
|
+
* (`rebootUnderFreshName`/`retryRestoreAfterAccessDenied`) that escalate to
|
|
36
|
+
* it once — and only once — a restore attempt on a Windows host hits that
|
|
37
|
+
* classified access-denied failure (POLICY v2; see those methods' own doc).
|
|
38
|
+
* It changes nothing for a healthy environment: the first attempt of any
|
|
39
|
+
* reboot is always a direct spawn, and a host that never hits the access-
|
|
40
|
+
* denied signature never touches this module at all.
|
|
41
|
+
*/
|
|
42
|
+
/** One brokered restore attempt whose own outcome was captured — classify `output` with the exact same predicates a direct attempt's output goes through. */
|
|
43
|
+
export interface RestoreBrokerCompleted {
|
|
44
|
+
kind: "completed";
|
|
45
|
+
exitCode: number;
|
|
46
|
+
output: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The broker's own `ecFile` (see `buildOuterBrokerScript`) never appeared
|
|
50
|
+
* within its bounded wait, so no exit code/output could be read back — but
|
|
51
|
+
* the WMI `Create` call itself was accepted (`ReturnValue=0`), so the
|
|
52
|
+
* restore may well be running in the background regardless. The caller
|
|
53
|
+
* (`MsbCliBackend.bootRestoreOnce`) treats this exactly like a confirmed
|
|
54
|
+
* exit 0: skip classification and fall through to its own `msb ls`
|
|
55
|
+
* readiness poll, which — being activation-gated — settles the question on
|
|
56
|
+
* its own either way.
|
|
57
|
+
*/
|
|
58
|
+
export interface RestoreBrokerUnconfirmed {
|
|
59
|
+
kind: "unconfirmed";
|
|
60
|
+
}
|
|
61
|
+
export type RestoreBrokerResult = RestoreBrokerCompleted | RestoreBrokerUnconfirmed;
|
|
62
|
+
/**
|
|
63
|
+
* The injectable seam `MsbCliBackend` calls through for every brokered
|
|
64
|
+
* restore attempt — `msbPath`/`argv` are exactly what a direct attempt would
|
|
65
|
+
* spawn (`MsbCommands.restore(spec)`'s own argv, unmodified), `timeoutMs` the
|
|
66
|
+
* same boot budget a direct attempt is held to. Rejects only for a broker-
|
|
67
|
+
* INFRASTRUCTURE failure (missing `powershell.exe`, a script-file write
|
|
68
|
+
* failure, WMI refusing to even create the process) — the caller falls back
|
|
69
|
+
* to a direct attempt on a rejection (POLICY v2 point 5: the broker must
|
|
70
|
+
* never become a new single point of failure). Resolves for every other
|
|
71
|
+
* outcome, including the brokered restore itself failing (access-denied
|
|
72
|
+
* again, an "already exists" collision, or any other msb refusal) — that is
|
|
73
|
+
* ordinary classified output for the caller to interpret, not a broker
|
|
74
|
+
* failure.
|
|
75
|
+
*/
|
|
76
|
+
export type RestoreBrokerLauncher = (msbPath: string, argv: readonly string[], timeoutMs: number) => Promise<RestoreBrokerResult>;
|
|
77
|
+
/**
|
|
78
|
+
* Doubles every embedded `'` so `value` can be dropped verbatim into a
|
|
79
|
+
* PowerShell single-quoted string literal (`'...'`) — PowerShell's own
|
|
80
|
+
* escaping rule for that quoting style, the same one a hand-written `''`
|
|
81
|
+
* inside a single-quoted string represents a literal `'`. Applied to every
|
|
82
|
+
* interpolated value this module builds a script around (the msb binary
|
|
83
|
+
* path, every restore argv token, and the temp out/ec file paths) — never
|
|
84
|
+
* only the ones expected to need it, since a caller-controlled mount path or
|
|
85
|
+
* artifact ref is exactly the kind of value that might one day carry one.
|
|
86
|
+
*/
|
|
87
|
+
export declare function escapePowerShellSingleQuoted(value: string): string;
|
|
88
|
+
/**
|
|
89
|
+
* The script text the WMI-created powershell process itself runs: the real
|
|
90
|
+
* `msb <argv>` invocation (the call operator `&`, since `msbPath` is a
|
|
91
|
+
* runtime string, not a literal command name), its combined stdout+stderr
|
|
92
|
+
* merged into `outFile` via PowerShell's `*>` all-streams redirection —
|
|
93
|
+
* mirroring `fetchStdoutExact`'s own reason for treating stdout+stderr as
|
|
94
|
+
* one channel — followed by writing the invocation's own `$LASTEXITCODE` to
|
|
95
|
+
* `ecFile`. Both files are how this reaches back across the WMI boundary:
|
|
96
|
+
* `Invoke-CimMethod`'s `Create` has no stdout/exit-code channel of its own
|
|
97
|
+
* (it returns only a `ProcessId` and a `ReturnValue` for the CREATE call
|
|
98
|
+
* itself, never anything about what that process goes on to do). Every path
|
|
99
|
+
* and argv token is its own single-quoted literal (`quoteSingle`) — never
|
|
100
|
+
* string-interpolated — so a value carrying spaces, `$`, or a literal `'`
|
|
101
|
+
* (an exotic mount path, in principle) round-trips as data, not script
|
|
102
|
+
* syntax.
|
|
103
|
+
*/
|
|
104
|
+
export declare function buildInnerRestoreScript(msbPath: string, argv: readonly string[], outFile: string, ecFile: string): string;
|
|
105
|
+
/**
|
|
106
|
+
* Encodes `script` the way `powershell -EncodedCommand` requires: UTF-16LE
|
|
107
|
+
* bytes, base64. Chosen over `-Command "<script>"` specifically to sidestep
|
|
108
|
+
* a second layer of quote-escaping entirely — `buildInnerRestoreScript`'s
|
|
109
|
+
* own output already carries single-quoted literals of its own, and nesting
|
|
110
|
+
* that inside ANOTHER quoted string (the `CommandLine` this module hands to
|
|
111
|
+
* `Invoke-CimMethod`) would mean escaping quotes-within-quotes-within-quotes.
|
|
112
|
+
* A base64 payload is alphanumeric-plus-`+/=` only, so embedding it inside
|
|
113
|
+
* the OUTER script's own single-quoted `CommandLine` literal (see
|
|
114
|
+
* `buildOuterBrokerScript`) needs no escaping of its own at all.
|
|
115
|
+
*/
|
|
116
|
+
export declare function encodeAsEncodedCommand(script: string): string;
|
|
117
|
+
/** How long `buildOuterBrokerScript`'s own polling loop waits for `ecFile` to appear before giving up and letting the caller fall through to its own `msb ls` readiness poll (see `RestoreBrokerUnconfirmed`'s own doc) — generous for `msb restore`'s own detach, which the rest of this codebase documents as "typically within seconds". */
|
|
118
|
+
export declare const RESTORE_BROKER_ECFILE_WAIT_SECONDS = 30;
|
|
119
|
+
/**
|
|
120
|
+
* The OUTER script — the one actually run as `powershell -NoProfile -File
|
|
121
|
+
* <script>` (see `realRestoreBroker`) — that performs the WMI escape itself
|
|
122
|
+
* and reports back what happened, all through plain stdout this module's own
|
|
123
|
+
* node-side caller can read directly (no temp file needed for THIS layer,
|
|
124
|
+
* unlike the inner restore's own output — this process's stdout pipe is
|
|
125
|
+
* ordinary, since it is never the one msb's job object would deny).
|
|
126
|
+
*
|
|
127
|
+
* `encodedInner` (see `encodeAsEncodedCommand`) is embedded inside this
|
|
128
|
+
* script's own single-quoted `$cmdLine` literal — safe with no escaping of
|
|
129
|
+
* its own, being pure base64 — as the `-EncodedCommand` argument of the
|
|
130
|
+
* powershell invocation `Invoke-CimMethod` launches. `outFile`/`ecFile` are
|
|
131
|
+
* each their own single-quoted literal via `quoteSingle`, the same as every
|
|
132
|
+
* path `buildInnerRestoreScript` embeds.
|
|
133
|
+
*
|
|
134
|
+
* `powershell.exe` (never the bare `powershell`, nor `pwsh`) both here and as
|
|
135
|
+
* the WMI-launched process's own executable — the task this whole module
|
|
136
|
+
* exists for names it explicitly "for maximum compatibility": every
|
|
137
|
+
* supported Windows release ships Windows PowerShell under that exact name,
|
|
138
|
+
* while PowerShell 7 (`pwsh`) is an opt-in install this backend has no
|
|
139
|
+
* business depending on.
|
|
140
|
+
*/
|
|
141
|
+
export declare function buildOuterBrokerScript(params: {
|
|
142
|
+
encodedInner: string;
|
|
143
|
+
outFile: string;
|
|
144
|
+
ecFile: string;
|
|
145
|
+
waitSeconds?: number;
|
|
146
|
+
}): string;
|
|
147
|
+
export interface ParsedBrokerScriptOutput {
|
|
148
|
+
/** The WMI `Create` call's own `ReturnValue` (0 == accepted), or `undefined` if the marker line itself never showed up (the outer script died before printing it). */
|
|
149
|
+
returnValue: number | undefined;
|
|
150
|
+
/** The WMI-created process's own pid, purely diagnostic — never load-bearing for classification. */
|
|
151
|
+
processId: number | undefined;
|
|
152
|
+
/** `outFile`'s contents (the brokered `msb restore`'s combined stdout+stderr), or `undefined` if `outFile` never existed when the outer script checked. */
|
|
153
|
+
outText: string | undefined;
|
|
154
|
+
/** `ecFile`'s contents (the brokered invocation's own `$LASTEXITCODE`, as text), or `undefined` if it never appeared within the wait bound. */
|
|
155
|
+
ecText: string | undefined;
|
|
156
|
+
}
|
|
157
|
+
/** Parses `buildOuterBrokerScript`'s own stdout back into its four parts — see `ParsedBrokerScriptOutput`'s own doc on each field. Tolerant of CRLF (PowerShell's own native line ending) and of any marker being absent, since the outer script can die (or its own wait bound expire) after printing only some of them. */
|
|
158
|
+
export declare function parseBrokerScriptOutput(stdout: string): ParsedBrokerScriptOutput;
|
|
159
|
+
/**
|
|
160
|
+
* The real, production `RestoreBrokerLauncher` — writes the outer script
|
|
161
|
+
* (see `buildOuterBrokerScript`) to a unique temp file, runs it via
|
|
162
|
+
* `powershell.exe -NoProfile -NonInteractive -File <script>`, and turns its
|
|
163
|
+
* parsed output (`parseBrokerScriptOutput`) into a `RestoreBrokerResult`:
|
|
164
|
+
*
|
|
165
|
+
* - WMI's own `ReturnValue` missing or non-zero (the process was never even
|
|
166
|
+
* created) throws — broker-infrastructure failure, per this function's own
|
|
167
|
+
* `RestoreBrokerLauncher` contract, for the caller to catch and fall back
|
|
168
|
+
* to a direct attempt.
|
|
169
|
+
* - `ecFile` never read back (`ecText === undefined`, or unparsable as an
|
|
170
|
+
* integer) resolves `{ kind: "unconfirmed" }` — see that type's own doc.
|
|
171
|
+
* - Otherwise resolves `{ kind: "completed", exitCode, output: outText ?? ""
|
|
172
|
+
* }` — the brokered restore's own result, for the caller to classify
|
|
173
|
+
* exactly like a direct attempt's.
|
|
174
|
+
*
|
|
175
|
+
* Every temp file (script, `outFile`, `ecFile`) is unique per call
|
|
176
|
+
* (`randomBytes`-suffixed, under `os.tmpdir()`) and best-effort removed in a
|
|
177
|
+
* `finally` — cleanup failures are swallowed, never allowed to mask the real
|
|
178
|
+
* outcome above.
|
|
179
|
+
*/
|
|
180
|
+
export declare const realRestoreBroker: RestoreBrokerLauncher;
|
|
181
|
+
//# sourceMappingURL=restore-broker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"restore-broker.d.ts","sourceRoot":"","sources":["../../src/backend-msb/restore-broker.ts"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,6JAA6J;AAC7J,MAAM,WAAW,sBAAsB;IACrC,IAAI,EAAE,WAAW,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,wBAAwB;IACvC,IAAI,EAAE,aAAa,CAAC;CACrB;AAED,MAAM,MAAM,mBAAmB,GAAG,sBAAsB,GAAG,wBAAwB,CAAC;AAEpF;;;;;;;;;;;;;GAaG;AACH,MAAM,MAAM,qBAAqB,GAAG,CAClC,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,SAAS,EAAE,MAAM,KACd,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAElC;;;;;;;;;GASG;AACH,wBAAgB,4BAA4B,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAElE;AAOD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAMzH;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE7D;AAED,+UAA+U;AAC/U,eAAO,MAAM,kCAAkC,KAAK,CAAC;AASrD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE;IAC7C,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,MAAM,CAwBT;AAED,MAAM,WAAW,wBAAwB;IACvC,sKAAsK;IACtK,WAAW,EAAE,MAAM,GAAG,SAAS,CAAC;IAChC,oGAAoG;IACpG,SAAS,EAAE,MAAM,GAAG,SAAS,CAAC;IAC9B,2JAA2J;IAC3J,OAAO,EAAE,MAAM,GAAG,SAAS,CAAC;IAC5B,+IAA+I;IAC/I,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAkCD,6TAA6T;AAC7T,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,wBAAwB,CAShF;AAoDD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,eAAO,MAAM,iBAAiB,EAAE,qBA2C/B,CAAC"}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import * as fs from "node:fs/promises";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import { CLOSED_STDIN } from "./invoke.js";
|
|
7
|
+
/**
|
|
8
|
+
* Doubles every embedded `'` so `value` can be dropped verbatim into a
|
|
9
|
+
* PowerShell single-quoted string literal (`'...'`) — PowerShell's own
|
|
10
|
+
* escaping rule for that quoting style, the same one a hand-written `''`
|
|
11
|
+
* inside a single-quoted string represents a literal `'`. Applied to every
|
|
12
|
+
* interpolated value this module builds a script around (the msb binary
|
|
13
|
+
* path, every restore argv token, and the temp out/ec file paths) — never
|
|
14
|
+
* only the ones expected to need it, since a caller-controlled mount path or
|
|
15
|
+
* artifact ref is exactly the kind of value that might one day carry one.
|
|
16
|
+
*/
|
|
17
|
+
export function escapePowerShellSingleQuoted(value) {
|
|
18
|
+
return value.replace(/'/g, "''");
|
|
19
|
+
}
|
|
20
|
+
/** `value` wrapped as its own single-quoted PowerShell literal (see `escapePowerShellSingleQuoted`). */
|
|
21
|
+
function quoteSingle(value) {
|
|
22
|
+
return `'${escapePowerShellSingleQuoted(value)}'`;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* The script text the WMI-created powershell process itself runs: the real
|
|
26
|
+
* `msb <argv>` invocation (the call operator `&`, since `msbPath` is a
|
|
27
|
+
* runtime string, not a literal command name), its combined stdout+stderr
|
|
28
|
+
* merged into `outFile` via PowerShell's `*>` all-streams redirection —
|
|
29
|
+
* mirroring `fetchStdoutExact`'s own reason for treating stdout+stderr as
|
|
30
|
+
* one channel — followed by writing the invocation's own `$LASTEXITCODE` to
|
|
31
|
+
* `ecFile`. Both files are how this reaches back across the WMI boundary:
|
|
32
|
+
* `Invoke-CimMethod`'s `Create` has no stdout/exit-code channel of its own
|
|
33
|
+
* (it returns only a `ProcessId` and a `ReturnValue` for the CREATE call
|
|
34
|
+
* itself, never anything about what that process goes on to do). Every path
|
|
35
|
+
* and argv token is its own single-quoted literal (`quoteSingle`) — never
|
|
36
|
+
* string-interpolated — so a value carrying spaces, `$`, or a literal `'`
|
|
37
|
+
* (an exotic mount path, in principle) round-trips as data, not script
|
|
38
|
+
* syntax.
|
|
39
|
+
*/
|
|
40
|
+
export function buildInnerRestoreScript(msbPath, argv, outFile, ecFile) {
|
|
41
|
+
const quotedArgv = argv.map(quoteSingle).join(" ");
|
|
42
|
+
return [
|
|
43
|
+
`& ${quoteSingle(msbPath)} ${quotedArgv} *> ${quoteSingle(outFile)}`,
|
|
44
|
+
`Set-Content -LiteralPath ${quoteSingle(ecFile)} -Value $LASTEXITCODE -NoNewline`,
|
|
45
|
+
].join("\n");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Encodes `script` the way `powershell -EncodedCommand` requires: UTF-16LE
|
|
49
|
+
* bytes, base64. Chosen over `-Command "<script>"` specifically to sidestep
|
|
50
|
+
* a second layer of quote-escaping entirely — `buildInnerRestoreScript`'s
|
|
51
|
+
* own output already carries single-quoted literals of its own, and nesting
|
|
52
|
+
* that inside ANOTHER quoted string (the `CommandLine` this module hands to
|
|
53
|
+
* `Invoke-CimMethod`) would mean escaping quotes-within-quotes-within-quotes.
|
|
54
|
+
* A base64 payload is alphanumeric-plus-`+/=` only, so embedding it inside
|
|
55
|
+
* the OUTER script's own single-quoted `CommandLine` literal (see
|
|
56
|
+
* `buildOuterBrokerScript`) needs no escaping of its own at all.
|
|
57
|
+
*/
|
|
58
|
+
export function encodeAsEncodedCommand(script) {
|
|
59
|
+
return Buffer.from(script, "utf16le").toString("base64");
|
|
60
|
+
}
|
|
61
|
+
/** How long `buildOuterBrokerScript`'s own polling loop waits for `ecFile` to appear before giving up and letting the caller fall through to its own `msb ls` readiness poll (see `RestoreBrokerUnconfirmed`'s own doc) — generous for `msb restore`'s own detach, which the rest of this codebase documents as "typically within seconds". */
|
|
62
|
+
export const RESTORE_BROKER_ECFILE_WAIT_SECONDS = 30;
|
|
63
|
+
const RETURN_VALUE_PREFIX = "RIGHTSIZE-BROKER-RETURNVALUE=";
|
|
64
|
+
const PROCESS_ID_PREFIX = "RIGHTSIZE-BROKER-PROCESSID=";
|
|
65
|
+
const OUT_BEGIN = "RIGHTSIZE-BROKER-OUT-BEGIN";
|
|
66
|
+
const OUT_END = "RIGHTSIZE-BROKER-OUT-END";
|
|
67
|
+
const EC_BEGIN = "RIGHTSIZE-BROKER-EC-BEGIN";
|
|
68
|
+
const EC_END = "RIGHTSIZE-BROKER-EC-END";
|
|
69
|
+
/**
|
|
70
|
+
* The OUTER script — the one actually run as `powershell -NoProfile -File
|
|
71
|
+
* <script>` (see `realRestoreBroker`) — that performs the WMI escape itself
|
|
72
|
+
* and reports back what happened, all through plain stdout this module's own
|
|
73
|
+
* node-side caller can read directly (no temp file needed for THIS layer,
|
|
74
|
+
* unlike the inner restore's own output — this process's stdout pipe is
|
|
75
|
+
* ordinary, since it is never the one msb's job object would deny).
|
|
76
|
+
*
|
|
77
|
+
* `encodedInner` (see `encodeAsEncodedCommand`) is embedded inside this
|
|
78
|
+
* script's own single-quoted `$cmdLine` literal — safe with no escaping of
|
|
79
|
+
* its own, being pure base64 — as the `-EncodedCommand` argument of the
|
|
80
|
+
* powershell invocation `Invoke-CimMethod` launches. `outFile`/`ecFile` are
|
|
81
|
+
* each their own single-quoted literal via `quoteSingle`, the same as every
|
|
82
|
+
* path `buildInnerRestoreScript` embeds.
|
|
83
|
+
*
|
|
84
|
+
* `powershell.exe` (never the bare `powershell`, nor `pwsh`) both here and as
|
|
85
|
+
* the WMI-launched process's own executable — the task this whole module
|
|
86
|
+
* exists for names it explicitly "for maximum compatibility": every
|
|
87
|
+
* supported Windows release ships Windows PowerShell under that exact name,
|
|
88
|
+
* while PowerShell 7 (`pwsh`) is an opt-in install this backend has no
|
|
89
|
+
* business depending on.
|
|
90
|
+
*/
|
|
91
|
+
export function buildOuterBrokerScript(params) {
|
|
92
|
+
const { encodedInner, outFile, ecFile } = params;
|
|
93
|
+
const waitSeconds = params.waitSeconds ?? RESTORE_BROKER_ECFILE_WAIT_SECONDS;
|
|
94
|
+
return [
|
|
95
|
+
`$ErrorActionPreference = 'Stop'`,
|
|
96
|
+
`$cmdLine = 'powershell.exe -NoProfile -NonInteractive -WindowStyle Hidden -EncodedCommand ${encodedInner}'`,
|
|
97
|
+
`$create = Invoke-CimMethod -ClassName Win32_Process -MethodName Create -Arguments @{ CommandLine = $cmdLine }`,
|
|
98
|
+
`Write-Output "${RETURN_VALUE_PREFIX}$($create.ReturnValue)"`,
|
|
99
|
+
`Write-Output "${PROCESS_ID_PREFIX}$($create.ProcessId)"`,
|
|
100
|
+
`$deadline = (Get-Date).AddSeconds(${waitSeconds})`,
|
|
101
|
+
`while (-not (Test-Path -LiteralPath ${quoteSingle(ecFile)}) -and (Get-Date) -lt $deadline) {`,
|
|
102
|
+
` Start-Sleep -Milliseconds 250`,
|
|
103
|
+
`}`,
|
|
104
|
+
`if (Test-Path -LiteralPath ${quoteSingle(outFile)}) {`,
|
|
105
|
+
` Write-Output '${OUT_BEGIN}'`,
|
|
106
|
+
` Get-Content -LiteralPath ${quoteSingle(outFile)} -Raw`,
|
|
107
|
+
` Write-Output '${OUT_END}'`,
|
|
108
|
+
`}`,
|
|
109
|
+
`if (Test-Path -LiteralPath ${quoteSingle(ecFile)}) {`,
|
|
110
|
+
` Write-Output '${EC_BEGIN}'`,
|
|
111
|
+
` Get-Content -LiteralPath ${quoteSingle(ecFile)} -Raw`,
|
|
112
|
+
` Write-Output '${EC_END}'`,
|
|
113
|
+
`}`,
|
|
114
|
+
].join("\n");
|
|
115
|
+
}
|
|
116
|
+
function extractPrefixedInt(text, prefix) {
|
|
117
|
+
for (const line of text.split("\n")) {
|
|
118
|
+
const trimmed = line.trim();
|
|
119
|
+
if (trimmed.startsWith(prefix)) {
|
|
120
|
+
const parsed = Number.parseInt(trimmed.slice(prefix.length).trim(), 10);
|
|
121
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
function extractBetween(text, beginMarker, endMarker) {
|
|
127
|
+
const beginIdx = text.indexOf(beginMarker);
|
|
128
|
+
if (beginIdx === -1) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
const contentStart = text.indexOf("\n", beginIdx);
|
|
132
|
+
if (contentStart === -1) {
|
|
133
|
+
return undefined;
|
|
134
|
+
}
|
|
135
|
+
const endIdx = text.indexOf(endMarker, contentStart);
|
|
136
|
+
if (endIdx === -1) {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
const raw = text.slice(contentStart + 1, endIdx);
|
|
140
|
+
// The captured content always carries exactly one trailing "\n" of its
|
|
141
|
+
// own — the line break between the last content line and the end
|
|
142
|
+
// marker's own line — which is an artifact of this scan, never part of
|
|
143
|
+
// what `Write-Output`/`Get-Content -Raw` actually produced; strip it.
|
|
144
|
+
return raw.endsWith("\n") ? raw.slice(0, -1) : raw;
|
|
145
|
+
}
|
|
146
|
+
/** Parses `buildOuterBrokerScript`'s own stdout back into its four parts — see `ParsedBrokerScriptOutput`'s own doc on each field. Tolerant of CRLF (PowerShell's own native line ending) and of any marker being absent, since the outer script can die (or its own wait bound expire) after printing only some of them. */
|
|
147
|
+
export function parseBrokerScriptOutput(stdout) {
|
|
148
|
+
const normalized = stdout.replace(/\r\n/g, "\n");
|
|
149
|
+
const ecText = extractBetween(normalized, EC_BEGIN, EC_END);
|
|
150
|
+
return {
|
|
151
|
+
returnValue: extractPrefixedInt(normalized, RETURN_VALUE_PREFIX),
|
|
152
|
+
processId: extractPrefixedInt(normalized, PROCESS_ID_PREFIX),
|
|
153
|
+
outText: extractBetween(normalized, OUT_BEGIN, OUT_END),
|
|
154
|
+
ecText: ecText?.trim(),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function runPowerShellFile(scriptFile, timeoutMs) {
|
|
158
|
+
return new Promise((resolveRun, rejectRun) => {
|
|
159
|
+
const child = spawn("powershell.exe", ["-NoProfile", "-NonInteractive", "-File", scriptFile], {
|
|
160
|
+
stdio: [CLOSED_STDIN, "pipe", "pipe"],
|
|
161
|
+
});
|
|
162
|
+
let stdout = "";
|
|
163
|
+
let stderr = "";
|
|
164
|
+
child.stdout.setEncoding("utf8");
|
|
165
|
+
child.stderr.setEncoding("utf8");
|
|
166
|
+
child.stdout.on("data", (chunk) => {
|
|
167
|
+
stdout += chunk;
|
|
168
|
+
});
|
|
169
|
+
child.stderr.on("data", (chunk) => {
|
|
170
|
+
stderr += chunk;
|
|
171
|
+
});
|
|
172
|
+
let settled = false;
|
|
173
|
+
const timer = setTimeout(() => {
|
|
174
|
+
if (settled) {
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
settled = true;
|
|
178
|
+
child.kill("SIGKILL");
|
|
179
|
+
rejectRun(new Error(`restore broker: its own powershell.exe driver did not exit within ${timeoutMs}ms`));
|
|
180
|
+
}, timeoutMs);
|
|
181
|
+
child.once("error", (err) => {
|
|
182
|
+
if (settled) {
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
settled = true;
|
|
186
|
+
clearTimeout(timer);
|
|
187
|
+
rejectRun(new Error(`restore broker: failed to spawn powershell.exe: ${err.message}`));
|
|
188
|
+
});
|
|
189
|
+
child.once("close", () => {
|
|
190
|
+
if (settled) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
settled = true;
|
|
194
|
+
clearTimeout(timer);
|
|
195
|
+
// Combined for the same reason `fetchStdoutExact`'s sibling calls
|
|
196
|
+
// treat stdout/stderr as one channel elsewhere in this backend: the
|
|
197
|
+
// outer script's own diagnostics (a PowerShell terminating error,
|
|
198
|
+
// say) could land on either.
|
|
199
|
+
resolveRun({ stdout: stderr.length > 0 ? `${stdout}\n${stderr}` : stdout });
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* The real, production `RestoreBrokerLauncher` — writes the outer script
|
|
205
|
+
* (see `buildOuterBrokerScript`) to a unique temp file, runs it via
|
|
206
|
+
* `powershell.exe -NoProfile -NonInteractive -File <script>`, and turns its
|
|
207
|
+
* parsed output (`parseBrokerScriptOutput`) into a `RestoreBrokerResult`:
|
|
208
|
+
*
|
|
209
|
+
* - WMI's own `ReturnValue` missing or non-zero (the process was never even
|
|
210
|
+
* created) throws — broker-infrastructure failure, per this function's own
|
|
211
|
+
* `RestoreBrokerLauncher` contract, for the caller to catch and fall back
|
|
212
|
+
* to a direct attempt.
|
|
213
|
+
* - `ecFile` never read back (`ecText === undefined`, or unparsable as an
|
|
214
|
+
* integer) resolves `{ kind: "unconfirmed" }` — see that type's own doc.
|
|
215
|
+
* - Otherwise resolves `{ kind: "completed", exitCode, output: outText ?? ""
|
|
216
|
+
* }` — the brokered restore's own result, for the caller to classify
|
|
217
|
+
* exactly like a direct attempt's.
|
|
218
|
+
*
|
|
219
|
+
* Every temp file (script, `outFile`, `ecFile`) is unique per call
|
|
220
|
+
* (`randomBytes`-suffixed, under `os.tmpdir()`) and best-effort removed in a
|
|
221
|
+
* `finally` — cleanup failures are swallowed, never allowed to mask the real
|
|
222
|
+
* outcome above.
|
|
223
|
+
*/
|
|
224
|
+
export const realRestoreBroker = async (msbPath, argv, timeoutMs) => {
|
|
225
|
+
const unique = `rightsize-restore-broker-${process.pid}-${randomBytes(6).toString("hex")}`;
|
|
226
|
+
const base = path.join(os.tmpdir(), unique);
|
|
227
|
+
const scriptFile = `${base}.ps1`;
|
|
228
|
+
const outFile = `${base}.out.txt`;
|
|
229
|
+
const ecFile = `${base}.ec.txt`;
|
|
230
|
+
const inner = buildInnerRestoreScript(msbPath, argv, outFile, ecFile);
|
|
231
|
+
const outer = buildOuterBrokerScript({ encodedInner: encodeAsEncodedCommand(inner), outFile, ecFile });
|
|
232
|
+
try {
|
|
233
|
+
try {
|
|
234
|
+
await fs.writeFile(scriptFile, outer, "utf8");
|
|
235
|
+
}
|
|
236
|
+
catch (err) {
|
|
237
|
+
throw new Error(`restore broker: failed to write its own script file '${scriptFile}': ${err instanceof Error ? err.message : String(err)}`);
|
|
238
|
+
}
|
|
239
|
+
const { stdout } = await runPowerShellFile(scriptFile, timeoutMs);
|
|
240
|
+
const parsed = parseBrokerScriptOutput(stdout);
|
|
241
|
+
if (parsed.returnValue === undefined) {
|
|
242
|
+
throw new Error(`restore broker: could not find a WMI ReturnValue in its own driver script's output:\n${stdout}`);
|
|
243
|
+
}
|
|
244
|
+
if (parsed.returnValue !== 0) {
|
|
245
|
+
throw new Error(`restore broker: WMI 'Win32_Process Create' refused to launch the restore (ReturnValue=${parsed.returnValue}):\n${stdout}`);
|
|
246
|
+
}
|
|
247
|
+
if (parsed.ecText === undefined) {
|
|
248
|
+
return { kind: "unconfirmed" };
|
|
249
|
+
}
|
|
250
|
+
const exitCode = Number.parseInt(parsed.ecText, 10);
|
|
251
|
+
if (!Number.isFinite(exitCode)) {
|
|
252
|
+
return { kind: "unconfirmed" };
|
|
253
|
+
}
|
|
254
|
+
return { kind: "completed", exitCode, output: parsed.outText ?? "" };
|
|
255
|
+
}
|
|
256
|
+
finally {
|
|
257
|
+
await Promise.all([
|
|
258
|
+
fs.rm(scriptFile, { force: true }).catch(() => { }),
|
|
259
|
+
fs.rm(outFile, { force: true }).catch(() => { }),
|
|
260
|
+
fs.rm(ecFile, { force: true }).catch(() => { }),
|
|
261
|
+
]);
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
//# sourceMappingURL=restore-broker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"restore-broker.js","sourceRoot":"","sources":["../../src/backend-msb/restore-broker.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True if `output` (an `msb restore` invocation's combined stdout/stderr)
|
|
3
|
+
* names msb's own sandbox-name collision refusal — msb refuses to create a
|
|
4
|
+
* second sandbox under a name it already has one for. Captured shape:
|
|
5
|
+
*
|
|
6
|
+
* ```text
|
|
7
|
+
* error: sandbox 'rz-abc-1' already exists
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* EMPIRICALLY VERIFIED against msb 0.7.1's own source
|
|
11
|
+
* (`prepare_create_target` in `sdk/rust/lib/backend/local/sandbox/create.rs`):
|
|
12
|
+
* the check is `existing.is_some() || dir_exists` — the refusal fires when
|
|
13
|
+
* EITHER the sandbox's database record still exists OR its on-disk directory
|
|
14
|
+
* does, two independent things that can each still be mid-release on
|
|
15
|
+
* Windows. `MsbCliBackend.createCheckpoint`'s own stop/snapshot/`rm`/restore
|
|
16
|
+
* cycle removes the source sandbox and immediately restores a fresh one
|
|
17
|
+
* under the SAME name, so a lingering release on either side races the very
|
|
18
|
+
* next `restore` straight into this refusal.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately a broad, case-insensitive substring match (mirrors this
|
|
21
|
+
* backend's own `isPortBindConflictOutput`, and the sibling classifiers this
|
|
22
|
+
* whole family of msb backends — rust, kotlin — use for the identical
|
|
23
|
+
* signature) rather than a strict parse: msb has no structured/typed error
|
|
24
|
+
* for this, only combined process output, and a collision can be worded
|
|
25
|
+
* around either the sandbox's name or (rarely, alternate msb builds) its
|
|
26
|
+
* dedicated "already in use" phrasing. Only ever checked against a
|
|
27
|
+
* `msb restore` invocation's own output — a `msb snapshot load` "already
|
|
28
|
+
* exists" (see `isSnapshotAlreadyExistsError`) is a completely different
|
|
29
|
+
* command and never appears here.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isSandboxAlreadyExistsFailure(output: string): boolean;
|
|
32
|
+
//# sourceMappingURL=sandbox-already-exists.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-already-exists.d.ts","sourceRoot":"","sources":["../../src/backend-msb/sandbox-already-exists.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAgB,6BAA6B,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGrE"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True if `output` (an `msb restore` invocation's combined stdout/stderr)
|
|
3
|
+
* names msb's own sandbox-name collision refusal — msb refuses to create a
|
|
4
|
+
* second sandbox under a name it already has one for. Captured shape:
|
|
5
|
+
*
|
|
6
|
+
* ```text
|
|
7
|
+
* error: sandbox 'rz-abc-1' already exists
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* EMPIRICALLY VERIFIED against msb 0.7.1's own source
|
|
11
|
+
* (`prepare_create_target` in `sdk/rust/lib/backend/local/sandbox/create.rs`):
|
|
12
|
+
* the check is `existing.is_some() || dir_exists` — the refusal fires when
|
|
13
|
+
* EITHER the sandbox's database record still exists OR its on-disk directory
|
|
14
|
+
* does, two independent things that can each still be mid-release on
|
|
15
|
+
* Windows. `MsbCliBackend.createCheckpoint`'s own stop/snapshot/`rm`/restore
|
|
16
|
+
* cycle removes the source sandbox and immediately restores a fresh one
|
|
17
|
+
* under the SAME name, so a lingering release on either side races the very
|
|
18
|
+
* next `restore` straight into this refusal.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately a broad, case-insensitive substring match (mirrors this
|
|
21
|
+
* backend's own `isPortBindConflictOutput`, and the sibling classifiers this
|
|
22
|
+
* whole family of msb backends — rust, kotlin — use for the identical
|
|
23
|
+
* signature) rather than a strict parse: msb has no structured/typed error
|
|
24
|
+
* for this, only combined process output, and a collision can be worded
|
|
25
|
+
* around either the sandbox's name or (rarely, alternate msb builds) its
|
|
26
|
+
* dedicated "already in use" phrasing. Only ever checked against a
|
|
27
|
+
* `msb restore` invocation's own output — a `msb snapshot load` "already
|
|
28
|
+
* exists" (see `isSnapshotAlreadyExistsError`) is a completely different
|
|
29
|
+
* command and never appears here.
|
|
30
|
+
*/
|
|
31
|
+
export function isSandboxAlreadyExistsFailure(output) {
|
|
32
|
+
const m = output.toLowerCase();
|
|
33
|
+
return m.includes("already exists") || (m.includes("already in use") && m.includes("name"));
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=sandbox-already-exists.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sandbox-already-exists.js","sourceRoot":"","sources":["../../src/backend-msb/sandbox-already-exists.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,UAAU,6BAA6B,CAAC,MAAc;IAC1D,MAAM,CAAC,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;IAC/B,OAAO,CAAC,CAAC,QAAQ,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,gBAAgB,CAAC,IAAI,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC;AAC9F,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts the artifact path msb prints as the LAST line of a successful
|
|
3
|
+
* `msb snapshot create --from-sandbox ...` invocation's stdout. EMPIRICALLY
|
|
4
|
+
* VERIFIED against a real msb 0.7.1 binary: the command prints the snapshot
|
|
5
|
+
* ID line first, then the absolute artifact path as its final line —
|
|
6
|
+
* `<destDir-or-default>/<sourceSandbox>/snap_<32-hex-digest>` (see
|
|
7
|
+
* `MsbCommands.snapshotCreate`'s own doc for why the caller-supplied name
|
|
8
|
+
* never determines this path). `MsbCliBackend.createCheckpoint` uses the
|
|
9
|
+
* returned path — never the ref it asked msb to create the snapshot under —
|
|
10
|
+
* as the effective checkpoint ref for every later restore/rm/inspect call.
|
|
11
|
+
*
|
|
12
|
+
* Deliberately conservative, matching the HARD CONSTRAINT this parser was
|
|
13
|
+
* written to satisfy: trims the whole output, keeps only non-empty lines,
|
|
14
|
+
* takes the LAST one, and requires it to be an absolute path (`path.isAbsolute`)
|
|
15
|
+
* — anything else (empty output, a relative-looking last line, some future
|
|
16
|
+
* extra trailing line this parser has never seen) resolves `undefined`
|
|
17
|
+
* rather than guessing, so the caller can fail loudly with the raw,
|
|
18
|
+
* unparsed output quoted verbatim instead of minting a checkpoint ref from a
|
|
19
|
+
* misread line.
|
|
20
|
+
*/
|
|
21
|
+
export declare function parseSnapshotCreateArtifactPath(stdout: string): string | undefined;
|
|
22
|
+
//# sourceMappingURL=snapshot-create.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-create.d.ts","sourceRoot":"","sources":["../../src/backend-msb/snapshot-create.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,+BAA+B,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAUlF"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
/**
|
|
3
|
+
* Extracts the artifact path msb prints as the LAST line of a successful
|
|
4
|
+
* `msb snapshot create --from-sandbox ...` invocation's stdout. EMPIRICALLY
|
|
5
|
+
* VERIFIED against a real msb 0.7.1 binary: the command prints the snapshot
|
|
6
|
+
* ID line first, then the absolute artifact path as its final line —
|
|
7
|
+
* `<destDir-or-default>/<sourceSandbox>/snap_<32-hex-digest>` (see
|
|
8
|
+
* `MsbCommands.snapshotCreate`'s own doc for why the caller-supplied name
|
|
9
|
+
* never determines this path). `MsbCliBackend.createCheckpoint` uses the
|
|
10
|
+
* returned path — never the ref it asked msb to create the snapshot under —
|
|
11
|
+
* as the effective checkpoint ref for every later restore/rm/inspect call.
|
|
12
|
+
*
|
|
13
|
+
* Deliberately conservative, matching the HARD CONSTRAINT this parser was
|
|
14
|
+
* written to satisfy: trims the whole output, keeps only non-empty lines,
|
|
15
|
+
* takes the LAST one, and requires it to be an absolute path (`path.isAbsolute`)
|
|
16
|
+
* — anything else (empty output, a relative-looking last line, some future
|
|
17
|
+
* extra trailing line this parser has never seen) resolves `undefined`
|
|
18
|
+
* rather than guessing, so the caller can fail loudly with the raw,
|
|
19
|
+
* unparsed output quoted verbatim instead of minting a checkpoint ref from a
|
|
20
|
+
* misread line.
|
|
21
|
+
*/
|
|
22
|
+
export function parseSnapshotCreateArtifactPath(stdout) {
|
|
23
|
+
const lines = stdout
|
|
24
|
+
.split("\n")
|
|
25
|
+
.map((line) => line.trim())
|
|
26
|
+
.filter((line) => line.length > 0);
|
|
27
|
+
const lastLine = lines.at(-1);
|
|
28
|
+
if (lastLine === undefined || !path.isAbsolute(lastLine)) {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
return lastLine;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=snapshot-create.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"snapshot-create.js","sourceRoot":"","sources":["../../src/backend-msb/snapshot-create.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAElC;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,+BAA+B,CAAC,MAAc;IAC5D,MAAM,KAAK,GAAG,MAAM;SACjB,KAAK,CAAC,IAAI,CAAC;SACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC;SAC1B,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IACrC,MAAM,QAAQ,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IAC9B,IAAI,QAAQ,KAAK,SAAS,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;QACzD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|