@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.
Files changed (218) hide show
  1. package/README.md +6 -5
  2. package/dist/api/source-location.d.ts.map +1 -1
  3. package/dist/api/source-location.js +3 -1
  4. package/dist/browser/builds.d.ts +35 -6
  5. package/dist/browser/builds.d.ts.map +1 -1
  6. package/dist/browser/builds.js +115 -16
  7. package/dist/browser/cdp/transport.d.ts.map +1 -1
  8. package/dist/browser/cdp/transport.js +4 -4
  9. package/dist/browser/chromium-process.d.ts +23 -0
  10. package/dist/browser/chromium-process.d.ts.map +1 -1
  11. package/dist/browser/chromium-process.js +78 -13
  12. package/dist/browser/electron.d.ts +1 -1
  13. package/dist/browser/electron.d.ts.map +1 -1
  14. package/dist/browser/electron.js +6 -3
  15. package/dist/browser/executables.d.ts +16 -1
  16. package/dist/browser/executables.d.ts.map +1 -1
  17. package/dist/browser/executables.js +50 -18
  18. package/dist/browser/firefox/accessible-names.d.ts +5 -4
  19. package/dist/browser/firefox/accessible-names.d.ts.map +1 -1
  20. package/dist/browser/firefox/accessible-names.js +11 -6
  21. package/dist/browser/firefox/bridge.js +3 -2
  22. package/dist/browser/firefox/executable.d.ts +14 -14
  23. package/dist/browser/firefox/executable.d.ts.map +1 -1
  24. package/dist/browser/firefox/executable.js +29 -21
  25. package/dist/browser/firefox/launch.js +1 -1
  26. package/dist/browser/firefox/navigation.d.ts +5 -2
  27. package/dist/browser/firefox/navigation.d.ts.map +1 -1
  28. package/dist/browser/firefox/navigation.js +48 -14
  29. package/dist/browser/firefox/orphans.d.ts.map +1 -1
  30. package/dist/browser/firefox/orphans.js +34 -2
  31. package/dist/browser/firefox/page.d.ts.map +1 -1
  32. package/dist/browser/firefox/page.js +113 -19
  33. package/dist/browser/firefox/process.d.ts +20 -1
  34. package/dist/browser/firefox/process.d.ts.map +1 -1
  35. package/dist/browser/firefox/process.js +124 -22
  36. package/dist/browser/firefox/profile.d.ts +5 -0
  37. package/dist/browser/firefox/profile.d.ts.map +1 -1
  38. package/dist/browser/firefox/profile.js +17 -1
  39. package/dist/browser/firefox/storage.d.ts.map +1 -1
  40. package/dist/browser/firefox/storage.js +4 -2
  41. package/dist/browser/firefox/subscriptions.d.ts +26 -0
  42. package/dist/browser/firefox/subscriptions.d.ts.map +1 -0
  43. package/dist/browser/firefox/subscriptions.js +10 -0
  44. package/dist/browser/launch.d.ts +6 -0
  45. package/dist/browser/launch.d.ts.map +1 -1
  46. package/dist/browser/launch.js +14 -1
  47. package/dist/cli/commands/doctor.js +3 -3
  48. package/dist/cli/commands/inspect.js +3 -2
  49. package/dist/cli/commands/install.d.ts.map +1 -1
  50. package/dist/cli/commands/install.js +26 -13
  51. package/dist/cli/commands/licences.d.ts +22 -0
  52. package/dist/cli/commands/licences.d.ts.map +1 -1
  53. package/dist/cli/commands/licences.js +62 -20
  54. package/dist/cli/commands/run.js +2 -2
  55. package/dist/cli/doctor/checks.js +4 -1
  56. package/dist/cli/inspect/diagnostics.js +3 -3
  57. package/dist/cli/inspect/test-timeline.js +10 -10
  58. package/dist/cli/install/doctor-rows.d.ts.map +1 -1
  59. package/dist/cli/install/doctor-rows.js +5 -4
  60. package/dist/cli/install/file-changes.d.ts +44 -0
  61. package/dist/cli/install/file-changes.d.ts.map +1 -0
  62. package/dist/cli/install/file-changes.js +81 -0
  63. package/dist/cli/install/install-archive.d.ts +3 -1
  64. package/dist/cli/install/install-archive.d.ts.map +1 -1
  65. package/dist/cli/install/install-archive.js +80 -33
  66. package/dist/cli/install/lock.d.ts.map +1 -1
  67. package/dist/cli/install/lock.js +108 -21
  68. package/dist/cli/install/media-doctor.d.ts.map +1 -1
  69. package/dist/cli/install/media-doctor.js +2 -1
  70. package/dist/cli/install/media-install.d.ts.map +1 -1
  71. package/dist/cli/install/media-install.js +38 -19
  72. package/dist/cli/install/media-pins.d.ts +7 -1
  73. package/dist/cli/install/media-pins.d.ts.map +1 -1
  74. package/dist/cli/install/media-pins.js +23 -12
  75. package/dist/cli/install/media-record.d.ts.map +1 -1
  76. package/dist/cli/install/media-record.js +10 -9
  77. package/dist/cli/install/media-report.d.ts +3 -1
  78. package/dist/cli/install/media-report.d.ts.map +1 -1
  79. package/dist/cli/install/media-report.js +7 -5
  80. package/dist/cli/install/media-tools.d.ts.map +1 -1
  81. package/dist/cli/install/media-tools.js +6 -4
  82. package/dist/cli/install/process-start.d.ts +3 -2
  83. package/dist/cli/install/process-start.d.ts.map +1 -1
  84. package/dist/cli/install/process-start.js +4 -0
  85. package/dist/cli/install/report.d.ts.map +1 -1
  86. package/dist/cli/install/report.js +11 -7
  87. package/dist/cli/install/supervisor-install.d.ts +33 -0
  88. package/dist/cli/install/supervisor-install.d.ts.map +1 -0
  89. package/dist/cli/install/supervisor-install.js +158 -0
  90. package/dist/cli/install/supervisor-locate.d.ts +47 -0
  91. package/dist/cli/install/supervisor-locate.d.ts.map +1 -0
  92. package/dist/cli/install/supervisor-locate.js +113 -0
  93. package/dist/cli/install/supervisor-pins.d.ts +21 -0
  94. package/dist/cli/install/supervisor-pins.d.ts.map +1 -0
  95. package/dist/cli/install/supervisor-pins.js +28 -0
  96. package/dist/cli/install/supervisor-record.d.ts +31 -0
  97. package/dist/cli/install/supervisor-record.d.ts.map +1 -0
  98. package/dist/cli/install/supervisor-record.js +104 -0
  99. package/dist/cli/install/supervisor-report.d.ts +11 -0
  100. package/dist/cli/install/supervisor-report.d.ts.map +1 -0
  101. package/dist/cli/install/supervisor-report.js +33 -0
  102. package/dist/cli/install/supervisor-tools.d.ts +43 -0
  103. package/dist/cli/install/supervisor-tools.d.ts.map +1 -0
  104. package/dist/cli/install/supervisor-tools.js +114 -0
  105. package/dist/cli/install/unpack.d.ts +23 -3
  106. package/dist/cli/install/unpack.d.ts.map +1 -1
  107. package/dist/cli/install/unpack.js +124 -10
  108. package/dist/cli/install/zip-member.d.ts +15 -0
  109. package/dist/cli/install/zip-member.d.ts.map +1 -0
  110. package/dist/cli/install/zip-member.js +77 -0
  111. package/dist/cli/interrupt.d.ts +1 -1
  112. package/dist/cli/interrupt.d.ts.map +1 -1
  113. package/dist/cli/interrupt.js +15 -10
  114. package/dist/cli/terminal.d.ts +9 -1
  115. package/dist/cli/terminal.d.ts.map +1 -1
  116. package/dist/cli/terminal.js +14 -0
  117. package/dist/config/define.d.ts +2 -2
  118. package/dist/config/define.js +2 -2
  119. package/dist/diagnostics/firefox-collector.d.ts +11 -10
  120. package/dist/diagnostics/firefox-collector.d.ts.map +1 -1
  121. package/dist/diagnostics/firefox-collector.js +16 -11
  122. package/dist/media/client.d.ts +2 -1
  123. package/dist/media/client.d.ts.map +1 -1
  124. package/dist/media/client.js +16 -5
  125. package/dist/media/host.d.ts +32 -0
  126. package/dist/media/host.d.ts.map +1 -0
  127. package/dist/media/host.js +50 -0
  128. package/dist/media/locate.d.ts.map +1 -1
  129. package/dist/media/locate.js +4 -3
  130. package/dist/native/processes.d.ts.map +1 -1
  131. package/dist/native/processes.js +7 -3
  132. package/dist/protocol/files.d.ts +1 -1
  133. package/dist/protocol/files.d.ts.map +1 -1
  134. package/dist/protocol/files.js +10 -5
  135. package/dist/reporters/commands.d.ts +12 -3
  136. package/dist/reporters/commands.d.ts.map +1 -1
  137. package/dist/reporters/commands.js +29 -11
  138. package/dist/reporters/human-card.js +1 -1
  139. package/dist/reporters/human.d.ts +3 -1
  140. package/dist/reporters/human.d.ts.map +1 -1
  141. package/dist/reporters/human.js +8 -8
  142. package/dist/reporters/style.d.ts +21 -2
  143. package/dist/reporters/style.d.ts.map +1 -1
  144. package/dist/reporters/style.js +11 -2
  145. package/dist/runner/app-server.d.ts +27 -7
  146. package/dist/runner/app-server.d.ts.map +1 -1
  147. package/dist/runner/app-server.js +74 -24
  148. package/dist/runner/child.js +6 -2
  149. package/dist/runner/media-leftovers.d.ts +7 -3
  150. package/dist/runner/media-leftovers.d.ts.map +1 -1
  151. package/dist/runner/media-leftovers.js +27 -6
  152. package/dist/runner/resources.d.ts +8 -0
  153. package/dist/runner/resources.d.ts.map +1 -1
  154. package/dist/runner/resources.js +12 -1
  155. package/dist/runner/run-session.d.ts.map +1 -1
  156. package/dist/runner/run-session.js +9 -4
  157. package/dist/runner/run.d.ts.map +1 -1
  158. package/dist/runner/run.js +2 -1
  159. package/dist/runner/target-drivers.d.ts +5 -2
  160. package/dist/runner/target-drivers.d.ts.map +1 -1
  161. package/dist/runner/target-drivers.js +10 -1
  162. package/dist/runner/test-file-process.d.ts +8 -0
  163. package/dist/runner/test-file-process.d.ts.map +1 -1
  164. package/dist/runner/test-file-process.js +26 -3
  165. package/dist/runner/test-pages.d.ts.map +1 -1
  166. package/dist/runner/test-pages.js +5 -2
  167. package/dist/shared/environment.d.ts +17 -0
  168. package/dist/shared/environment.d.ts.map +1 -0
  169. package/dist/shared/environment.js +27 -0
  170. package/dist/shared/metadata-process-worker.js +15 -7
  171. package/dist/shared/metadata-process.d.ts +5 -0
  172. package/dist/shared/metadata-process.d.ts.map +1 -1
  173. package/dist/shared/metadata-process.js +36 -6
  174. package/dist/shared/posix-path.d.ts +12 -0
  175. package/dist/shared/posix-path.d.ts.map +1 -1
  176. package/dist/shared/posix-path.js +16 -0
  177. package/dist/shared/private-file.d.ts +38 -0
  178. package/dist/shared/private-file.d.ts.map +1 -0
  179. package/dist/shared/private-file.js +132 -0
  180. package/dist/shared/process-ownership.d.ts +52 -2
  181. package/dist/shared/process-ownership.d.ts.map +1 -1
  182. package/dist/shared/process-ownership.js +104 -11
  183. package/dist/shared/process-supervisor.d.ts +309 -0
  184. package/dist/shared/process-supervisor.d.ts.map +1 -0
  185. package/dist/shared/process-supervisor.js +737 -0
  186. package/dist/shared/process-system.d.ts +113 -0
  187. package/dist/shared/process-system.d.ts.map +1 -0
  188. package/dist/shared/process-system.js +254 -0
  189. package/dist/shared/regular-file.d.ts +2 -2
  190. package/dist/shared/regular-file.d.ts.map +1 -1
  191. package/dist/shared/regular-file.js +19 -2
  192. package/dist/store/artifacts.d.ts +5 -4
  193. package/dist/store/artifacts.d.ts.map +1 -1
  194. package/dist/store/artifacts.js +18 -5
  195. package/dist/store/run-store.d.ts +5 -2
  196. package/dist/store/run-store.d.ts.map +1 -1
  197. package/dist/store/run-store.js +48 -4
  198. package/media/Cargo.lock +16 -0
  199. package/media/Cargo.toml +16 -1
  200. package/media/src/encoder.rs +56 -12
  201. package/media/src/frame.rs +7 -1
  202. package/media/src/main.rs +20 -2
  203. package/media/src/place.rs +230 -11
  204. package/media/src/process_ownership_windows.rs +1058 -0
  205. package/media/src/recording.rs +15 -7
  206. package/media/src/replies.rs +55 -0
  207. package/media/src/server.rs +72 -2
  208. package/media/src/store.rs +71 -2
  209. package/package.json +9 -4
  210. package/src/cli/install/media-notices.txt +465 -1
  211. package/supervisor/Cargo.lock +25 -0
  212. package/supervisor/Cargo.toml +22 -0
  213. package/supervisor/NOTICES +464 -0
  214. package/supervisor/build.rs +44 -0
  215. package/supervisor/src/command.rs +612 -0
  216. package/supervisor/src/jobs.rs +1650 -0
  217. package/supervisor/src/main.rs +71 -0
  218. package/supervisor/src/reply.rs +367 -0
