@hominis/fireforge 0.47.0 → 0.47.1

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 (35) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/bin/fireforge.js +6 -0
  3. package/dist/build-info.json +4 -4
  4. package/dist/src/commands/test-harness-teardown.d.ts +62 -0
  5. package/dist/src/commands/test-harness-teardown.js +128 -0
  6. package/dist/src/commands/test-register.js +2 -1
  7. package/dist/src/commands/test-run.d.ts +2 -0
  8. package/dist/src/commands/test-run.js +36 -7
  9. package/dist/src/commands/test-verdict.d.ts +19 -4
  10. package/dist/src/commands/test-verdict.js +22 -4
  11. package/dist/src/commands/test.js +18 -10
  12. package/dist/src/core/config-paths.d.ts +1 -1
  13. package/dist/src/core/config-paths.js +1 -0
  14. package/dist/src/core/config-validate-test-toolchains.js +7 -0
  15. package/dist/src/core/file-lock.js +16 -9
  16. package/dist/src/core/harness-helper-pids.d.ts +98 -0
  17. package/dist/src/core/harness-helper-pids.js +240 -0
  18. package/dist/src/core/harness-orphans.d.ts +53 -8
  19. package/dist/src/core/harness-orphans.js +125 -44
  20. package/dist/src/core/mach.d.ts +16 -0
  21. package/dist/src/core/mach.js +6 -1
  22. package/dist/src/core/marionette-preflight.js +9 -3
  23. package/dist/src/core/parent-exit-watchdog.d.ts +62 -0
  24. package/dist/src/core/parent-exit-watchdog.js +69 -0
  25. package/dist/src/types/commands/options.d.ts +11 -1
  26. package/dist/src/types/config.d.ts +7 -0
  27. package/dist/src/utils/process-group.d.ts +5 -1
  28. package/dist/src/utils/process-group.js +7 -3
  29. package/dist/src/utils/process.d.ts +18 -0
  30. package/dist/src/utils/process.js +14 -2
  31. package/docs/configuration.md +7 -1
  32. package/docs/lifecycle-invariants.md +17 -1
  33. package/docs/machine-output.md +22 -9
  34. package/docs/testing.md +68 -4
  35. package/package.json +1 -1
@@ -284,7 +284,10 @@ Wait ergonomics are opt-in: `pollMaxMs` (the poll interval doubles from
284
284
  `pollMs` up to the cap) and `waitProgressMs` with `onWaitProgress` (a periodic
285
285
  progress callback carrying the holder's PID, liveness, and owner-metadata
286
286
  lines from the pid file). All three are inert when unset, so existing callers
287
- keep byte-identical behavior.
287
+ keep byte-identical behavior. Within one poll iteration a due progress probe
288
+ runs before the deadline check, so a wait that times out has always reported
289
+ the holder at least once when its budget exceeds `waitProgressMs`, however
290
+ slow the host (`file-lock.test.ts`, "budget shorter than one iteration").
288
291
 
289
292
  ### `src/core/file-lock-owner.ts`
290
293
 
@@ -430,10 +433,23 @@ On SIGINT or SIGTERM, `bin/fireforge.ts`:
430
433
  - `waitForActiveCriticalSections(5 s)`, which holds the exit for in-flight
431
434
  "apply plus persist" pairs (invariant 4). It is bounded so that a stuck
432
435
  write cannot postpone the exit the user asked for.
436
+ - `waitForActiveChildShutdown(12 s)`, which holds the exit until every
437
+ tracked child has closed. For a process-group dispatch (`mach` under
438
+ `test`, `build`, `run`) "closed" includes the exec layer's post-close
439
+ work: the group sweep and the caller's `postCloseSweep` (the harness
440
+ helper reap in `fireforge test`). Both run before the child's closure
441
+ settles, so a forwarded SIGTERM cannot race `process.exit` past them.
433
442
  3. Force-releases the furnace lock directories of active operations, since
434
443
  `withFileLock`'s `finally` will never run past `process.exit`.
435
444
  4. Calls `process.exit(130 | 143)`.
436
445
 
446
+ A parent that dies without signalling FireForge (SIGKILL on the `npm` above
447
+ it) enters no handler here. `fireforge test` covers that case itself with
448
+ the parent-exit watchdog (`src/core/parent-exit-watchdog.ts`): it writes the
449
+ killed verdict, reaps the mach group and the tracked helpers, and lets the
450
+ dispatch loop end the run; the process then exits through the ordinary
451
+ error path, not through this pipeline.
452
+
437
453
  ## Choosing a primitive for a new mutation
438
454
 
439
455
  | Your operation | Use |
@@ -86,15 +86,15 @@ moved.
86
86
  `FIREFORGE-VERDICT: FAIL reason=<reason>` uses a closed set
