@avantf/dsh-plugin-base 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -15
- package/dist/bootstrap.d.ts +3 -2
- package/dist/bootstrap.d.ts.map +1 -1
- package/dist/bootstrap.js +3 -2
- package/dist/compat.d.ts +2 -2
- package/dist/compat.js +5 -5
- package/dist/errors.d.ts +6 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/index.d.ts +23 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +45 -22
- package/dist/interface.d.ts +296 -23
- package/dist/interface.d.ts.map +1 -1
- package/dist/interface.js +206 -8
- package/dist/interface_gate.d.ts +20 -6
- package/dist/interface_gate.d.ts.map +1 -1
- package/dist/interface_gate.js +37 -7
- package/dist/internal.d.ts +26 -1
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +27 -1
- package/dist/kit/family.d.ts +2 -2
- package/dist/kit/family.js +3 -3
- package/dist/kit/index.d.ts +5 -1
- package/dist/kit/index.d.ts.map +1 -1
- package/dist/kit/index.js +5 -1
- package/dist/kit/logger.d.ts +1 -1
- package/dist/kit/logger.js +1 -1
- package/dist/kit/prompt_files.d.ts +37 -3
- package/dist/kit/prompt_files.d.ts.map +1 -1
- package/dist/kit/prompt_files.js +45 -0
- package/dist/kit/typert.d.ts +2 -2
- package/dist/kit/wellformed.d.ts +79 -0
- package/dist/kit/wellformed.d.ts.map +1 -0
- package/dist/kit/wellformed.js +155 -0
- package/dist/layout.d.ts +17 -2
- package/dist/layout.d.ts.map +1 -1
- package/dist/layout.js +15 -5
- package/dist/lint.d.ts.map +1 -1
- package/dist/lint.js +2 -2
- package/dist/lock.d.ts +18 -0
- package/dist/lock.d.ts.map +1 -1
- package/dist/lock.js +77 -10
- package/dist/net.d.ts +106 -3
- package/dist/net.d.ts.map +1 -1
- package/dist/net.js +128 -30
- package/dist/providers/archive.d.ts.map +1 -1
- package/dist/providers/archive.js +108 -20
- package/dist/providers/model.d.ts +26 -3
- package/dist/providers/model.d.ts.map +1 -1
- package/dist/providers/model.js +159 -32
- package/dist/providers/npm.d.ts +5 -0
- package/dist/providers/npm.d.ts.map +1 -1
- package/dist/providers/npm.js +62 -21
- package/dist/provisioner.d.ts.map +1 -1
- package/dist/provisioner.js +128 -31
- package/dist/state.d.ts.map +1 -1
- package/dist/state.js +8 -3
- package/dist/tar.d.ts +7 -0
- package/dist/tar.d.ts.map +1 -1
- package/dist/tar.js +12 -5
- package/dist/types.d.ts +11 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/zip.d.ts.map +1 -1
- package/dist/zip.js +10 -1
- package/package.json +18 -19
- package/dist/artifact.js.map +0 -1
- package/dist/bootstrap.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/compat.js.map +0 -1
- package/dist/conformance.js.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/fs.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/integrity.js.map +0 -1
- package/dist/interface.js.map +0 -1
- package/dist/interface_gate.js.map +0 -1
- package/dist/internal.js.map +0 -1
- package/dist/kit/family.js.map +0 -1
- package/dist/kit/index.js.map +0 -1
- package/dist/kit/logger.js.map +0 -1
- package/dist/kit/prompt_files.js.map +0 -1
- package/dist/kit/typert.js.map +0 -1
- package/dist/layout.js.map +0 -1
- package/dist/lint.js.map +0 -1
- package/dist/lock.js.map +0 -1
- package/dist/manifest.js.map +0 -1
- package/dist/net.js.map +0 -1
- package/dist/package-name.js.map +0 -1
- package/dist/preset.js.map +0 -1
- package/dist/providers/archive.js.map +0 -1
- package/dist/providers/model.js.map +0 -1
- package/dist/providers/npm.js.map +0 -1
- package/dist/provisioner.js.map +0 -1
- package/dist/semver.js.map +0 -1
- package/dist/state.js.map +0 -1
- package/dist/tar.js.map +0 -1
- package/dist/types.js.map +0 -1
- package/dist/zip.js.map +0 -1
package/dist/net.d.ts
CHANGED
|
@@ -3,15 +3,118 @@ import type { ProgressEvent, ProviderContext } from './types.js';
|
|
|
3
3
|
export declare function fetchImplOf(ctx: ProviderContext): typeof fetch;
|
|
4
4
|
/** Default hard byte cap for one downloaded body. */
|
|
5
5
|
export declare const DEFAULT_MAX_BYTES: number;
|
|
6
|
+
/**
|
|
7
|
+
* Hard byte cap for a METADATA body: an npm packument, a model revision response, a siblings list.
|
|
8
|
+
*
|
|
9
|
+
* Metadata is a small JSON document that is then `JSON.parse`d (`providers/npm.ts`,
|
|
10
|
+
* `providers/model.ts`), and parsing amplifies it several-fold in heap. The 256 MiB archive cap only
|
|
11
|
+
* stops the extreme; it still lets a "KB-scale document, GB-scale heap" through.
|
|
12
|
+
*
|
|
13
|
+
* 16 MiB is MEASURED, not guessed. The npm provider asks for `accept: application/json`, i.e. the
|
|
14
|
+
* FULL packument, so the real sizes are the full ones (registry.npmjs.org, 2026-10-02):
|
|
15
|
+
* `lodash` 0.2 MB, `react` 6.7 MB, `aws-sdk` 10.1 MB, `@types/node` 11.2 MB, `typescript` 15.7 MB.
|
|
16
|
+
* 16 MiB keeps every ordinary package installable with headroom while bounding the parse to a
|
|
17
|
+
* four-digit number of MB. The largest packuments on the registry (`npm`, 25.8 MB) now fail
|
|
18
|
+
* terminally as `fetch/too-large` — the intended fail-closed answer for a document that would
|
|
19
|
+
* otherwise be parsed into a multi-GB heap. Anything in that range wants a streamed download with a
|
|
20
|
+
* real cap, not `JSON.parse`. (The abbreviated form — `accept: application/vnd.npm.install-v1+json`
|
|
21
|
+
* — measured 2–10× smaller on the same packages, e.g. `npm` 25.8 MB → 2.5 MB and `typescript`
|
|
22
|
+
* 15.7 MB → 8.7 MB; switching to it would be a separate, observable change to what the provider
|
|
23
|
+
* reads, so it is recorded here as an option rather than done silently.)
|
|
24
|
+
*/
|
|
25
|
+
export declare const METADATA_MAX_BYTES: number;
|
|
6
26
|
/** Default per-request timeout in milliseconds. */
|
|
7
27
|
export declare const DEFAULT_REQUEST_TIMEOUT_MS = 300000;
|
|
8
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Combine the caller's `AbortSignal` with a timeout signal.
|
|
30
|
+
*
|
|
31
|
+
* `timeoutMs` (or, when omitted, the item's `policy.timeoutMs`) is the item's per-request budget;
|
|
32
|
+
* an undeclared value keeps the 300 s default. `0` means NO timeout — the escape hatch a 2 GiB model
|
|
33
|
+
* needs on a link slower than ~7 MB/s, which the fixed default could never finish. `ctx.signal`
|
|
34
|
+
* (shutdown/caller abort) still applies in every case.
|
|
35
|
+
*/
|
|
9
36
|
export declare function signalFor(ctx: ProviderContext, timeoutMs?: number): AbortSignal;
|
|
10
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Bodies at or below this size are assembled in memory exactly as before; a larger body is handed to
|
|
39
|
+
* a caller-supplied {@link BodySpill} instead, so the peak cost of a download stops tracking the file
|
|
40
|
+
* size. The threshold is the ONLY switch, which keeps the common path (a packument, a tarball, a small
|
|
41
|
+
* model file) on the untouched, simplest code while a multi-GB model — the case measured at roughly 2×
|
|
42
|
+
* its size in RSS — streams one chunk at a time.
|
|
43
|
+
*
|
|
44
|
+
* 64 MiB is a deliberate middle: every ordinary body stays in memory, and when one does not, the
|
|
45
|
+
* in-memory cost is already bounded by `2 × spillAtBytes` (the chunks plus the joined copy) regardless
|
|
46
|
+
* of how large the body turns out to be.
|
|
47
|
+
*/
|
|
48
|
+
export declare const SPILL_THRESHOLD_BYTES: number;
|
|
49
|
+
/**
|
|
50
|
+
* Where a body that outgrew the in-memory budget goes instead of a `Uint8Array`.
|
|
51
|
+
*
|
|
52
|
+
* `readCappedOrSpill` writes each chunk in arrival order (the prefix that was already buffered is
|
|
53
|
+
* replayed first) and then calls EXACTLY ONE of `finish` (the body ended and its length checked out) or
|
|
54
|
+
* `abort` (any failure, including the cap being crossed or a declared/actual mismatch). A sink must
|
|
55
|
+
* therefore never assume `finish` follows `write`: the reader can reject mid-body.
|
|
56
|
+
*/
|
|
57
|
+
export interface BodySpill<T> {
|
|
58
|
+
/** Append one chunk; awaiting it may apply backpressure. */
|
|
59
|
+
write(chunk: Uint8Array): Promise<void>;
|
|
60
|
+
/** The body is complete and length-checked; flush it and produce the caller's result. */
|
|
61
|
+
finish(total: number): Promise<T>;
|
|
62
|
+
/** Discard everything written so far. Must be safe to call after `finish` failed, and must not throw. */
|
|
63
|
+
abort(): Promise<void>;
|
|
64
|
+
}
|
|
65
|
+
/** Options of {@link readCappedOrSpill}. */
|
|
66
|
+
export interface CappedReadOptions<T> {
|
|
67
|
+
/** Hard cap; defaults to {@link DEFAULT_MAX_BYTES}. */
|
|
68
|
+
readonly maxBytes?: number;
|
|
69
|
+
/** Defaults to {@link SPILL_THRESHOLD_BYTES}. */
|
|
70
|
+
readonly spillAtBytes?: number;
|
|
71
|
+
/** Absent ⇒ the whole body is assembled in memory, whatever its size (the historical path). */
|
|
72
|
+
readonly spill?: () => Promise<BodySpill<T>>;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Read a response body with a hard byte cap, optionally spilling a large body to a caller's sink.
|
|
76
|
+
*
|
|
77
|
+
* Two failures that used to share `fetch/failed` are deliberately DIFFERENT codes, because a caller
|
|
78
|
+
* has to treat them differently:
|
|
79
|
+
*
|
|
80
|
+
* - the cap is crossed (declared `content-length`, or bytes actually read) → `fetch/too-large`, a
|
|
81
|
+
* TERMINAL failure: another mirror serves the same oversized object, so falling through cannot
|
|
82
|
+
* help;
|
|
83
|
+
* - the declared length does not match what was read → `fetch/failed`, a TRANSPORT failure: that is
|
|
84
|
+
* a truncated download, exactly the shape that should try the next candidate source.
|
|
85
|
+
*
|
|
86
|
+
* Without a `spill` this is exactly the historical `readCapped`: same codes, same messages, same
|
|
87
|
+
* single up-front `Uint8Array`. With one, the body crosses `spillAtBytes` and from then on every chunk
|
|
88
|
+
* goes straight to the sink — the buffered prefix is replayed once and dropped — so peak memory is one
|
|
89
|
+
* chunk, not the whole file. `docs/DESIGN.md` §6 states the same invariant; `providers/model.ts` and
|
|
90
|
+
* `downloadBytes` below are the two places that branch on it.
|
|
91
|
+
*/
|
|
92
|
+
export declare function readCappedOrSpill(response: Response, options?: {
|
|
93
|
+
readonly maxBytes?: number;
|
|
94
|
+
}): Promise<Uint8Array>;
|
|
95
|
+
export declare function readCappedOrSpill<T>(response: Response, options: CappedReadOptions<T> & {
|
|
96
|
+
readonly spill: () => Promise<BodySpill<T>>;
|
|
97
|
+
}): Promise<Uint8Array | T>;
|
|
98
|
+
/**
|
|
99
|
+
* {@link readCappedOrSpill} without a sink: the entire body is assembled in memory.
|
|
100
|
+
*
|
|
101
|
+
* This is the frozen historical behavior — used by everything that NEEDS the bytes in hand (a
|
|
102
|
+
* `JSON.parse`, an archive digest, an in-memory extract). A caller that lands a file on disk should
|
|
103
|
+
* pass a `spill` so a large body never becomes two copies of itself in the heap.
|
|
104
|
+
*/
|
|
11
105
|
export declare function readCapped(response: Response, maxBytes?: number): Promise<Uint8Array>;
|
|
12
106
|
/** `{url}` templates → concrete URLs, official source first. */
|
|
13
107
|
export declare function candidateUrls(url: string, mirrors: readonly string[] | undefined): readonly string[];
|
|
14
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* Download the first candidate that answers 2xx; throws `fetch/failed` when none does (and
|
|
110
|
+
* `fetch/too-large` immediately when a body crosses the hard cap — see {@link readCapped}).
|
|
111
|
+
*
|
|
112
|
+
* DOWNLOADING IS THE SLOWEST THING THIS FRAMEWORK DOES and it used to be completely silent: a 223 MB
|
|
113
|
+
* archive on a mirror that eventually times out looked like eight minutes of nothing, and the first
|
|
114
|
+
* evidence of trouble was the final `fetch/failed` after every candidate had been tried (measured on
|
|
115
|
+
* DSH Desktop — GitHub plus three mirrors, all aborted). One line per attempt, one when a candidate
|
|
116
|
+
* is passed over with its reason, and one on success makes the wait legible while it is happening.
|
|
117
|
+
*/
|
|
15
118
|
export declare function downloadBytes(ctx: ProviderContext, urls: readonly string[], key: string, onProgress?: (event: ProgressEvent) => void): Promise<{
|
|
16
119
|
readonly bytes: Uint8Array;
|
|
17
120
|
readonly url: string;
|
package/dist/net.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"net.d.ts","sourceRoot":"","sources":["../src/net.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEhE,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,KAAK,CAI9D;AAED,qDAAqD;AACrD,eAAO,MAAM,iBAAiB,QAAoB,CAAA;AAElD,mDAAmD;AACnD,eAAO,MAAM,0BAA0B,SAAU,CAAA;AAEjD
|
|
1
|
+
{"version":3,"file":"net.d.ts","sourceRoot":"","sources":["../src/net.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAEhE,oFAAoF;AACpF,wBAAgB,WAAW,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,KAAK,CAI9D;AAED,qDAAqD;AACrD,eAAO,MAAM,iBAAiB,QAAoB,CAAA;AAElD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,kBAAkB,QAAmB,CAAA;AAElD,mDAAmD;AACnD,eAAO,MAAM,0BAA0B,SAAU,CAAA;AAEjD;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,GAAG,EAAE,eAAe,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,WAAW,CAQ/E;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,qBAAqB,QAAmB,CAAA;AAErD;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,4DAA4D;IAC5D,KAAK,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IACvC,yFAAyF;IACzF,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IACjC,yGAAyG;IACzG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACvB;AAED,4CAA4C;AAC5C,MAAM,WAAW,iBAAiB,CAAC,CAAC;IAClC,uDAAuD;IACvD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,iDAAiD;IACjD,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;IAC9B,+FAA+F;IAC/F,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;CAC7C;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,EAAE;IAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,UAAU,CAAC,CAAA;AACpH,wBAAgB,iBAAiB,CAAC,CAAC,EACjC,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAA;CAAE,GAC9E,OAAO,CAAC,UAAU,GAAG,CAAC,CAAC,CAAA;AA+D1B;;;;;;GAMG;AACH,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,QAAQ,EAClB,QAAQ,GAAE,MAA0B,GACnC,OAAO,CAAC,UAAU,CAAC,CAErB;AAED,gEAAgE;AAChE,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,SAAS,GAAG,SAAS,MAAM,EAAE,CAOpG;AAED;;;;;;;;;GASG;AACH,wBAAsB,aAAa,CACjC,GAAG,EAAE,eAAe,EACpB,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,GAAG,EAAE,MAAM,EACX,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,aAAa,KAAK,IAAI,GAC1C,OAAO,CAAC;IAAE,QAAQ,CAAC,KAAK,EAAE,UAAU,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC,CAiC/D;AAED,yCAAyC;AACzC,wBAAgB,YAAY,CAAC,KAAK,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAKtE;AAED,yFAAyF;AACzF,wBAAgB,WAAW,IAAI,MAAM,CAEpC"}
|
package/dist/net.js
CHANGED
|
@@ -13,24 +13,69 @@ export function fetchImplOf(ctx) {
|
|
|
13
13
|
}
|
|
14
14
|
/** Default hard byte cap for one downloaded body. */
|
|
15
15
|
export const DEFAULT_MAX_BYTES = 256 * 1024 * 1024;
|
|
16
|
+
/**
|
|
17
|
+
* Hard byte cap for a METADATA body: an npm packument, a model revision response, a siblings list.
|
|
18
|
+
*
|
|
19
|
+
* Metadata is a small JSON document that is then `JSON.parse`d (`providers/npm.ts`,
|
|
20
|
+
* `providers/model.ts`), and parsing amplifies it several-fold in heap. The 256 MiB archive cap only
|
|
21
|
+
* stops the extreme; it still lets a "KB-scale document, GB-scale heap" through.
|
|
22
|
+
*
|
|
23
|
+
* 16 MiB is MEASURED, not guessed. The npm provider asks for `accept: application/json`, i.e. the
|
|
24
|
+
* FULL packument, so the real sizes are the full ones (registry.npmjs.org, 2026-10-02):
|
|
25
|
+
* `lodash` 0.2 MB, `react` 6.7 MB, `aws-sdk` 10.1 MB, `@types/node` 11.2 MB, `typescript` 15.7 MB.
|
|
26
|
+
* 16 MiB keeps every ordinary package installable with headroom while bounding the parse to a
|
|
27
|
+
* four-digit number of MB. The largest packuments on the registry (`npm`, 25.8 MB) now fail
|
|
28
|
+
* terminally as `fetch/too-large` — the intended fail-closed answer for a document that would
|
|
29
|
+
* otherwise be parsed into a multi-GB heap. Anything in that range wants a streamed download with a
|
|
30
|
+
* real cap, not `JSON.parse`. (The abbreviated form — `accept: application/vnd.npm.install-v1+json`
|
|
31
|
+
* — measured 2–10× smaller on the same packages, e.g. `npm` 25.8 MB → 2.5 MB and `typescript`
|
|
32
|
+
* 15.7 MB → 8.7 MB; switching to it would be a separate, observable change to what the provider
|
|
33
|
+
* reads, so it is recorded here as an option rather than done silently.)
|
|
34
|
+
*/
|
|
35
|
+
export const METADATA_MAX_BYTES = 16 * 1024 * 1024;
|
|
16
36
|
/** Default per-request timeout in milliseconds. */
|
|
17
37
|
export const DEFAULT_REQUEST_TIMEOUT_MS = 300_000;
|
|
18
|
-
/**
|
|
19
|
-
|
|
20
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Combine the caller's `AbortSignal` with a timeout signal.
|
|
40
|
+
*
|
|
41
|
+
* `timeoutMs` (or, when omitted, the item's `policy.timeoutMs`) is the item's per-request budget;
|
|
42
|
+
* an undeclared value keeps the 300 s default. `0` means NO timeout — the escape hatch a 2 GiB model
|
|
43
|
+
* needs on a link slower than ~7 MB/s, which the fixed default could never finish. `ctx.signal`
|
|
44
|
+
* (shutdown/caller abort) still applies in every case.
|
|
45
|
+
*/
|
|
46
|
+
export function signalFor(ctx, timeoutMs) {
|
|
47
|
+
const configured = timeoutMs ?? ctx.policy.timeoutMs;
|
|
48
|
+
if (configured === 0) {
|
|
49
|
+
return ctx.signal ?? new AbortController().signal;
|
|
50
|
+
}
|
|
51
|
+
const effective = configured === undefined || !Number.isFinite(configured) || configured < 0 ? DEFAULT_REQUEST_TIMEOUT_MS : configured;
|
|
52
|
+
const timeout = AbortSignal.timeout(effective);
|
|
21
53
|
return ctx.signal === undefined ? timeout : AbortSignal.any([ctx.signal, timeout]);
|
|
22
54
|
}
|
|
23
|
-
/**
|
|
24
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Bodies at or below this size are assembled in memory exactly as before; a larger body is handed to
|
|
57
|
+
* a caller-supplied {@link BodySpill} instead, so the peak cost of a download stops tracking the file
|
|
58
|
+
* size. The threshold is the ONLY switch, which keeps the common path (a packument, a tarball, a small
|
|
59
|
+
* model file) on the untouched, simplest code while a multi-GB model — the case measured at roughly 2×
|
|
60
|
+
* its size in RSS — streams one chunk at a time.
|
|
61
|
+
*
|
|
62
|
+
* 64 MiB is a deliberate middle: every ordinary body stays in memory, and when one does not, the
|
|
63
|
+
* in-memory cost is already bounded by `2 × spillAtBytes` (the chunks plus the joined copy) regardless
|
|
64
|
+
* of how large the body turns out to be.
|
|
65
|
+
*/
|
|
66
|
+
export const SPILL_THRESHOLD_BYTES = 64 * 1024 * 1024;
|
|
67
|
+
export async function readCappedOrSpill(response, options = {}) {
|
|
68
|
+
const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
|
|
69
|
+
const spillAtBytes = options.spillAtBytes ?? SPILL_THRESHOLD_BYTES;
|
|
25
70
|
const declared = Number(response.headers.get('content-length') ?? Number.NaN);
|
|
26
71
|
if (Number.isFinite(declared) && declared > maxBytes) {
|
|
27
|
-
throw new ProvisionError('fetch/
|
|
72
|
+
throw new ProvisionError('fetch/too-large', `响应声明 ${String(declared)} 字节,超过上限 ${String(maxBytes)}`);
|
|
28
73
|
}
|
|
29
74
|
const body = response.body;
|
|
30
75
|
if (body === null) {
|
|
31
76
|
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
32
77
|
if (bytes.byteLength > maxBytes) {
|
|
33
|
-
throw new ProvisionError('fetch/
|
|
78
|
+
throw new ProvisionError('fetch/too-large', `响应 ${String(bytes.byteLength)} 字节,超过上限 ${String(maxBytes)}`);
|
|
34
79
|
}
|
|
35
80
|
if (Number.isFinite(declared) && declared !== bytes.byteLength) {
|
|
36
81
|
throw new ProvisionError('fetch/failed', `响应声明 ${String(declared)} 字节,实际收到 ${String(bytes.byteLength)} 字节`);
|
|
@@ -40,29 +85,60 @@ export async function readCapped(response, maxBytes = DEFAULT_MAX_BYTES) {
|
|
|
40
85
|
const reader = body.getReader();
|
|
41
86
|
const chunks = [];
|
|
42
87
|
let total = 0;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
88
|
+
let spill;
|
|
89
|
+
try {
|
|
90
|
+
for (;;) {
|
|
91
|
+
const { done, value } = await reader.read();
|
|
92
|
+
if (done)
|
|
93
|
+
break;
|
|
94
|
+
if (value === undefined)
|
|
95
|
+
continue;
|
|
96
|
+
total += value.byteLength;
|
|
97
|
+
if (total > maxBytes) {
|
|
98
|
+
throw new ProvisionError('fetch/too-large', `响应超过上限 ${String(maxBytes)} 字节(已读 ${String(total)})`);
|
|
99
|
+
}
|
|
100
|
+
// The body has outgrown the in-memory budget: hand the buffered prefix to the sink and never
|
|
101
|
+
// assemble the whole thing again.
|
|
102
|
+
if (spill === undefined && options.spill !== undefined && total > spillAtBytes) {
|
|
103
|
+
spill = await options.spill();
|
|
104
|
+
for (const buffered of chunks)
|
|
105
|
+
await spill.write(buffered);
|
|
106
|
+
chunks.length = 0;
|
|
107
|
+
}
|
|
108
|
+
if (spill === undefined)
|
|
109
|
+
chunks.push(value);
|
|
110
|
+
else
|
|
111
|
+
await spill.write(value);
|
|
53
112
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
113
|
+
if (Number.isFinite(declared) && declared !== total) {
|
|
114
|
+
throw new ProvisionError('fetch/failed', `响应声明 ${String(declared)} 字节,实际收到 ${String(total)} 字节`);
|
|
115
|
+
}
|
|
116
|
+
if (spill !== undefined)
|
|
117
|
+
return await spill.finish(total);
|
|
118
|
+
const bytes = new Uint8Array(total);
|
|
119
|
+
let offset = 0;
|
|
120
|
+
for (const chunk of chunks) {
|
|
121
|
+
bytes.set(chunk, offset);
|
|
122
|
+
offset += chunk.byteLength;
|
|
123
|
+
}
|
|
124
|
+
return bytes;
|
|
58
125
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
126
|
+
catch (error) {
|
|
127
|
+
await reader.cancel().catch(() => undefined);
|
|
128
|
+
if (spill !== undefined)
|
|
129
|
+
await spill.abort().catch(() => undefined);
|
|
130
|
+
throw error;
|
|
64
131
|
}
|
|
65
|
-
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* {@link readCappedOrSpill} without a sink: the entire body is assembled in memory.
|
|
135
|
+
*
|
|
136
|
+
* This is the frozen historical behavior — used by everything that NEEDS the bytes in hand (a
|
|
137
|
+
* `JSON.parse`, an archive digest, an in-memory extract). A caller that lands a file on disk should
|
|
138
|
+
* pass a `spill` so a large body never becomes two copies of itself in the heap.
|
|
139
|
+
*/
|
|
140
|
+
export async function readCapped(response, maxBytes = DEFAULT_MAX_BYTES) {
|
|
141
|
+
return readCappedOrSpill(response, { maxBytes });
|
|
66
142
|
}
|
|
67
143
|
/** `{url}` templates → concrete URLs, official source first. */
|
|
68
144
|
export function candidateUrls(url, mirrors) {
|
|
@@ -74,14 +150,28 @@ export function candidateUrls(url, mirrors) {
|
|
|
74
150
|
}
|
|
75
151
|
return candidates;
|
|
76
152
|
}
|
|
77
|
-
/**
|
|
153
|
+
/**
|
|
154
|
+
* Download the first candidate that answers 2xx; throws `fetch/failed` when none does (and
|
|
155
|
+
* `fetch/too-large` immediately when a body crosses the hard cap — see {@link readCapped}).
|
|
156
|
+
*
|
|
157
|
+
* DOWNLOADING IS THE SLOWEST THING THIS FRAMEWORK DOES and it used to be completely silent: a 223 MB
|
|
158
|
+
* archive on a mirror that eventually times out looked like eight minutes of nothing, and the first
|
|
159
|
+
* evidence of trouble was the final `fetch/failed` after every candidate had been tried (measured on
|
|
160
|
+
* DSH Desktop — GitHub plus three mirrors, all aborted). One line per attempt, one when a candidate
|
|
161
|
+
* is passed over with its reason, and one on success makes the wait legible while it is happening.
|
|
162
|
+
*/
|
|
78
163
|
export async function downloadBytes(ctx, urls, key, onProgress) {
|
|
79
164
|
const problems = [];
|
|
80
|
-
for (const url of urls) {
|
|
165
|
+
for (const [index, url] of urls.entries()) {
|
|
166
|
+
const position = `${String(index + 1)}/${String(urls.length)}`;
|
|
167
|
+
// The last candidate gets no "trying the next one" promise: there is none.
|
|
168
|
+
const next = index + 1 < urls.length ? ',改用下一个候选源' : ',没有更多候选源';
|
|
169
|
+
ctx.logger.info(`${key}:尝试候选源 ${position} ${url}`);
|
|
81
170
|
try {
|
|
82
171
|
const response = await fetchImplOf(ctx)(url, { signal: signalFor(ctx) });
|
|
83
172
|
if (!response.ok) {
|
|
84
173
|
problems.push(`${url} → HTTP ${String(response.status)}`);
|
|
174
|
+
ctx.logger.warn(`${key}:${url} 返回 HTTP ${String(response.status)}${next}`);
|
|
85
175
|
continue;
|
|
86
176
|
}
|
|
87
177
|
// A short body is a TRANSPORT failure and retries the next mirror, like a bad status or a
|
|
@@ -90,10 +180,18 @@ export async function downloadBytes(ctx, urls, key, onProgress) {
|
|
|
90
180
|
// distinction IS the invariant; `docs/DESIGN.md` §6 states it in the same words.
|
|
91
181
|
const bytes = await readCapped(response);
|
|
92
182
|
onProgress?.({ key, phase: 'download', loaded: bytes.byteLength, total: bytes.byteLength });
|
|
183
|
+
ctx.logger.info(`${key}:下载完成 ${String(bytes.byteLength)} 字节(${url})`);
|
|
93
184
|
return { bytes, url };
|
|
94
185
|
}
|
|
95
186
|
catch (error) {
|
|
96
|
-
|
|
187
|
+
// The byte cap is the one fetch failure that is TERMINAL: every mirror serves the same
|
|
188
|
+
// oversized object, so retrying just burns the remaining candidates. `readCapped` separates it
|
|
189
|
+
// from a truncated body (`fetch/failed`) for exactly this branch.
|
|
190
|
+
if (error instanceof ProvisionError && error.code === 'fetch/too-large')
|
|
191
|
+
throw error;
|
|
192
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
193
|
+
problems.push(`${url} → ${reason}`);
|
|
194
|
+
ctx.logger.warn(`${key}:${url} 失败(${reason})${next}`);
|
|
97
195
|
}
|
|
98
196
|
}
|
|
99
197
|
throw new ProvisionError('fetch/failed', `所有候选源都失败:${problems.join('; ')}`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../../src/providers/archive.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../../src/providers/archive.ts"],"names":[],"mappings":"AAcA,OAAO,KAAK,EAA+B,QAAQ,EAAuE,MAAM,aAAa,CAAA;AAG7I,eAAO,MAAM,mBAAmB,mBAAmB,CAAA;AAOnD,gDAAgD;AAChD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,wBAAwB;IACxB,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,GAAG,KAAK,CAAA;IACnC,kEAAkE;IAClE,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,6CAA6C;AAC7C,MAAM,WAAW,iBAAiB;IAChC,iDAAiD;IACjD,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAA;IACnB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,oCAAoC;IACpC,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAA;IACrD,6CAA6C;IAC7C,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,iFAAiF;IACjF,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAA;IACvB,6DAA6D;IAC7D,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB,wEAAwE;IACxE,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CACzC;AAmJD,kDAAkD;AAClD,wBAAgB,qBAAqB,CAAC,OAAO,GAAE;IAAE,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,QAAQ,CAyLtF"}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* The built-in `binary-archive` provider.
|
|
3
3
|
* @module providers/archive
|
|
4
4
|
*/
|
|
5
|
-
import { delimiter, dirname, join } from 'node:path';
|
|
5
|
+
import { basename, delimiter, dirname, join } from 'node:path';
|
|
6
6
|
import { execFile } from 'node:child_process';
|
|
7
7
|
import { ProvisionError } from '../errors.js';
|
|
8
8
|
import { exists } from '../fs.js';
|
|
9
9
|
import { sriOfSha256 } from '../integrity.js';
|
|
10
|
+
import { assertSafeRelativePath } from '../layout.js';
|
|
10
11
|
import { decodeJson, readInstallManifest } from '../manifest.js';
|
|
11
12
|
import { candidateUrls, downloadBytes, platformKey, verifySha256 } from '../net.js';
|
|
12
13
|
import { extractTarGz } from '../tar.js';
|
|
@@ -88,16 +89,65 @@ function runVersionProbe(path, args, options = {}) {
|
|
|
88
89
|
execFile(path, [...args], {
|
|
89
90
|
timeout: options.timeoutMs ?? VERIFY_TIMEOUT_MS,
|
|
90
91
|
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
91
|
-
}, error => {
|
|
92
|
+
}, (error, stdout) => {
|
|
93
|
+
const text = typeof stdout === 'string' ? stdout : '';
|
|
92
94
|
if (error === null) {
|
|
93
|
-
resolve('ok');
|
|
95
|
+
resolve({ outcome: 'ok', stdout: text });
|
|
94
96
|
return;
|
|
95
97
|
}
|
|
96
98
|
const killed = error.killed === true || error.code === 'ETIMEDOUT';
|
|
97
|
-
resolve(killed ? 'timeout' : 'failed');
|
|
99
|
+
resolve({ outcome: killed ? 'timeout' : 'failed', stdout: text });
|
|
98
100
|
});
|
|
99
101
|
});
|
|
100
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Version-shaped tokens in a probe's output (a leading `v` is part of the decoration, not the number).
|
|
105
|
+
*
|
|
106
|
+
* A version is confirmed by an EQUAL token, never by the exit code: any same-named binary on PATH
|
|
107
|
+
* answers `--version` with 0. An exact-token rule can only produce a false NEGATIVE (a tool that
|
|
108
|
+
* prints more precision than the spec pins, e.g. `1.0.0.1` for `1.0.0`), which degrades to "present,
|
|
109
|
+
* version unknown" — the same safe shape as a timeout.
|
|
110
|
+
*/
|
|
111
|
+
const VERSION_TOKEN = /(?:^|[^0-9A-Za-z])v?(\d+(?:\.\d+)*(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)/g;
|
|
112
|
+
/** The bare numeric core of a version: `4.4.2-0ubuntu0.22.04.1` → `4.4.2`. */
|
|
113
|
+
const VERSION_CORE = /^\d+(?:\.\d+)*/;
|
|
114
|
+
/** A pin that is itself nothing but the numeric core (no distro/prerelease/build decoration). */
|
|
115
|
+
const PLAIN_NUMERIC = /^\d+(?:\.\d+)*$/;
|
|
116
|
+
/**
|
|
117
|
+
* Does the probe output report `expected`?
|
|
118
|
+
*
|
|
119
|
+
* The observed token may carry a DISTRO/BUILD decoration that is not part of the version identity —
|
|
120
|
+
* measured: `ffmpeg version 4.4.2-0ubuntu0.22.04.1` against a pin of `4.4.2`. That produced a MISS,
|
|
121
|
+
* which is fail-closed (the caller re-downloads a tool it already has) but wrong: the decorated
|
|
122
|
+
* token IS the pinned version. So a PLAIN NUMERIC pin is compared against each token's numeric core;
|
|
123
|
+
* a pin that is not plain numeric (a prerelease/build pin such as `1.2.3-rc.1`) keeps the exact-token
|
|
124
|
+
* rule, because there the suffix is exactly what is being pinned. A genuinely different version still
|
|
125
|
+
* misses: the core is compared whole (`4.5.0` and `1.0.0.1` are not `4.4.2`/`1.0.0`), never as a
|
|
126
|
+
* string prefix, so `4.4` cannot match `4.4.2` and `1.0.0` cannot match `1.0.0.1`.
|
|
127
|
+
*/
|
|
128
|
+
function reportsVersion(stdout, expected) {
|
|
129
|
+
const wanted = expected.replace(/^v/, '');
|
|
130
|
+
const compareCore = PLAIN_NUMERIC.test(wanted);
|
|
131
|
+
for (const match of stdout.matchAll(VERSION_TOKEN)) {
|
|
132
|
+
const token = match[1];
|
|
133
|
+
if (token === undefined)
|
|
134
|
+
continue;
|
|
135
|
+
if (token === wanted)
|
|
136
|
+
return true;
|
|
137
|
+
if (!compareCore)
|
|
138
|
+
continue;
|
|
139
|
+
// `VERSION_CORE` is anchored, so this is the token's numeric head, not a substring search.
|
|
140
|
+
const core = VERSION_CORE.exec(token)?.[0];
|
|
141
|
+
if (core === wanted)
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
/** One line of probe output, for a warning that stays readable. */
|
|
147
|
+
function firstLine(stdout) {
|
|
148
|
+
const line = stdout.split('\n').find(candidate => candidate.trim() !== '');
|
|
149
|
+
return (line ?? '').trim().slice(0, 120);
|
|
150
|
+
}
|
|
101
151
|
/** Build the built-in binary-archive provider. */
|
|
102
152
|
export function binaryArchiveProvider(options = {}) {
|
|
103
153
|
const id = options.id ?? '@avantf/dsh-plugin-base/binary-archive';
|
|
@@ -123,14 +173,24 @@ export function binaryArchiveProvider(options = {}) {
|
|
|
123
173
|
const spec = parseSpec(item);
|
|
124
174
|
const binary = binaryOf(spec);
|
|
125
175
|
const args = spec.versionArgs ?? ['--version'];
|
|
126
|
-
// A PATH/explicit hit is confirmed
|
|
176
|
+
// A PATH/explicit hit is confirmed by the `--version` probe's OUTPUT, not its exit code: any
|
|
177
|
+
// same-named binary exits 0, and trusting that made an arbitrary `pandoc` on PATH masquerade as
|
|
178
|
+
// the pinned version (its directory then went into PATH and status.json). A mismatch is a miss.
|
|
127
179
|
const check = async (path) => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
|
|
180
|
+
// No probe command ⇒ the binary may exist, but there is no way to know its version. Say so
|
|
181
|
+
// (the same shape as a timeout) instead of copying the assertion in the spec.
|
|
182
|
+
if (args.length === 0) {
|
|
183
|
+
ctx.logger.warn(`${path} 未配置版本探针(versionArgs 为空);按"存在但版本未知"处理`);
|
|
184
|
+
return {};
|
|
185
|
+
}
|
|
186
|
+
const probed = await runVersionProbe(path, args, { timeoutMs: PROBE_TIMEOUT_MS, ...(ctx.signal === undefined ? {} : { signal: ctx.signal }) });
|
|
187
|
+
if (probed.outcome === 'ok') {
|
|
188
|
+
if (reportsVersion(probed.stdout, spec.version))
|
|
189
|
+
return { version: spec.version };
|
|
190
|
+
ctx.logger.warn(`${path} 的 ${args.join(' ')} 未报告 ${spec.version}(输出:${firstLine(probed.stdout)});按未命中处理`);
|
|
191
|
+
return undefined;
|
|
192
|
+
}
|
|
193
|
+
if (probed.outcome === 'timeout') {
|
|
134
194
|
ctx.logger.warn(`${path} 的 ${args.join(' ')} 探针超时;按"存在但版本未知"处理`);
|
|
135
195
|
return {};
|
|
136
196
|
}
|
|
@@ -153,12 +213,21 @@ export function binaryArchiveProvider(options = {}) {
|
|
|
153
213
|
const manifest = readInstallManifest(decodeJson(await ctx.fs.readFile(manifestPath)), BINARY_ARCHIVE_KIND);
|
|
154
214
|
if (manifest === undefined || manifest.version !== spec.version)
|
|
155
215
|
continue;
|
|
156
|
-
// The manifest's entry is authoritative
|
|
216
|
+
// The manifest's entry is authoritative — and the read-back side still validates it, so a
|
|
217
|
+
// hand-edited/corrupt install.json cannot point the join outside the version directory.
|
|
157
218
|
const relative = manifest.entry ?? `bin/${binaryOf(spec, spec.packs[platformKey()])}`;
|
|
158
|
-
|
|
219
|
+
let executable;
|
|
220
|
+
try {
|
|
221
|
+
assertSafeRelativePath(relative);
|
|
222
|
+
executable = join(root, entry, ...relative.split('/'));
|
|
223
|
+
}
|
|
224
|
+
catch {
|
|
225
|
+
ctx.logger.warn(`install.json 的 entry 不安全,已跳过:${relative}(${join(root, entry)})`);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
159
228
|
if (await exists(ctx.fs, executable)) {
|
|
160
229
|
const dir = dirname(executable);
|
|
161
|
-
return { found: true, version:
|
|
230
|
+
return { found: true, version: manifest.version, dir, source: 'managed', env: pathEnvOf(dir) };
|
|
162
231
|
}
|
|
163
232
|
}
|
|
164
233
|
}
|
|
@@ -205,7 +274,14 @@ export function binaryArchiveProvider(options = {}) {
|
|
|
205
274
|
}
|
|
206
275
|
const binDir = join(staging, 'bin');
|
|
207
276
|
await ctx.fs.mkdir(binDir);
|
|
208
|
-
|
|
277
|
+
// The name the archive ACTUALLY carries, not the declared one. A pack names its executable
|
|
278
|
+
// (`pandoc`) while a Windows archive ships `pandoc.exe`, and Windows cannot execute a PE image
|
|
279
|
+
// under a name without the extension: installing it as `bin/pandoc` made the mandatory
|
|
280
|
+
// `--version` probe fail and quarantined a freshly downloaded 223 MB tool (measured on DSH
|
|
281
|
+
// Desktop). The manifest records the real name, and `probe`/`verify` read the entry from there,
|
|
282
|
+
// so resolution follows it without a second rule.
|
|
283
|
+
const installed = basename(found);
|
|
284
|
+
const target = join(binDir, installed);
|
|
209
285
|
await ctx.fs.copyFile(found, target);
|
|
210
286
|
await ctx.fs.chmod(target, 0o755);
|
|
211
287
|
await ctx.fs.rm(unpack, { recursive: true });
|
|
@@ -215,7 +291,7 @@ export function binaryArchiveProvider(options = {}) {
|
|
|
215
291
|
integrity: sriOfSha256(pack.sha256),
|
|
216
292
|
tarball: pack.url,
|
|
217
293
|
source: 'installed',
|
|
218
|
-
entry: `bin/${
|
|
294
|
+
entry: `bin/${installed}`,
|
|
219
295
|
entryDir: 'bin',
|
|
220
296
|
};
|
|
221
297
|
return ctx.publish(staging, meta);
|
|
@@ -225,7 +301,19 @@ export function binaryArchiveProvider(options = {}) {
|
|
|
225
301
|
// The published entry is the manifest's.
|
|
226
302
|
const manifest = readInstallManifest(decodeJson(await ctx.fs.readFile(join(resolved.dir, 'install.json'))), BINARY_ARCHIVE_KIND);
|
|
227
303
|
const entry = manifest?.entry;
|
|
228
|
-
|
|
304
|
+
let path;
|
|
305
|
+
if (entry === undefined) {
|
|
306
|
+
path = join(resolved.entryDir, binaryOf(spec));
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
try {
|
|
310
|
+
assertSafeRelativePath(entry);
|
|
311
|
+
path = join(resolved.dir, ...entry.split('/'));
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
throw new ProvisionError('verify/failed', `install.json 的 entry 不安全:${entry}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
229
317
|
if (!(await exists(ctx.fs, path))) {
|
|
230
318
|
throw new ProvisionError('verify/failed', `归档可执行物不存在:${path}`);
|
|
231
319
|
}
|
|
@@ -233,13 +321,13 @@ export function binaryArchiveProvider(options = {}) {
|
|
|
233
321
|
const args = spec.versionArgs ?? ['--version'];
|
|
234
322
|
if (args.length === 0)
|
|
235
323
|
return;
|
|
236
|
-
const
|
|
237
|
-
if (outcome === 'timeout') {
|
|
324
|
+
const probed = await runVersionProbe(path, args, { ...(ctx.signal === undefined ? {} : { signal: ctx.signal }) });
|
|
325
|
+
if (probed.outcome === 'timeout') {
|
|
238
326
|
// A timeout leaves the binary usable; only a failed probe fails verification.
|
|
239
327
|
ctx.logger.warn(`${path} 的 ${args.join(' ')} 探针超时;按可用处理`);
|
|
240
328
|
return;
|
|
241
329
|
}
|
|
242
|
-
if (outcome === 'failed') {
|
|
330
|
+
if (probed.outcome === 'failed') {
|
|
243
331
|
throw new ProvisionError('verify/failed', `${path} 的 ${args.join(' ')} 探针失败`);
|
|
244
332
|
}
|
|
245
333
|
},
|
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import type { Provider } from '../types.js';
|
|
2
2
|
export declare const MODEL_CACHE_KIND = "model-cache";
|
|
3
|
+
/**
|
|
4
|
+
* Maximum bytes read for a single model file.
|
|
5
|
+
*
|
|
6
|
+
* 2 GiB, not the old 4 GiB. The cap is the number of bytes the MEMORY path would have to hold TWICE
|
|
7
|
+
* (the received chunks plus the joined copy), so it bounds the worst-case spike directly; the measured
|
|
8
|
+
* ~2× peak for a body of this size is ~4 GiB, which a 16 GB machine survives and a 4 GiB cap does not.
|
|
9
|
+
* 2 GiB is also still far above what the framework is for: a real `bge-small` is ~90 MB and even a
|
|
10
|
+
* multi-billion-parameter checkpoint is a handful of files well under this. Anything larger wants a
|
|
11
|
+
* different transport, not a bigger heap.
|
|
12
|
+
*
|
|
13
|
+
* (The review's "4 GiB is exactly Node's `kMaxLength`" does NOT reproduce on the target runtime:
|
|
14
|
+
* Node 22.23.2 / linux-x64 reports `buffer.constants.MAX_LENGTH` 2^53−1 and allocates a 4 GiB
|
|
15
|
+
* `Uint8Array` fine. The rejection boundary is a legitimate design choice here, not a crash we are
|
|
16
|
+
* ducking — see the report accompanying this change.)
|
|
17
|
+
*/
|
|
18
|
+
export declare const MAX_MODEL_BYTES: number;
|
|
3
19
|
/** How an item's files are placed under `target.root`.
|
|
4
20
|
* @stable */
|
|
5
21
|
export type ModelCacheLayout = 'hub' | 'flat';
|
|
@@ -18,8 +34,15 @@ export interface ModelCacheSpec {
|
|
|
18
34
|
/** Extra files that must exist for `verify` to pass. */
|
|
19
35
|
readonly requiredFiles?: readonly string[];
|
|
20
36
|
}
|
|
21
|
-
/**
|
|
22
|
-
|
|
37
|
+
/** The options of {@link modelCacheProvider}; the byte limits are injectable so tests can exercise the
|
|
38
|
+
* spill boundary without multi-hundred-MB bodies. `@experimental` */
|
|
39
|
+
export interface ModelCacheProviderOptions {
|
|
23
40
|
readonly id?: string;
|
|
24
|
-
})
|
|
41
|
+
/** Hard per-file byte cap; default {@link MAX_MODEL_BYTES} (2 GiB). */
|
|
42
|
+
readonly maxBytes?: number;
|
|
43
|
+
/** Body size above which a file streams to disk; default {@link SPILL_THRESHOLD_BYTES} (64 MiB). */
|
|
44
|
+
readonly spillAtBytes?: number;
|
|
45
|
+
}
|
|
46
|
+
/** Build the built-in model-cache provider. */
|
|
47
|
+
export declare function modelCacheProvider(options?: ModelCacheProviderOptions): Provider;
|
|
25
48
|
//# sourceMappingURL=model.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/providers/model.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"model.d.ts","sourceRoot":"","sources":["../../src/providers/model.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAGV,QAAQ,EAOT,MAAM,aAAa,CAAA;AAEpB,eAAO,MAAM,gBAAgB,gBAAgB,CAAA;AAG7C;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,eAAe,QAAyB,CAAA;AAuBrD;cACc;AACd,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,MAAM,CAAA;AAY7C,0CAA0C;AAC1C,MAAM,WAAW,cAAc;IAC7B,4DAA4D;IAC5D,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,sCAAsC;IACtC,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,qCAAqC;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,gBAAgB,CAAA;IAClC,2DAA2D;IAC3D,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IAClC,wDAAwD;IACxD,QAAQ,CAAC,aAAa,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;CAC3C;AA4gBD;sEACsE;AACtE,MAAM,WAAW,yBAAyB;IACxC,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAA;IACpB,uEAAuE;IACvE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,oGAAoG;IACpG,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAA;CAC/B;AAED,+CAA+C;AAC/C,wBAAgB,kBAAkB,CAAC,OAAO,GAAE,yBAA8B,GAAG,QAAQ,CA8GpF"}
|