@@ -1,4 +1,4 @@
1
- import { buildEngines, buildPlatform, cacheFolders, describePlatform, findPin, inspectBuilds } from "../../browser/builds.js";
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('HOME is not set to an absolute folder, so Retest has no cache folder to install into or read.');
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.stdout.write(renderMediaListing(renderInspections({ platform, folders, inspections, json: parsed.flag('json'), style }), media, parsed.flag('json')));
63
- return media?.state === 'damaged' || media?.state === 'unverifiable' || inspections.some((inspection) => inspection.state === 'damaged' || inspection.state === 'unverifiable') ? 2 : 0;
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: `Retest pins no ${engine} build for ${describePlatform(platform)}.`, stopped: false }, style));
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
- return engines;
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 Linux x64, and this machine is ${process.platform} ${process.arch}.`);
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;AAgB5C,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"}
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. No path from a build becomes a command or archive member name.
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 maximum = file.path.endsWith('/omni.ja') ? 128 * 1024 * 1024 : 16 * 1024 * 1024;
145
- if (opened.size > maximum)
146
- return { ok: false, problem: `${file.path}: notice exceeds the byte limit.` };
147
- const buffer = Buffer.alloc(opened.size + 1);
148
- let filled = 0;
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, 'chrome/toolkit/content/global/license.html'], { timeoutMs: 10_000, signal });
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 };
@@ -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 [`▸ ${event.name}`];
117
+ return [`${style.marks.step} ${event.name}`];
118
118
  case 'step.finished': {
119
- const mark = event.status === 'passed' ? style.green('✓') : style.red('✗');
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('✗')} ${text} ${style.red(event.failure.class)}` : text];
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('✓')} host check ${describeHostCheck(event.check)} ${style.dim(hostCheckFacts(event))}`];
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('✗')} host check ${describeHostCheck(event.check)} ${style.dim(hostCheckFacts(event))} ${style.red(event.failure.class)}`,
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('✗')} ${text} ${style.red(event.failure.class)}`];
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('✗')} ${describeDownload(event)} ${style.red(event.failure.class)}`];
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('✗')} ${describeRouteChange(event)} ${style.red(event.failure.class)}`];
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('✓') : evaluation.mode === 'advisory' ? style.yellow('!') : style.red('✗');
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('✓') : style.red('✗');
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;AAoBD,+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
+ {"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
- const problem = inspection.problems.find(problem => problem.includes(file.path));
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: `HOME is not set to an absolute folder, so Retest has no cache to read the ${named === undefined ? engine : describePin(named)} build from.` };
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":"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"}
1
+ {"version":3,"file":"install-archive.d.ts","sourceRoot":"","sources":["../../../src/cli/install/install-archive.ts"],"names":[],"mappings":"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"}