87
87
  (`FireforgeVerdictReason`, `src/commands/test-verdict.ts`):
88
88
 
89
- | Reason | Meaning |
90
- | --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
91
- | `crash` | The harness or the browser died, so there is no trustworthy suite result. |
92
- | `no-tests` | The run dispatched but nothing ran. |
93
- | `test-failures` | The suite ran and reported unexpected results. |
94
- | `preflight` | The run was refused before the harness was reached. |
95
- | `inconclusive` | A result exists, but `engine/` moved under it, so the result was discarded. |
96
- | `lock-timeout` | The run never started: the engine session lock stayed contended. |
97
- | `killed` | A signal terminated the run. Written from the signal handler so that a log tail always describes itself. A killed run must never be silent, or "killed", "still running" and "never started" all look alike. |
89
+ | Reason | Meaning |
90
+ | --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
91
+ | `crash` | The harness or the browser died, so there is no trustworthy suite result. |
92
+ | `no-tests` | The run dispatched but nothing ran. |
93
+ | `test-failures` | The suite ran and reported unexpected results. |
94
+ | `preflight` | The run was refused before the harness was reached. |
95
+ | `inconclusive` | A result exists, but `engine/` moved under it, so the result was discarded. |
96
+ | `lock-timeout` | The run never started: the engine session lock stayed contended. |
97
+ | `killed` | A signal terminated the run, or FireForge ended it because its parent process vanished. Written from the signal handler (or the parent-exit watchdog) so that a log tail always describes itself. A killed run must never be silent, or "killed", "still running" and "never started" all look alike. `signal=` names the cause: a signal name, or `parent-exit`. |
98
98
 
99
99
  ### The additive `note=` key
100
100
 
@@ -124,6 +124,19 @@ tasks _inside_ a file is harness code that reads the exported seed; FireForge
124
124
  owns the seed, the plumbing and the record. Absent when the run did not
125
125
  shuffle.
126
126
 
127
+ ### The additive `orphans-reaped=` key
128
+
129
+ How many harness processes FireForge terminated itself during the run: at
130
+ preflight, survivors of an earlier run in this objdir under `--reap-orphans`
131
+ or `test.reapOrphans: "reap"`; at teardown, helpers this run's harness
132
+ launched (httpd, websocket server, ssltunnel, process bridge, the browser,
133
+ `moz-http2`) that were still alive after mach exited. Summed across the census, every
134
+ retry attempt and every shard. Absent when nothing was reaped, so a green on
135
+ a quiet machine prints the line it always did, and a green after a reap is
136
+ distinguishable from it. A run ended by SIGTERM or by the parent-exit
137
+ watchdog writes its verdict before the teardown reap runs, so that line never
138
+ carries the key; the reap is reported on stderr instead.
139
+
127
140
  A preflight refusal's own text is written to stdout before the verdict line,
128
141
  and into the run log, so the verdict stays the last stdout write while the
129
142
  reason survives both a redirect and a `tail`. See
package/docs/testing.md CHANGED
@@ -87,21 +87,85 @@ a slow build for a silently wrong test.
87
87
  earlier run in this project's objdir. It runs at preflight, before this run
88
88
  spawns anything, so every hit is necessarily a survivor. A match has to be
89
89
  objdir-anchored, since `xpcshell` and `server.js` are far too generic to
90
- report on their own. Survivors slow every later run without appearing in
90
+ report on their own, and structural: the process's executable is a helper
91
+ binary, or an interpreter whose script argument is a helper script. A
92
+ process that merely mentions such a path (an editor, a `grep`, the shell
93
+ FireForge runs under) is not a helper, and FireForge's own ancestors are
94
+ never candidates. Survivors slow every later run without appearing in
91
95
  its output. The usual symptom is a three-second suite taking minutes of
92
- wall clock. The census is report-only by default, and `--reap-orphans`
93
- terminates what it finds. It never refuses a run. A different shape,
96
+ wall clock. The census is report-only by default; `--reap-orphans`, or
97
+ `test.reapOrphans: "reap"` in `fireforge.json`, terminates what it finds
98
+ (SIGTERM, a 500 ms grace, SIGKILL). It never refuses a run. Every reap is
99
+ stamped on the verdict line as `orphans-reaped=<n>`. A different shape,
94
100
  reparented Python `multiprocessing` workers, is covered by the
95
101
  `Orphaned harness workers` doctor check.
