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