@rehearsal-labs/retest 0.2.0 → 0.3.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 +6 -5
- package/dist/api/source-location.d.ts.map +1 -1
- package/dist/api/source-location.js +3 -1
- package/dist/browser/builds.d.ts +35 -6
- package/dist/browser/builds.d.ts.map +1 -1
- package/dist/browser/builds.js +115 -16
- package/dist/browser/cdp/transport.d.ts.map +1 -1
- package/dist/browser/cdp/transport.js +4 -4
- package/dist/browser/chromium-process.d.ts +23 -0
- package/dist/browser/chromium-process.d.ts.map +1 -1
- package/dist/browser/chromium-process.js +78 -13
- package/dist/browser/electron.d.ts +1 -1
- package/dist/browser/electron.d.ts.map +1 -1
- package/dist/browser/electron.js +6 -3
- package/dist/browser/executables.d.ts +16 -1
- package/dist/browser/executables.d.ts.map +1 -1
- package/dist/browser/executables.js +50 -18
- package/dist/browser/firefox/accessible-names.d.ts +5 -4
- package/dist/browser/firefox/accessible-names.d.ts.map +1 -1
- package/dist/browser/firefox/accessible-names.js +11 -6
- package/dist/browser/firefox/bridge.js +3 -2
- package/dist/browser/firefox/executable.d.ts +14 -14
- package/dist/browser/firefox/executable.d.ts.map +1 -1
- package/dist/browser/firefox/executable.js +29 -21
- package/dist/browser/firefox/launch.js +1 -1
- package/dist/browser/firefox/navigation.d.ts +5 -2
- package/dist/browser/firefox/navigation.d.ts.map +1 -1
- package/dist/browser/firefox/navigation.js +48 -14
- package/dist/browser/firefox/orphans.d.ts.map +1 -1
- package/dist/browser/firefox/orphans.js +34 -2
- package/dist/browser/firefox/page.d.ts.map +1 -1
- package/dist/browser/firefox/page.js +113 -19
- package/dist/browser/firefox/process.d.ts +20 -1
- package/dist/browser/firefox/process.d.ts.map +1 -1
- package/dist/browser/firefox/process.js +124 -22
- package/dist/browser/firefox/profile.d.ts +5 -0
- package/dist/browser/firefox/profile.d.ts.map +1 -1
- package/dist/browser/firefox/profile.js +17 -1
- package/dist/browser/firefox/storage.d.ts.map +1 -1
- package/dist/browser/firefox/storage.js +4 -2
- package/dist/browser/firefox/subscriptions.d.ts +26 -0
- package/dist/browser/firefox/subscriptions.d.ts.map +1 -0
- package/dist/browser/firefox/subscriptions.js +10 -0
- package/dist/browser/launch.d.ts +6 -0
- package/dist/browser/launch.d.ts.map +1 -1
- package/dist/browser/launch.js +14 -1
- package/dist/cli/commands/doctor.js +3 -3
- package/dist/cli/commands/inspect.js +3 -2
- package/dist/cli/commands/install.d.ts.map +1 -1
- package/dist/cli/commands/install.js +26 -13
- package/dist/cli/commands/licences.d.ts +22 -0
- package/dist/cli/commands/licences.d.ts.map +1 -1
- package/dist/cli/commands/licences.js +62 -20
- package/dist/cli/commands/run.js +2 -2
- package/dist/cli/doctor/checks.js +4 -1
- package/dist/cli/inspect/diagnostics.js +3 -3
- package/dist/cli/inspect/test-timeline.js +10 -10
- package/dist/cli/install/doctor-rows.d.ts.map +1 -1
- package/dist/cli/install/doctor-rows.js +5 -4
- package/dist/cli/install/file-changes.d.ts +44 -0
- package/dist/cli/install/file-changes.d.ts.map +1 -0
- package/dist/cli/install/file-changes.js +81 -0
- package/dist/cli/install/install-archive.d.ts +3 -1
- package/dist/cli/install/install-archive.d.ts.map +1 -1
- package/dist/cli/install/install-archive.js +80 -33
- package/dist/cli/install/lock.d.ts.map +1 -1
- package/dist/cli/install/lock.js +108 -21
- package/dist/cli/install/media-doctor.d.ts.map +1 -1
- package/dist/cli/install/media-doctor.js +2 -1
- package/dist/cli/install/media-install.d.ts.map +1 -1
- package/dist/cli/install/media-install.js +38 -19
- package/dist/cli/install/media-pins.d.ts +7 -1
- package/dist/cli/install/media-pins.d.ts.map +1 -1
- package/dist/cli/install/media-pins.js +23 -12
- package/dist/cli/install/media-record.d.ts.map +1 -1
- package/dist/cli/install/media-record.js +10 -9
- package/dist/cli/install/media-report.d.ts +3 -1
- package/dist/cli/install/media-report.d.ts.map +1 -1
- package/dist/cli/install/media-report.js +7 -5
- package/dist/cli/install/media-tools.d.ts.map +1 -1
- package/dist/cli/install/media-tools.js +6 -4
- package/dist/cli/install/process-start.d.ts +3 -2
- package/dist/cli/install/process-start.d.ts.map +1 -1
- package/dist/cli/install/process-start.js +4 -0
- package/dist/cli/install/report.d.ts.map +1 -1
- package/dist/cli/install/report.js +11 -7
- package/dist/cli/install/supervisor-install.d.ts +33 -0
- package/dist/cli/install/supervisor-install.d.ts.map +1 -0
- package/dist/cli/install/supervisor-install.js +158 -0
- package/dist/cli/install/supervisor-locate.d.ts +47 -0
- package/dist/cli/install/supervisor-locate.d.ts.map +1 -0
- package/dist/cli/install/supervisor-locate.js +113 -0
- package/dist/cli/install/supervisor-pins.d.ts +21 -0
- package/dist/cli/install/supervisor-pins.d.ts.map +1 -0
- package/dist/cli/install/supervisor-pins.js +28 -0
- package/dist/cli/install/supervisor-record.d.ts +31 -0
- package/dist/cli/install/supervisor-record.d.ts.map +1 -0
- package/dist/cli/install/supervisor-record.js +104 -0
- package/dist/cli/install/supervisor-report.d.ts +11 -0
- package/dist/cli/install/supervisor-report.d.ts.map +1 -0
- package/dist/cli/install/supervisor-report.js +33 -0
- package/dist/cli/install/supervisor-tools.d.ts +43 -0
- package/dist/cli/install/supervisor-tools.d.ts.map +1 -0
- package/dist/cli/install/supervisor-tools.js +114 -0
- package/dist/cli/install/unpack.d.ts +23 -3
- package/dist/cli/install/unpack.d.ts.map +1 -1
- package/dist/cli/install/unpack.js +124 -10
- package/dist/cli/install/zip-member.d.ts +15 -0
- package/dist/cli/install/zip-member.d.ts.map +1 -0
- package/dist/cli/install/zip-member.js +77 -0
- package/dist/cli/interrupt.d.ts +1 -1
- package/dist/cli/interrupt.d.ts.map +1 -1
- package/dist/cli/interrupt.js +15 -10
- package/dist/cli/terminal.d.ts +9 -1
- package/dist/cli/terminal.d.ts.map +1 -1
- package/dist/cli/terminal.js +14 -0
- package/dist/config/define.d.ts +2 -2
- package/dist/config/define.js +2 -2
- package/dist/diagnostics/firefox-collector.d.ts +11 -10
- package/dist/diagnostics/firefox-collector.d.ts.map +1 -1
- package/dist/diagnostics/firefox-collector.js +16 -11
- package/dist/media/client.d.ts +2 -1
- package/dist/media/client.d.ts.map +1 -1
- package/dist/media/client.js +16 -5
- package/dist/media/host.d.ts +32 -0
- package/dist/media/host.d.ts.map +1 -0
- package/dist/media/host.js +50 -0
- package/dist/media/locate.d.ts.map +1 -1
- package/dist/media/locate.js +4 -3
- package/dist/native/processes.d.ts.map +1 -1
- package/dist/native/processes.js +7 -3
- package/dist/protocol/files.d.ts +1 -1
- package/dist/protocol/files.d.ts.map +1 -1
- package/dist/protocol/files.js +10 -5
- package/dist/reporters/commands.d.ts +12 -3
- package/dist/reporters/commands.d.ts.map +1 -1
- package/dist/reporters/commands.js +29 -11
- package/dist/reporters/human-card.js +1 -1
- package/dist/reporters/human.d.ts +3 -1
- package/dist/reporters/human.d.ts.map +1 -1
- package/dist/reporters/human.js +8 -8
- package/dist/reporters/style.d.ts +21 -2
- package/dist/reporters/style.d.ts.map +1 -1
- package/dist/reporters/style.js +11 -2
- package/dist/runner/app-server.d.ts +27 -7
- package/dist/runner/app-server.d.ts.map +1 -1
- package/dist/runner/app-server.js +74 -24
- package/dist/runner/child.js +6 -2
- package/dist/runner/media-leftovers.d.ts +7 -3
- package/dist/runner/media-leftovers.d.ts.map +1 -1
- package/dist/runner/media-leftovers.js +27 -6
- package/dist/runner/resources.d.ts +8 -0
- package/dist/runner/resources.d.ts.map +1 -1
- package/dist/runner/resources.js +12 -1
- package/dist/runner/run-session.d.ts.map +1 -1
- package/dist/runner/run-session.js +9 -4
- package/dist/runner/run.d.ts.map +1 -1
- package/dist/runner/run.js +2 -1
- package/dist/runner/target-drivers.d.ts +5 -2
- package/dist/runner/target-drivers.d.ts.map +1 -1
- package/dist/runner/target-drivers.js +10 -1
- package/dist/runner/test-file-process.d.ts +8 -0
- package/dist/runner/test-file-process.d.ts.map +1 -1
- package/dist/runner/test-file-process.js +26 -3
- package/dist/runner/test-pages.d.ts.map +1 -1
- package/dist/runner/test-pages.js +5 -2
- package/dist/shared/environment.d.ts +17 -0
- package/dist/shared/environment.d.ts.map +1 -0
- package/dist/shared/environment.js +27 -0
- package/dist/shared/metadata-process-worker.js +15 -7
- package/dist/shared/metadata-process.d.ts +5 -0
- package/dist/shared/metadata-process.d.ts.map +1 -1
- package/dist/shared/metadata-process.js +36 -6
- package/dist/shared/posix-path.d.ts +12 -0
- package/dist/shared/posix-path.d.ts.map +1 -1
- package/dist/shared/posix-path.js +16 -0
- package/dist/shared/private-file.d.ts +38 -0
- package/dist/shared/private-file.d.ts.map +1 -0
- package/dist/shared/private-file.js +132 -0
- package/dist/shared/process-ownership.d.ts +52 -2
- package/dist/shared/process-ownership.d.ts.map +1 -1
- package/dist/shared/process-ownership.js +104 -11
- package/dist/shared/process-supervisor.d.ts +309 -0
- package/dist/shared/process-supervisor.d.ts.map +1 -0
- package/dist/shared/process-supervisor.js +737 -0
- package/dist/shared/process-system.d.ts +113 -0
- package/dist/shared/process-system.d.ts.map +1 -0
- package/dist/shared/process-system.js +254 -0
- package/dist/shared/regular-file.d.ts +2 -2
- package/dist/shared/regular-file.d.ts.map +1 -1
- package/dist/shared/regular-file.js +19 -2
- package/dist/store/artifacts.d.ts +5 -4
- package/dist/store/artifacts.d.ts.map +1 -1
- package/dist/store/artifacts.js +18 -5
- package/dist/store/run-store.d.ts +5 -2
- package/dist/store/run-store.d.ts.map +1 -1
- package/dist/store/run-store.js +48 -4
- package/media/Cargo.lock +16 -0
- package/media/Cargo.toml +16 -1
- package/media/src/encoder.rs +56 -12
- package/media/src/frame.rs +7 -1
- package/media/src/main.rs +20 -2
- package/media/src/place.rs +230 -11
- package/media/src/process_ownership_windows.rs +1058 -0
- package/media/src/recording.rs +15 -7
- package/media/src/replies.rs +55 -0
- package/media/src/server.rs +72 -2
- package/media/src/store.rs +71 -2
- package/package.json +9 -4
- package/src/cli/install/media-notices.txt +465 -1
- package/supervisor/Cargo.lock +25 -0
- package/supervisor/Cargo.toml +22 -0
- package/supervisor/NOTICES +464 -0
- package/supervisor/build.rs +44 -0
- package/supervisor/src/command.rs +612 -0
- package/supervisor/src/jobs.rs +1650 -0
- package/supervisor/src/main.rs +71 -0
- package/supervisor/src/reply.rs +367 -0
|
@@ -34,6 +34,14 @@ export type SpawnOptions = {
|
|
|
34
34
|
/** The process's whole environment, in place of this process's own. The hidden variables are left out of it too. */
|
|
35
35
|
environment?: Readonly<Record<string, string>>;
|
|
36
36
|
};
|
|
37
|
+
/**
|
|
38
|
+
* The environment a test file's process starts with: this process's, or `environment`, without the hidden variables. On
|
|
39
|
+
* Windows NODE_COMPILE_CACHE is left out too: Node turns the compile cache on as it starts when that variable is set, and
|
|
40
|
+
* the test file's process keeps the cache off there (`child.ts`).
|
|
41
|
+
*
|
|
42
|
+
* @example testFileEnvironment({ environment: { NODE_COMPILE_CACHE: 'C:\\cache' } }, 'win32') // {}
|
|
43
|
+
*/
|
|
44
|
+
export declare function testFileEnvironment(options: Pick<SpawnOptions, 'environment' | 'hiddenVariables'>, platform?: NodeJS.Platform): Record<string, string | undefined>;
|
|
37
45
|
/**
|
|
38
46
|
* The process one test file runs in. The parent owns it: it is killed when the run gives up on it, and,
|
|
39
47
|
* as a last resort, when the parent process exits.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-file-process.d.ts","sourceRoot":"","sources":["../../src/runner/test-file-process.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAA;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;
|
|
1
|
+
{"version":3,"file":"test-file-process.d.ts","sourceRoot":"","sources":["../../src/runner/test-file-process.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAA;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAC1E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAY5D,2GAA2G;AAC3G,MAAM,MAAM,eAAe,GAAG,OAAO,CAAC,YAAY,EAAE;IAAE,IAAI,EAAE,eAAe,CAAA;CAAE,CAAC,CAAA;AAE9E,4GAA4G;AAC5G,MAAM,MAAM,YAAY,GACpB;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,eAAe,CAAA;CAAE,GAC7C;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,WAAW,CAAA;CAAE,CAAA;AAEvC;;;GAGG;AACH,MAAM,MAAM,aAAa,GAAG;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE,CAAA;AAElE,MAAM,MAAM,YAAY,GAAG;IACzB,6GAA6G;IAC7G,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,gFAAgF;IAChF,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,EAAE,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IAC9D,8FAA8F;IAC9F,eAAe,CAAC,EAAE,SAAS,MAAM,EAAE,CAAA;IACnC,oHAAoH;IACpH,WAAW,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAC/C,CAAA;AAED;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,IAAI,CAAC,YAAY,EAAE,aAAa,GAAG,iBAAiB,CAAC,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAGpL;AAaD;;;GAGG;AACH,qBAAa,eAAe;;IAQ1B,OAAO;IA0BP;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,OAAO,GAAE,YAAiB,GAAG,eAAe;IAyBzD,IAAI,GAAG,IAAI,MAAM,GAAG,SAAS,CAE5B;IAED,IAAI,KAAK,IAAI,OAAO,CAEnB;IAED,0CAA0C;IAC1C,IAAI,IAAI,IAAI,WAAW,GAAG,SAAS,CAElC;IAED,yCAAyC;IACzC,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,4GAA4G;IAC5G,IAAI,MAAM,IAAI,SAAS,OAAO,EAAE,CAE/B;IAED,yFAAyF;IACzF,IAAI,MAAM,IAAI,OAAO,CAAC,WAAW,CAAC,CAEjC;IAED,oEAAoE;IACpE,MAAM,CAAC,QAAQ,EAAE,CAAC,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC,GAAG,SAAS,GAAG,IAAI;IAInE,6GAA6G;IAC7G,IAAI,CAAC,OAAO,EAAE,aAAa,GAAG,IAAI;IAOlC,4DAA4D;IAC5D,IAAI,IAAI,OAAO,CAAC,WAAW,CAAC;IAQ5B,+EAA+E;IACzE,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC;CAmCrD;AAED,4DAA4D;AAC5D,wBAAgB,YAAY,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAEvD;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,EAAE,CAQxE"}
|
|
@@ -3,8 +3,22 @@ import { extname } from 'node:path';
|
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { testProcessFlags } from "../loader/node.js";
|
|
5
5
|
import { playwrightArgument } from "../loader/resolve.js";
|
|
6
|
+
import { failure } from "../protocol/failures.js";
|
|
6
7
|
import { childMessageSchema } from "../protocol/messages.js";
|
|
7
8
|
import { parse } from "../protocol/schema.js";
|
|
9
|
+
import { withoutVariables } from "../shared/environment.js";
|
|
10
|
+
import { holdOwnChild, prepareOwnedLaunch } from "../shared/process-system.js";
|
|
11
|
+
/**
|
|
12
|
+
* The environment a test file's process starts with: this process's, or `environment`, without the hidden variables. On
|
|
13
|
+
* Windows NODE_COMPILE_CACHE is left out too: Node turns the compile cache on as it starts when that variable is set, and
|
|
14
|
+
* the test file's process keeps the cache off there (`child.ts`).
|
|
15
|
+
*
|
|
16
|
+
* @example testFileEnvironment({ environment: { NODE_COMPILE_CACHE: 'C:\\cache' } }, 'win32') // {}
|
|
17
|
+
*/
|
|
18
|
+
export function testFileEnvironment(options, platform = process.platform) {
|
|
19
|
+
const hidden = platform === 'win32' ? [...(options.hiddenVariables ?? []), 'NODE_COMPILE_CACHE'] : options.hiddenVariables;
|
|
20
|
+
return withoutVariables(options.environment ?? process.env, hidden, platform);
|
|
21
|
+
}
|
|
8
22
|
// The entry beside this module: child.ts when running from source, child.js once built.
|
|
9
23
|
const childEntry = fileURLToPath(new URL(`./child${extname(fileURLToPath(import.meta.url))}`, import.meta.url));
|
|
10
24
|
const liveChildren = new Set();
|
|
@@ -56,19 +70,28 @@ export class TestFileProcess {
|
|
|
56
70
|
*/
|
|
57
71
|
static spawn(options = {}) {
|
|
58
72
|
const output = options.onOutput;
|
|
59
|
-
|
|
73
|
+
// On Windows the runner is held before its first test file's process starts, and each one is held as a launch of
|
|
74
|
+
// its own, so whatever a test starts below it is held too, and ends with Retest.
|
|
75
|
+
prepareOwnedLaunch();
|
|
60
76
|
const child = fork(childEntry, options.playwright === true ? [playwrightArgument] : [], {
|
|
61
77
|
execArgv: [...conditionArguments(process.execArgv), ...testProcessFlags],
|
|
62
|
-
env:
|
|
78
|
+
env: testFileEnvironment(options),
|
|
63
79
|
serialization: 'json',
|
|
64
80
|
stdio: output === undefined ? ['ignore', 'ignore', 'ignore', 'ipc'] : ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
81
|
+
// On Windows a console of its own with no window, so a key pressed on Retest's console stops the run as Retest
|
|
82
|
+
// decides, rather than each test file's process at once; Node ignores it elsewhere.
|
|
83
|
+
windowsHide: true,
|
|
65
84
|
});
|
|
66
85
|
own(child);
|
|
86
|
+
const holding = holdOwnChild(child);
|
|
67
87
|
if (output !== undefined) {
|
|
68
88
|
child.stdout?.setEncoding('utf8').on('data', (text) => output('stdout', text));
|
|
69
89
|
child.stderr?.setEncoding('utf8').on('data', (text) => output('stderr', text));
|
|
70
90
|
}
|
|
71
|
-
|
|
91
|
+
const spawned = new TestFileProcess(child);
|
|
92
|
+
for (const problem of holding)
|
|
93
|
+
spawned.#errors.push(failure('setup_failed', `Retest could not hold the test file's process: ${problem}`));
|
|
94
|
+
return spawned;
|
|
72
95
|
}
|
|
73
96
|
get pid() {
|
|
74
97
|
return this.#child.pid;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"test-pages.d.ts","sourceRoot":"","sources":["../../src/runner/test-pages.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACvH,OAAO,KAAK,EAAe,aAAa,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAClF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAEtD,OAAO,KAAK,EAAE,OAAO,EAAgB,MAAM,yBAAyB,CAAA;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;
|
|
1
|
+
{"version":3,"file":"test-pages.d.ts","sourceRoot":"","sources":["../../src/runner/test-pages.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,YAAY,EAAE,SAAS,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAA;AACvH,OAAO,KAAK,EAAe,aAAa,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAClF,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAA;AAEtD,OAAO,KAAK,EAAE,OAAO,EAAgB,MAAM,yBAAyB,CAAA;AACpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAA;AAChE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AACvD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,uBAAuB,CAAA;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAS/C;;;;GAIG;AACH,MAAM,MAAM,OAAO,GAAG;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,SAAS,CAAC;IAAC,OAAO,EAAE,YAAY,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,eAAe,CAAC;IAAC,KAAK,CAAC,EAAE,IAAI,CAAA;CAAE,CAAA;AAErI,kDAAkD;AAClD,MAAM,MAAM,YAAY,GAAG;IACzB,KAAK,EAAE,QAAQ,CAAA;IACf,QAAQ,EAAE,QAAQ,CAAA;IAClB,2CAA2C;IAC3C,OAAO,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;IACtB,YAAY,EAAE,MAAM,OAAO,GAAG,SAAS,CAAA;IACvC,SAAS,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAA;IAC7C,yFAAyF;IACzF,OAAO,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,OAAO,CAAC,KAAK,IAAI,CAAA;IACzC,kFAAkF;IAClF,KAAK,EAAE,OAAO,CAAA;IACd,sCAAsC;IACtC,IAAI,EAAE,CAAC,IAAI,EAAE,SAAS,KAAK,IAAI,CAAA;IAC/B,wGAAwG;IACxG,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,MAAM,CAAA;IAChC,MAAM,EAAE,MAAM,CAAA;IACd,SAAS,EAAE,MAAM,CAAA;IACjB;;;OAGG;IACH,MAAM,CAAC,EAAE,UAAU,CAAA;CACpB,CAAA;AAED,+HAA+H;AAC/H,MAAM,MAAM,UAAU,GAAG;IAAE,MAAM,CAAC,OAAO,EAAE,YAAY,GAAG,aAAa,CAAC;IAAC,KAAK,IAAI,MAAM,CAAA;CAAE,CAAA;AAE1F,gGAAgG;AAChG,wBAAsB,QAAQ,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,YAAY,EAAE,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAehI;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAI1G;AAED;;;GAGG;AACH,wBAAsB,SAAS,CAAC,OAAO,EAAE,YAAY,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,GAAG,SAAS,CAAC,CAmBnJ;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,eAAe,GAAG,iBAAiB,GAAG,SAAS,CAIxF;AAED,uHAAuH;AACvH,wBAAsB,YAAY,CAAC,OAAO,EAAE,YAAY,EAAE,KAAK,EAAE,SAAS,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAYvG"}
|
|
@@ -3,6 +3,7 @@ import { NativeError } from "../native/session.js";
|
|
|
3
3
|
import { BrowserError } from "../browser/browser-error.js";
|
|
4
4
|
import { errorMessage, failure } from "../protocol/failures.js";
|
|
5
5
|
import { failureScreenshotFile, stateFile } from "../protocol/run-folder.js";
|
|
6
|
+
import { PrivateFolderError } from "../shared/private-file.js";
|
|
6
7
|
import { bounded } from "./bounded.js";
|
|
7
8
|
import { abortGraceMs } from "./running-test.js";
|
|
8
9
|
import { timerMs } from "./timer.js";
|
|
@@ -54,10 +55,12 @@ export async function saveState(context, { app, page, session }, state, target)
|
|
|
54
55
|
if (captured.status === 'failed')
|
|
55
56
|
return unsaved(errorMessage(captured.error));
|
|
56
57
|
try {
|
|
57
|
-
context.store.writeState(stateFile(state, target), captured.value);
|
|
58
|
+
await context.store.writeState(stateFile(state, target), captured.value);
|
|
58
59
|
}
|
|
59
60
|
catch (error) {
|
|
60
|
-
|
|
61
|
+
// A folder Windows would not keep private says so in a sentence; which tool refused, and how, is its detail.
|
|
62
|
+
const unsavedState = unsaved(errorMessage(error));
|
|
63
|
+
return error instanceof PrivateFolderError ? { ...unsavedState, details: { cause: error.detail } } : unsavedState;
|
|
61
64
|
}
|
|
62
65
|
context.emit({ type: 'state.saved', testId: context.testId, attemptId: context.attemptId, state, app, target, sessionId: session.sessionId });
|
|
63
66
|
return undefined;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An environment without the variables a process must not see, such as the ones secrets and judges read. On Windows a
|
|
3
|
+
* variable's name is one name whatever its case, so `Path` and `PATH` are the same variable there, and a hidden name
|
|
4
|
+
* hides it in every case; elsewhere names are compared exactly, as the system compares them.
|
|
5
|
+
*
|
|
6
|
+
* @example withoutVariables(process.env, ['RETEST_PASSWORD']) // process.env without RETEST_PASSWORD
|
|
7
|
+
*/
|
|
8
|
+
export declare function withoutVariables(environment: Readonly<Record<string, string | undefined>>, hidden: Iterable<string> | undefined, platform?: NodeJS.Platform): Record<string, string | undefined>;
|
|
9
|
+
/**
|
|
10
|
+
* A variable name as Windows compares it: each character in its upper case, one character for one, as the system
|
|
11
|
+
* compares names character by character. A character whose upper case is longer, such as ß (SS), stays as it is, so
|
|
12
|
+
* `straße` and `STRASSE` stay two names.
|
|
13
|
+
*
|
|
14
|
+
* @example foldCase('Path') // 'PATH'
|
|
15
|
+
*/
|
|
16
|
+
export declare function foldCase(name: string): string;
|
|
17
|
+
//# sourceMappingURL=environment.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"environment.d.ts","sourceRoot":"","sources":["../../src/shared/environment.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAAC,EACzD,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,GAAG,SAAS,EACpC,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAC3C,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,CAIpC;AAED;;;;;;GAMG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAO7C"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An environment without the variables a process must not see, such as the ones secrets and judges read. On Windows a
|
|
3
|
+
* variable's name is one name whatever its case, so `Path` and `PATH` are the same variable there, and a hidden name
|
|
4
|
+
* hides it in every case; elsewhere names are compared exactly, as the system compares them.
|
|
5
|
+
*
|
|
6
|
+
* @example withoutVariables(process.env, ['RETEST_PASSWORD']) // process.env without RETEST_PASSWORD
|
|
7
|
+
*/
|
|
8
|
+
export function withoutVariables(environment, hidden, platform = process.platform) {
|
|
9
|
+
const named = platform === 'win32' ? foldCase : (name) => name;
|
|
10
|
+
const hide = new Set([...(hidden ?? [])].map(named));
|
|
11
|
+
return Object.fromEntries(Object.entries(environment).filter(([name]) => !hide.has(named(name))));
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* A variable name as Windows compares it: each character in its upper case, one character for one, as the system
|
|
15
|
+
* compares names character by character. A character whose upper case is longer, such as ß (SS), stays as it is, so
|
|
16
|
+
* `straße` and `STRASSE` stay two names.
|
|
17
|
+
*
|
|
18
|
+
* @example foldCase('Path') // 'PATH'
|
|
19
|
+
*/
|
|
20
|
+
export function foldCase(name) {
|
|
21
|
+
let folded = '';
|
|
22
|
+
for (const character of name) {
|
|
23
|
+
const upper = character.toUpperCase();
|
|
24
|
+
folded += [...upper].length === 1 ? upper : character;
|
|
25
|
+
}
|
|
26
|
+
return folded;
|
|
27
|
+
}
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { performance } from 'node:perf_hooks';
|
|
3
3
|
import { parentPort } from 'node:worker_threads';
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
//
|
|
4
|
+
import { errorCode } from "./error-code.js";
|
|
5
|
+
import { metadataAbandoned, metadataComplete, metadataDeadlineVariable, metadataFailure, metadataPending, metadataRunning, metadataSuccess } from "./metadata-process.js";
|
|
6
|
+
import { launchesHeldByJob } from "./process-system.js";
|
|
7
|
+
// Children whose query failed, until each one's own exit is seen. On macOS and Linux none is ever signalled. A stuck one
|
|
8
|
+
// fails only its own query, and queries in flight at once are not counted, but while this many are outstanding later
|
|
9
|
+
// queries are refused, so stuck children cannot pile up.
|
|
8
10
|
const unresolved = new Set();
|
|
9
11
|
const unresolvedLimit = 4;
|
|
10
12
|
parentPort?.on('message', (request) => {
|
|
@@ -25,7 +27,7 @@ parentPort?.on('message', (request) => {
|
|
|
25
27
|
}
|
|
26
28
|
let child;
|
|
27
29
|
try {
|
|
28
|
-
child = spawn(request.command, request.args, { detached: true, stdio: ['ignore', 'pipe', 'pipe'], env: { ...request.environment, TZ: 'UTC0' } });
|
|
30
|
+
child = spawn(request.command, request.args, { detached: true, stdio: ['ignore', 'pipe', 'pipe'], env: { ...request.environment, TZ: 'UTC0', [metadataDeadlineVariable]: `${Math.floor(performance.timeOrigin + deadline)}` } });
|
|
29
31
|
}
|
|
30
32
|
catch {
|
|
31
33
|
finish(control, output, metadataFailure, 'The metadata process could not start.');
|
|
@@ -51,6 +53,10 @@ parentPort?.on('message', (request) => {
|
|
|
51
53
|
settled = true;
|
|
52
54
|
clearTimeout(timer);
|
|
53
55
|
stopReading();
|
|
56
|
+
// On Windows a query is a run of retest-supervisor, which may still be making or filling a job. One given up on is
|
|
57
|
+
// ended, through Node's own handle on it, before the failure is told, so no job appears after its caller moved on.
|
|
58
|
+
if (launchesHeldByJob && !closed)
|
|
59
|
+
child.kill();
|
|
54
60
|
if (!closed)
|
|
55
61
|
unresolved.add(child);
|
|
56
62
|
finish(control, output, metadataFailure, message);
|
|
@@ -81,10 +87,12 @@ parentPort?.on('message', (request) => {
|
|
|
81
87
|
child.stderr?.on('data', (chunk) => { read(chunk, false); });
|
|
82
88
|
child.stdout?.on('error', () => { fail('The metadata process output could not be read.'); });
|
|
83
89
|
child.stderr?.on('error', () => { fail('The metadata process output could not be read.'); });
|
|
84
|
-
child.on('error', () => {
|
|
90
|
+
child.on('error', (error) => {
|
|
85
91
|
if (child.pid === undefined)
|
|
86
92
|
release();
|
|
87
|
-
|
|
93
|
+
// The system's code says why, such as a program that is gone or a system out of room for another process.
|
|
94
|
+
const code = errorCode(error);
|
|
95
|
+
fail(code === undefined ? 'The metadata process could not run.' : `The metadata process could not run (${code}).`);
|
|
88
96
|
});
|
|
89
97
|
child.once('close', (code, signal) => {
|
|
90
98
|
release();
|
|
@@ -25,6 +25,11 @@ export type MetadataProcessRequest = {
|
|
|
25
25
|
readonly output: SharedArrayBuffer;
|
|
26
26
|
};
|
|
27
27
|
export declare const metadataOutputLimit: number;
|
|
28
|
+
/**
|
|
29
|
+
* The variable each metadata process finds the time its query is given up at in, as milliseconds since the epoch, for a
|
|
30
|
+
* program that must change nothing once its answer can no longer be heard, as the stand-in supervisor the tests run.
|
|
31
|
+
*/
|
|
32
|
+
export declare const metadataDeadlineVariable = "RETEST_METADATA_DEADLINE_MS";
|
|
28
33
|
/**
|
|
29
34
|
* The bound a whole process table is read under. A `ps -ww` table passed 4 MiB on a busy Mac (4.53 MB with about 3,600
|
|
30
35
|
* processes), and a table refused for its size leaves every ownership decision without a reading, so nothing could be
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata-process.d.ts","sourceRoot":"","sources":["../../src/shared/metadata-process.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAIvD,sFAAsF;AACtF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;IACxC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B,CAAA;AAED,yFAAyF;AACzF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAA;IACnC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAA;CACnC,CAAA;AAED,eAAO,MAAM,mBAAmB,EAAE,MAAwB,CAAA;AAC1D;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAyB,CAAA;AAC/D,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,gBAAgB,EAAE,MAAU,CAAA;AACzC,eAAO,MAAM,iBAAiB,EAAE,MAAU,CAAA;AAC1C,kHAAkH;AAClH,eAAO,MAAM,mBAAmB,EAAE,MAAU,CAAA;AAC5C,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"metadata-process.d.ts","sourceRoot":"","sources":["../../src/shared/metadata-process.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAIvD,sFAAsF;AACtF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;IACxC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B,CAAA;AAED,yFAAyF;AACzF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAA;IACnC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAA;CACnC,CAAA;AAED,eAAO,MAAM,mBAAmB,EAAE,MAAwB,CAAA;AAC1D;;;GAGG;AACH,eAAO,MAAM,wBAAwB,gCAAgC,CAAA;AACrE;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAyB,CAAA;AAC/D,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,gBAAgB,EAAE,MAAU,CAAA;AACzC,eAAO,MAAM,iBAAiB,EAAE,MAAU,CAAA;AAC1C,kHAAkH;AAClH,eAAO,MAAM,mBAAmB,EAAE,MAAU,CAAA;AAC5C,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AAoBxC;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAsB3E;AAED;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8B/F;AAmGD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,IAAI,CAQ7D"}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { performance } from 'node:perf_hooks';
|
|
2
2
|
import { Worker } from 'node:worker_threads';
|
|
3
3
|
export const metadataOutputLimit = 4 * 1024 * 1024;
|
|
4
|
+
/**
|
|
5
|
+
* The variable each metadata process finds the time its query is given up at in, as milliseconds since the epoch, for a
|
|
6
|
+
* program that must change nothing once its answer can no longer be heard, as the stand-in supervisor the tests run.
|
|
7
|
+
*/
|
|
8
|
+
export const metadataDeadlineVariable = 'RETEST_METADATA_DEADLINE_MS';
|
|
4
9
|
/**
|
|
5
10
|
* The bound a whole process table is read under. A `ps -ww` table passed 4 MiB on a busy Mac (4.53 MB with about 3,600
|
|
6
11
|
* processes), and a table refused for its size leaves every ownership decision without a reading, so nothing could be
|
|
@@ -18,6 +23,9 @@ export const metadataSuccess = 0;
|
|
|
18
23
|
export const metadataFailure = 1;
|
|
19
24
|
const queryLimitMs = 5000;
|
|
20
25
|
const workerAllowanceMs = 1000;
|
|
26
|
+
// How many fresh workers one query is asked of after workers it was posted to ended by themselves before answering
|
|
27
|
+
// anything, as one whose module cannot load does, which would otherwise be replaced as fast as each one ends.
|
|
28
|
+
const workerRetries = 2;
|
|
21
29
|
// One spare of each production capacity. Only a completed query can return its buffer: an abandoned worker may
|
|
22
30
|
// still be writing, and a concurrent query must hold a different buffer until its completion signal is consumed.
|
|
23
31
|
const spareOutputs = new Map();
|
|
@@ -25,6 +33,10 @@ const spareOutputs = new Map();
|
|
|
25
33
|
// leaving each to wait out its whole bound for an answer that cannot come.
|
|
26
34
|
const inFlight = new Set();
|
|
27
35
|
let bridge;
|
|
36
|
+
// Workers that answered a query, and workers ended here for a query other than the one asking again; an ending of
|
|
37
|
+
// either says nothing against the next worker.
|
|
38
|
+
const answered = new WeakSet();
|
|
39
|
+
const endedForAnother = new WeakSet();
|
|
28
40
|
let workerModule = new URL(import.meta.url.endsWith('.ts') ? './metadata-process-worker.ts' : './metadata-process-worker.js', import.meta.url);
|
|
29
41
|
/**
|
|
30
42
|
* A synchronous bridge for fresh ownership readings, including exit hooks. The worker uses an asynchronous child
|
|
@@ -43,7 +55,7 @@ export function readMetadataProcess(options) {
|
|
|
43
55
|
return answer(query);
|
|
44
56
|
if (state === metadataWorkerEnded) {
|
|
45
57
|
settle(query);
|
|
46
|
-
query =
|
|
58
|
+
query = askAgain(query, options);
|
|
47
59
|
continue;
|
|
48
60
|
}
|
|
49
61
|
const remaining = query.deadline - performance.now();
|
|
@@ -81,7 +93,7 @@ export async function readMetadataProcessAsync(options) {
|
|
|
81
93
|
return answer(query);
|
|
82
94
|
if (state === metadataWorkerEnded) {
|
|
83
95
|
settle(query);
|
|
84
|
-
query =
|
|
96
|
+
query = askAgain(query, options);
|
|
85
97
|
continue;
|
|
86
98
|
}
|
|
87
99
|
const remaining = query.deadline - performance.now();
|
|
@@ -107,7 +119,18 @@ function checkCaller(query, deadline) {
|
|
|
107
119
|
abandoned(query);
|
|
108
120
|
throw new Error('The metadata query was not confirmed before its caller deadline ended or was cancelled.');
|
|
109
121
|
}
|
|
110
|
-
|
|
122
|
+
// A query whose worker ended is asked again of a fresh worker, within its own caller's deadline. Only a worker that
|
|
123
|
+
// ended by itself before it answered anything counts towards the query's few retries; a worker ended for another
|
|
124
|
+
// query, or one that had answered, is replaced as before.
|
|
125
|
+
function askAgain(query, options) {
|
|
126
|
+
const broken = !answered.has(query.worker) && !endedForAnother.has(query.worker);
|
|
127
|
+
if (!broken)
|
|
128
|
+
return submit(options, query.retries);
|
|
129
|
+
if (query.retries >= workerRetries)
|
|
130
|
+
throw new Error('The metadata worker ended before it answered the query.');
|
|
131
|
+
return submit(options, query.retries + 1);
|
|
132
|
+
}
|
|
133
|
+
function submit(options, retries = 0) {
|
|
111
134
|
if (options.deadline?.signal?.aborted === true || options.deadline?.reached === true)
|
|
112
135
|
throw new Error('The metadata query was not started because its caller deadline ended or was cancelled.');
|
|
113
136
|
const timeoutMs = options.timeoutMs ?? queryLimitMs;
|
|
@@ -134,7 +157,7 @@ function submit(options) {
|
|
|
134
157
|
discardWorker(worker);
|
|
135
158
|
throw new Error('The metadata worker could not receive the query.');
|
|
136
159
|
}
|
|
137
|
-
const query = { worker, control, output, deadline: Math.min(performance.now() + timeoutMs + workerAllowanceMs, request.deadline === undefined ? Infinity : request.deadline - performance.timeOrigin) };
|
|
160
|
+
const query = { worker, control, output, deadline: Math.min(performance.now() + timeoutMs + workerAllowanceMs, request.deadline === undefined ? Infinity : request.deadline - performance.timeOrigin), retries };
|
|
138
161
|
inFlight.add(query);
|
|
139
162
|
return query;
|
|
140
163
|
}
|
|
@@ -163,6 +186,7 @@ function abandoned(query) {
|
|
|
163
186
|
return false;
|
|
164
187
|
if (Atomics.compareExchange(query.control, 0, latest, metadataAbandoned) !== latest)
|
|
165
188
|
continue;
|
|
189
|
+
endedForAnother.add(query.worker);
|
|
166
190
|
discardWorker(query.worker);
|
|
167
191
|
return true;
|
|
168
192
|
}
|
|
@@ -177,6 +201,7 @@ function releaseOutput(query) {
|
|
|
177
201
|
spareOutputs.set(capacity, query.output);
|
|
178
202
|
}
|
|
179
203
|
function answer(query) {
|
|
204
|
+
answered.add(query.worker);
|
|
180
205
|
const length = Atomics.load(query.control, 2);
|
|
181
206
|
if (length < 0 || length > query.output.byteLength) {
|
|
182
207
|
discardWorker(query.worker);
|
|
@@ -203,11 +228,11 @@ export function useMetadataWorkerModule(file) {
|
|
|
203
228
|
const previous = workerModule;
|
|
204
229
|
workerModule = file;
|
|
205
230
|
if (bridge !== undefined)
|
|
206
|
-
|
|
231
|
+
retire(bridge);
|
|
207
232
|
return () => {
|
|
208
233
|
workerModule = previous;
|
|
209
234
|
if (bridge !== undefined)
|
|
210
|
-
|
|
235
|
+
retire(bridge);
|
|
211
236
|
};
|
|
212
237
|
}
|
|
213
238
|
function metadataWorker() {
|
|
@@ -234,6 +259,11 @@ function metadataWorker() {
|
|
|
234
259
|
bridge = worker;
|
|
235
260
|
return worker;
|
|
236
261
|
}
|
|
262
|
+
// A worker set aside for a module swapped in, which says nothing against the worker after it.
|
|
263
|
+
function retire(worker) {
|
|
264
|
+
endedForAnother.add(worker);
|
|
265
|
+
discardWorker(worker);
|
|
266
|
+
}
|
|
237
267
|
// The query that met the failure reports it; the next one starts a fresh worker, and the others it held are asked again.
|
|
238
268
|
function discardWorker(worker) {
|
|
239
269
|
if (bridge === worker)
|
|
@@ -5,4 +5,16 @@
|
|
|
5
5
|
* @example relativePosixPath('/work', '/work/tests/tasks.retest.ts') // 'tests/tasks.retest.ts'
|
|
6
6
|
*/
|
|
7
7
|
export declare function relativePosixPath(from: string, to: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* A path with forward slashes on every platform, as a record stores it, so a run on macOS and one on Windows read alike.
|
|
10
|
+
*
|
|
11
|
+
* @example posixPath('C:\\work\\.data\\desk') // 'C:/work/.data/desk' on Windows; a POSIX path is unchanged
|
|
12
|
+
*/
|
|
13
|
+
export declare function posixPath(path: string): string;
|
|
14
|
+
/**
|
|
15
|
+
* A path a record stores, as this platform writes it, for a person.
|
|
16
|
+
*
|
|
17
|
+
* @example platformPath('C:/work/.data/desk') // 'C:\\work\\.data\\desk' on Windows; unchanged on macOS and Linux
|
|
18
|
+
*/
|
|
19
|
+
export declare function platformPath(path: string): string;
|
|
8
20
|
//# sourceMappingURL=posix-path.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"posix-path.d.ts","sourceRoot":"","sources":["../../src/shared/posix-path.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAElE"}
|
|
1
|
+
{"version":3,"file":"posix-path.d.ts","sourceRoot":"","sources":["../../src/shared/posix-path.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAElE;AAED;;;;GAIG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE9C;AAED;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEjD"}
|
|
@@ -8,3 +8,19 @@ import { relative, sep } from 'node:path';
|
|
|
8
8
|
export function relativePosixPath(from, to) {
|
|
9
9
|
return relative(from, to).split(sep).join('/');
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* A path with forward slashes on every platform, as a record stores it, so a run on macOS and one on Windows read alike.
|
|
13
|
+
*
|
|
14
|
+
* @example posixPath('C:\\work\\.data\\desk') // 'C:/work/.data/desk' on Windows; a POSIX path is unchanged
|
|
15
|
+
*/
|
|
16
|
+
export function posixPath(path) {
|
|
17
|
+
return path.split(sep).join('/');
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A path a record stores, as this platform writes it, for a person.
|
|
21
|
+
*
|
|
22
|
+
* @example platformPath('C:/work/.data/desk') // 'C:\\work\\.data\\desk' on Windows; unchanged on macOS and Linux
|
|
23
|
+
*/
|
|
24
|
+
export function platformPath(path) {
|
|
25
|
+
return path.split('/').join(sep);
|
|
26
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Why a folder could not be kept to this user alone: `message` is a sentence for a person, `detail` names the Windows
|
|
3
|
+
* tool and what it said.
|
|
4
|
+
*/
|
|
5
|
+
export declare class PrivateFolderError extends Error {
|
|
6
|
+
readonly detail: string;
|
|
7
|
+
constructor(message: string, detail: string);
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Leaves `folder`, an empty folder this process just made, readable and writable by the user this process runs as and by
|
|
11
|
+
* no other user, and makes every file and folder later made in it inherit that. Nothing runs on macOS and Linux, where
|
|
12
|
+
* the folder's mode does it. On Windows every inherited entry is removed, Users' read among them, and this user's SID
|
|
13
|
+
* gets full control, passed on to what the folder will hold. An entry the folder holds as its own stays: under a parent
|
|
14
|
+
* whose list is not marked as passed down, such as a temporary folder Node makes, Windows writes the system's and the
|
|
15
|
+
* administrators' that way, and they read any file, as root does on Unix. Throws a `PrivateFolderError` when that cannot
|
|
16
|
+
* be done.
|
|
17
|
+
*
|
|
18
|
+
* @example mkdirSync(folder, { mode: 0o700 }); await keepFolderToUser(folder)
|
|
19
|
+
*/
|
|
20
|
+
export declare function keepFolderToUser(folder: string, platform?: NodeJS.Platform): Promise<void>;
|
|
21
|
+
/**
|
|
22
|
+
* The SID of the user this process runs as, from whoami, read once.
|
|
23
|
+
*
|
|
24
|
+
* @example await currentUserSid() // 'S-1-5-21-…-1001'
|
|
25
|
+
*/
|
|
26
|
+
export declare function currentUserSid(): Promise<string>;
|
|
27
|
+
/**
|
|
28
|
+
* Runs a Windows tool to its end through the launch seam, with an environment of `SystemRoot` alone: undefined once it
|
|
29
|
+
* ended with 0, otherwise how it ended, as a clause for a detail. One that has not ended within `timeoutMs` is ended,
|
|
30
|
+
* with everything it started. What it printed on stdout is pushed to `output`.
|
|
31
|
+
*
|
|
32
|
+
* @example await runOwnedTool('C:\\Windows\\System32\\whoami.exe', []) // undefined
|
|
33
|
+
*/
|
|
34
|
+
export declare function runOwnedTool(program: string, args: readonly string[], options?: {
|
|
35
|
+
output?: string[];
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
}): Promise<string | undefined>;
|
|
38
|
+
//# sourceMappingURL=private-file.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"private-file.d.ts","sourceRoot":"","sources":["../../src/shared/private-file.ts"],"names":[],"mappings":"AAiBA;;;GAGG;AACH,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;gBAEX,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;CAK5C;AAID;;;;;;;;;;GAUG;AACH,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,OAAO,CAAC,IAAI,CAAC,CAKlH;AAED;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAAC,MAAM,CAAC,CAOhD;AAWD;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,OAAO,GAAE;IAAE,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAuCjK"}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { win32 } from 'node:path';
|
|
3
|
+
import { consolesDetached, holdLaunch, prepareOwnedLaunch } from "./process-system.js";
|
|
4
|
+
// A folder that holds secrets, such as a run's saved sign-in states, is open to this user alone, and so is every file
|
|
5
|
+
// later made in it. On macOS and Linux the mode it is created with does that. Windows ignores the mode and gives a new
|
|
6
|
+
// folder the access list of its parent, which under C:\ lets every local user read it, and a file made in it takes that
|
|
7
|
+
// list too. There the folder keeps nothing of its parent's list and gets one entry, this user's full control, which
|
|
8
|
+
// every file and folder made in it inherits. That is done before any file is made in it: Windows checks access when a
|
|
9
|
+
// file is opened, so a file another user opened while it was still open to them would stay open to them.
|
|
10
|
+
//
|
|
11
|
+
// whoami and icacls run as every program Retest starts does, through the launch seam, and are waited for without
|
|
12
|
+
// blocking: each is held in a job of its own on Windows, and one that has not ended within its time is ended with it.
|
|
13
|
+
const toolTimeoutMs = 10_000;
|
|
14
|
+
const outputLimit = 64 * 1024;
|
|
15
|
+
/**
|
|
16
|
+
* Why a folder could not be kept to this user alone: `message` is a sentence for a person, `detail` names the Windows
|
|
17
|
+
* tool and what it said.
|
|
18
|
+
*/
|
|
19
|
+
export class PrivateFolderError extends Error {
|
|
20
|
+
detail;
|
|
21
|
+
constructor(message, detail) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'PrivateFolderError';
|
|
24
|
+
this.detail = detail;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
let userSid;
|
|
28
|
+
/**
|
|
29
|
+
* Leaves `folder`, an empty folder this process just made, readable and writable by the user this process runs as and by
|
|
30
|
+
* no other user, and makes every file and folder later made in it inherit that. Nothing runs on macOS and Linux, where
|
|
31
|
+
* the folder's mode does it. On Windows every inherited entry is removed, Users' read among them, and this user's SID
|
|
32
|
+
* gets full control, passed on to what the folder will hold. An entry the folder holds as its own stays: under a parent
|
|
33
|
+
* whose list is not marked as passed down, such as a temporary folder Node makes, Windows writes the system's and the
|
|
34
|
+
* administrators' that way, and they read any file, as root does on Unix. Throws a `PrivateFolderError` when that cannot
|
|
35
|
+
* be done.
|
|
36
|
+
*
|
|
37
|
+
* @example mkdirSync(folder, { mode: 0o700 }); await keepFolderToUser(folder)
|
|
38
|
+
*/
|
|
39
|
+
export async function keepFolderToUser(folder, platform = process.platform) {
|
|
40
|
+
if (platform !== 'win32')
|
|
41
|
+
return;
|
|
42
|
+
const sid = await currentUserSid();
|
|
43
|
+
const ended = await runOwnedTool(systemTool('icacls.exe'), [folder, '/inheritance:r', '/grant:r', `*${sid}:(OI)(CI)(F)`]);
|
|
44
|
+
if (ended !== undefined)
|
|
45
|
+
throw new PrivateFolderError('its folder could not be kept to this user alone.', `icacls.exe ${ended}`);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The SID of the user this process runs as, from whoami, read once.
|
|
49
|
+
*
|
|
50
|
+
* @example await currentUserSid() // 'S-1-5-21-…-1001'
|
|
51
|
+
*/
|
|
52
|
+
export function currentUserSid() {
|
|
53
|
+
const reading = (userSid ??= readUserSid());
|
|
54
|
+
// A reading that failed is not kept, so the next save asks again.
|
|
55
|
+
reading.catch(() => {
|
|
56
|
+
if (userSid === reading)
|
|
57
|
+
userSid = undefined;
|
|
58
|
+
});
|
|
59
|
+
return reading;
|
|
60
|
+
}
|
|
61
|
+
async function readUserSid() {
|
|
62
|
+
const output = [];
|
|
63
|
+
const ended = await runOwnedTool(systemTool('whoami.exe'), ['/user', '/fo', 'csv', '/nh'], { output });
|
|
64
|
+
const printed = (output[0] ?? '').trim();
|
|
65
|
+
const sid = ended === undefined ? /"(S-1-[0-9-]+)"\s*$/.exec(printed)?.[1] : undefined;
|
|
66
|
+
if (sid === undefined)
|
|
67
|
+
throw new PrivateFolderError('Retest could not tell which user it runs as.', `whoami.exe ${ended ?? `printed no SID: ${printed}`}`);
|
|
68
|
+
return sid;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Runs a Windows tool to its end through the launch seam, with an environment of `SystemRoot` alone: undefined once it
|
|
72
|
+
* ended with 0, otherwise how it ended, as a clause for a detail. One that has not ended within `timeoutMs` is ended,
|
|
73
|
+
* with everything it started. What it printed on stdout is pushed to `output`.
|
|
74
|
+
*
|
|
75
|
+
* @example await runOwnedTool('C:\\Windows\\System32\\whoami.exe', []) // undefined
|
|
76
|
+
*/
|
|
77
|
+
export async function runOwnedTool(program, args, options = {}) {
|
|
78
|
+
const { output = [], timeoutMs = toolTimeoutMs } = options;
|
|
79
|
+
let child;
|
|
80
|
+
try {
|
|
81
|
+
prepareOwnedLaunch();
|
|
82
|
+
child = spawn(program, args, { env: { SystemRoot: systemRoot() }, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true, detached: consolesDetached });
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return `could not start: ${error instanceof Error ? error.message : String(error)}`;
|
|
86
|
+
}
|
|
87
|
+
let stdout = '';
|
|
88
|
+
let stderr = '';
|
|
89
|
+
child.stdout?.setEncoding('utf8').on('data', (chunk) => {
|
|
90
|
+
if (stdout.length < outputLimit)
|
|
91
|
+
stdout += chunk;
|
|
92
|
+
});
|
|
93
|
+
child.stderr?.setEncoding('utf8').on('data', (chunk) => {
|
|
94
|
+
if (stderr.length < outputLimit)
|
|
95
|
+
stderr += chunk;
|
|
96
|
+
});
|
|
97
|
+
const ended = new Promise((resolve) => {
|
|
98
|
+
child.once('error', (error) => resolve({ code: null, error }));
|
|
99
|
+
child.once('close', (code) => resolve({ code }));
|
|
100
|
+
});
|
|
101
|
+
const ownership = child.pid === undefined ? undefined : holdLaunch(child);
|
|
102
|
+
let timedOut = false;
|
|
103
|
+
const timer = setTimeout(() => {
|
|
104
|
+
timedOut = true;
|
|
105
|
+
if (ownership === undefined)
|
|
106
|
+
child.kill('SIGKILL');
|
|
107
|
+
else
|
|
108
|
+
ownership.signal('SIGKILL');
|
|
109
|
+
}, timeoutMs);
|
|
110
|
+
try {
|
|
111
|
+
const { code, error } = await ended;
|
|
112
|
+
output.push(stdout);
|
|
113
|
+
if (error !== undefined)
|
|
114
|
+
return `could not start: ${error.message}`;
|
|
115
|
+
if (timedOut)
|
|
116
|
+
return `did not finish within ${timeoutMs} ms and was ended`;
|
|
117
|
+
if (code === 0)
|
|
118
|
+
return undefined;
|
|
119
|
+
const said = (stderr.trim() || stdout.trim()).split(/\r?\n/, 1)[0] ?? '';
|
|
120
|
+
return `ended with exit code ${code ?? 'none'}${said === '' ? '' : `: ${said}`}`;
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
clearTimeout(timer);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// The tools are named by their place in the system folder, never looked up on PATH.
|
|
127
|
+
function systemTool(name) {
|
|
128
|
+
return win32.join(systemRoot(), 'System32', name);
|
|
129
|
+
}
|
|
130
|
+
function systemRoot() {
|
|
131
|
+
return process.env['SystemRoot'] ?? 'C:\\Windows';
|
|
132
|
+
}
|