102
+ - Every dispatch owns its own kill path. The harness announces what it
103
+ launches (`runtests.py | Server pid: <n>`, the websocket server, the SSL
104
+ tunnel, the websocket/process bridge, and the browser as `Application
105
+ pid`); FireForge tracks those pids and, after mach has exited for any
106
+ reason (a clean finish, a harness crash before a retry, a no-output
107
+ timeout, a forwarded SIGTERM, the parent-exit watchdog below), terminates
108
+ any of them still alive. A tracked pid is re-read from `ps` first and must
109
+ still be anchored to this objdir and still look like the thing that was
110
+ launched, so a recycled pid is never signalled. One helper is never
111
+ announced: mozserve starts `moz-http2` in a session of its own and logs no
112
+ pid, so it is in neither the group nor the announcements. For it, FireForge
113
+ snapshots the same-objdir helpers before the dispatch spawns and, after
114
+ mach exits, reaps any helper-shaped process under this checkout's absolute
115
+ objdir that is new since that snapshot and reparented to launchd. An
116
+ earlier run's survivor is in the snapshot and stays the census's call. This
117
+ reap runs inside the exec layer's close path, so the signal handler's
118
+ bounded child-shutdown wait covers it. The count joins `orphans-reaped=`.
119
+ - FireForge watches its own parent while a dispatch runs. A supervisor that
120
+ SIGKILLs the process above `fireforge test` (an `npm run` step killed at
121
+ its bound) signals nothing below it; FireForge notices the reparenting
122
+ within about two seconds, writes `FIREFORGE-VERDICT: FAIL reason=killed
123
+ signal=parent-exit`, reaps the harness process group and the tracked
124
+ helpers, and ends the run without classifying or retrying.
96
125
  - `--perf-samples <path>` publishes a perf-sample artifact path to the
97
126
  harness (exported as `<BINARYNAME>_PERF_SAMPLE_JSON`).
98
127
 
128
+ ## Process groups and supervisors
129
+
130
+ `fireforge test` spawns mach as the leader of its **own** process group
131
+ (`detached: true`), so that FireForge can signal the whole harness tree with
132
+ one negative-pid kill and sweep the group after mach exits. Two consequences
133
+ for anything that supervises `fireforge test`:
134
+
135
+ - A supervisor that kills _its own_ process group (`kill -- -<npm pid>`) does
136
+ not reach mach: mach is not in that group. Killing the `npm`/`node` chain
137
+ above FireForge leaves the harness tree running.
138
+ - SIGTERM to FireForge is the right signal. FireForge forwards it to the
139
+ mach group, escalates to SIGKILL after a grace period, sweeps the group,
140
+ reaps the tracked helpers, and writes `FAIL reason=killed signal=SIGTERM`.
141
+ (That line, like the watchdog's `signal=parent-exit` line, is written
142
+ before the teardown reap runs, so it cannot carry `orphans-reaped=`; the
143
+ reap reports to stderr.)
144
+ - A supervisor that has to SIGKILL FireForge should pass
145
+ `--pgid-file <path>`. FireForge rewrites the file with the harness group id
146
+ on every mach spawn (each retry attempt and shard) and removes it when the
147
+ run ends under its own control. After a SIGKILL the file is still there:
148
+ `kill -TERM -- -$(cat <path>)`, then `-KILL`, takes mach and every helper
149
+ that shares its group. Two processes are outside it: the browser
150
+ (mozprocess puts it in a group of its own) and `moz-http2` (mozserve
151
+ starts it in its own session). A SIGTERM to the group gives `runtests.py`
152
+ the chance to run its own cleanup, which normally takes the browser with
153
+ it; nothing takes `moz-http2`, so after a SIGKILL of FireForge it stays
154
+ until the next run's census, which is why that run should carry the reap
155
+ posture. A browser that does survive is the Marionette preflight's and
156
+ `--kill-stale-marionette`'s to remove.
157
+
158
+ The mochitest helpers (httpd, websocket server, ssltunnel, process bridge)
159
+ are plain children of `runtests.py` and share mach's group. The browser and
160
+ `moz-http2` do not, which is why the teardown reap above exists alongside the
161
+ group sweep.
162
+
99
163
  ## The verdict line
100
164
 
101
165
  Every test run ends with one machine-readable line:
102
166
 
103
167
  ```
104
- FIREFORGE-VERDICT: PASS|FAIL reason=… [note=<class>] [shuffle=<seed>] [log=<path>]
168
+ FIREFORGE-VERDICT: PASS|FAIL reason=… [note=<class>] [shuffle=<seed>] [orphans-reaped=<n>] [log=<path>]
105
169
  ```
106
170
 
107
171
  Automation should branch on this line rather than on the raw process code,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hominis/fireforge",
3
- "version": "0.47.0",
3
+ "version": "0.47.1",
4
4
  "description": "FireForge — a build tool for customizing Firefox",
5
5
  "type": "module",
6
6
  "main": "./dist/src/index.js",