@orkestrel/test 0.0.7 → 0.0.8
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/src/browser/index.d.ts +684 -10
- package/dist/src/browser/index.js +971 -82
- package/dist/src/browser/index.js.map +1 -1
- package/dist/src/core/index.cjs +156 -0
- package/dist/src/core/index.cjs.map +1 -1
- package/dist/src/core/index.d.cts +87 -0
- package/dist/src/core/index.d.ts +87 -0
- package/dist/src/core/index.js +153 -1
- package/dist/src/core/index.js.map +1 -1
- package/dist/src/server/index.cjs +199 -2
- package/dist/src/server/index.cjs.map +1 -1
- package/dist/src/server/index.d.cts +122 -4
- package/dist/src/server/index.d.ts +122 -4
- package/dist/src/server/index.js +195 -3
- package/dist/src/server/index.js.map +1 -1
- package/package.json +6 -4
|
@@ -2,6 +2,7 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
2
2
|
let node_fs = require("node:fs");
|
|
3
3
|
let node_path = require("node:path");
|
|
4
4
|
let node_url = require("node:url");
|
|
5
|
+
let _src_core = require("../core/index.cjs");
|
|
5
6
|
let node_events = require("node:events");
|
|
6
7
|
let node_os = require("node:os");
|
|
7
8
|
//#region src/server/constants.ts
|
|
@@ -60,6 +61,30 @@ function isExcluded(key, exclusions) {
|
|
|
60
61
|
return exclusions.some((rule) => rule === "" || key === rule || key.startsWith(`${rule}/`));
|
|
61
62
|
}
|
|
62
63
|
/**
|
|
64
|
+
* Creates a symbolic link with a directory-junction fallback for hosts that refuse symbolic links.
|
|
65
|
+
*
|
|
66
|
+
* @param path - The path where the link is created.
|
|
67
|
+
* @param source - The destination path the link points at.
|
|
68
|
+
* @throws The original link error when its code is not `EPERM`, or when the source names an
|
|
69
|
+
* existing non-directory; otherwise, any error from inspecting the source or creating the junction.
|
|
70
|
+
* @remarks Only `EPERM` from the first symbolic-link attempt triggers the fallback. The fallback
|
|
71
|
+
* resolves the source against the link's directory. An existing non-directory rethrows the original
|
|
72
|
+
* `EPERM`, while a directory or missing source is passed to a junction attempt. A missing source is
|
|
73
|
+
* accepted to create a dangling junction. Where the host creates a junction, its stored value is the
|
|
74
|
+
* resolved absolute path.
|
|
75
|
+
*/
|
|
76
|
+
function createLink(path, source) {
|
|
77
|
+
try {
|
|
78
|
+
(0, node_fs.symlinkSync)(source, path);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if ((typeof error === "object" && error !== null && "code" in error && typeof error.code === "string" ? error.code : void 0) !== "EPERM") throw error;
|
|
81
|
+
const resolved = (0, node_path.resolve)((0, node_path.dirname)(path), source);
|
|
82
|
+
const status = (0, node_fs.statSync)(resolved, { throwIfNoEntry: false });
|
|
83
|
+
if (status !== void 0 && !status.isDirectory()) throw error;
|
|
84
|
+
(0, node_fs.symlinkSync)(resolved, path, "junction");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
63
88
|
* Removes a directory tree, retrying past a transient Windows handle-release race.
|
|
64
89
|
*
|
|
65
90
|
* @param path - The absolute directory to remove.
|
|
@@ -70,7 +95,8 @@ function isExcluded(key, exclusions) {
|
|
|
70
95
|
* `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed
|
|
71
96
|
* against a real held directory, they neither delay nor retry before rethrowing, so the retry
|
|
72
97
|
* is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait
|
|
73
|
-
* at roughly one second.
|
|
98
|
+
* at roughly one second. A hold that outlasts that second is {@link destroyScratch}'s case, which
|
|
99
|
+
* retries every refusal inside a caller's budget rather than the codes named here.
|
|
74
100
|
*/
|
|
75
101
|
function removeTree(path) {
|
|
76
102
|
for (let attempt = 1;; attempt++) try {
|
|
@@ -154,6 +180,139 @@ function readInventory(root, targets, options) {
|
|
|
154
180
|
}
|
|
155
181
|
return Object.fromEntries(Array.from(contents).sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0));
|
|
156
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Reports whether a process id names a live process.
|
|
185
|
+
*
|
|
186
|
+
* @param pid - The process id to read.
|
|
187
|
+
* @returns True if a process holds that id at the moment of the call; false otherwise, including a
|
|
188
|
+
* pid the host refuses.
|
|
189
|
+
* @throws Nothing. Every host refusal reads as false.
|
|
190
|
+
* @remarks This is an instantaneous observation rather than a claim of ownership. A host reuses a
|
|
191
|
+
* process id after the process holding it exits, so a true answer says some process holds that id now
|
|
192
|
+
* and never says it is the process the caller started. Two host answers are worth knowing. A POSIX
|
|
193
|
+
* host refuses signal `0` to a process another user owns with `EPERM`, and that refusal reads as
|
|
194
|
+
* false here. A pid of `0` names the caller's own process group on POSIX and the system idle process
|
|
195
|
+
* on Windows, so it reads as true on both without naming a process anyone started.
|
|
196
|
+
*
|
|
197
|
+
* A Linux zombie — a process that has exited and whose parent has not reaped it — still accepts
|
|
198
|
+
* signal `0`, so its `/proc` status is read and a `Z` state reads as false.
|
|
199
|
+
*/
|
|
200
|
+
function isRunning(pid) {
|
|
201
|
+
try {
|
|
202
|
+
process.kill(pid, 0);
|
|
203
|
+
} catch {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
if (process.platform !== "linux") return true;
|
|
207
|
+
try {
|
|
208
|
+
const status = (0, node_fs.readFileSync)(`/proc/${String(pid)}/stat`, "utf8");
|
|
209
|
+
const boundary = status.lastIndexOf(") ");
|
|
210
|
+
return boundary < 0 || status.slice(boundary + 2, boundary + 3) !== "Z";
|
|
211
|
+
} catch {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Waits for a socket to close, accepting a peer reset as a forced close.
|
|
217
|
+
*
|
|
218
|
+
* @param socket - The socket to wait on. One that has already closed resolves without listening.
|
|
219
|
+
* @param options - The time bounds and abort signal.
|
|
220
|
+
* @returns A promise that resolves when the socket emits `close`.
|
|
221
|
+
* @throws The socket's own error when its code is not `ECONNRESET`, the abort reason, or an `Error`
|
|
222
|
+
* when a bound is invalid or the socket does not close within the budget.
|
|
223
|
+
* @remarks Default budget: `1000` milliseconds. A reset is the peer forcing the connection down, and
|
|
224
|
+
* the socket still emits `close` afterwards, so `ECONNRESET` is waited past rather than raised while
|
|
225
|
+
* every other error ends the wait. The interval is validated for consistency with the wait family but
|
|
226
|
+
* is not used, because this helper parks on the socket's events. Both listeners are removed on every
|
|
227
|
+
* settlement, so a caller may wait on one socket repeatedly.
|
|
228
|
+
*/
|
|
229
|
+
async function waitForSocketClose(socket, options) {
|
|
230
|
+
const budget = options?.budget ?? 1e3;
|
|
231
|
+
const interval = options?.interval ?? 10;
|
|
232
|
+
if (!Number.isFinite(budget) || budget < 0) throw new Error("Socket budget must be finite and non-negative");
|
|
233
|
+
if (!Number.isFinite(interval) || interval < 0) throw new Error("Socket interval must be finite and non-negative");
|
|
234
|
+
const signal = options?.signal;
|
|
235
|
+
signal?.throwIfAborted();
|
|
236
|
+
if (socket.closed) return;
|
|
237
|
+
const closed = Promise.withResolvers();
|
|
238
|
+
const failed = Promise.withResolvers();
|
|
239
|
+
const expiry = Promise.withResolvers();
|
|
240
|
+
const aborted = Promise.withResolvers();
|
|
241
|
+
const subscription = new AbortController();
|
|
242
|
+
socket.on("close", closed.resolve);
|
|
243
|
+
socket.on("error", failed.resolve);
|
|
244
|
+
signal?.addEventListener("abort", () => aborted.reject(signal.reason), {
|
|
245
|
+
once: true,
|
|
246
|
+
signal: subscription.signal
|
|
247
|
+
});
|
|
248
|
+
const timer = setTimeout(() => {
|
|
249
|
+
expiry.reject(/* @__PURE__ */ new Error(`Socket did not close within ${budget}ms`));
|
|
250
|
+
}, budget);
|
|
251
|
+
try {
|
|
252
|
+
const error = await Promise.race([
|
|
253
|
+
closed.promise.then(() => void 0),
|
|
254
|
+
failed.promise,
|
|
255
|
+
expiry.promise,
|
|
256
|
+
aborted.promise
|
|
257
|
+
]);
|
|
258
|
+
if (error === void 0) return;
|
|
259
|
+
if (error.code !== "ECONNRESET") throw error;
|
|
260
|
+
await Promise.race([
|
|
261
|
+
closed.promise,
|
|
262
|
+
expiry.promise,
|
|
263
|
+
aborted.promise
|
|
264
|
+
]);
|
|
265
|
+
} finally {
|
|
266
|
+
clearTimeout(timer);
|
|
267
|
+
subscription.abort();
|
|
268
|
+
socket.off("close", closed.resolve);
|
|
269
|
+
socket.off("error", failed.resolve);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* Destroys a scratch directory, retrying until the host releases it.
|
|
274
|
+
*
|
|
275
|
+
* @param scratch - The scratch directory to destroy.
|
|
276
|
+
* @param options - The time bounds and abort signal.
|
|
277
|
+
* @returns A promise that resolves once `destroy()` returns without throwing.
|
|
278
|
+
* @throws The abort reason, or an `Error` when a bound is invalid or the budget elapses. The
|
|
279
|
+
* exhaustion error carries the last host refusal as its `cause`.
|
|
280
|
+
* @remarks Default budget: `10000` milliseconds. Default interval: `25` milliseconds. A host holds a
|
|
281
|
+
* directory for a short interval after the process that held it exits, and a just-stopped child's
|
|
282
|
+
* working directory is the case this exists for, so removal is attempted until the host lets go
|
|
283
|
+
* rather than exactly once. {@link ScratchInterface.destroy} stays synchronous and is unchanged; this
|
|
284
|
+
* is the bounded retry around it. A directory nothing releases still fails, with the host's own
|
|
285
|
+
* refusal as the `cause`.
|
|
286
|
+
*
|
|
287
|
+
* Every refusal is retried, deliberately, and that is wider than {@link removeTree}'s policy: that
|
|
288
|
+
* one retries the codes {@link REMOVE_TREE_RETRYABLE_CODES} names and rethrows the rest at once.
|
|
289
|
+
* The hold this waits out is not classifiable across hosts — Windows reports a working-directory
|
|
290
|
+
* hold as `EPERM`, POSIX hosts and network filesystems report their own — so a code list here would
|
|
291
|
+
* be a list of the hosts it had been run on. The residual is the cost of that: a fault no wait can
|
|
292
|
+
* clear, such as a path removed from under the allocation or a permission the process never had,
|
|
293
|
+
* spends the whole budget before it surfaces, and it surfaces wrapped in the exhaustion error with
|
|
294
|
+
* the host's refusal as `cause` rather than by identity. Pass a shorter `budget` or a `signal`
|
|
295
|
+
* wherever a caller must bound that cost.
|
|
296
|
+
*/
|
|
297
|
+
async function destroyScratch(scratch, options) {
|
|
298
|
+
const budget = options?.budget ?? 1e4;
|
|
299
|
+
const interval = options?.interval ?? 25;
|
|
300
|
+
if (!Number.isFinite(budget) || budget < 0) throw new Error("Scratch budget must be finite and non-negative");
|
|
301
|
+
if (!Number.isFinite(interval) || interval < 0) throw new Error("Scratch interval must be finite and non-negative");
|
|
302
|
+
const start = performance.now();
|
|
303
|
+
let refusal;
|
|
304
|
+
while (true) {
|
|
305
|
+
options?.signal?.throwIfAborted();
|
|
306
|
+
try {
|
|
307
|
+
scratch.destroy();
|
|
308
|
+
return;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
refusal = error;
|
|
311
|
+
}
|
|
312
|
+
if (performance.now() - start >= budget) throw new Error(`Scratch directory was not destroyed within ${budget}ms`, { cause: refusal });
|
|
313
|
+
await (0, _src_core.waitForDelay)(interval);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
157
316
|
//#endregion
|
|
158
317
|
//#region src/server/factories.ts
|
|
159
318
|
/**
|
|
@@ -244,7 +403,7 @@ function createScratch(options) {
|
|
|
244
403
|
if (candidate === void 0) throw new Error(`${outside}: ${target}`);
|
|
245
404
|
if (!scratch.has(".")) throw new Error("Scratch directory does not exist");
|
|
246
405
|
(0, node_fs.mkdirSync)((0, node_path.dirname)(candidate), { recursive: true });
|
|
247
|
-
(
|
|
406
|
+
createLink(candidate, source);
|
|
248
407
|
},
|
|
249
408
|
remove(target) {
|
|
250
409
|
const candidate = resolveContained(path, target);
|
|
@@ -303,16 +462,54 @@ async function createLoopback(server) {
|
|
|
303
462
|
}
|
|
304
463
|
};
|
|
305
464
|
}
|
|
465
|
+
/**
|
|
466
|
+
* Creates a cookie jar that records a real response's cookies and replays them as one header.
|
|
467
|
+
*
|
|
468
|
+
* @returns The rendered request header, and the members that read and capture cookies.
|
|
469
|
+
* @remarks Selection is by name alone: no `Domain` or `Path` matching, no `Expires` or `Secure`
|
|
470
|
+
* handling, and no persistence beyond the jar. That is what a test driving one origin over one path
|
|
471
|
+
* needs, and a fixture needing a browser's cookie store needs a browser rather than this.
|
|
472
|
+
*/
|
|
473
|
+
function createCookieJar() {
|
|
474
|
+
const cookies = /* @__PURE__ */ new Map();
|
|
475
|
+
return {
|
|
476
|
+
get header() {
|
|
477
|
+
const pairs = [...cookies].map(([name, value]) => `${name}=${value}`);
|
|
478
|
+
return pairs.length === 0 ? void 0 : pairs.join("; ");
|
|
479
|
+
},
|
|
480
|
+
read(name) {
|
|
481
|
+
return cookies.get(name);
|
|
482
|
+
},
|
|
483
|
+
capture(response) {
|
|
484
|
+
const fields = response.headers.getSetCookie();
|
|
485
|
+
for (const field of fields) {
|
|
486
|
+
const boundary = field.indexOf(";");
|
|
487
|
+
const pair = boundary < 0 ? field : field.slice(0, boundary);
|
|
488
|
+
const separator = pair.indexOf("=");
|
|
489
|
+
if (separator < 1) continue;
|
|
490
|
+
const name = pair.slice(0, separator);
|
|
491
|
+
if (/;\s*max-age\s*=\s*0\s*(?:;|$)/iu.test(field)) cookies.delete(name);
|
|
492
|
+
else cookies.set(name, pair.slice(separator + 1));
|
|
493
|
+
}
|
|
494
|
+
return fields;
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
}
|
|
306
498
|
//#endregion
|
|
307
499
|
exports.REMOVE_TREE_MAX_ATTEMPTS = REMOVE_TREE_MAX_ATTEMPTS;
|
|
308
500
|
exports.REMOVE_TREE_RETRYABLE_CODES = REMOVE_TREE_RETRYABLE_CODES;
|
|
309
501
|
exports.REMOVE_TREE_RETRY_DELAY_MS = REMOVE_TREE_RETRY_DELAY_MS;
|
|
502
|
+
exports.createCookieJar = createCookieJar;
|
|
503
|
+
exports.createLink = createLink;
|
|
310
504
|
exports.createLoopback = createLoopback;
|
|
311
505
|
exports.createScratch = createScratch;
|
|
506
|
+
exports.destroyScratch = destroyScratch;
|
|
312
507
|
exports.isExcluded = isExcluded;
|
|
508
|
+
exports.isRunning = isRunning;
|
|
313
509
|
exports.matchesIdentity = matchesIdentity;
|
|
314
510
|
exports.readInventory = readInventory;
|
|
315
511
|
exports.removeTree = removeTree;
|
|
316
512
|
exports.resolveContained = resolveContained;
|
|
513
|
+
exports.waitForSocketClose = waitForSocketClose;
|
|
317
514
|
|
|
318
515
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/server/constants.ts","../../../src/server/helpers.ts","../../../src/server/factories.ts"],"sourcesContent":["/**\n * The attempts `removeTree` makes before rethrowing a retryable removal error.\n */\nexport const REMOVE_TREE_MAX_ATTEMPTS = 10\n\n/**\n * The synchronous delay, in milliseconds, `removeTree` waits between attempts.\n */\nexport const REMOVE_TREE_RETRY_DELAY_MS = 100\n\n/**\n * The error codes `removeTree` retries; every other code rethrows immediately.\n */\nexport const REMOVE_TREE_RETRYABLE_CODES: readonly string[] = Object.freeze([\n\t'EBUSY',\n\t'ENOTEMPTY',\n\t'EPERM',\n])\n","import type { InventoryOptions, ScratchIdentity } from './types.js'\nimport { lstatSync, readdirSync, readFileSync, realpathSync, rmSync } from 'node:fs'\nimport { isAbsolute, relative, resolve, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport {\n\tREMOVE_TREE_MAX_ATTEMPTS,\n\tREMOVE_TREE_RETRY_DELAY_MS,\n\tREMOVE_TREE_RETRYABLE_CODES,\n} from './constants.js'\n\n/**\n * Resolves a target that stays below a root directory.\n *\n * @param root - The absolute root directory.\n * @param target - The relative or absolute target to resolve.\n * @returns The absolute target, or `undefined` when the target escapes the root.\n */\nexport function resolveContained(root: string, target: string): string | undefined {\n\tconst candidate = resolve(root, target)\n\tconst contained = relative(root, candidate)\n\t// Cross-drive containment is unproven because POSIX `relative` never returns an absolute path;\n\t// a Windows gate would drive this branch.\n\tif (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {\n\t\treturn undefined\n\t}\n\treturn candidate\n}\n\n/**\n * Reports whether two directory identities name the same allocation.\n *\n * @param current - The identity read from the path now.\n * @param allocation - The identity recorded when the directory was allocated.\n * @returns Whether the device, the index node, and the creation time all match.\n * @remarks All three fields are compared because none of them alone identifies an allocation. A\n * device is shared by every directory on one filesystem, an index node is reused once its directory\n * is removed, and a creation time repeats within the host's timestamp resolution.\n */\nexport function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean {\n\treturn (\n\t\tcurrent.device === allocation.device &&\n\t\tcurrent.inode === allocation.inode &&\n\t\tcurrent.birth === allocation.birth\n\t)\n}\n\n/**\n * Reports whether a root-relative key matches an exclusion.\n *\n * @param key - The root-relative key to test.\n * @param exclusions - The normalized root-relative exclusion keys.\n * @returns Whether an exclusion names the key or one of its ancestors.\n */\nexport function isExcluded(key: string, exclusions: readonly string[]): boolean {\n\treturn exclusions.some((rule) => rule === '' || key === rule || key.startsWith(`${rule}/`))\n}\n\n/**\n * Removes a directory tree, retrying past a transient Windows handle-release race.\n *\n * @param path - The absolute directory to remove.\n * @throws The last removal error once {@link REMOVE_TREE_MAX_ATTEMPTS} attempts are exhausted,\n * or immediately for any error whose code is not in {@link REMOVE_TREE_RETRYABLE_CODES}.\n * @remarks On Windows, a directory that a just-exited process still holds as its current\n * working directory throws `EPERM` for a short interval after that process exits. Node's own\n * `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed\n * against a real held directory, they neither delay nor retry before rethrowing, so the retry\n * is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait\n * at roughly one second.\n */\nexport function removeTree(path: string): void {\n\tfor (let attempt = 1; ; attempt++) {\n\t\ttry {\n\t\t\trmSync(path, { force: true, recursive: true })\n\t\t\treturn\n\t\t} catch (error) {\n\t\t\tconst code =\n\t\t\t\ttypeof error === 'object' &&\n\t\t\t\terror !== null &&\n\t\t\t\t'code' in error &&\n\t\t\t\ttypeof error.code === 'string'\n\t\t\t\t\t? error.code\n\t\t\t\t\t: undefined\n\t\t\tif (\n\t\t\t\tcode === undefined ||\n\t\t\t\t!REMOVE_TREE_RETRYABLE_CODES.includes(code) ||\n\t\t\t\tattempt >= REMOVE_TREE_MAX_ATTEMPTS\n\t\t\t) {\n\t\t\t\tthrow error\n\t\t\t}\n\t\t\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, REMOVE_TREE_RETRY_DELAY_MS)\n\t\t}\n\t}\n}\n\n/**\n * Reads files from selected targets below a root directory.\n *\n * @param root - The root directory as a path or file URL.\n * @param targets - The files to read directly and directories to visit below the root.\n * @param options - Optional file extension and path exclusions.\n * @returns File contents keyed by sorted root-relative paths.\n * @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves\n * outside the root.\n * @remarks A named file is included regardless of the extension filter. An absent extension filter\n * includes every walked file. An exclusion matches whole root-relative key segments and covers every\n * key below it, and it applies to a named target and a walked entry alike.\n */\nexport function readInventory(\n\troot: URL | string,\n\ttargets: readonly string[],\n\toptions?: InventoryOptions,\n): Readonly<Record<string, string>> {\n\tconst supplied = resolve(typeof root === 'string' ? root : fileURLToPath(root))\n\tconst rootStatus = lstatSync(supplied)\n\tif (rootStatus.isSymbolicLink()) throw new Error('Root is a symbolic link')\n\tif (!rootStatus.isDirectory()) throw new Error('Root is not a directory')\n\n\tconst base = realpathSync.native(supplied)\n\tif (targets.length === 0) return Object.fromEntries([])\n\n\tconst exclusions = (options?.exclude ?? []).map((rule) => {\n\t\tconst unprefixed = rule.startsWith('./') ? rule.slice(2) : rule\n\t\tconst collapsed = unprefixed.replace(/\\/+/g, '/')\n\t\tconst untrailed = collapsed.endsWith('/') ? collapsed.slice(0, -1) : collapsed\n\t\treturn untrailed === '.' ? '' : untrailed\n\t})\n\tconst pending: string[] = []\n\tconst queued = new Set<string>()\n\tconst contents = new Map<string, string>()\n\n\tfor (const target of targets) {\n\t\tconst candidate = resolveContained(base, target)\n\t\tif (candidate === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst status = lstatSync(candidate)\n\t\tif (status.isSymbolicLink()) throw new Error(`Target is a symbolic link: ${target}`)\n\t\tif (!status.isDirectory() && !status.isFile()) {\n\t\t\tthrow new Error(`Target is not a file or directory: ${target}`)\n\t\t}\n\n\t\tconst physical = realpathSync.native(candidate)\n\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\tif (resolved === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst key = relative(base, resolved).split(sep).join('/')\n\t\tif (isExcluded(key, exclusions)) continue\n\t\tif (status.isFile()) {\n\t\t\tcontents.set(key, readFileSync(physical, 'utf8'))\n\t\t\tcontinue\n\t\t}\n\t\tif (queued.has(physical)) continue\n\t\tqueued.add(physical)\n\t\tpending.push(physical)\n\t}\n\n\twhile (pending.length > 0) {\n\t\tconst directory = pending.pop()\n\t\tif (directory === undefined) continue\n\n\t\tfor (const entry of readdirSync(directory, { withFileTypes: true })) {\n\t\t\tconst path = resolve(directory, entry.name)\n\t\t\tconst status = lstatSync(path)\n\t\t\tif (status.isSymbolicLink()) continue\n\n\t\t\tconst key = relative(base, path).split(sep).join('/')\n\t\t\tif (isExcluded(key, exclusions)) continue\n\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tconst physical = realpathSync.native(path)\n\t\t\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\t\t\t// Walk containment is unproven because POSIX CI skips links before `realpath`;\n\t\t\t\t// a host that resolves a walked directory outside `base` would drive this branch.\n\t\t\t\tif (resolved === undefined || queued.has(physical)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tqueued.add(physical)\n\t\t\t\tpending.push(physical)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!status.isFile() ||\n\t\t\t\t(options?.extensions !== undefined &&\n\t\t\t\t\t!options.extensions.some((extension) => entry.name.endsWith(extension)))\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcontents.set(key, readFileSync(path, 'utf8'))\n\t\t}\n\t}\n\n\treturn Object.fromEntries(\n\t\tArray.from(contents).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),\n\t)\n}\n","import type { Server } from 'node:net'\nimport type {\n\tLoopbackInterface,\n\tScratchIdentity,\n\tScratchInterface,\n\tScratchOptions,\n} from './types.js'\nimport { once } from 'node:events'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treadFileSync,\n\treaddirSync,\n\tstatSync,\n\tsymlinkSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { dirname, resolve, sep } from 'node:path'\nimport { matchesIdentity, removeTree, resolveContained } from './helpers.js'\n\n/**\n * Allocates an owned temporary directory with contained file operations.\n *\n * @param options - Optional parent directory, name prefix, and initial files.\n * @returns The scratch directory and its file operations.\n * @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains\n * `/` or `\\`; or when allocation or seeding fails.\n * @remarks The parent defaults to the host temporary directory. The prefix defaults to\n * `orkestrel-test-`. Seed keys use root-relative paths.\n */\nexport function createScratch(options?: ScratchOptions): ScratchInterface {\n\tconst parent = resolve(options?.parent ?? tmpdir())\n\tconst parentStatus = lstatSync(parent, { throwIfNoEntry: false })\n\tif (parentStatus === undefined) throw new Error('Scratch parent does not exist')\n\tif (parentStatus.isSymbolicLink()) throw new Error('Scratch parent is a symbolic link')\n\tif (!parentStatus.isDirectory()) throw new Error('Scratch parent is not a directory')\n\n\tconst prefix = options?.prefix ?? 'orkestrel-test-'\n\tif (prefix.includes('/') || prefix.includes('\\\\')) {\n\t\tthrow new Error('Scratch prefix must be a name fragment')\n\t}\n\n\tconst path = mkdtempSync(`${parent}${sep}${prefix}`)\n\tconst allocated = statSync(path)\n\tconst allocation: ScratchIdentity = {\n\t\tbirth: allocated.birthtimeMs,\n\t\tdevice: allocated.dev,\n\t\tinode: allocated.ino,\n\t}\n\tconst outside = 'Path outside scratch directory'\n\tconst unremovable = 'Scratch directory is not a removable target'\n\ttry {\n\t\tfor (const [target, text] of Object.entries(options?.files ?? {})) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t}\n\t} catch (error) {\n\t\tremoveTree(path)\n\t\tthrow error\n\t}\n\n\tconst scratch: ScratchInterface = {\n\t\tpath,\n\t\twrite(target, text) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t},\n\t\tread(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has(target)) return undefined\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return undefined\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is a directory: ${target}`)\n\t\t\t}\n\t\t\treturn readFileSync(candidate, 'utf8')\n\t\t},\n\t\thas(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tconst rootStatus = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (rootStatus === undefined) return false\n\t\t\tif (rootStatus.isSymbolicLink()) throw new Error('Scratch directory is a symbolic link')\n\t\t\tif (!rootStatus.isDirectory()) throw new Error('Scratch path is not a directory')\n\n\t\t\treturn lstatSync(candidate, { throwIfNoEntry: false }) !== undefined\n\t\t},\n\t\tnames(target = '.') {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) throw new Error(`Scratch path does not exist: ${target}`)\n\t\t\tif (!status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`)\n\t\t\treturn readdirSync(candidate).sort()\n\t\t},\n\t\tensure(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined && !status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is not a directory: ${target}`)\n\t\t\t}\n\t\t\tif (status === undefined) mkdirSync(candidate, { recursive: true })\n\t\t\treturn candidate\n\t\t},\n\t\tlink(target, source) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\tsymlinkSync(source, candidate)\n\t\t},\n\t\tremove(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (candidate === path) throw new Error(`${unremovable}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = lstatSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined) {\n\t\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\t\tdevice: status.dev,\n\t\t\t\t\tinode: status.ino,\n\t\t\t\t}\n\t\t\t\tif (matchesIdentity(identity, allocation)) {\n\t\t\t\t\tthrow new Error(`${unremovable}: ${target}`)\n\t\t\t\t}\n\t\t\t}\n\t\t\tremoveTree(candidate)\n\t\t},\n\t\tdestroy() {\n\t\t\tconst status = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return\n\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\tdevice: status.dev,\n\t\t\t\tinode: status.ino,\n\t\t\t}\n\t\t\tif (!matchesIdentity(identity, allocation)) return\n\t\t\tremoveTree(path)\n\t\t},\n\t}\n\treturn scratch\n}\n\n/**\n * Starts a server on an ephemeral IPv4 loopback port.\n *\n * @param server - The unstarted server to bind.\n * @returns The bound origin, assigned port, and asynchronous teardown.\n * @throws When the server cannot bind or reports an address without a numeric port.\n */\nexport async function createLoopback(server: Server): Promise<LoopbackInterface> {\n\tserver.listen(0, '127.0.0.1')\n\tawait once(server, 'listening')\n\n\tconst address = server.address()\n\tif (\n\t\ttypeof address !== 'object' ||\n\t\taddress === null ||\n\t\t!('port' in address) ||\n\t\ttypeof address.port !== 'number'\n\t) {\n\t\tthrow new Error(`Loopback address must have a numeric port; found ${String(address)}`)\n\t}\n\n\tconst port = address.port\n\tlet destruction: Promise<void> | undefined\n\treturn {\n\t\turl: `http://127.0.0.1:${port}`,\n\t\tport,\n\t\tdestroy() {\n\t\t\tif (destruction === undefined) {\n\t\t\t\tdestruction = new Promise<void>((resolveClose, rejectClose) => {\n\t\t\t\t\tif ('closeAllConnections' in server && typeof server.closeAllConnections === 'function') {\n\t\t\t\t\t\tserver.closeAllConnections()\n\t\t\t\t\t}\n\t\t\t\t\tserver.close((error) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\terror === undefined ||\n\t\t\t\t\t\t\t('code' in error && error.code === 'ERR_SERVER_NOT_RUNNING')\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tresolveClose()\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trejectClose(error)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn destruction\n\t\t},\n\t}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.cjs","names":[],"sources":["../../../src/server/constants.ts","../../../src/server/helpers.ts","../../../src/server/factories.ts"],"sourcesContent":["/**\n * The attempts `removeTree` makes before rethrowing a retryable removal error.\n */\nexport const REMOVE_TREE_MAX_ATTEMPTS = 10\n\n/**\n * The synchronous delay, in milliseconds, `removeTree` waits between attempts.\n */\nexport const REMOVE_TREE_RETRY_DELAY_MS = 100\n\n/**\n * The error codes `removeTree` retries; every other code rethrows immediately.\n */\nexport const REMOVE_TREE_RETRYABLE_CODES: readonly string[] = Object.freeze([\n\t'EBUSY',\n\t'ENOTEMPTY',\n\t'EPERM',\n])\n","import type { Socket } from 'node:net'\nimport type { WaitOptions } from '@src/core'\nimport type { InventoryOptions, ScratchIdentity, ScratchInterface } from './types.js'\nimport {\n\tlstatSync,\n\treaddirSync,\n\treadFileSync,\n\trealpathSync,\n\trmSync,\n\tstatSync,\n\tsymlinkSync,\n} from 'node:fs'\nimport { dirname, isAbsolute, relative, resolve, sep } from 'node:path'\nimport { fileURLToPath } from 'node:url'\nimport { waitForDelay } from '@src/core'\nimport {\n\tREMOVE_TREE_MAX_ATTEMPTS,\n\tREMOVE_TREE_RETRY_DELAY_MS,\n\tREMOVE_TREE_RETRYABLE_CODES,\n} from './constants.js'\n\n/**\n * Resolves a target that stays below a root directory.\n *\n * @param root - The absolute root directory.\n * @param target - The relative or absolute target to resolve.\n * @returns The absolute target, or `undefined` when the target escapes the root.\n */\nexport function resolveContained(root: string, target: string): string | undefined {\n\tconst candidate = resolve(root, target)\n\tconst contained = relative(root, candidate)\n\t// Cross-drive containment is unproven because POSIX `relative` never returns an absolute path;\n\t// a Windows gate would drive this branch.\n\tif (contained === '..' || contained.startsWith(`..${sep}`) || isAbsolute(contained)) {\n\t\treturn undefined\n\t}\n\treturn candidate\n}\n\n/**\n * Reports whether two directory identities name the same allocation.\n *\n * @param current - The identity read from the path now.\n * @param allocation - The identity recorded when the directory was allocated.\n * @returns Whether the device, the index node, and the creation time all match.\n * @remarks All three fields are compared because none of them alone identifies an allocation. A\n * device is shared by every directory on one filesystem, an index node is reused once its directory\n * is removed, and a creation time repeats within the host's timestamp resolution.\n */\nexport function matchesIdentity(current: ScratchIdentity, allocation: ScratchIdentity): boolean {\n\treturn (\n\t\tcurrent.device === allocation.device &&\n\t\tcurrent.inode === allocation.inode &&\n\t\tcurrent.birth === allocation.birth\n\t)\n}\n\n/**\n * Reports whether a root-relative key matches an exclusion.\n *\n * @param key - The root-relative key to test.\n * @param exclusions - The normalized root-relative exclusion keys.\n * @returns Whether an exclusion names the key or one of its ancestors.\n */\nexport function isExcluded(key: string, exclusions: readonly string[]): boolean {\n\treturn exclusions.some((rule) => rule === '' || key === rule || key.startsWith(`${rule}/`))\n}\n\n/**\n * Creates a symbolic link with a directory-junction fallback for hosts that refuse symbolic links.\n *\n * @param path - The path where the link is created.\n * @param source - The destination path the link points at.\n * @throws The original link error when its code is not `EPERM`, or when the source names an\n * existing non-directory; otherwise, any error from inspecting the source or creating the junction.\n * @remarks Only `EPERM` from the first symbolic-link attempt triggers the fallback. The fallback\n * resolves the source against the link's directory. An existing non-directory rethrows the original\n * `EPERM`, while a directory or missing source is passed to a junction attempt. A missing source is\n * accepted to create a dangling junction. Where the host creates a junction, its stored value is the\n * resolved absolute path.\n */\nexport function createLink(path: string, source: string): void {\n\ttry {\n\t\tsymlinkSync(source, path)\n\t} catch (error) {\n\t\tconst code =\n\t\t\ttypeof error === 'object' &&\n\t\t\terror !== null &&\n\t\t\t'code' in error &&\n\t\t\ttypeof error.code === 'string'\n\t\t\t\t? error.code\n\t\t\t\t: undefined\n\t\tif (code !== 'EPERM') throw error\n\n\t\tconst resolved = resolve(dirname(path), source)\n\t\tconst status = statSync(resolved, { throwIfNoEntry: false })\n\t\tif (status !== undefined && !status.isDirectory()) throw error\n\t\tsymlinkSync(resolved, path, 'junction')\n\t}\n}\n\n/**\n * Removes a directory tree, retrying past a transient Windows handle-release race.\n *\n * @param path - The absolute directory to remove.\n * @throws The last removal error once {@link REMOVE_TREE_MAX_ATTEMPTS} attempts are exhausted,\n * or immediately for any error whose code is not in {@link REMOVE_TREE_RETRYABLE_CODES}.\n * @remarks On Windows, a directory that a just-exited process still holds as its current\n * working directory throws `EPERM` for a short interval after that process exits. Node's own\n * `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed\n * against a real held directory, they neither delay nor retry before rethrowing, so the retry\n * is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait\n * at roughly one second. A hold that outlasts that second is {@link destroyScratch}'s case, which\n * retries every refusal inside a caller's budget rather than the codes named here.\n */\nexport function removeTree(path: string): void {\n\tfor (let attempt = 1; ; attempt++) {\n\t\ttry {\n\t\t\trmSync(path, { force: true, recursive: true })\n\t\t\treturn\n\t\t} catch (error) {\n\t\t\tconst code =\n\t\t\t\ttypeof error === 'object' &&\n\t\t\t\terror !== null &&\n\t\t\t\t'code' in error &&\n\t\t\t\ttypeof error.code === 'string'\n\t\t\t\t\t? error.code\n\t\t\t\t\t: undefined\n\t\t\tif (\n\t\t\t\tcode === undefined ||\n\t\t\t\t!REMOVE_TREE_RETRYABLE_CODES.includes(code) ||\n\t\t\t\tattempt >= REMOVE_TREE_MAX_ATTEMPTS\n\t\t\t) {\n\t\t\t\tthrow error\n\t\t\t}\n\t\t\tAtomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, REMOVE_TREE_RETRY_DELAY_MS)\n\t\t}\n\t}\n}\n\n/**\n * Reads files from selected targets below a root directory.\n *\n * @param root - The root directory as a path or file URL.\n * @param targets - The files to read directly and directories to visit below the root.\n * @param options - Optional file extension and path exclusions.\n * @returns File contents keyed by sorted root-relative paths.\n * @throws When the root or a named target is a symbolic link, is not a supported entry, or resolves\n * outside the root.\n * @remarks A named file is included regardless of the extension filter. An absent extension filter\n * includes every walked file. An exclusion matches whole root-relative key segments and covers every\n * key below it, and it applies to a named target and a walked entry alike.\n */\nexport function readInventory(\n\troot: URL | string,\n\ttargets: readonly string[],\n\toptions?: InventoryOptions,\n): Readonly<Record<string, string>> {\n\tconst supplied = resolve(typeof root === 'string' ? root : fileURLToPath(root))\n\tconst rootStatus = lstatSync(supplied)\n\tif (rootStatus.isSymbolicLink()) throw new Error('Root is a symbolic link')\n\tif (!rootStatus.isDirectory()) throw new Error('Root is not a directory')\n\n\tconst base = realpathSync.native(supplied)\n\tif (targets.length === 0) return Object.fromEntries([])\n\n\tconst exclusions = (options?.exclude ?? []).map((rule) => {\n\t\tconst unprefixed = rule.startsWith('./') ? rule.slice(2) : rule\n\t\tconst collapsed = unprefixed.replace(/\\/+/g, '/')\n\t\tconst untrailed = collapsed.endsWith('/') ? collapsed.slice(0, -1) : collapsed\n\t\treturn untrailed === '.' ? '' : untrailed\n\t})\n\tconst pending: string[] = []\n\tconst queued = new Set<string>()\n\tconst contents = new Map<string, string>()\n\n\tfor (const target of targets) {\n\t\tconst candidate = resolveContained(base, target)\n\t\tif (candidate === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst status = lstatSync(candidate)\n\t\tif (status.isSymbolicLink()) throw new Error(`Target is a symbolic link: ${target}`)\n\t\tif (!status.isDirectory() && !status.isFile()) {\n\t\t\tthrow new Error(`Target is not a file or directory: ${target}`)\n\t\t}\n\n\t\tconst physical = realpathSync.native(candidate)\n\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\tif (resolved === undefined) {\n\t\t\tthrow new Error(`Target outside root: ${target}`)\n\t\t}\n\n\t\tconst key = relative(base, resolved).split(sep).join('/')\n\t\tif (isExcluded(key, exclusions)) continue\n\t\tif (status.isFile()) {\n\t\t\tcontents.set(key, readFileSync(physical, 'utf8'))\n\t\t\tcontinue\n\t\t}\n\t\tif (queued.has(physical)) continue\n\t\tqueued.add(physical)\n\t\tpending.push(physical)\n\t}\n\n\twhile (pending.length > 0) {\n\t\tconst directory = pending.pop()\n\t\tif (directory === undefined) continue\n\n\t\tfor (const entry of readdirSync(directory, { withFileTypes: true })) {\n\t\t\tconst path = resolve(directory, entry.name)\n\t\t\tconst status = lstatSync(path)\n\t\t\tif (status.isSymbolicLink()) continue\n\n\t\t\tconst key = relative(base, path).split(sep).join('/')\n\t\t\tif (isExcluded(key, exclusions)) continue\n\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tconst physical = realpathSync.native(path)\n\t\t\t\tconst resolved = resolveContained(base, relative(base, physical))\n\t\t\t\t// Walk containment is unproven because POSIX CI skips links before `realpath`;\n\t\t\t\t// a host that resolves a walked directory outside `base` would drive this branch.\n\t\t\t\tif (resolved === undefined || queued.has(physical)) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tqueued.add(physical)\n\t\t\t\tpending.push(physical)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif (\n\t\t\t\t!status.isFile() ||\n\t\t\t\t(options?.extensions !== undefined &&\n\t\t\t\t\t!options.extensions.some((extension) => entry.name.endsWith(extension)))\n\t\t\t) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tcontents.set(key, readFileSync(path, 'utf8'))\n\t\t}\n\t}\n\n\treturn Object.fromEntries(\n\t\tArray.from(contents).sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)),\n\t)\n}\n\n/**\n * Reports whether a process id names a live process.\n *\n * @param pid - The process id to read.\n * @returns True if a process holds that id at the moment of the call; false otherwise, including a\n * pid the host refuses.\n * @throws Nothing. Every host refusal reads as false.\n * @remarks This is an instantaneous observation rather than a claim of ownership. A host reuses a\n * process id after the process holding it exits, so a true answer says some process holds that id now\n * and never says it is the process the caller started. Two host answers are worth knowing. A POSIX\n * host refuses signal `0` to a process another user owns with `EPERM`, and that refusal reads as\n * false here. A pid of `0` names the caller's own process group on POSIX and the system idle process\n * on Windows, so it reads as true on both without naming a process anyone started.\n *\n * A Linux zombie — a process that has exited and whose parent has not reaped it — still accepts\n * signal `0`, so its `/proc` status is read and a `Z` state reads as false.\n */\nexport function isRunning(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0)\n\t} catch {\n\t\treturn false\n\t}\n\tif (process.platform !== 'linux') return true\n\n\t// The zombie refinement is unproven on a host that carries no `/proc`; a Linux gate drives it.\n\ttry {\n\t\tconst status = readFileSync(`/proc/${String(pid)}/stat`, 'utf8')\n\t\tconst boundary = status.lastIndexOf(') ')\n\t\treturn boundary < 0 || status.slice(boundary + 2, boundary + 3) !== 'Z'\n\t} catch {\n\t\treturn false\n\t}\n}\n\n/**\n * Waits for a socket to close, accepting a peer reset as a forced close.\n *\n * @param socket - The socket to wait on. One that has already closed resolves without listening.\n * @param options - The time bounds and abort signal.\n * @returns A promise that resolves when the socket emits `close`.\n * @throws The socket's own error when its code is not `ECONNRESET`, the abort reason, or an `Error`\n * when a bound is invalid or the socket does not close within the budget.\n * @remarks Default budget: `1000` milliseconds. A reset is the peer forcing the connection down, and\n * the socket still emits `close` afterwards, so `ECONNRESET` is waited past rather than raised while\n * every other error ends the wait. The interval is validated for consistency with the wait family but\n * is not used, because this helper parks on the socket's events. Both listeners are removed on every\n * settlement, so a caller may wait on one socket repeatedly.\n */\nexport async function waitForSocketClose(socket: Socket, options?: WaitOptions): Promise<void> {\n\tconst budget = options?.budget ?? 1000\n\tconst interval = options?.interval ?? 10\n\tif (!Number.isFinite(budget) || budget < 0) {\n\t\tthrow new Error('Socket budget must be finite and non-negative')\n\t}\n\tif (!Number.isFinite(interval) || interval < 0) {\n\t\tthrow new Error('Socket interval must be finite and non-negative')\n\t}\n\n\tconst signal = options?.signal\n\tsignal?.throwIfAborted()\n\tif (socket.closed) return\n\n\t// The resolvers are the listeners themselves, so the same references remove them afterwards.\n\tconst closed = Promise.withResolvers<boolean>()\n\tconst failed = Promise.withResolvers<NodeJS.ErrnoException>()\n\tconst expiry = Promise.withResolvers<never>()\n\tconst aborted = Promise.withResolvers<never>()\n\tconst subscription = new AbortController()\n\tsocket.on('close', closed.resolve)\n\tsocket.on('error', failed.resolve)\n\tsignal?.addEventListener('abort', () => aborted.reject(signal.reason), {\n\t\tonce: true,\n\t\tsignal: subscription.signal,\n\t})\n\tconst timer = setTimeout(() => {\n\t\texpiry.reject(new Error(`Socket did not close within ${budget}ms`))\n\t}, budget)\n\n\ttry {\n\t\tconst error = await Promise.race([\n\t\t\tclosed.promise.then(() => undefined),\n\t\t\tfailed.promise,\n\t\t\texpiry.promise,\n\t\t\taborted.promise,\n\t\t])\n\t\tif (error === undefined) return\n\t\tif (error.code !== 'ECONNRESET') throw error\n\t\tawait Promise.race([closed.promise, expiry.promise, aborted.promise])\n\t} finally {\n\t\tclearTimeout(timer)\n\t\tsubscription.abort()\n\t\tsocket.off('close', closed.resolve)\n\t\tsocket.off('error', failed.resolve)\n\t}\n}\n\n/**\n * Destroys a scratch directory, retrying until the host releases it.\n *\n * @param scratch - The scratch directory to destroy.\n * @param options - The time bounds and abort signal.\n * @returns A promise that resolves once `destroy()` returns without throwing.\n * @throws The abort reason, or an `Error` when a bound is invalid or the budget elapses. The\n * exhaustion error carries the last host refusal as its `cause`.\n * @remarks Default budget: `10000` milliseconds. Default interval: `25` milliseconds. A host holds a\n * directory for a short interval after the process that held it exits, and a just-stopped child's\n * working directory is the case this exists for, so removal is attempted until the host lets go\n * rather than exactly once. {@link ScratchInterface.destroy} stays synchronous and is unchanged; this\n * is the bounded retry around it. A directory nothing releases still fails, with the host's own\n * refusal as the `cause`.\n *\n * Every refusal is retried, deliberately, and that is wider than {@link removeTree}'s policy: that\n * one retries the codes {@link REMOVE_TREE_RETRYABLE_CODES} names and rethrows the rest at once.\n * The hold this waits out is not classifiable across hosts — Windows reports a working-directory\n * hold as `EPERM`, POSIX hosts and network filesystems report their own — so a code list here would\n * be a list of the hosts it had been run on. The residual is the cost of that: a fault no wait can\n * clear, such as a path removed from under the allocation or a permission the process never had,\n * spends the whole budget before it surfaces, and it surfaces wrapped in the exhaustion error with\n * the host's refusal as `cause` rather than by identity. Pass a shorter `budget` or a `signal`\n * wherever a caller must bound that cost.\n */\nexport async function destroyScratch(\n\tscratch: ScratchInterface,\n\toptions?: WaitOptions,\n): Promise<void> {\n\tconst budget = options?.budget ?? 10_000\n\tconst interval = options?.interval ?? 25\n\tif (!Number.isFinite(budget) || budget < 0) {\n\t\tthrow new Error('Scratch budget must be finite and non-negative')\n\t}\n\tif (!Number.isFinite(interval) || interval < 0) {\n\t\tthrow new Error('Scratch interval must be finite and non-negative')\n\t}\n\n\tconst start = performance.now()\n\tlet refusal: unknown\n\twhile (true) {\n\t\toptions?.signal?.throwIfAborted()\n\t\ttry {\n\t\t\tscratch.destroy()\n\t\t\treturn\n\t\t} catch (error) {\n\t\t\trefusal = error\n\t\t}\n\n\t\tif (performance.now() - start >= budget) {\n\t\t\tthrow new Error(`Scratch directory was not destroyed within ${budget}ms`, {\n\t\t\t\tcause: refusal,\n\t\t\t})\n\t\t}\n\t\tawait waitForDelay(interval)\n\t}\n}\n","import type { Server } from 'node:net'\nimport type {\n\tCookieJarInterface,\n\tLoopbackInterface,\n\tScratchIdentity,\n\tScratchInterface,\n\tScratchOptions,\n} from './types.js'\nimport { once } from 'node:events'\nimport {\n\tlstatSync,\n\tmkdirSync,\n\tmkdtempSync,\n\treadFileSync,\n\treaddirSync,\n\tstatSync,\n\twriteFileSync,\n} from 'node:fs'\nimport { tmpdir } from 'node:os'\nimport { dirname, resolve, sep } from 'node:path'\nimport { createLink, matchesIdentity, removeTree, resolveContained } from './helpers.js'\n\n/**\n * Allocates an owned temporary directory with contained file operations.\n *\n * @param options - Optional parent directory, name prefix, and initial files.\n * @returns The scratch directory and its file operations.\n * @throws When the parent is missing, a symbolic link, or not a directory; when the prefix contains\n * `/` or `\\`; or when allocation or seeding fails.\n * @remarks The parent defaults to the host temporary directory. The prefix defaults to\n * `orkestrel-test-`. Seed keys use root-relative paths.\n */\nexport function createScratch(options?: ScratchOptions): ScratchInterface {\n\tconst parent = resolve(options?.parent ?? tmpdir())\n\tconst parentStatus = lstatSync(parent, { throwIfNoEntry: false })\n\tif (parentStatus === undefined) throw new Error('Scratch parent does not exist')\n\tif (parentStatus.isSymbolicLink()) throw new Error('Scratch parent is a symbolic link')\n\tif (!parentStatus.isDirectory()) throw new Error('Scratch parent is not a directory')\n\n\tconst prefix = options?.prefix ?? 'orkestrel-test-'\n\tif (prefix.includes('/') || prefix.includes('\\\\')) {\n\t\tthrow new Error('Scratch prefix must be a name fragment')\n\t}\n\n\tconst path = mkdtempSync(`${parent}${sep}${prefix}`)\n\tconst allocated = statSync(path)\n\tconst allocation: ScratchIdentity = {\n\t\tbirth: allocated.birthtimeMs,\n\t\tdevice: allocated.dev,\n\t\tinode: allocated.ino,\n\t}\n\tconst outside = 'Path outside scratch directory'\n\tconst unremovable = 'Scratch directory is not a removable target'\n\ttry {\n\t\tfor (const [target, text] of Object.entries(options?.files ?? {})) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t}\n\t} catch (error) {\n\t\tremoveTree(path)\n\t\tthrow error\n\t}\n\n\tconst scratch: ScratchInterface = {\n\t\tpath,\n\t\twrite(target, text) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\twriteFileSync(candidate, text)\n\t\t},\n\t\tread(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has(target)) return undefined\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return undefined\n\t\t\tif (status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is a directory: ${target}`)\n\t\t\t}\n\t\t\treturn readFileSync(candidate, 'utf8')\n\t\t},\n\t\thas(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\n\t\t\tconst rootStatus = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (rootStatus === undefined) return false\n\t\t\tif (rootStatus.isSymbolicLink()) throw new Error('Scratch directory is a symbolic link')\n\t\t\tif (!rootStatus.isDirectory()) throw new Error('Scratch path is not a directory')\n\n\t\t\treturn lstatSync(candidate, { throwIfNoEntry: false }) !== undefined\n\t\t},\n\t\tnames(target = '.') {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) throw new Error(`Scratch path does not exist: ${target}`)\n\t\t\tif (!status.isDirectory()) throw new Error(`Scratch path is not a directory: ${target}`)\n\t\t\treturn readdirSync(candidate).sort()\n\t\t},\n\t\tensure(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = statSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined && !status.isDirectory()) {\n\t\t\t\tthrow new Error(`Scratch path is not a directory: ${target}`)\n\t\t\t}\n\t\t\tif (status === undefined) mkdirSync(candidate, { recursive: true })\n\t\t\treturn candidate\n\t\t},\n\t\tlink(target, source) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tmkdirSync(dirname(candidate), { recursive: true })\n\t\t\tcreateLink(candidate, source)\n\t\t},\n\t\tremove(target) {\n\t\t\tconst candidate = resolveContained(path, target)\n\t\t\tif (candidate === undefined) throw new Error(`${outside}: ${target}`)\n\t\t\tif (candidate === path) throw new Error(`${unremovable}: ${target}`)\n\t\t\tif (!scratch.has('.')) throw new Error('Scratch directory does not exist')\n\n\t\t\tconst status = lstatSync(candidate, { throwIfNoEntry: false })\n\t\t\tif (status !== undefined) {\n\t\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\t\tdevice: status.dev,\n\t\t\t\t\tinode: status.ino,\n\t\t\t\t}\n\t\t\t\tif (matchesIdentity(identity, allocation)) {\n\t\t\t\t\tthrow new Error(`${unremovable}: ${target}`)\n\t\t\t\t}\n\t\t\t}\n\t\t\tremoveTree(candidate)\n\t\t},\n\t\tdestroy() {\n\t\t\tconst status = lstatSync(path, { throwIfNoEntry: false })\n\t\t\tif (status === undefined) return\n\t\t\tconst identity: ScratchIdentity = {\n\t\t\t\tbirth: status.birthtimeMs,\n\t\t\t\tdevice: status.dev,\n\t\t\t\tinode: status.ino,\n\t\t\t}\n\t\t\tif (!matchesIdentity(identity, allocation)) return\n\t\t\tremoveTree(path)\n\t\t},\n\t}\n\treturn scratch\n}\n\n/**\n * Starts a server on an ephemeral IPv4 loopback port.\n *\n * @param server - The unstarted server to bind.\n * @returns The bound origin, assigned port, and asynchronous teardown.\n * @throws When the server cannot bind or reports an address without a numeric port.\n */\nexport async function createLoopback(server: Server): Promise<LoopbackInterface> {\n\tserver.listen(0, '127.0.0.1')\n\tawait once(server, 'listening')\n\n\tconst address = server.address()\n\tif (\n\t\ttypeof address !== 'object' ||\n\t\taddress === null ||\n\t\t!('port' in address) ||\n\t\ttypeof address.port !== 'number'\n\t) {\n\t\tthrow new Error(`Loopback address must have a numeric port; found ${String(address)}`)\n\t}\n\n\tconst port = address.port\n\tlet destruction: Promise<void> | undefined\n\treturn {\n\t\turl: `http://127.0.0.1:${port}`,\n\t\tport,\n\t\tdestroy() {\n\t\t\tif (destruction === undefined) {\n\t\t\t\tdestruction = new Promise<void>((resolveClose, rejectClose) => {\n\t\t\t\t\tif ('closeAllConnections' in server && typeof server.closeAllConnections === 'function') {\n\t\t\t\t\t\tserver.closeAllConnections()\n\t\t\t\t\t}\n\t\t\t\t\tserver.close((error) => {\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\terror === undefined ||\n\t\t\t\t\t\t\t('code' in error && error.code === 'ERR_SERVER_NOT_RUNNING')\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tresolveClose()\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\trejectClose(error)\n\t\t\t\t\t\t}\n\t\t\t\t\t})\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn destruction\n\t\t},\n\t}\n}\n\n/**\n * Creates a cookie jar that records a real response's cookies and replays them as one header.\n *\n * @returns The rendered request header, and the members that read and capture cookies.\n * @remarks Selection is by name alone: no `Domain` or `Path` matching, no `Expires` or `Secure`\n * handling, and no persistence beyond the jar. That is what a test driving one origin over one path\n * needs, and a fixture needing a browser's cookie store needs a browser rather than this.\n */\nexport function createCookieJar(): CookieJarInterface {\n\tconst cookies = new Map<string, string>()\n\treturn {\n\t\tget header() {\n\t\t\tconst pairs = [...cookies].map(([name, value]) => `${name}=${value}`)\n\t\t\treturn pairs.length === 0 ? undefined : pairs.join('; ')\n\t\t},\n\t\tread(name) {\n\t\t\treturn cookies.get(name)\n\t\t},\n\t\tcapture(response) {\n\t\t\tconst fields = response.headers.getSetCookie()\n\t\t\tfor (const field of fields) {\n\t\t\t\tconst boundary = field.indexOf(';')\n\t\t\t\tconst pair = boundary < 0 ? field : field.slice(0, boundary)\n\t\t\t\tconst separator = pair.indexOf('=')\n\t\t\t\tif (separator < 1) continue\n\n\t\t\t\tconst name = pair.slice(0, separator)\n\t\t\t\t// An origin spells a deletion `Max-Age=0` in whatever case and spacing it likes, so the\n\t\t\t\t// attribute is matched rather than compared.\n\t\t\t\tif (/;\\s*max-age\\s*=\\s*0\\s*(?:;|$)/iu.test(field)) cookies.delete(name)\n\t\t\t\telse cookies.set(name, pair.slice(separator + 1))\n\t\t\t}\n\t\t\treturn fields\n\t\t},\n\t}\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,56 @@
|
|
|
1
1
|
import { Server } from 'node:net';
|
|
2
|
+
import { Socket } from 'node:net';
|
|
3
|
+
import { WaitOptions } from '@orkestrel/test';
|
|
4
|
+
|
|
5
|
+
/** A name-keyed cookie store a test drives one origin with, filled from real responses. */
|
|
6
|
+
export declare interface CookieJarInterface {
|
|
7
|
+
/**
|
|
8
|
+
* The `Cookie` request header naming every stored cookie, or `undefined` while the jar holds none.
|
|
9
|
+
*/
|
|
10
|
+
readonly header: string | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* Reads one stored cookie value.
|
|
13
|
+
*
|
|
14
|
+
* @param name - The cookie name.
|
|
15
|
+
* @returns The stored value, or `undefined` when the jar holds no cookie of that name.
|
|
16
|
+
*/
|
|
17
|
+
read(name: string): string | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Applies every `Set-Cookie` field a response carries.
|
|
20
|
+
*
|
|
21
|
+
* @param response - The response whose `Set-Cookie` fields are applied.
|
|
22
|
+
* @returns Those fields unmodified, in the order the response carried them.
|
|
23
|
+
* @remarks Selection is by name alone. A field spelling `Max-Age=0` deletes its cookie and every
|
|
24
|
+
* other field stores or replaces one, so `Domain`, `Path`, `Expires`, and `Secure` are read past
|
|
25
|
+
* rather than honoured. Nothing outlives the jar.
|
|
26
|
+
*/
|
|
27
|
+
capture(response: Response): readonly string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates a cookie jar that records a real response's cookies and replays them as one header.
|
|
32
|
+
*
|
|
33
|
+
* @returns The rendered request header, and the members that read and capture cookies.
|
|
34
|
+
* @remarks Selection is by name alone: no `Domain` or `Path` matching, no `Expires` or `Secure`
|
|
35
|
+
* handling, and no persistence beyond the jar. That is what a test driving one origin over one path
|
|
36
|
+
* needs, and a fixture needing a browser's cookie store needs a browser rather than this.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createCookieJar(): CookieJarInterface;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates a symbolic link with a directory-junction fallback for hosts that refuse symbolic links.
|
|
42
|
+
*
|
|
43
|
+
* @param path - The path where the link is created.
|
|
44
|
+
* @param source - The destination path the link points at.
|
|
45
|
+
* @throws The original link error when its code is not `EPERM`, or when the source names an
|
|
46
|
+
* existing non-directory; otherwise, any error from inspecting the source or creating the junction.
|
|
47
|
+
* @remarks Only `EPERM` from the first symbolic-link attempt triggers the fallback. The fallback
|
|
48
|
+
* resolves the source against the link's directory. An existing non-directory rethrows the original
|
|
49
|
+
* `EPERM`, while a directory or missing source is passed to a junction attempt. A missing source is
|
|
50
|
+
* accepted to create a dangling junction. Where the host creates a junction, its stored value is the
|
|
51
|
+
* resolved absolute path.
|
|
52
|
+
*/
|
|
53
|
+
export declare function createLink(path: string, source: string): void;
|
|
2
54
|
|
|
3
55
|
/**
|
|
4
56
|
* Starts a server on an ephemeral IPv4 loopback port.
|
|
@@ -21,6 +73,33 @@ export declare function createLoopback(server: Server): Promise<LoopbackInterfac
|
|
|
21
73
|
*/
|
|
22
74
|
export declare function createScratch(options?: ScratchOptions): ScratchInterface;
|
|
23
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Destroys a scratch directory, retrying until the host releases it.
|
|
78
|
+
*
|
|
79
|
+
* @param scratch - The scratch directory to destroy.
|
|
80
|
+
* @param options - The time bounds and abort signal.
|
|
81
|
+
* @returns A promise that resolves once `destroy()` returns without throwing.
|
|
82
|
+
* @throws The abort reason, or an `Error` when a bound is invalid or the budget elapses. The
|
|
83
|
+
* exhaustion error carries the last host refusal as its `cause`.
|
|
84
|
+
* @remarks Default budget: `10000` milliseconds. Default interval: `25` milliseconds. A host holds a
|
|
85
|
+
* directory for a short interval after the process that held it exits, and a just-stopped child's
|
|
86
|
+
* working directory is the case this exists for, so removal is attempted until the host lets go
|
|
87
|
+
* rather than exactly once. {@link ScratchInterface.destroy} stays synchronous and is unchanged; this
|
|
88
|
+
* is the bounded retry around it. A directory nothing releases still fails, with the host's own
|
|
89
|
+
* refusal as the `cause`.
|
|
90
|
+
*
|
|
91
|
+
* Every refusal is retried, deliberately, and that is wider than {@link removeTree}'s policy: that
|
|
92
|
+
* one retries the codes {@link REMOVE_TREE_RETRYABLE_CODES} names and rethrows the rest at once.
|
|
93
|
+
* The hold this waits out is not classifiable across hosts — Windows reports a working-directory
|
|
94
|
+
* hold as `EPERM`, POSIX hosts and network filesystems report their own — so a code list here would
|
|
95
|
+
* be a list of the hosts it had been run on. The residual is the cost of that: a fault no wait can
|
|
96
|
+
* clear, such as a path removed from under the allocation or a permission the process never had,
|
|
97
|
+
* spends the whole budget before it surfaces, and it surfaces wrapped in the exhaustion error with
|
|
98
|
+
* the host's refusal as `cause` rather than by identity. Pass a shorter `budget` or a `signal`
|
|
99
|
+
* wherever a caller must bound that cost.
|
|
100
|
+
*/
|
|
101
|
+
export declare function destroyScratch(scratch: ScratchInterface, options?: WaitOptions): Promise<void>;
|
|
102
|
+
|
|
24
103
|
/** Options for reading a source inventory. */
|
|
25
104
|
export declare interface InventoryOptions {
|
|
26
105
|
/** The file extensions to include, each written with its leading dot. */
|
|
@@ -41,6 +120,25 @@ export declare interface InventoryOptions {
|
|
|
41
120
|
*/
|
|
42
121
|
export declare function isExcluded(key: string, exclusions: readonly string[]): boolean;
|
|
43
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Reports whether a process id names a live process.
|
|
125
|
+
*
|
|
126
|
+
* @param pid - The process id to read.
|
|
127
|
+
* @returns True if a process holds that id at the moment of the call; false otherwise, including a
|
|
128
|
+
* pid the host refuses.
|
|
129
|
+
* @throws Nothing. Every host refusal reads as false.
|
|
130
|
+
* @remarks This is an instantaneous observation rather than a claim of ownership. A host reuses a
|
|
131
|
+
* process id after the process holding it exits, so a true answer says some process holds that id now
|
|
132
|
+
* and never says it is the process the caller started. Two host answers are worth knowing. A POSIX
|
|
133
|
+
* host refuses signal `0` to a process another user owns with `EPERM`, and that refusal reads as
|
|
134
|
+
* false here. A pid of `0` names the caller's own process group on POSIX and the system idle process
|
|
135
|
+
* on Windows, so it reads as true on both without naming a process anyone started.
|
|
136
|
+
*
|
|
137
|
+
* A Linux zombie — a process that has exited and whose parent has not reaped it — still accepts
|
|
138
|
+
* signal `0`, so its `/proc` status is read and a `Z` state reads as false.
|
|
139
|
+
*/
|
|
140
|
+
export declare function isRunning(pid: number): boolean;
|
|
141
|
+
|
|
44
142
|
/** A server a test owns, listening on an ephemeral loopback port until the test releases it. */
|
|
45
143
|
export declare interface LoopbackInterface {
|
|
46
144
|
/**
|
|
@@ -112,7 +210,8 @@ export declare const REMOVE_TREE_RETRYABLE_CODES: readonly string[];
|
|
|
112
210
|
* `rmSync` `maxRetries`/`retryDelay` options do not cover this error class on that host: probed
|
|
113
211
|
* against a real held directory, they neither delay nor retry before rethrowing, so the retry
|
|
114
212
|
* is implemented here with a synchronous sleep instead. Ten attempts 100ms apart bound the wait
|
|
115
|
-
* at roughly one second.
|
|
213
|
+
* at roughly one second. A hold that outlasts that second is {@link destroyScratch}'s case, which
|
|
214
|
+
* retries every refusal inside a caller's budget rather than the codes named here.
|
|
116
215
|
*/
|
|
117
216
|
export declare function removeTree(path: string): void;
|
|
118
217
|
|
|
@@ -192,10 +291,13 @@ export declare interface ScratchInterface {
|
|
|
192
291
|
* @param target - The relative or absolute contained path where the link is created. Unlike
|
|
193
292
|
* `node:fs`'s `symlinkSync(target, path)` vocabulary, this interface consistently calls the
|
|
194
293
|
* contained path the target.
|
|
195
|
-
* @param source - The
|
|
196
|
-
*
|
|
294
|
+
* @param source - The destination path the link points at. The stored value is a path naming that
|
|
295
|
+
* destination, but its exact text is not promised. The path may name a destination outside the
|
|
296
|
+
* scratch directory and is not containment-checked.
|
|
197
297
|
* @throws When the target escapes the scratch directory, the scratch root is missing, a symbolic
|
|
198
|
-
* link, or a file, or the host refuses to create the link
|
|
298
|
+
* link, or a file, or the host refuses to create the link, including a host that creates no
|
|
299
|
+
* symbolic link when the source names an existing non-directory.
|
|
300
|
+
* @remarks {@link createLink} owns the host-specific link mechanism.
|
|
199
301
|
*/
|
|
200
302
|
link(target: string, source: string): void;
|
|
201
303
|
/**
|
|
@@ -237,4 +339,20 @@ export declare interface ScratchOptions {
|
|
|
237
339
|
readonly files?: Readonly<Record<string, string>>;
|
|
238
340
|
}
|
|
239
341
|
|
|
342
|
+
/**
|
|
343
|
+
* Waits for a socket to close, accepting a peer reset as a forced close.
|
|
344
|
+
*
|
|
345
|
+
* @param socket - The socket to wait on. One that has already closed resolves without listening.
|
|
346
|
+
* @param options - The time bounds and abort signal.
|
|
347
|
+
* @returns A promise that resolves when the socket emits `close`.
|
|
348
|
+
* @throws The socket's own error when its code is not `ECONNRESET`, the abort reason, or an `Error`
|
|
349
|
+
* when a bound is invalid or the socket does not close within the budget.
|
|
350
|
+
* @remarks Default budget: `1000` milliseconds. A reset is the peer forcing the connection down, and
|
|
351
|
+
* the socket still emits `close` afterwards, so `ECONNRESET` is waited past rather than raised while
|
|
352
|
+
* every other error ends the wait. The interval is validated for consistency with the wait family but
|
|
353
|
+
* is not used, because this helper parks on the socket's events. Both listeners are removed on every
|
|
354
|
+
* settlement, so a caller may wait on one socket repeatedly.
|
|
355
|
+
*/
|
|
356
|
+
export declare function waitForSocketClose(socket: Socket, options?: WaitOptions): Promise<void>;
|
|
357
|
+
|
|
240
358
|
export { }
|