@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildEngines, buildPlatform, cacheFolders,
|
|
1
|
+
import { buildEngines, buildPlatform, cacheFolders, cacheVariable, findPin, inspectBuilds, unpinnedMessage } from "../../browser/builds.js";
|
|
2
2
|
import { createStyle } from "../../reporters/style.js";
|
|
3
3
|
import { interruptedExitCode } from "../../runner/outcome.js";
|
|
4
4
|
import { listWords } from "../../shared/list-words.js";
|
|
@@ -11,9 +11,12 @@ import { systemUnpackTools } from "../install/unpack.js";
|
|
|
11
11
|
import { installMedia } from "../install/media-install.js";
|
|
12
12
|
import { inspectMedia } from "../install/media-record.js";
|
|
13
13
|
import { renderMediaInstall, renderMediaListing } from "../install/media-report.js";
|
|
14
|
+
import { installSupervisor } from "../install/supervisor-install.js";
|
|
15
|
+
import { inspectSupervisor } from "../install/supervisor-record.js";
|
|
16
|
+
import { renderSupervisorInstall, renderSupervisorListing } from "../install/supervisor-report.js";
|
|
14
17
|
import { suggest } from "../suggest.js";
|
|
15
|
-
import { shouldUseColor } from "../terminal.js";
|
|
16
|
-
const installEngines = [...buildEngines, 'media'];
|
|
18
|
+
import { shouldUseColor, terminalMarks } from "../terminal.js";
|
|
19
|
+
const installEngines = [...buildEngines, 'media', 'supervisor'];
|
|
17
20
|
const options = {
|
|
18
21
|
'media-binary': value({ placeholder: '<path>', description: 'Install a prebuilt media binary only if its exact build has a pinned checksum' }),
|
|
19
22
|
'media-mirror': value({ placeholder: '<url>', description: 'Install a pinned prebuilt media binary from this mirror; checksums still apply' }),
|
|
@@ -29,12 +32,13 @@ export const installCommand = {
|
|
|
29
32
|
description: [
|
|
30
33
|
'Downloads the pinned build of each engine named, checks its size and SHA-256 against the pin before it unpacks',
|
|
31
34
|
'anything, checks that the build carries every licence notice the pin names, and records it in the cache:',
|
|
32
|
-
'~/Library/Caches/retest on macOS, $XDG_CACHE_HOME/retest or ~/.cache/retest on Linux.',
|
|
35
|
+
'~/Library/Caches/retest on macOS, $XDG_CACHE_HOME/retest or ~/.cache/retest on Linux, %LOCALAPPDATA%\\retest on Windows.',
|
|
33
36
|
'Nothing is downloaded unless an engine is named here: run, doctor and --list never download.',
|
|
34
37
|
'A pin with no checksum, or a build published without the notices it must carry, is refused before anything is',
|
|
35
38
|
'fetched. The native executors are built on this Mac from their pinned commits, from the checkouts a native run',
|
|
36
39
|
'builds from; nothing is cloned. install media builds the shipped Rust crate with cargo --locked.',
|
|
37
40
|
'Media needs Rust 1.88 or later and the host ffmpeg. --media-binary and --media-mirror refuse without an exact pin.',
|
|
41
|
+
'install supervisor builds the shipped Rust crate that ends what Retest starts on Windows, with Rust 1.88 or later.',
|
|
38
42
|
`${mirrorVariable} names a mirror to fetch from instead, with each publisher's host as its first folder. The`,
|
|
39
43
|
'checksums still apply. A mirror address with a user name, password, query or fragment is refused, and only the',
|
|
40
44
|
'origin and path of any address are printed or recorded.',
|
|
@@ -54,16 +58,17 @@ export const installCommand = {
|
|
|
54
58
|
const platform = thisPlatform();
|
|
55
59
|
const folders = cacheFolders(dependencies.env);
|
|
56
60
|
if (folders === undefined)
|
|
57
|
-
throw new CliError(
|
|
58
|
-
const style = createStyle(shouldUseColor(dependencies.stdout, dependencies.env));
|
|
61
|
+
throw new CliError(`${cacheVariable()} is not set to an absolute folder, so Retest has no cache folder to install into or read.`);
|
|
62
|
+
const style = createStyle(shouldUseColor(dependencies.stdout, dependencies.env), terminalMarks(dependencies.stdout, dependencies.env));
|
|
59
63
|
if (listing) {
|
|
60
64
|
const inspections = await inspectBuilds(platform, folders, { verify: parsed.flag('verify') });
|
|
61
65
|
const media = await inspectMedia(dependencies.env, { verify: parsed.flag('verify') });
|
|
62
|
-
dependencies.
|
|
63
|
-
|
|
66
|
+
const supervisor = await inspectSupervisor(dependencies.env, { verify: parsed.flag('verify') });
|
|
67
|
+
dependencies.stdout.write(renderSupervisorListing(renderMediaListing(renderInspections({ platform, folders, inspections, json: parsed.flag('json'), style }), media, parsed.flag('json')), supervisor, parsed.flag('json')));
|
|
68
|
+
return supervisor?.state === 'damaged' || media?.state === 'damaged' || media?.state === 'unverifiable' || inspections.some((inspection) => inspection.state === 'damaged' || inspection.state === 'unverifiable') ? 2 : 0;
|
|
64
69
|
}
|
|
65
70
|
const mirror = dependencies.env[mirrorVariable];
|
|
66
|
-
const problem = engines.every((engine) => engine === 'media') || mirror === undefined || mirror === '' ? undefined : mirrorProblem(mirror);
|
|
71
|
+
const problem = engines.every((engine) => engine === 'media' || engine === 'supervisor') || mirror === undefined || mirror === '' ? undefined : mirrorProblem(mirror);
|
|
67
72
|
if (problem !== undefined)
|
|
68
73
|
throw new CliError(problem);
|
|
69
74
|
const home = dependencies.env['HOME'] ?? '';
|
|
@@ -71,9 +76,16 @@ export const installCommand = {
|
|
|
71
76
|
for (const engine of engines) {
|
|
72
77
|
if (dependencies.signal.aborted)
|
|
73
78
|
break;
|
|
79
|
+
if (engine === 'supervisor') {
|
|
80
|
+
const result = await installSupervisor({ env: dependencies.env, signal: dependencies.signal, version: dependencies.version, report: (line) => void dependencies.stdout.write(` ${style.dim(line)}\n`) });
|
|
81
|
+
dependencies.stdout.write(renderSupervisorInstall(result, style.marks));
|
|
82
|
+
if (!result.ok)
|
|
83
|
+
failed = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
74
86
|
if (engine === 'media') {
|
|
75
87
|
const result = await installMedia({ env: dependencies.env, signal: dependencies.signal, version: dependencies.version, report: (line) => void dependencies.stdout.write(` ${style.dim(line)}\n`), binary: parsed.value('media-binary'), mirror: parsed.value('media-mirror'), offline: parsed.flag('offline') });
|
|
76
|
-
dependencies.stdout.write(renderMediaInstall(result));
|
|
88
|
+
dependencies.stdout.write(renderMediaInstall(result, style.marks));
|
|
77
89
|
if (!result.ok)
|
|
78
90
|
failed = true;
|
|
79
91
|
continue;
|
|
@@ -81,7 +93,7 @@ export const installCommand = {
|
|
|
81
93
|
const pin = findPin(engine, platform);
|
|
82
94
|
if (pin === undefined) {
|
|
83
95
|
failed = true;
|
|
84
|
-
dependencies.stdout.write(describeResult(engine, { ok: false, message:
|
|
96
|
+
dependencies.stdout.write(describeResult(engine, { ok: false, message: unpinnedMessage(engine, platform), stopped: false }, style));
|
|
85
97
|
continue;
|
|
86
98
|
}
|
|
87
99
|
const notices = installLicenceText(pin, folders);
|
|
@@ -117,11 +129,12 @@ function readEngines(names) {
|
|
|
117
129
|
if (!engines.includes(engine))
|
|
118
130
|
engines.push(engine);
|
|
119
131
|
}
|
|
120
|
-
|
|
132
|
+
// On Windows every other install runs its tools in a job the supervisor makes, so the supervisor is built first.
|
|
133
|
+
return engines.includes('supervisor') ? ['supervisor', ...engines.filter((engine) => engine !== 'supervisor')] : engines;
|
|
121
134
|
}
|
|
122
135
|
function thisPlatform() {
|
|
123
136
|
const platform = buildPlatform();
|
|
124
137
|
if (platform === undefined)
|
|
125
|
-
throw new CliError(`Retest pins builds for macOS arm64 and
|
|
138
|
+
throw new CliError(`Retest pins builds for macOS arm64, Linux x64 and Windows x64, and this machine is ${process.platform} ${process.arch}.`);
|
|
126
139
|
return platform;
|
|
127
140
|
}
|
|
@@ -9,4 +9,26 @@ export type LicenceNotices = {
|
|
|
9
9
|
};
|
|
10
10
|
/** Reads only local, checksum-checked bytes. An installed build's missing notice never falls back to a bundled copy. */
|
|
11
11
|
export declare function readLicenceNotices(pin: BuildPin, folders: CacheFolders | undefined, signal: AbortSignal): Promise<LicenceNotices>;
|
|
12
|
+
/** A notice's bytes once both readings had its checksum, or why they are not kept, as a clause with no full stop. */
|
|
13
|
+
export type CheckedNotice = {
|
|
14
|
+
readonly ok: true;
|
|
15
|
+
readonly bytes: Buffer;
|
|
16
|
+
} | {
|
|
17
|
+
readonly ok: false;
|
|
18
|
+
readonly problem: string;
|
|
19
|
+
};
|
|
20
|
+
/** Reads `length` bytes of an opened file at `position` into `buffer` at `offset`, as a file handle does. */
|
|
21
|
+
export type PositionedRead = (buffer: Buffer, offset: number, length: number, position: number) => Promise<{
|
|
22
|
+
readonly bytesRead: number;
|
|
23
|
+
}>;
|
|
24
|
+
/**
|
|
25
|
+
* The bytes of an opened notice of `size` bytes, kept only when they have `checksum`. The recorded checksum, not a
|
|
26
|
+
* size, says what a notice must be, and notices differ widely in size: Chromium's licence page in Electron is 20 MB.
|
|
27
|
+
* So the file is first read through a chunk at a time and kept only when it has that checksum, then read again into a
|
|
28
|
+
* buffer of the size just read and checked once more: a file of other bytes is never held, whatever its size, and one
|
|
29
|
+
* that changed between the readings is refused.
|
|
30
|
+
*
|
|
31
|
+
* @example await readCheckedNotice((buffer, offset, length, position) => handle.read(buffer, offset, length, position), size, checksum, signal)
|
|
32
|
+
*/
|
|
33
|
+
export declare function readCheckedNotice(read: PositionedRead, size: number, checksum: string, signal: AbortSignal): Promise<CheckedNotice>;
|
|
12
34
|
//# sourceMappingURL=licences.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"licences.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/licences.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAe,MAAM,yBAAyB,CAAA;AAClF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;
|
|
1
|
+
{"version":3,"file":"licences.d.ts","sourceRoot":"","sources":["../../../src/cli/commands/licences.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAe,MAAM,yBAAyB,CAAA;AAClF,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,eAAe,CAAA;AAiB5C,eAAO,MAAM,eAAe,EAAE,OAmC7B,CAAA;AAED,0GAA0G;AAC1G,wBAAgB,eAAe,CAAC,IAAI,EAAE,SAAS,QAAQ,EAAE,GAAG,MAAM,CAQjE;AAED,MAAM,MAAM,cAAc,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAAA;AAElF,wHAAwH;AACxH,wBAAsB,kBAAkB,CAAC,GAAG,EAAE,QAAQ,EAAE,OAAO,EAAE,YAAY,GAAG,SAAS,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,CAAC,CA0CvI;AAaD,qHAAqH;AACrH,MAAM,MAAM,aAAa,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAA;AAE5H,6GAA6G;AAC7G,MAAM,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC;IAAE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAAA;AAE1I;;;;;;;;GAQG;AACH,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC,CA2BzI"}
|
|
@@ -11,6 +11,7 @@ import { listWords } from "../../shared/list-words.js";
|
|
|
11
11
|
import { parseArguments } from "../arguments.js";
|
|
12
12
|
import { CliError, UsageError } from "../errors.js";
|
|
13
13
|
import { licenceSummary } from "../install/report.js";
|
|
14
|
+
import { readZipMember } from "../install/zip-member.js";
|
|
14
15
|
import { suggest } from "../suggest.js";
|
|
15
16
|
export const licencesCommand = {
|
|
16
17
|
name: 'licences',
|
|
@@ -129,8 +130,58 @@ function sourcePointers(pin) {
|
|
|
129
130
|
return [`Source ${pin.archive.url}`];
|
|
130
131
|
return [`Source ${pin.sourceCode.repository}/tree/${pin.sourceCode.revision}`, `Patches ${pin.sourceCode.patches}`];
|
|
131
132
|
}
|
|
133
|
+
/** How much of a notice is read at a time while its checksum is checked, before any of it is kept. */
|
|
134
|
+
const checkChunkBytes = 1024 * 1024;
|
|
135
|
+
/**
|
|
136
|
+
* The bytes of an opened notice of `size` bytes, kept only when they have `checksum`. The recorded checksum, not a
|
|
137
|
+
* size, says what a notice must be, and notices differ widely in size: Chromium's licence page in Electron is 20 MB.
|
|
138
|
+
* So the file is first read through a chunk at a time and kept only when it has that checksum, then read again into a
|
|
139
|
+
* buffer of the size just read and checked once more: a file of other bytes is never held, whatever its size, and one
|
|
140
|
+
* that changed between the readings is refused.
|
|
141
|
+
*
|
|
142
|
+
* @example await readCheckedNotice((buffer, offset, length, position) => handle.read(buffer, offset, length, position), size, checksum, signal)
|
|
143
|
+
*/
|
|
144
|
+
export async function readCheckedNotice(read, size, checksum, signal) {
|
|
145
|
+
const stopped = { ok: false, problem: 'reading was stopped' };
|
|
146
|
+
const changed = { ok: false, problem: 'notice changed size while read' };
|
|
147
|
+
const chunk = Buffer.allocUnsafe(Math.min(checkChunkBytes, size + 1));
|
|
148
|
+
const first = createHash('sha256');
|
|
149
|
+
let through = 0;
|
|
150
|
+
for (;;) {
|
|
151
|
+
if (signal.aborted)
|
|
152
|
+
return stopped;
|
|
153
|
+
const { bytesRead } = await read(chunk, 0, chunk.length, through);
|
|
154
|
+
if (bytesRead === 0)
|
|
155
|
+
break;
|
|
156
|
+
first.update(chunk.subarray(0, bytesRead));
|
|
157
|
+
through += bytesRead;
|
|
158
|
+
if (through > size)
|
|
159
|
+
return changed;
|
|
160
|
+
}
|
|
161
|
+
if (through !== size)
|
|
162
|
+
return changed;
|
|
163
|
+
if (first.digest('hex') !== checksum)
|
|
164
|
+
return { ok: false, problem: 'notice checksum does not match' };
|
|
165
|
+
const buffer = Buffer.alloc(size + 1);
|
|
166
|
+
let filled = 0;
|
|
167
|
+
while (filled < buffer.length) {
|
|
168
|
+
if (signal.aborted)
|
|
169
|
+
return stopped;
|
|
170
|
+
const { bytesRead } = await read(buffer, filled, buffer.length - filled, filled);
|
|
171
|
+
if (bytesRead === 0)
|
|
172
|
+
break;
|
|
173
|
+
filled += bytesRead;
|
|
174
|
+
}
|
|
175
|
+
const bytes = buffer.subarray(0, filled);
|
|
176
|
+
if (filled !== size || createHash('sha256').update(bytes).digest('hex') !== checksum)
|
|
177
|
+
return { ok: false, problem: 'notice changed while read' };
|
|
178
|
+
return { ok: true, bytes };
|
|
179
|
+
}
|
|
180
|
+
/** The member of Firefox's omni.ja that is its about:license page. */
|
|
181
|
+
const firefoxLicencePage = 'chrome/toolkit/content/global/license.html';
|
|
132
182
|
// Firefox keeps its notice page in omni.ja. Read the archive from the checked descriptor before asking the host
|
|
133
|
-
// unzip to print that one member from a private copy
|
|
183
|
+
// unzip to print that one member from a private copy, or on Windows, which has no unzip, reading the member from the
|
|
184
|
+
// checked bytes. No path from a build becomes a command or archive member name.
|
|
134
185
|
async function readNoticeFile(path, file, checksum, signal) {
|
|
135
186
|
if (checksum === undefined)
|
|
136
187
|
return { ok: false, problem: `${file.path}: no notice checksum is recorded.` };
|
|
@@ -141,35 +192,26 @@ async function readNoticeFile(path, file, checksum, signal) {
|
|
|
141
192
|
return { ok: false, problem: `${describeRefusal(opened, file.path)}.` };
|
|
142
193
|
let bytes;
|
|
143
194
|
try {
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
while (filled < buffer.length) {
|
|
150
|
-
if (signal.aborted)
|
|
151
|
-
return { ok: false, problem: `${file.path}: reading was stopped.` };
|
|
152
|
-
const { bytesRead } = await opened.handle.read(buffer, filled, buffer.length - filled, null);
|
|
153
|
-
if (bytesRead === 0)
|
|
154
|
-
break;
|
|
155
|
-
filled += bytesRead;
|
|
156
|
-
}
|
|
157
|
-
if (filled !== opened.size)
|
|
158
|
-
return { ok: false, problem: `${file.path}: notice changed size while read.` };
|
|
159
|
-
bytes = buffer.subarray(0, filled);
|
|
160
|
-
if (createHash('sha256').update(bytes).digest('hex') !== checksum)
|
|
161
|
-
return { ok: false, problem: `${file.path}: notice checksum does not match.` };
|
|
195
|
+
const { handle } = opened;
|
|
196
|
+
const checked = await readCheckedNotice((buffer, offset, length, position) => handle.read(buffer, offset, length, position), opened.size, checksum, signal);
|
|
197
|
+
if (!checked.ok)
|
|
198
|
+
return { ok: false, problem: `${file.path}: ${checked.problem}.` };
|
|
199
|
+
bytes = checked.bytes;
|
|
162
200
|
}
|
|
163
201
|
finally {
|
|
164
202
|
await opened.handle.close();
|
|
165
203
|
}
|
|
166
204
|
if (!file.path.endsWith('/omni.ja'))
|
|
167
205
|
return { ok: true, text: bytes.toString('utf8') };
|
|
206
|
+
if (process.platform === 'win32') {
|
|
207
|
+
const page = readZipMember(bytes, firefoxLicencePage, 16 * 1024 * 1024 - 1);
|
|
208
|
+
return page.ok && page.bytes.length > 0 ? { ok: true, text: page.bytes.toString('utf8') } : { ok: false, problem: `${file.path}: its licence page could not be read completely.` };
|
|
209
|
+
}
|
|
168
210
|
const folder = await mkdtemp(join(tmpdir(), 'retest-licences-'));
|
|
169
211
|
try {
|
|
170
212
|
const archive = join(folder, 'omni.ja');
|
|
171
213
|
await writeFile(archive, bytes, { mode: 0o600, flag: 'wx' });
|
|
172
|
-
const result = await runCommand('/usr/bin/unzip', ['-p', archive,
|
|
214
|
+
const result = await runCommand('/usr/bin/unzip', ['-p', archive, firefoxLicencePage], { timeoutMs: 10_000, signal });
|
|
173
215
|
if (result.code !== 0 || result.cleanupProblems.length > 0 || result.stdout.length >= 16 * 1024 * 1024 || result.stdout === '')
|
|
174
216
|
return { ok: false, problem: `${file.path}: its licence page could not be read completely.` };
|
|
175
217
|
return { ok: true, text: result.stdout };
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -21,7 +21,7 @@ import { isContinuousIntegration, onlyInContinuousIntegration } from "../continu
|
|
|
21
21
|
import { CliError, UsageError } from "../errors.js";
|
|
22
22
|
import { statIfPresent } from "../file-system.js";
|
|
23
23
|
import { readTestScope, selectionArguments, selectionOptions } from "../selection.js";
|
|
24
|
-
import { shouldUseColor } from "../terminal.js";
|
|
24
|
+
import { shouldUseColor, terminalMarks } from "../terminal.js";
|
|
25
25
|
const reporterNames = ['human', 'jsonl', 'agent', 'html'];
|
|
26
26
|
const timeoutList = formatTimeouts(defaultTimeouts);
|
|
27
27
|
const options = {
|
|
@@ -220,7 +220,7 @@ function createReporter(name, runFolder, dependencies) {
|
|
|
220
220
|
return { reporter: createJsonlReporter({ stdout }) };
|
|
221
221
|
if (name === 'agent')
|
|
222
222
|
return { reporter: createAgentReporter({ stdout, runFolder }) };
|
|
223
|
-
const human = createHumanReporter({ stdout, stderr, color: shouldUseColor(stdout, dependencies.env), runFolder });
|
|
223
|
+
const human = createHumanReporter({ stdout, stderr, color: shouldUseColor(stdout, dependencies.env), marks: terminalMarks(stdout, dependencies.env), runFolder });
|
|
224
224
|
const reporter = name === 'html' ? createHtmlReporter({ terminal: human, stdout, runFolder, directory: resolve(dependencies.cwd, runFolder) }) : human;
|
|
225
225
|
return { reporter, onOutput: (output) => human.onOutput(output) };
|
|
226
226
|
}
|
|
@@ -71,10 +71,13 @@ function redactChecks(checks, redactor) {
|
|
|
71
71
|
}));
|
|
72
72
|
}
|
|
73
73
|
// A target whose driver does not exist is refused as a run refuses it, and nothing is launched for it. A native target
|
|
74
|
-
// has the drivers a run gives it; its row is read from the executor builds in the cache, and nothing is started.
|
|
74
|
+
// has the drivers a run gives it; its row is read from the executor builds in the cache, and nothing is started. On
|
|
75
|
+
// Windows, where those drivers do not run, its row is the refusal a run gives it.
|
|
75
76
|
async function checkTarget(app, loaded, context) {
|
|
76
77
|
const subject = targetCall(loaded);
|
|
77
78
|
const native = targetDriver(app.name, loaded, { native: true });
|
|
79
|
+
if (!native.ok && 'platform' in loaded)
|
|
80
|
+
return { group: app.name, subject, ok: false, text: native.failure.message };
|
|
78
81
|
if (native.ok && (native.driver === 'ios-simulator' || native.driver === 'macos'))
|
|
79
82
|
return checkNativeTarget({ app: app.name, subject, target: native.target, env: context.dependencies.env });
|
|
80
83
|
const driver = targetDriver(app.name, loaded);
|
|
@@ -85,7 +85,7 @@ function consoleLine(entry, clock, style) {
|
|
|
85
85
|
const kind = entry.type === 'runtime_error' ? (entry.kind === 'uncaught' ? 'uncaught' : 'rejection') : entry.consoleType;
|
|
86
86
|
const mark = level === 'error' ? style.red(level.padEnd(7)) : level === 'warning' ? style.yellow(level.padEnd(7)) : level.padEnd(7);
|
|
87
87
|
const origin = entry.type === 'console' && entry.origin !== 'page' ? ` ${entry.origin}` : '';
|
|
88
|
-
const text = oneLine(entry.type === 'runtime_error' ? entry.message.text : entry.text.text);
|
|
88
|
+
const text = oneLine(entry.type === 'runtime_error' ? entry.message.text : entry.text.text, style.marks.lineBreak);
|
|
89
89
|
const place = entry.url === undefined ? '' : ` ${style.dim(`${printable(entry.url)}${entry.line === undefined ? '' : `:${entry.line}`}`)}`;
|
|
90
90
|
const during = elapsed === undefined ? undefined : clock.during(elapsed);
|
|
91
91
|
const action = during === undefined ? '' : ` ${style.dim(`during ${during}`)}`;
|
|
@@ -140,8 +140,8 @@ function describeScope(scope) {
|
|
|
140
140
|
const words = (areas) => areas.map((area) => area.replaceAll('_', ' ')).join(', ');
|
|
141
141
|
return `${words(scope.covered) || 'nothing'}; not ${words(scope.notCovered) || 'anything else'}`;
|
|
142
142
|
}
|
|
143
|
-
function oneLine(text) {
|
|
144
|
-
const flat = printable(text.replace(/\s*\n\s*/g,
|
|
143
|
+
function oneLine(text, lineBreak) {
|
|
144
|
+
const flat = printable(text.replace(/\s*\n\s*/g, ` ${lineBreak} `));
|
|
145
145
|
return flat.length > textWidth ? `${flat.slice(0, textWidth)}…` : flat;
|
|
146
146
|
}
|
|
147
147
|
function formatMilliseconds(milliseconds) {
|
|
@@ -114,23 +114,23 @@ function describe(entry, stepNames, options) {
|
|
|
114
114
|
case 'cleanup.finished':
|
|
115
115
|
return [describeCleanup(event.cleanup)];
|
|
116
116
|
case 'step.started':
|
|
117
|
-
return [
|
|
117
|
+
return [`${style.marks.step} ${event.name}`];
|
|
118
118
|
case 'step.finished': {
|
|
119
|
-
const mark = event.status === 'passed' ? style.green(
|
|
119
|
+
const mark = event.status === 'passed' ? style.green(style.marks.passed) : style.red(style.marks.failed);
|
|
120
120
|
return [`${mark} ${stepNames.get(event.stepId) ?? event.stepId} ${style.dim(formatDuration(event.durationMs))}`];
|
|
121
121
|
}
|
|
122
122
|
case 'action.completed':
|
|
123
123
|
case 'action.failed': {
|
|
124
124
|
const text = `${describeAction(event)} ${style.dim(formatDuration(event.durationMs))}`;
|
|
125
|
-
return [event.type === 'action.failed' ? `${style.red(
|
|
125
|
+
return [event.type === 'action.failed' ? `${style.red(style.marks.failed)} ${text} ${style.red(event.failure.class)}` : text];
|
|
126
126
|
}
|
|
127
127
|
case 'navigation':
|
|
128
128
|
return [style.dim(describeNavigation(event))];
|
|
129
129
|
case 'host_check.passed':
|
|
130
|
-
return [`${style.green(
|
|
130
|
+
return [`${style.green(style.marks.passed)} host check ${describeHostCheck(event.check)} ${style.dim(hostCheckFacts(event))}`];
|
|
131
131
|
case 'host_check.failed':
|
|
132
132
|
return [
|
|
133
|
-
`${style.red(
|
|
133
|
+
`${style.red(style.marks.failed)} host check ${describeHostCheck(event.check)} ${style.dim(hostCheckFacts(event))} ${style.red(event.failure.class)}`,
|
|
134
134
|
`page ${hostCheckPage(event.check, event.actual)}`,
|
|
135
135
|
];
|
|
136
136
|
case 'state.saved':
|
|
@@ -149,7 +149,7 @@ function describe(entry, stepNames, options) {
|
|
|
149
149
|
return describeEvaluationEvent(event, options);
|
|
150
150
|
case 'inbox.read': {
|
|
151
151
|
const text = `${describeInboxRead(event)} ${style.dim(formatDuration(event.waitedMs))}`;
|
|
152
|
-
return [event.failure === undefined ? text : `${style.red(
|
|
152
|
+
return [event.failure === undefined ? text : `${style.red(style.marks.failed)} ${text} ${style.red(event.failure.class)}`];
|
|
153
153
|
}
|
|
154
154
|
case 'dialog':
|
|
155
155
|
return [event.by === 'test' ? describeDialog(event) : style.yellow(describeDialog(event))];
|
|
@@ -163,10 +163,10 @@ function describe(entry, stepNames, options) {
|
|
|
163
163
|
return [describeDownload(event), ...file];
|
|
164
164
|
if (event.failure === undefined)
|
|
165
165
|
return [style.yellow(describeDownload(event))];
|
|
166
|
-
return [`${style.red(
|
|
166
|
+
return [`${style.red(style.marks.failed)} ${describeDownload(event)} ${style.red(event.failure.class)}`];
|
|
167
167
|
}
|
|
168
168
|
case 'route':
|
|
169
|
-
return [event.failure === undefined ? describeRouteChange(event) : `${style.red(
|
|
169
|
+
return [event.failure === undefined ? describeRouteChange(event) : `${style.red(style.marks.failed)} ${describeRouteChange(event)} ${style.red(event.failure.class)}`];
|
|
170
170
|
case 'diagnostics.started':
|
|
171
171
|
return [style.dim('diagnostics capture started')];
|
|
172
172
|
case 'diagnostics.finished':
|
|
@@ -224,7 +224,7 @@ function describeScreenshot(event, options) {
|
|
|
224
224
|
function describeEvaluationEvent(event, options) {
|
|
225
225
|
const { style } = options;
|
|
226
226
|
const { evaluation } = event;
|
|
227
|
-
const mark = evaluation.verdict === 'pass' ? style.green(
|
|
227
|
+
const mark = evaluation.verdict === 'pass' ? style.green(style.marks.passed) : evaluation.mode === 'advisory' ? style.yellow('!') : style.red(style.marks.failed);
|
|
228
228
|
const failed = evaluation.failure === undefined ? '' : ` ${style.red(evaluation.failure.class)}`;
|
|
229
229
|
const details = evaluationDetails(evaluation, options.runFolder).map(({ label, value }) => `${label.toLowerCase()} ${value}`);
|
|
230
230
|
return [`${mark} AI check ${describeEvaluation(evaluation)} ${style.dim(formatDuration(evaluation.durationMs))}${failed}`, ...details];
|
|
@@ -245,7 +245,7 @@ function describeNavigation(event) {
|
|
|
245
245
|
// file's process judged alone, on a value only it holds, says so.
|
|
246
246
|
function describeAssertion(event, looks, style) {
|
|
247
247
|
const passed = event.type === 'assertion.passed';
|
|
248
|
-
const mark = passed ? style.green(
|
|
248
|
+
const mark = passed ? style.green(style.marks.passed) : style.red(style.marks.failed);
|
|
249
249
|
const target = event.locator === undefined ? '' : ` ${describeLocator(event.locator)}`;
|
|
250
250
|
const facts = [
|
|
251
251
|
formatDuration(event.durationMs),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor-rows.d.ts","sourceRoot":"","sources":["../../../src/cli/install/doctor-rows.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA8C,QAAQ,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAClH,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAE5F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAC5D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAWjD;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,QAAQ,EAAE,CAAA;IACnC,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,GAAG,SAAS,CAAA;IAC7C,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,KAAK,OAAO,CAAA;CACjE,CAAA;AAiBD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAkCzH;
|
|
1
|
+
{"version":3,"file":"doctor-rows.d.ts","sourceRoot":"","sources":["../../../src/cli/install/doctor-rows.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA8C,QAAQ,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAA;AAClH,OAAO,KAAK,EAAE,YAAY,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAA;AAE5F,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAC5D,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,qBAAqB,CAAA;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAWjD;;;GAGG;AACH,MAAM,MAAM,eAAe,GAAG;IAC5B,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,QAAQ,EAAE,CAAA;IACnC,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,GAAG,SAAS,CAAA;IAC7C,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,YAAY,KAAK,OAAO,CAAA;CACjE,CAAA;AAiBD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,WAAW,CAAC,MAAM,EAAE,YAAY,EAAE,GAAG,EAAE,WAAW,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CAkCzH;AAqBD,+HAA+H;AAC/H,MAAM,MAAM,gBAAgB,GAAG;IAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,SAAS,QAAQ,EAAE,CAAC;IAAC,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,GAAG,SAAS,CAAA;CAAE,CAAA;AAErH;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IAAC,QAAQ,CAAC,GAAG,EAAE,WAAW,CAAA;CAAE,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,KAAK,CAAC,CAwBnN;AAED,8GAA8G;AAC9G,MAAM,MAAM,sBAAsB,GAAG;IAAE,QAAQ,CAAC,KAAK,CAAC,EAAE,WAAW,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;CAAE,CAAA;AAEhH;;;;;;;GAOG;AACH,wBAAsB,oBAAoB,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,GAAE,sBAA2B,GAAG,OAAO,CAAC,KAAK,EAAE,CAAC,CASvH"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { lstat, realpath } from 'node:fs/promises';
|
|
3
|
-
import { isAbsolute, relative, sep } from 'node:path';
|
|
4
|
-
import { buildPlatform, cacheFolders, describePin, describePlatform, findPin, inspectBuild, pinnedBuilds, pinRefusal, readBundledLicence } from "../../browser/builds.js";
|
|
3
|
+
import { isAbsolute, normalize, relative, sep } from 'node:path';
|
|
4
|
+
import { buildPlatform, cacheFolders, cacheVariable, describePin, describePlatform, findPin, inspectBuild, pinnedBuilds, pinRefusal, readBundledLicence } from "../../browser/builds.js";
|
|
5
5
|
import { screenRecordingAllowed, screenRecordingGrant, screenRecordingHolder } from "../../native/macos-app.js";
|
|
6
6
|
import { systemTools } from "../../native/processes.js";
|
|
7
7
|
import { retestCommand } from "../../reporters/commands.js";
|
|
@@ -72,7 +72,8 @@ function checkedNoticeText(pin, inspection) {
|
|
|
72
72
|
if (!pin.licences.inspected)
|
|
73
73
|
return pin.licences.reason;
|
|
74
74
|
for (const file of pin.licences.files) {
|
|
75
|
-
|
|
75
|
+
// A problem names the file by the pin's path, or by its path on disk, which Windows writes with backslashes.
|
|
76
|
+
const problem = inspection.problems.find(problem => problem.includes(file.path) || problem.includes(normalize(file.path)));
|
|
76
77
|
if (problem !== undefined && inspection.state === 'damaged')
|
|
77
78
|
return problem;
|
|
78
79
|
}
|
|
@@ -104,7 +105,7 @@ export async function checkNativeTarget(request, options = {}) {
|
|
|
104
105
|
const folders = cacheFolders(request.env);
|
|
105
106
|
if (folders === undefined) {
|
|
106
107
|
const named = findPin(engine, 'mac-arm64', pins);
|
|
107
|
-
return { ...check, ok: false, text:
|
|
108
|
+
return { ...check, ok: false, text: `${cacheVariable()} is not set to an absolute folder, so Retest has no cache to read the ${named === undefined ? engine : describePin(named)} build from.` };
|
|
108
109
|
}
|
|
109
110
|
const platform = 'platform' in options ? options.platform : buildPlatform();
|
|
110
111
|
const pin = platform === undefined ? undefined : findPin(engine, platform, pins);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** What a change is told: the platform, the install's signal, and how long Windows is given to let go of a file. */
|
|
2
|
+
export type ChangeOptions = {
|
|
3
|
+
readonly platform: NodeJS.Platform;
|
|
4
|
+
readonly signal?: AbortSignal | undefined;
|
|
5
|
+
readonly waitMs?: number;
|
|
6
|
+
};
|
|
7
|
+
/** How a change ended: done, or why not, as a clause with no full stop, and whether it ended because of a stop. */
|
|
8
|
+
export type Settled = {
|
|
9
|
+
readonly ok: true;
|
|
10
|
+
} | {
|
|
11
|
+
readonly ok: false;
|
|
12
|
+
readonly reason: string;
|
|
13
|
+
readonly stopped: boolean;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* One rename or removal: what it does, the folder it changes, whose permission says whether this user may make it, and
|
|
17
|
+
* for a rename the path it makes, which must not exist yet.
|
|
18
|
+
*/
|
|
19
|
+
export type FileChange = {
|
|
20
|
+
readonly run: () => Promise<void>;
|
|
21
|
+
readonly folder: string;
|
|
22
|
+
readonly target?: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Removes a file or a folder and everything in it, if it is there.
|
|
26
|
+
*
|
|
27
|
+
* @example await removePath('/…/browsers/downloads/electron.zip', { platform: process.platform, signal })
|
|
28
|
+
*/
|
|
29
|
+
export declare function removePath(path: string, options: ChangeOptions): Promise<Settled>;
|
|
30
|
+
/**
|
|
31
|
+
* Renames `from` to `to`, which must not exist yet.
|
|
32
|
+
*
|
|
33
|
+
* @example await renamePath(staging, folder, { platform: process.platform, signal })
|
|
34
|
+
*/
|
|
35
|
+
export declare function renamePath(from: string, to: string, options: ChangeOptions): Promise<Settled>;
|
|
36
|
+
/**
|
|
37
|
+
* Makes one change and says how it ended, without throwing. On Windows a refusal while a program holds a file open is
|
|
38
|
+
* tried again until it is let go, `waitMs` passes or the signal stops it; a rename onto a path that exists, and a
|
|
39
|
+
* folder this user may not change, are said at once. Elsewhere, and for any other error, the change is tried once.
|
|
40
|
+
*
|
|
41
|
+
* @example await settleChange({ run: () => rm(path, { force: true }), folder: dirname(path) }, { platform: 'win32', signal })
|
|
42
|
+
*/
|
|
43
|
+
export declare function settleChange(change: FileChange, options: ChangeOptions): Promise<Settled>;
|
|
44
|
+
//# sourceMappingURL=file-changes.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"file-changes.d.ts","sourceRoot":"","sources":["../../../src/cli/install/file-changes.ts"],"names":[],"mappings":"AAcA,oHAAoH;AACpH,MAAM,MAAM,aAAa,GAAG;IAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,SAAS,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEvI,mHAAmH;AACnH,MAAM,MAAM,OAAO,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAAA;AAKxH;;;GAGG;AACH,MAAM,MAAM,UAAU,GAAG;IAAE,QAAQ,CAAC,GAAG,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAA;AAEjH;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAEjF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAE7F;AAOD;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,MAAM,EAAE,UAAU,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAoB/F"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { lstat, open, rename, rm } from 'node:fs/promises';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
4
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
|
+
import { errorMessage } from "../../protocol/failures.js";
|
|
6
|
+
import { Deadline } from "../../protocol/deadline.js";
|
|
7
|
+
import { errorCode } from "../../shared/error-code.js";
|
|
8
|
+
/** How long Windows is given to let go of a file, as a virus scanner holds each file an unpack has just written. */
|
|
9
|
+
const defaultWaitMs = 10_000;
|
|
10
|
+
/**
|
|
11
|
+
* Removes a file or a folder and everything in it, if it is there.
|
|
12
|
+
*
|
|
13
|
+
* @example await removePath('/…/browsers/downloads/electron.zip', { platform: process.platform, signal })
|
|
14
|
+
*/
|
|
15
|
+
export function removePath(path, options) {
|
|
16
|
+
return settleChange({ run: () => rm(path, { recursive: true, force: true }), folder: dirname(path) }, options);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Renames `from` to `to`, which must not exist yet.
|
|
20
|
+
*
|
|
21
|
+
* @example await renamePath(staging, folder, { platform: process.platform, signal })
|
|
22
|
+
*/
|
|
23
|
+
export function renamePath(from, to, options) {
|
|
24
|
+
return settleChange({ run: () => rename(from, to), folder: dirname(to), target: to }, options);
|
|
25
|
+
}
|
|
26
|
+
// Windows gives EBUSY for a file another program holds open, and EPERM or EACCES both for that and for a folder this
|
|
27
|
+
// user may not change; a folder that refuses a new file of Retest's tells the second apart, and is not waited on.
|
|
28
|
+
const inUseCodes = new Set(['EBUSY', 'EPERM', 'EACCES']);
|
|
29
|
+
const deniedCodes = new Set(['EPERM', 'EACCES']);
|
|
30
|
+
/**
|
|
31
|
+
* Makes one change and says how it ended, without throwing. On Windows a refusal while a program holds a file open is
|
|
32
|
+
* tried again until it is let go, `waitMs` passes or the signal stops it; a rename onto a path that exists, and a
|
|
33
|
+
* folder this user may not change, are said at once. Elsewhere, and for any other error, the change is tried once.
|
|
34
|
+
*
|
|
35
|
+
* @example await settleChange({ run: () => rm(path, { force: true }), folder: dirname(path) }, { platform: 'win32', signal })
|
|
36
|
+
*/
|
|
37
|
+
export async function settleChange(change, options) {
|
|
38
|
+
const deadline = new Deadline(options.waitMs ?? defaultWaitMs);
|
|
39
|
+
let writable;
|
|
40
|
+
for (;;) {
|
|
41
|
+
let failure;
|
|
42
|
+
try {
|
|
43
|
+
await change.run();
|
|
44
|
+
return { ok: true };
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
failure = error;
|
|
48
|
+
}
|
|
49
|
+
const code = errorCode(failure) ?? '';
|
|
50
|
+
if (options.platform !== 'win32' || !inUseCodes.has(code))
|
|
51
|
+
return { ok: false, reason: errorMessage(failure), stopped: false };
|
|
52
|
+
if (change.target !== undefined && (await exists(change.target)))
|
|
53
|
+
return { ok: false, reason: `${change.target} already exists`, stopped: false };
|
|
54
|
+
if (code !== 'EBUSY')
|
|
55
|
+
writable ??= await canWriteIn(change.folder);
|
|
56
|
+
if (writable === false)
|
|
57
|
+
return { ok: false, reason: `Windows does not let this user change ${change.folder} (${code})`, stopped: false };
|
|
58
|
+
if (options.signal?.aborted === true)
|
|
59
|
+
return { ok: false, reason: `the install was stopped while a program held a file in it open (${code})`, stopped: true };
|
|
60
|
+
if (deadline.reached)
|
|
61
|
+
return { ok: false, reason: `a program still held a file in it open after ${Number((deadline.budgetMs / 1000).toFixed(1))} s (${code})`, stopped: false };
|
|
62
|
+
await sleep(Math.min(250, deadline.waitToEndMs), undefined, options.signal === undefined ? {} : { signal: options.signal }).catch(() => undefined);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
async function exists(path) {
|
|
66
|
+
return lstat(path).then(() => true, () => false);
|
|
67
|
+
}
|
|
68
|
+
// Whether this user may make a file in `folder`, by making one there and removing it again. Only a refusal of that file
|
|
69
|
+
// says no; a folder that cannot be tried for another reason is waited on as one a program holds.
|
|
70
|
+
async function canWriteIn(folder) {
|
|
71
|
+
const probe = join(folder, `.retest-write-${process.pid}-${randomUUID()}`);
|
|
72
|
+
try {
|
|
73
|
+
const handle = await open(probe, 'wx');
|
|
74
|
+
await handle.close();
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
return !deniedCodes.has(errorCode(error) ?? '');
|
|
78
|
+
}
|
|
79
|
+
await rm(probe, { force: true }).catch(() => undefined);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
@@ -56,7 +56,9 @@ export declare const maximumArchiveBytes: number;
|
|
|
56
56
|
* bit and links that stay inside it, and where the executable, the pinned files and every licence notice the pin names
|
|
57
57
|
* must be present with their checksums. Only then is the record written and the staging folder renamed into place, so
|
|
58
58
|
* a build folder always holds a finished, checked build, and the archive is deleted. A verified archive whose
|
|
59
|
-
* unpacking failed is kept for the next attempt.
|
|
59
|
+
* unpacking failed is kept for the next attempt. A rename or removal that does not happen is never thrown: before the
|
|
60
|
+
* build is in place it ends the install with its reason, and once the build is in place the install stands and what
|
|
61
|
+
* was left behind is reported. On Windows each is tried again while a program holds a file open (`file-changes.ts`).
|
|
60
62
|
*
|
|
61
63
|
* @example await installArchive({ pin, folders, signal, tools: systemUnpackTools, platform: process.platform, version: retestVersion, report: console.log })
|
|
62
64
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"install-archive.d.ts","sourceRoot":"","sources":["../../../src/cli/install/install-archive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,YAAY,EAAwB,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAC9H,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;
|
|
1
|
+
{"version":3,"file":"install-archive.d.ts","sourceRoot":"","sources":["../../../src/cli/install/install-archive.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,YAAY,EAAwB,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAC9H,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAA;AAE5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAc9C,iGAAiG;AACjG,MAAM,MAAM,cAAc,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,KAAK,EAAE,SAAS;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,CAAA;AAEpI;;;GAGG;AACH,MAAM,MAAM,aAAa,GACrB;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,WAAW,GAAG,mBAAmB,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,eAAe,CAAA;CAAE,GAC/G;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,cAAc,CAAA;CAAE,CAAA;AAElH,yHAAyH;AACzH,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,CAAC,GAAG,EAAE,UAAU,CAAA;IACxB,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAA;IAC9B,wGAAwG;IACxG,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAA;IAC5B,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAA;IAC3B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAA;IAClC,gFAAgF;IAChF,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,sEAAsE;IACtE,QAAQ,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACvC,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,GAAG,SAAS,CAAA;IACzC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS,CAAA;IACvC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACpC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAA;IACrC,kIAAkI;IAClI,QAAQ,CAAC,SAAS,CAAC,EAAE,WAAW,GAAG,SAAS,CAAA;CAC7C,CAAA;AAED,wEAAwE;AACxE,eAAO,MAAM,mBAAmB,EAAE,MAA+B,CAAA;AAKjE;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,cAAc,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,aAAa,CAAC,CAsB3F;AAiED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,UAAU,GAAG,MAAM,CAE9E;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,GAAG,EAAE,UAAU,GAAG,MAAM,CAE1E;AAkID,yGAAyG;AACzG,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAmBpG;AAED,KAAK,OAAO,GACR;IAAE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAC;IAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GACnI;IAAE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,SAAS;QAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAA;KAAE,EAAE,CAAA;CAAE,CAAA;AAErI;;;;;;;GAOG;AACH,wBAAsB,aAAa,CAAC,GAAG,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA6BnF;AAyBD,uDAAuD;AACvD,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEjD"}
|