@hominis/fireforge 0.36.0 → 0.37.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -0
- package/dist/src/commands/build.js +1 -1
- package/dist/src/commands/doctor-orphaned-harness.d.ts +52 -0
- package/dist/src/commands/doctor-orphaned-harness.js +132 -0
- package/dist/src/commands/doctor.js +2 -0
- package/dist/src/commands/export.js +7 -0
- package/dist/src/commands/patch/staged-dependency.d.ts +1 -1
- package/dist/src/commands/patch/staged-dependency.js +113 -51
- package/dist/src/commands/re-export-files.js +4 -0
- package/dist/src/commands/re-export.js +8 -0
- package/dist/src/commands/test-diagnose.js +20 -1
- package/dist/src/commands/test.js +75 -23
- package/dist/src/core/build-baseline-types.d.ts +24 -0
- package/dist/src/core/build-baseline.d.ts +5 -2
- package/dist/src/core/build-baseline.js +5 -1
- package/dist/src/core/furnace-config-custom.js +10 -0
- package/dist/src/core/furnace-stale-export.d.ts +59 -0
- package/dist/src/core/furnace-stale-export.js +123 -0
- package/dist/src/core/furnace-validate-compatibility.js +9 -2
- package/dist/src/core/furnace-validate-structure.js +3 -2
- package/dist/src/core/mach-known-noise-filter.d.ts +60 -0
- package/dist/src/core/mach-known-noise-filter.js +193 -0
- package/dist/src/core/mach.d.ts +8 -0
- package/dist/src/core/mach.js +36 -3
- package/dist/src/core/patch-lint-cross.js +80 -2
- package/dist/src/core/patch-manifest-io.d.ts +3 -2
- package/dist/src/core/patch-manifest-io.js +27 -16
- package/dist/src/core/patch-manifest-validate.js +24 -0
- package/dist/src/core/test-harness-crash.d.ts +16 -0
- package/dist/src/core/test-harness-crash.js +55 -0
- package/dist/src/core/test-stale-check.d.ts +29 -1
- package/dist/src/core/test-stale-check.js +59 -0
- package/dist/src/types/commands/index.d.ts +1 -1
- package/dist/src/types/commands/options.d.ts +27 -4
- package/dist/src/types/commands/patches.d.ts +29 -0
- package/dist/src/types/furnace.d.ts +13 -0
- package/dist/src/utils/process-group.d.ts +33 -0
- package/dist/src/utils/process-group.js +161 -0
- package/dist/src/utils/process.d.ts +15 -0
- package/dist/src/utils/process.js +88 -44
- package/package.json +2 -2
|
@@ -3,6 +3,7 @@ import { spawn } from 'node:child_process';
|
|
|
3
3
|
import { constants as osConstants } from 'node:os';
|
|
4
4
|
import { StringDecoder } from 'node:string_decoder';
|
|
5
5
|
import { ExecTimeoutError } from '../errors/base.js';
|
|
6
|
+
import { killProcessTree, sweepProcessGroup } from './process-group.js';
|
|
6
7
|
// 50 MB cap per stream to prevent OOM — large toolchain builds (e.g. Firefox, Chromium)
|
|
7
8
|
// can easily blow past this, so we truncate rather than let the buffer grow unbounded.
|
|
8
9
|
// Callers for whom truncation is unacceptable (e.g. the archive-safety preflight
|
|
@@ -115,12 +116,15 @@ export async function exec(command, args, options = {}) {
|
|
|
115
116
|
*/
|
|
116
117
|
export async function execStream(command, args, options = {}) {
|
|
117
118
|
return new Promise((resolve, reject) => {
|
|
119
|
+
const usesProcessGroup = options.processGroup === true && process.platform !== 'win32';
|
|
118
120
|
const child = spawn(command, args, {
|
|
119
121
|
cwd: options.cwd,
|
|
120
122
|
env: { ...process.env, ...options.env },
|
|
121
123
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
122
124
|
signal: buildSignalFromTimeout(options.timeout),
|
|
125
|
+
detached: usesProcessGroup,
|
|
123
126
|
});
|
|
127
|
+
const groupPid = usesProcessGroup ? child.pid : undefined;
|
|
124
128
|
const stdoutDecoder = new StringDecoder('utf8');
|
|
125
129
|
const stderrDecoder = new StringDecoder('utf8');
|
|
126
130
|
child.stdout.on('data', (data) => {
|
|
@@ -133,11 +137,35 @@ export async function execStream(command, args, options = {}) {
|
|
|
133
137
|
if (chunk.length > 0)
|
|
134
138
|
options.onStderr?.(chunk);
|
|
135
139
|
});
|
|
140
|
+
// A detached group leader no longer receives the terminal's Ctrl+C, so
|
|
141
|
+
// the forwarder is mandatory (not just graceful-UX) when processGroup
|
|
142
|
+
// is set. Group-aware: kills route to the whole tree.
|
|
143
|
+
const forwarder = options.processGroup === true
|
|
144
|
+
? installGracefulShutdownForwarder(child, 1500, (signal) => {
|
|
145
|
+
killProcessTree(child, signal, usesProcessGroup);
|
|
146
|
+
})
|
|
147
|
+
: undefined;
|
|
136
148
|
child.on('error', (error) => {
|
|
149
|
+
// Abort/startup failure: make sure a partially-started tree does not
|
|
150
|
+
// outlive the dispatch (a mach dying at startup used to strand
|
|
151
|
+
// multiprocessing workers).
|
|
152
|
+
if (options.processGroup === true) {
|
|
153
|
+
killProcessTree(child, 'SIGKILL', usesProcessGroup);
|
|
154
|
+
}
|
|
155
|
+
forwarder?.dispose();
|
|
137
156
|
reject(toExecRejection(error, command, args, options.timeout));
|
|
138
157
|
});
|
|
139
158
|
child.on('close', (code, signal) => {
|
|
140
|
-
|
|
159
|
+
forwarder?.dispose();
|
|
160
|
+
const finish = () => {
|
|
161
|
+
resolve(exitCodeFromClose(code, signal));
|
|
162
|
+
};
|
|
163
|
+
if (groupPid !== undefined) {
|
|
164
|
+
sweepProcessGroup(groupPid).then(finish, finish);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
finish();
|
|
168
|
+
}
|
|
141
169
|
});
|
|
142
170
|
});
|
|
143
171
|
}
|
|
@@ -199,24 +227,42 @@ export async function waitForActiveChildShutdown(timeoutMs) {
|
|
|
199
227
|
*/
|
|
200
228
|
export async function execInherit(command, args, options = {}) {
|
|
201
229
|
return new Promise((resolve, reject) => {
|
|
230
|
+
const usesProcessGroup = options.processGroup === true && process.platform !== 'win32';
|
|
202
231
|
const child = spawn(command, args, {
|
|
203
232
|
cwd: options.cwd,
|
|
204
233
|
env: { ...process.env, ...options.env },
|
|
205
234
|
stdio: 'inherit',
|
|
206
235
|
signal: buildSignalFromTimeout(options.timeout),
|
|
236
|
+
detached: usesProcessGroup,
|
|
207
237
|
});
|
|
238
|
+
const groupPid = usesProcessGroup ? child.pid : undefined;
|
|
208
239
|
const graceMs = options.shutdownGraceMs ?? 1500;
|
|
209
|
-
const { dispose } = installGracefulShutdownForwarder(child, graceMs
|
|
240
|
+
const { dispose } = installGracefulShutdownForwarder(child, graceMs, options.processGroup === true
|
|
241
|
+
? (signal) => {
|
|
242
|
+
killProcessTree(child, signal, usesProcessGroup);
|
|
243
|
+
}
|
|
244
|
+
: undefined);
|
|
210
245
|
const closure = trackChildClosure();
|
|
211
246
|
child.on('error', (error) => {
|
|
247
|
+
if (options.processGroup === true) {
|
|
248
|
+
killProcessTree(child, 'SIGKILL', usesProcessGroup);
|
|
249
|
+
}
|
|
212
250
|
dispose();
|
|
213
251
|
closure.settle();
|
|
214
252
|
reject(toExecRejection(error, command, args, options.timeout));
|
|
215
253
|
});
|
|
216
254
|
child.on('close', (code, signal) => {
|
|
217
255
|
dispose();
|
|
218
|
-
|
|
219
|
-
|
|
256
|
+
const finish = () => {
|
|
257
|
+
closure.settle();
|
|
258
|
+
resolve(exitCodeFromClose(code, signal));
|
|
259
|
+
};
|
|
260
|
+
if (groupPid !== undefined) {
|
|
261
|
+
sweepProcessGroup(groupPid).then(finish, finish);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
finish();
|
|
265
|
+
}
|
|
220
266
|
});
|
|
221
267
|
});
|
|
222
268
|
}
|
|
@@ -228,14 +274,21 @@ export async function execInherit(command, args, options = {}) {
|
|
|
228
274
|
* `close` and `error` handlers so the process does not accumulate signal
|
|
229
275
|
* listeners across repeated spawns.
|
|
230
276
|
*/
|
|
231
|
-
function installGracefulShutdownForwarder(child, graceMs) {
|
|
277
|
+
function installGracefulShutdownForwarder(child, graceMs, killTarget) {
|
|
232
278
|
let graceTimer;
|
|
233
279
|
const forwarded = new Set();
|
|
280
|
+
const sendKill = (signal) => {
|
|
281
|
+
if (killTarget) {
|
|
282
|
+
killTarget(signal);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
child.kill(signal);
|
|
286
|
+
};
|
|
234
287
|
const escalate = () => {
|
|
235
288
|
if (child.exitCode !== null || child.signalCode !== null)
|
|
236
289
|
return;
|
|
237
290
|
try {
|
|
238
|
-
|
|
291
|
+
sendKill('SIGKILL');
|
|
239
292
|
}
|
|
240
293
|
catch {
|
|
241
294
|
// Child is already gone — nothing to do.
|
|
@@ -249,7 +302,7 @@ function installGracefulShutdownForwarder(child, graceMs) {
|
|
|
249
302
|
}
|
|
250
303
|
forwarded.add(signal);
|
|
251
304
|
try {
|
|
252
|
-
|
|
305
|
+
sendKill('SIGTERM');
|
|
253
306
|
}
|
|
254
307
|
catch {
|
|
255
308
|
// If the child can't accept SIGTERM (already dead), nothing to do.
|
|
@@ -286,34 +339,52 @@ function installGracefulShutdownForwarder(child, graceMs) {
|
|
|
286
339
|
*/
|
|
287
340
|
export async function execInheritCapture(command, args, options = {}) {
|
|
288
341
|
return new Promise((resolve, reject) => {
|
|
342
|
+
const usesProcessGroup = options.processGroup === true && process.platform !== 'win32';
|
|
289
343
|
const child = spawn(command, args, {
|
|
290
344
|
cwd: options.cwd,
|
|
291
345
|
env: { ...process.env, ...options.env },
|
|
292
346
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
293
347
|
signal: buildSignalFromTimeout(options.timeout),
|
|
348
|
+
detached: usesProcessGroup,
|
|
294
349
|
});
|
|
350
|
+
const groupPid = usesProcessGroup ? child.pid : undefined;
|
|
295
351
|
const out = createStreamCollector(process.stdout);
|
|
296
352
|
const err = createStreamCollector(process.stderr);
|
|
297
353
|
child.stdout.on('data', out.onData);
|
|
298
354
|
child.stderr.on('data', err.onData);
|
|
299
355
|
const graceMs = options.shutdownGraceMs ?? 1500;
|
|
300
|
-
const { dispose } = installGracefulShutdownForwarder(child, graceMs
|
|
356
|
+
const { dispose } = installGracefulShutdownForwarder(child, graceMs, options.processGroup === true
|
|
357
|
+
? (signal) => {
|
|
358
|
+
killProcessTree(child, signal, usesProcessGroup);
|
|
359
|
+
}
|
|
360
|
+
: undefined);
|
|
301
361
|
const closure = trackChildClosure();
|
|
302
362
|
child.on('error', (error) => {
|
|
363
|
+
if (options.processGroup === true) {
|
|
364
|
+
killProcessTree(child, 'SIGKILL', usesProcessGroup);
|
|
365
|
+
}
|
|
303
366
|
dispose();
|
|
304
367
|
closure.settle();
|
|
305
368
|
reject(toExecRejection(error, command, args, options.timeout));
|
|
306
369
|
});
|
|
307
370
|
child.on('close', (code, signal) => {
|
|
308
371
|
dispose();
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
372
|
+
const finish = () => {
|
|
373
|
+
closure.settle();
|
|
374
|
+
resolve({
|
|
375
|
+
stdout: out.getText(),
|
|
376
|
+
stderr: err.getText(),
|
|
377
|
+
exitCode: exitCodeFromClose(code, signal),
|
|
378
|
+
stdoutTruncated: out.wasTruncated(),
|
|
379
|
+
stderrTruncated: err.wasTruncated(),
|
|
380
|
+
});
|
|
381
|
+
};
|
|
382
|
+
if (groupPid !== undefined) {
|
|
383
|
+
sweepProcessGroup(groupPid).then(finish, finish);
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
finish();
|
|
387
|
+
}
|
|
317
388
|
});
|
|
318
389
|
});
|
|
319
390
|
}
|
|
@@ -371,34 +442,7 @@ export async function execSmokeRun(command, args, options) {
|
|
|
371
442
|
let graceTimer;
|
|
372
443
|
let signalGraceTimer;
|
|
373
444
|
const signalChildGroup = (signal) => {
|
|
374
|
-
|
|
375
|
-
return;
|
|
376
|
-
const targetPid = child.pid;
|
|
377
|
-
if (targetPid === undefined)
|
|
378
|
-
return;
|
|
379
|
-
try {
|
|
380
|
-
if (usesProcessGroup) {
|
|
381
|
-
// Negative PID routes to the process group, killing the Python
|
|
382
|
-
// wrapper, the firefox it forked, and every content process
|
|
383
|
-
// that inherited the group.
|
|
384
|
-
process.kill(-targetPid, signal);
|
|
385
|
-
}
|
|
386
|
-
else {
|
|
387
|
-
// No process group on Windows — taskkill /T walks the descendant
|
|
388
|
-
// tree instead. Always forced (/F): there is no SIGTERM analogue,
|
|
389
|
-
// so the grace window only exists on POSIX.
|
|
390
|
-
spawn('taskkill', ['/pid', String(targetPid), '/T', '/F'], {
|
|
391
|
-
stdio: 'ignore',
|
|
392
|
-
}).on('error', () => {
|
|
393
|
-
// taskkill unavailable — nothing more we can do beyond the
|
|
394
|
-
// direct-child kill below.
|
|
395
|
-
});
|
|
396
|
-
child.kill(signal);
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
catch {
|
|
400
|
-
// Already gone. Nothing to do.
|
|
401
|
-
}
|
|
445
|
+
killProcessTree(child, signal, usesProcessGroup);
|
|
402
446
|
};
|
|
403
447
|
const deadlineTimer = setTimeout(() => {
|
|
404
448
|
timedOut = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hominis/fireforge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "FireForge — a build tool for customizing Firefox",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/index.js",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"eslint-plugin-simple-import-sort": "^13.0.0",
|
|
74
74
|
"fast-check": "^4.6.0",
|
|
75
75
|
"husky": "^9.1.7",
|
|
76
|
-
"knip": "6.
|
|
76
|
+
"knip": "6.25.0",
|
|
77
77
|
"lint-staged": "^17.0.4",
|
|
78
78
|
"prettier": "3.9.4",
|
|
79
79
|
"tsx": "^4.7.0",
|