@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
@@ -0,0 +1,737 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { win32 } from 'node:path';
3
+ import { errorMessage } from "../protocol/failures.js";
4
+ import { parse, s } from "../protocol/schema.js";
5
+ import { readMetadataProcess, readMetadataProcessAsync } from "./metadata-process.js";
6
+ // How many times a job an adoption was not answered for is asked to end, and how long apart.
7
+ const endAttempts = 3;
8
+ const endPauseMs = 100;
9
+ const pause = new Int32Array(new SharedArrayBuffer(4));
10
+ const identity = s.object({ pid: s.number({ integer: true, min: 1 }), parentPid: s.nullable(s.number({ integer: true, min: 0 })), startedAt: s.string(), image: s.nullable(s.string()), commandLine: s.nullable(s.string()) });
11
+ const reply = s.discriminatedUnion('type', [
12
+ s.object({ schemaVersion: s.literal(1), type: s.literal('held'), job: s.string(), runner: s.number({ integer: true, min: 1 }), handle: s.number({ integer: true, min: 4 }) }),
13
+ s.object({ schemaVersion: s.literal(1), type: s.literal('adopted'), job: s.string(), handle: s.number({ integer: true, min: 4 }), root: identity, ended: s.boolean(), late: s.array(identity), byParent: s.array(identity), unheld: s.array(s.number({ integer: true, min: 1 })), complete: s.boolean() }),
14
+ s.object({ schemaVersion: s.literal(1), type: s.literal('members'), job: s.string(), processes: s.array(identity) }),
15
+ s.object({ schemaVersion: s.literal(1), type: s.literal('process'), process: s.nullable(identity), inJob: s.nullable(s.boolean()) }),
16
+ s.object({ schemaVersion: s.literal(1), type: s.literal('ended'), job: s.string() }),
17
+ s.object({ schemaVersion: s.literal(1), type: s.literal('terminated'), pid: s.number({ integer: true, min: 1 }), outcome: s.enum(['terminated', 'absent', 'different']) }),
18
+ s.object({ schemaVersion: s.literal(1), type: s.literal('released'), job: s.string() }),
19
+ s.object({ schemaVersion: s.literal(1), type: s.literal('strays'), job: s.string(), processes: s.array(identity) }),
20
+ s.object({ schemaVersion: s.literal(1), type: s.literal('interrupted'), job: s.string(), console: s.number({ integer: true, min: 1 }), reached: s.array(s.number({ integer: true, min: 1 })) }),
21
+ s.object({ schemaVersion: s.literal(1), type: s.literal('refused'), problem: s.string(), detail: s.nullable(s.string()) }),
22
+ ]);
23
+ /**
24
+ * The supervisor's answers for one runner, the process that asks. Every job it makes is named after the runner and a
25
+ * random word chosen here, so the names are new and nobody else's; a name already taken is refused by the supervisor.
26
+ * A launch's job also carries the launch's pid and a count, so a later launch given the same pid gets a name of its
27
+ * own. Each method runs the supervisor once; a refusal throws with the supervisor's own sentence, and the Windows call
28
+ * behind it, when one failed, as the error's cause.
29
+ *
30
+ * @example const supervisor = new Supervisor(program); supervisor.hold(); const launch = supervisor.adopt(child.pid)
31
+ */
32
+ export class Supervisor {
33
+ program;
34
+ runner;
35
+ #prefix;
36
+ // The launches whose jobs' handles the runner keeps, by job, read beside the runner's own job by `strays`, each with
37
+ // what it held by its parent when it was held and its markers.
38
+ #launches = new Map();
39
+ // Processes already named as left running, by pid and start, so each is named once, for the launch it was first
40
+ // seen after.
41
+ #named = new Set();
42
+ #count = 0;
43
+ constructor(program, runner = process.pid, word = randomBytes(8).toString('hex')) {
44
+ if (!/^[0-9a-f]{8,32}$/.test(word))
45
+ throw new RangeError('A job word is 8 to 32 lowercase hexadecimal digits.');
46
+ this.program = program;
47
+ this.runner = runner;
48
+ this.#prefix = `Local\\retest-${runner}-${word}`;
49
+ }
50
+ /** The name of the runner's own job. */
51
+ get runnerJob() {
52
+ return this.#prefix;
53
+ }
54
+ /** The name the next launch with this pid is held under. */
55
+ launchJob(pid) {
56
+ return `${this.#prefix}-${pid}-${this.#count + 1}`;
57
+ }
58
+ /** The jobs of launches the runner holds a handle to, oldest first. */
59
+ get launchJobs() {
60
+ return [...this.#launches.keys()];
61
+ }
62
+ /**
63
+ * The launches the runner holds a handle to, each by its job, the process it was made for, what that held by its
64
+ * parent when it was held, and the markers it was held with.
65
+ */
66
+ get launches() {
67
+ return [...this.#launches].map(([job, held]) => ({ job, ...held }));
68
+ }
69
+ /** The processes named so far, by pid and start, as they stand now: what a later `forgetEnded` may forget. */
70
+ get named() {
71
+ return new Set(this.#named);
72
+ }
73
+ /**
74
+ * Forgets those of `named`, taken before `runnerJob` was read, that this reading of the runner's job no longer holds:
75
+ * each has ended, and a process given its pid later is another one. A process named after `named` was taken is kept,
76
+ * whatever the reading says, since the reading may be older than its naming.
77
+ */
78
+ forgetEnded(named, runnerJob) {
79
+ const running = new Set(runnerJob.flatMap((entry) => [`${entry.pid}:${entry.startedAt}`, `${entry.pid}:?`]));
80
+ for (const key of named)
81
+ if (!running.has(key))
82
+ this.#named.delete(key);
83
+ }
84
+ /** How many processes are named now, which `forgetEnded` keeps to those still running. */
85
+ get namedCount() {
86
+ return this.#named.size;
87
+ }
88
+ /**
89
+ * Of these processes, those not named before, each counted as named from now on. One no handle could read, whose
90
+ * start is 0, is named by its pid alone, and a later reading of the same pid is that process, named already.
91
+ */
92
+ firstNamed(processes) {
93
+ return processes.filter((entry) => {
94
+ const unread = `${entry.pid}:?`;
95
+ const key = entry.startedAt === '0' ? unread : `${entry.pid}:${entry.startedAt}`;
96
+ if (this.#named.has(key))
97
+ return false;
98
+ if (this.#named.has(unread)) {
99
+ this.#named.delete(unread);
100
+ this.#named.add(key);
101
+ return false;
102
+ }
103
+ this.#named.add(key);
104
+ return true;
105
+ });
106
+ }
107
+ /** Puts the runner in its own job, which ends everything in it once the runner ends. */
108
+ hold(deadline) {
109
+ const answer = this.#ask(['hold', '--runner', `${this.runner}`, '--job', this.#prefix], deadline);
110
+ if (answer.type !== 'held')
111
+ throw unexpected(answer, 'held');
112
+ return { job: answer.job, handle: answer.handle };
113
+ }
114
+ /**
115
+ * Puts a child the runner just started in a job of its own, with whatever it already started. When no answer says it
116
+ * was held, whatever the job came to hold is ended: a reply lost after the job was made leaves only an empty job,
117
+ * whose handle goes with the runner.
118
+ */
119
+ adopt(pid, deadline, markers = []) {
120
+ const job = this.launchJob(pid);
121
+ this.#count += 1;
122
+ let reply;
123
+ try {
124
+ reply = readMetadataProcess(this.#query(['adopt', '--runner', `${this.runner}`, '--within', this.#prefix, '--job', job, '--pid', `${pid}`], deadline));
125
+ }
126
+ catch (error) {
127
+ // No answer came. The metadata worker ends a run it gives up on before it says so, so a job that run made is
128
+ // there by now; one it made just as it was ended may take a moment to be seen.
129
+ this.#endWhenSeen(job, endAttempts);
130
+ throw error;
131
+ }
132
+ let answer;
133
+ try {
134
+ answer = readReply(reply);
135
+ if (answer.type !== 'adopted')
136
+ throw unexpected(answer, 'adopted');
137
+ }
138
+ catch (error) {
139
+ // The run answered and ended, so whatever job it made is there now, or there is none.
140
+ this.#endWhenSeen(job, 1);
141
+ throw error;
142
+ }
143
+ const root = supervised(answer.root);
144
+ const byParent = answer.byParent.map(supervised);
145
+ this.#launches.set(answer.job, { root, byParent, markers });
146
+ return { job: answer.job, handle: answer.handle, root, ended: answer.ended, late: answer.late.map(supervised), byParent, unheld: answer.unheld, complete: answer.complete };
147
+ }
148
+ // Ends whatever a job no adoption holds came to hold, asking again while no such job is seen, up to `attempts`. An
149
+ // adoption refused before it made its job leaves none, and the last asking finds none.
150
+ #endWhenSeen(job, attempts) {
151
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
152
+ try {
153
+ this.end(job);
154
+ return;
155
+ }
156
+ catch {
157
+ if (attempt < attempts)
158
+ Atomics.wait(pause, 0, 0, endPauseMs);
159
+ }
160
+ }
161
+ }
162
+ /** The processes a job holds now. */
163
+ members(job, deadline) {
164
+ return membersOf(this.#ask(['members', '--job', job], deadline));
165
+ }
166
+ async membersAsync(job, deadline) {
167
+ return membersOf(await this.#askAsync(['members', '--job', job], deadline));
168
+ }
169
+ /** One process as it is now, or undefined when no process runs under the pid, and whether the job, if named, holds it. */
170
+ process(pid, job, deadline) {
171
+ return heldProcess(this.#ask(['process', '--pid', `${pid}`, ...(job === undefined ? [] : ['--job', job])], deadline));
172
+ }
173
+ async processAsync(pid, job, deadline) {
174
+ return heldProcess(await this.#askAsync(['process', '--pid', `${pid}`, ...(job === undefined ? [] : ['--job', job])], deadline));
175
+ }
176
+ /** Ends every process a launch's job holds. The supervisor refuses a job that holds the runner. */
177
+ end(job, deadline) {
178
+ const answer = this.#ask(['end', '--runner', `${this.runner}`, '--job', job], deadline);
179
+ if (answer.type !== 'ended')
180
+ throw unexpected(answer, 'ended');
181
+ }
182
+ async endAsync(job, deadline) {
183
+ const answer = await this.#askAsync(['end', '--runner', `${this.runner}`, '--job', job], deadline);
184
+ if (answer.type !== 'ended')
185
+ throw unexpected(answer, 'ended');
186
+ }
187
+ /** Ends one process of a job only while it is the process that started at `startedAt`. */
188
+ terminate(job, pid, startedAt, deadline) {
189
+ return terminated(this.#ask(['terminate', '--runner', `${this.runner}`, '--job', job, '--pid', `${pid}`, '--started', startedAt], deadline));
190
+ }
191
+ async terminateAsync(job, pid, startedAt, deadline) {
192
+ return terminated(await this.#askAsync(['terminate', '--runner', `${this.runner}`, '--job', job, '--pid', `${pid}`, '--started', startedAt], deadline));
193
+ }
194
+ /**
195
+ * The pids every launch but `job`'s holds now, in its job or, as it was held, by its parent, for a claim that rests on
196
+ * a marker or on a browser read one reading late, which must never take another launch's process; undefined when a
197
+ * job still held could not be read.
198
+ */
199
+ otherLaunchMembers(job, deadline) {
200
+ const pids = new Set();
201
+ for (const [other, held] of [...this.#launches]) {
202
+ if (other === job)
203
+ continue;
204
+ for (const entry of held.byParent)
205
+ pids.add(entry.pid);
206
+ try {
207
+ for (const entry of this.members(other, deadline))
208
+ pids.add(entry.pid);
209
+ }
210
+ catch {
211
+ if (this.#launches.has(other))
212
+ return undefined;
213
+ }
214
+ }
215
+ return pids;
216
+ }
217
+ // One job at a time, as the synchronous form reads them.
218
+ async otherLaunchMembersAsync(job, deadline) {
219
+ const pids = new Set();
220
+ for (const [other, held] of [...this.#launches]) {
221
+ if (other === job)
222
+ continue;
223
+ for (const entry of held.byParent)
224
+ pids.add(entry.pid);
225
+ try {
226
+ for (const entry of await this.membersAsync(other, deadline))
227
+ pids.add(entry.pid);
228
+ }
229
+ catch {
230
+ if (this.#launches.has(other))
231
+ return undefined;
232
+ }
233
+ }
234
+ return pids;
235
+ }
236
+ /** Closes the runner's handle to a job that holds nothing any more, which frees the job. */
237
+ release(job, handle, deadline) {
238
+ const answer = this.#ask(['release', '--runner', `${this.runner}`, '--job', job, '--handle', `${handle}`], deadline);
239
+ if (answer.type !== 'released')
240
+ throw unexpected(answer, 'released');
241
+ this.#launches.delete(job);
242
+ }
243
+ async releaseAsync(job, handle, deadline) {
244
+ const answer = await this.#askAsync(['release', '--runner', `${this.runner}`, '--job', job, '--handle', `${handle}`], deadline);
245
+ if (answer.type !== 'released')
246
+ throw unexpected(answer, 'released');
247
+ this.#launches.delete(job);
248
+ }
249
+ /**
250
+ * At the end of a run, each process still running in a job of a launch whose own process has ended, as a helper a test
251
+ * file left running is, with the launch's pid, leaving out console hosts; and each such launch's job that could not be
252
+ * read. A job given back meanwhile holds nothing, and one whose answer was lost may be gone too, its processes ended
253
+ * with its last handle; leaving the unread ones out of `straysAsync` names whatever they still hold there instead.
254
+ * `only`, when given, names the launches' jobs to read, such as one run's own among several a host holds at once.
255
+ */
256
+ async leftoversAsync(deadline, only) {
257
+ const found = [];
258
+ const unread = [];
259
+ for (const [job, { root }] of [...this.#launches]) {
260
+ if (only !== undefined && !only.has(job))
261
+ continue;
262
+ if ((await this.processAsync(root.pid, undefined, deadline)).process?.startedAt === root.startedAt)
263
+ continue;
264
+ let members;
265
+ try {
266
+ members = await this.membersAsync(job, deadline);
267
+ }
268
+ catch {
269
+ if (this.#launches.has(job))
270
+ unread.push(job);
271
+ continue;
272
+ }
273
+ for (const process of members)
274
+ if (!consoleHost(process))
275
+ found.push({ root: root.pid, process });
276
+ }
277
+ return { found, unread };
278
+ }
279
+ /**
280
+ * What the runner's job holds that no launch's job does and that nothing still running stands behind: a process left
281
+ * by one that had ended before its launch was held, which no launch's job took, and which runs until the runner ends.
282
+ * Console hosts are left out. `leaveOut` names launches' jobs to read as no launch's, so what they hold is named.
283
+ */
284
+ strays(deadline) {
285
+ return straysOf(this.#ask(this.#straysArguments([]), deadline));
286
+ }
287
+ async straysAsync(deadline, leaveOut = []) {
288
+ return straysOf(await this.#askAsync(this.#straysArguments(leaveOut), deadline));
289
+ }
290
+ #straysArguments(leaveOut) {
291
+ const launches = [...this.#launches.keys()].filter((job) => !leaveOut.includes(job)).join(',');
292
+ // A command line on Windows holds 32767 characters; thousands of launches held at once would not fit.
293
+ if (launches.length > 30_000)
294
+ throw new Error(`Retest holds ${this.#launches.size} launches at once, too many to name to retest-supervisor in one question.`);
295
+ return ['strays', '--runner', `${this.runner}`, '--job', this.#prefix, ...(launches === '' ? [] : ['--launches', launches])];
296
+ }
297
+ /**
298
+ * Sends Ctrl+C to the console of a launch's processes, or of `pid` among them, unless Retest itself is on that console;
299
+ * Ctrl+Break with `key: 'break'`, which a console program cannot be set to ignore.
300
+ */
301
+ interrupt(job, options = {}) {
302
+ const answer = this.#ask(['interrupt', '--runner', `${this.runner}`, '--job', job, ...(options.pid === undefined ? [] : ['--pid', `${options.pid}`]), ...(options.key === undefined ? [] : ['--key', options.key])], options.deadline);
303
+ if (answer.type !== 'interrupted')
304
+ throw unexpected(answer, 'interrupted');
305
+ return { console: answer.console, reached: answer.reached };
306
+ }
307
+ #query(args, deadline) {
308
+ return { command: this.program.command, args: [...this.program.args, ...args], environment: this.program.environment, deadline };
309
+ }
310
+ #ask(args, deadline) {
311
+ return readReply(readMetadataProcess(this.#query(args, deadline)));
312
+ }
313
+ async #askAsync(args, deadline) {
314
+ // Neither the metadata worker nor its wait keeps Node alive; a host with nothing else to do must still hear the answer.
315
+ const holding = setInterval(() => undefined, 1000);
316
+ try {
317
+ return readReply(await readMetadataProcessAsync(this.#query(args, deadline)));
318
+ }
319
+ finally {
320
+ clearInterval(holding);
321
+ }
322
+ }
323
+ }
324
+ /**
325
+ * The supervisor's one line of output as a checked reply. Anything else, a second line included, is no answer.
326
+ *
327
+ * @example readReply('{"schemaVersion":1,"type":"ended","job":"Local\\\\retest-1-ab-2"}\n').type // 'ended'
328
+ */
329
+ export function readReply(text) {
330
+ const line = text.endsWith('\n') ? text.slice(0, -1) : text;
331
+ if (line.includes('\n') || line.trim() === '')
332
+ throw new Error('retest-supervisor did not answer with one line.');
333
+ let value;
334
+ try {
335
+ value = JSON.parse(line);
336
+ }
337
+ catch {
338
+ throw new Error('retest-supervisor answered with something that is not JSON.');
339
+ }
340
+ const parsed = parse(reply, value);
341
+ if (!parsed.ok)
342
+ throw new Error(`retest-supervisor answered in a form this Retest cannot read: ${parsed.issues.map((issue) => `${issue.path} ${issue.message}`).join('; ')}.`);
343
+ return parsed.value;
344
+ }
345
+ // A refusal throws the supervisor's own sentence, with the Windows call behind it as the cause; any other answer to the
346
+ // wrong question is no answer.
347
+ function unexpected(answer, wanted) {
348
+ if (answer.type !== 'refused')
349
+ return new Error(`retest-supervisor answered ${answer.type} where ${wanted} was asked for.`);
350
+ return answer.detail === null ? new Error(answer.problem) : new Error(answer.problem, { cause: new Error(answer.detail) });
351
+ }
352
+ function straysOf(answer) {
353
+ if (answer.type !== 'strays')
354
+ throw unexpected(answer, 'strays');
355
+ return answer.processes.map(supervised).filter((entry) => !consoleHost(entry));
356
+ }
357
+ /**
358
+ * Whether a process is the console host Windows starts beside a console program given a console of its own, as each test
359
+ * file's process, app server and media process is. It belongs to the programs on that console, runs nothing anyone gave
360
+ * it and ends a moment after the last of them, so it is never named as left running: a program still on its console is
361
+ * named itself.
362
+ *
363
+ * @example consoleHost({ pid: 7300, parentPid: 7200, startedAt: '1', image: 'C:\\Windows\\System32\\conhost.exe' }) // true
364
+ */
365
+ export function consoleHost(process) {
366
+ return process.image?.toLowerCase().endsWith('\\system32\\conhost.exe') === true;
367
+ }
368
+ function terminated(answer) {
369
+ if (answer.type !== 'terminated')
370
+ throw unexpected(answer, 'terminated');
371
+ return answer.outcome;
372
+ }
373
+ function membersOf(answer) {
374
+ if (answer.type !== 'members')
375
+ throw unexpected(answer, 'members');
376
+ return answer.processes.map(supervised);
377
+ }
378
+ function supervised(entry) {
379
+ if (!/^\d{1,20}$/.test(entry.startedAt))
380
+ throw new Error('retest-supervisor gave a process start that is not a count of ticks.');
381
+ return { pid: entry.pid, parentPid: entry.parentPid ?? undefined, startedAt: entry.startedAt, image: entry.image ?? undefined, commandLine: entry.commandLine ?? undefined };
382
+ }
383
+ function heldProcess(answer) {
384
+ if (answer.type !== 'process')
385
+ throw unexpected(answer, 'process');
386
+ return { process: answer.process === null ? undefined : supervised(answer.process), inJob: answer.inJob === true };
387
+ }
388
+ // How many times a launch's end reads the runner's job for what the processes its parent holds started meanwhile.
389
+ const parentedEndRounds = 3;
390
+ // How soon after the reading before it a reading of the runner's job may count what a browser gone since started. A close
391
+ // reads as it asks the browser to close, waits up to a second for each answer and exit, then reads every 100 ms, each
392
+ // reading two runs of retest-supervisor, which take a second each on a busy runner; an end's rounds come a run apart.
393
+ // The bound keeps short the time in which the browser's pid could have gone to a process that started another and ended.
394
+ const lateParentMs = 10_000;
395
+ // The processes of one reading of the runner's job that a launch holds without their being in its job, by one of these
396
+ // facts and no other. A process on record by its pid and start is the launch's: a pid and a start name one process. So
397
+ // is a process whose parent runs in this same reading with the start the launch has for it, as one of its job's members
398
+ // or one it holds, and started no earlier than that parent: Windows names a running process's parent truly. And so is a
399
+ // process whose command line names one of the launch's markers as a folder of its own, the folder only this launch uses,
400
+ // which its browser carries whatever started it. A parent that has ended is no evidence: Windows keeps the pid it had,
401
+ // and a later process given that pid passes it on to what it starts. So nothing is counted through one, with one bounded
402
+ // exception: `late` names the launch's browsers, held by their parent, that ran in a reading taken a moment before this
403
+ // one; one that is gone now, its pid held by nothing else in the runner's job, still stands behind what it started, for
404
+ // this one reading, as a browser's content processes outlive it a moment. A process `elsewhere` names, another
405
+ // launch's, is never this one's and stands behind nothing of it. A claim that rests on a marker or a late browser is
406
+ // `weak`, for the caller to read the other launches' jobs. The records the reading leaves are what it found.
407
+ function readParented(options) {
408
+ const { records, reading } = options;
409
+ const markers = options.markers ?? [];
410
+ const elsewhere = options.elsewhere ?? new Set();
411
+ const inReading = new Map(reading.map((entry) => [entry.pid, entry.startedAt]));
412
+ // A member counts as a running parent only as this reading shows it: one that ended after its job was read, its pid
413
+ // given to another process, runs here under another start.
414
+ const running = new Map((options.members ?? []).filter((entry) => inReading.get(entry.pid) === entry.startedAt).map((entry) => [entry.pid, entry.startedAt]));
415
+ const memberPids = new Set((options.members ?? []).map((entry) => entry.pid));
416
+ // The late browsers gone from this reading, their pids taken by nothing else.
417
+ const late = options.late ?? new Set();
418
+ const gone = new Map([...records].filter(([pid]) => late.has(pid) && !inReading.has(pid)));
419
+ const claimed = [];
420
+ let weak = false;
421
+ const claim = (entry) => {
422
+ running.set(entry.pid, entry.startedAt);
423
+ claimed.push(entry);
424
+ };
425
+ const open = (entry) => !memberPids.has(entry.pid) && !running.has(entry.pid) && !elsewhere.has(entry.pid);
426
+ for (const entry of reading) {
427
+ if (!open(entry))
428
+ continue;
429
+ if (records.get(entry.pid) === entry.startedAt)
430
+ claim(entry);
431
+ else if (markers.some((marker) => namesFolder(entry.commandLine, marker))) {
432
+ claim(entry);
433
+ weak = true;
434
+ }
435
+ }
436
+ let changed = true;
437
+ while (changed) {
438
+ changed = false;
439
+ for (const entry of reading) {
440
+ if (!open(entry) || entry.parentPid === undefined)
441
+ continue;
442
+ const parent = running.get(entry.parentPid);
443
+ const lately = parent === undefined ? gone.get(entry.parentPid) : undefined;
444
+ const start = parent ?? lately;
445
+ if (start === undefined || BigInt(start) > BigInt(entry.startedAt))
446
+ continue;
447
+ claim(entry);
448
+ weak ||= lately !== undefined;
449
+ changed = true;
450
+ }
451
+ }
452
+ return { claimed, records: new Map(claimed.map((entry) => [entry.pid, entry.startedAt])), weak };
453
+ }
454
+ /**
455
+ * Whether a command line names `marker` as a folder of its own: between two separators, or after one at the end of a
456
+ * path, never as part of a longer name. Windows compares names whatever their case.
457
+ *
458
+ * @example namesFolder('firefox.exe --profile "C:\\Temp\\retest-firefox-1-a\\profile"', 'retest-firefox-1-a') // true
459
+ */
460
+ export function namesFolder(commandLine, marker) {
461
+ if (commandLine === undefined || marker === '')
462
+ return false;
463
+ const line = commandLine.toLowerCase();
464
+ const wanted = marker.toLowerCase();
465
+ for (let at = line.indexOf(wanted); at >= 0; at = line.indexOf(wanted, at + 1)) {
466
+ const before = line[at - 1];
467
+ const after = line[at + wanted.length];
468
+ if ((before === '\\' || before === '/') && (after === undefined || after === '\\' || after === '/' || after === '"' || after === "'" || after.trim() === ''))
469
+ return true;
470
+ }
471
+ return false;
472
+ }
473
+ /**
474
+ * Of the processes no launch's job holds and nothing running stands behind, those a run can tie to itself by the facts
475
+ * a launch's own reading goes by: each one on a launch's record by its pid and start, each one whose parent runs in this
476
+ * reading on such a record, and each one whose command line names a launch's marker. A record of any age is only ever
477
+ * its own process, so nothing is tied through a process that has ended.
478
+ *
479
+ * @example tiedTo([{ onRecord: () => new Map([[5001, '13437009000005001']]), markers: [] }], strays) // 5001, and what 5001 runs
480
+ */
481
+ export function tiedTo(launches, strays) {
482
+ const tied = new Set();
483
+ for (const launch of launches) {
484
+ for (const entry of readParented({ records: launch.onRecord(), reading: strays, markers: launch.markers }).claimed)
485
+ tied.add(`${entry.pid}:${entry.startedAt}`);
486
+ }
487
+ return strays.filter((entry) => tied.has(`${entry.pid}:${entry.startedAt}`));
488
+ }
489
+ function recordsOf(processes) {
490
+ return new Map(processes.map((entry) => [entry.pid, entry.startedAt]));
491
+ }
492
+ /**
493
+ * A process left running, as cleanup names it; `when` says when it was seen, such as `after process 5000 ended`.
494
+ *
495
+ * @example stillRunningProblem({ pid: 6100, parentPid: 6050, startedAt: '1', image: 'C:\\Apps\\helper.exe' }, 'after process 5000 ended') // 'Process 6100 (helper.exe) is still running after process 5000 ended. Retest ends it when Retest exits.'
496
+ */
497
+ export function stillRunningProblem(process, when) {
498
+ return `Process ${process.pid} (${process.image === undefined ? 'a program Windows would not name' : win32.basename(process.image)}) is still running ${when}. Retest ends it when Retest exits.`;
499
+ }
500
+ /** What cleanup says when it could not look for processes left running; `when` as for `stillRunningProblem`. */
501
+ export function leftoversUnreadProblem(error, when) {
502
+ return `Retest could not look for processes still running ${when}: ${errorMessage(error)}`;
503
+ }
504
+ /**
505
+ * The command a process stands under when Windows would not name its executable. It has the shape the ownership rule
506
+ * reads as a command that cannot be read, so it is never compared as one.
507
+ */
508
+ export const unreadableImage = '(unreadable)';
509
+ /**
510
+ * A launch's ownership readings on Windows: its job's own process list, where every member has the launch's pid as its
511
+ * group, a pid read with whether the job holds it, and the job's end, which ends every member at once. A job that was
512
+ * read empty holds nothing from then on, since only Retest puts processes in it and only when it is made, so its
513
+ * handle is given back and every later reading answers empty without asking.
514
+ *
515
+ * A process held by its parent, already in a job of its own when the launch was held, as the browser Firefox's Windows
516
+ * launcher starts is, belongs to the launch as well, with everything it starts while it runs: they are read from the
517
+ * runner's job as members, by the starts on record, through parents running in the same reading, and by `markers`, the
518
+ * names in a command line only this launch's processes carry, such as its profile folder.
519
+ *
520
+ * An empty job is not yet proof that the launch left nothing. A process whose own parent had ended before the launch
521
+ * was held cannot be tied to it, and stays in the runner's job only; so once the job is read empty, the runner's job is
522
+ * read too, and anything there that no launch's job holds and nothing running stands behind leaves the launch's
523
+ * cleanup unconfirmed, naming it. Each such process is named once in the host, for the first launch it is seen after.
524
+ *
525
+ * @example const ownership = new OwnedProcessGroup(child.pid, process.pid, jobSystem(supervisor, supervisor.adopt(child.pid)))
526
+ */
527
+ export function jobSystem(supervisor, launch, options = {}) {
528
+ const root = launch.root.pid;
529
+ const markers = options.markers ?? [];
530
+ const lateWindowMs = options.lateParentMs ?? lateParentMs;
531
+ const browsers = new Map(launch.byParent.map((entry) => [entry.pid, entry.startedAt]));
532
+ let inJob = recordsOf([...(launch.ended ? [] : [launch.root]), ...launch.late]);
533
+ let parented = recordsOf(launch.byParent);
534
+ // When the runner's job was last read for the launch, for `readParented`'s reading a moment after the one before.
535
+ let lastRead = Number.NEGATIVE_INFINITY;
536
+ let emptied = false;
537
+ const unconfirmed = [];
538
+ const owned = (entry, member) => ({
539
+ pid: entry.pid, parentPid: entry.parentPid ?? 0, groupId: member ? root : 0, startedAt: entry.startedAt, command: entry.image ?? unreadableImage,
540
+ });
541
+ const release = (deadline) => {
542
+ emptied = true;
543
+ try {
544
+ supervisor.release(launch.job, launch.handle, deadline);
545
+ }
546
+ catch {
547
+ // An empty job whose handle stays open holds nothing and goes with the runner; the reading itself stands.
548
+ }
549
+ };
550
+ const releaseAsync = async (deadline) => {
551
+ emptied = true;
552
+ try {
553
+ await supervisor.releaseAsync(launch.job, launch.handle, deadline);
554
+ }
555
+ catch {
556
+ // As for `release`.
557
+ }
558
+ };
559
+ // Each stray first seen as the launch's job is read empty is named: by the launch, after its process ended, when the
560
+ // launch's own facts tie it to the launch; otherwise by what it is, a process whose starter has ended, which blames no
561
+ // launch. One whose parent's pid is that of a browser another launch the host holds had by its parent is left to that
562
+ // launch, whose next reading ends it or names it, as a content process outlives its browser a moment.
563
+ const named = (strays) => {
564
+ const elsewhere = new Set(supervisor.launches.flatMap((other) => other.job === launch.job ? [] : other.byParent.map((entry) => entry.pid)));
565
+ const left = strays.filter((stray) => stray.parentPid === undefined || !elsewhere.has(stray.parentPid));
566
+ const tied = new Set(readParented({ records: parented, reading: left, markers }).claimed.map((entry) => entry.pid));
567
+ return supervisor.firstNamed(left).map((stray) => tied.has(stray.pid) ? stillRunningProblem(stray, `after process ${root} ended`) : stillRunningProblem(stray, 'after the process that started it ended'));
568
+ };
569
+ const seen = (members, deadline) => {
570
+ if (members.length > 0)
571
+ return members.map((entry) => owned(entry, true));
572
+ release(deadline);
573
+ try {
574
+ unconfirmed.push(...named(supervisor.strays(deadline)));
575
+ }
576
+ catch (error) {
577
+ unconfirmed.push(leftoversUnreadProblem(error, `after process ${root} ended`));
578
+ }
579
+ return [];
580
+ };
581
+ const seenAsync = async (members, deadline) => {
582
+ if (members.length > 0)
583
+ return members.map((entry) => owned(entry, true));
584
+ await releaseAsync(deadline);
585
+ try {
586
+ unconfirmed.push(...named(await supervisor.straysAsync(deadline)));
587
+ }
588
+ catch (error) {
589
+ unconfirmed.push(leftoversUnreadProblem(error, `after process ${root} ended`));
590
+ }
591
+ return [];
592
+ };
593
+ // What a reading of the runner's job, taken at `readAt`, finds the launch holds by its parents (`readParented`). A claim
594
+ // that rests on a marker or on a parent read one reading late could be another launch's, so then the other launches'
595
+ // jobs are read once and what they hold is left alone; when one of them cannot be read, such claims are not made.
596
+ const strongly = (members, reading) => readParented({ records: parented, members, reading });
597
+ // The launch's browsers that ran in the reading before, when that reading was a moment ago.
598
+ const lately = (readAt) => new Set(readAt - lastRead > lateWindowMs ? [] : [...browsers].filter(([pid, startedAt]) => parented.get(pid) === startedAt).map(([pid]) => pid));
599
+ const parentHeld = (members, reading, readAt, deadline) => {
600
+ const late = lately(readAt);
601
+ let found = readParented({ records: parented, members, reading, markers, late });
602
+ if (found.weak) {
603
+ const elsewhere = supervisor.otherLaunchMembers(launch.job, deadline);
604
+ found = elsewhere === undefined ? strongly(members, reading) : readParented({ records: parented, members, reading, markers, late, elsewhere });
605
+ }
606
+ parented = found.records;
607
+ lastRead = readAt;
608
+ return found.claimed;
609
+ };
610
+ const parentHeldAsync = async (members, reading, readAt, deadline) => {
611
+ const late = lately(readAt);
612
+ let found = readParented({ records: parented, members, reading, markers, late });
613
+ if (found.weak) {
614
+ const elsewhere = await supervisor.otherLaunchMembersAsync(launch.job, deadline);
615
+ found = elsewhere === undefined ? strongly(members, reading) : readParented({ records: parented, members, reading, markers, late, elsewhere });
616
+ }
617
+ parented = found.records;
618
+ lastRead = readAt;
619
+ return found.claimed;
620
+ };
621
+ // A process held by its parent is in a job of its own beside the launch's, which the launch's job's end does not reach,
622
+ // and so is what it started, as the content processes of a browser Firefox's launcher started are. Each is ended by the
623
+ // start the runner's job gives it, while it is still that process there. A process can start another between the
624
+ // reading and its own end, so the runner's job is read again, up to `parentedEndRounds` times, until none is left: a
625
+ // round's reading comes a moment after the last, so what a process the last round ended started is still the launch's
626
+ // (`readParented`'s `late`). Whatever is left is the next reading's to name.
627
+ const endParented = (deadline) => {
628
+ for (let round = 0; round < parentedEndRounds && parented.size > 0; round += 1) {
629
+ let reading;
630
+ const readAt = performance.now();
631
+ try {
632
+ reading = supervisor.members(supervisor.runnerJob, deadline);
633
+ }
634
+ catch {
635
+ return;
636
+ }
637
+ const ending = parentHeld([], reading, readAt, deadline);
638
+ if (ending.length === 0)
639
+ return;
640
+ for (const entry of ending) {
641
+ try {
642
+ supervisor.terminate(supervisor.runnerJob, entry.pid, entry.startedAt, deadline);
643
+ }
644
+ catch {
645
+ // Not ended; the launch's next reading still counts it.
646
+ }
647
+ }
648
+ }
649
+ };
650
+ // As `endParented`, without holding the thread: one run of the supervisor at a time, as the synchronous end makes them.
651
+ const endParentedAsync = async (deadline) => {
652
+ for (let round = 0; round < parentedEndRounds && parented.size > 0; round += 1) {
653
+ let reading;
654
+ const readAt = performance.now();
655
+ try {
656
+ reading = await supervisor.membersAsync(supervisor.runnerJob, deadline);
657
+ }
658
+ catch {
659
+ return;
660
+ }
661
+ const ending = await parentHeldAsync([], reading, readAt, deadline);
662
+ if (ending.length === 0)
663
+ return;
664
+ for (const entry of ending) {
665
+ try {
666
+ await supervisor.terminateAsync(supervisor.runnerJob, entry.pid, entry.startedAt, deadline);
667
+ }
668
+ catch {
669
+ // As for `endParented`.
670
+ }
671
+ }
672
+ }
673
+ };
674
+ const job = {
675
+ root,
676
+ launched: owned(launch.root, true),
677
+ end: (deadline) => {
678
+ if (emptied)
679
+ return;
680
+ supervisor.end(launch.job, deadline);
681
+ endParented(deadline);
682
+ },
683
+ endAsync: async (deadline) => {
684
+ if (emptied)
685
+ return;
686
+ await supervisor.endAsync(launch.job, deadline);
687
+ await endParentedAsync(deadline);
688
+ },
689
+ interrupt: (deadline) => { if (!emptied)
690
+ supervisor.interrupt(launch.job, deadline === undefined ? {} : { deadline }); },
691
+ unconfirmed: () => unconfirmed,
692
+ };
693
+ return {
694
+ job,
695
+ onRecord: () => new Map([...inJob, ...parented]),
696
+ markers,
697
+ read: (deadline) => {
698
+ if (emptied)
699
+ return [];
700
+ const members = supervisor.members(launch.job, deadline);
701
+ inJob = recordsOf(members);
702
+ if (parented.size === 0)
703
+ return seen(members, deadline);
704
+ const readAt = performance.now();
705
+ return seen([...members, ...parentHeld(members, supervisor.members(supervisor.runnerJob, deadline), readAt, deadline)], deadline);
706
+ },
707
+ readAsync: async (deadline) => {
708
+ if (emptied)
709
+ return [];
710
+ const members = await supervisor.membersAsync(launch.job, deadline);
711
+ inJob = recordsOf(members);
712
+ if (parented.size === 0)
713
+ return seenAsync(members, deadline);
714
+ const readAt = performance.now();
715
+ return seenAsync([...members, ...(await parentHeldAsync(members, await supervisor.membersAsync(supervisor.runnerJob, deadline), readAt, deadline))], deadline);
716
+ },
717
+ readProcess: (pid, deadline) => {
718
+ const reading = supervisor.process(pid, launch.job, deadline);
719
+ return reading.process === undefined ? undefined : owned(reading.process, reading.inJob && !emptied);
720
+ },
721
+ readProcessAsync: async (pid, deadline) => {
722
+ const reading = await supervisor.processAsync(pid, launch.job, deadline);
723
+ return reading.process === undefined ? undefined : owned(reading.process, reading.inJob && !emptied);
724
+ },
725
+ signal: (pid, signal, expected) => {
726
+ if (signal !== 'SIGKILL' && signal !== 'SIGTERM')
727
+ throw new Error(`Windows has no ${signal === 0 ? 'signal 0' : signal}; Retest only ends a process there, through its job.`);
728
+ if (expected === undefined)
729
+ throw new Error(`Process ${pid} has no recorded start, so it was not ended.`);
730
+ const outcome = supervisor.terminate(launch.job, pid, expected.startedAt);
731
+ if (outcome === 'absent')
732
+ throw Object.assign(new Error(`Process ${pid} is not running.`), { code: 'ESRCH' });
733
+ if (outcome === 'different')
734
+ throw new Error(`Process ${pid} is no longer the process Retest recorded, so it was not ended.`);
735
+ },
736
+ };
737
+ }