@hominis/fireforge 0.47.1 → 0.49.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 +83 -0
- package/dist/bin/fireforge.js +5 -0
- package/dist/build-info.json +4 -4
- package/dist/src/cli.js +16 -28
- package/dist/src/commands/build.js +30 -4
- package/dist/src/commands/doctor-engine-eslint.d.ts +3 -0
- package/dist/src/commands/doctor-engine-eslint.js +12 -0
- package/dist/src/commands/doctor-macos-sdk.d.ts +8 -0
- package/dist/src/commands/doctor-macos-sdk.js +158 -0
- package/dist/src/commands/doctor-source-pin.js +9 -2
- package/dist/src/commands/doctor.js +4 -0
- package/dist/src/commands/download.js +64 -3
- package/dist/src/commands/furnace/apply.js +10 -4
- package/dist/src/commands/furnace/create-templates.js +0 -3
- package/dist/src/commands/import.js +5 -2
- package/dist/src/commands/lint-per-patch.js +44 -6
- package/dist/src/commands/lint-per-run-checkjs.d.ts +3 -0
- package/dist/src/commands/lint-per-run-checkjs.js +1 -0
- package/dist/src/commands/lint.js +14 -0
- package/dist/src/commands/manifest.d.ts +1 -2
- package/dist/src/commands/patch/delete.js +18 -4
- package/dist/src/commands/re-export-lint.d.ts +17 -2
- package/dist/src/commands/re-export-lint.js +33 -4
- package/dist/src/commands/re-export-options.js +7 -0
- package/dist/src/commands/re-export-register.js +1 -0
- package/dist/src/commands/re-export-removed.d.ts +4 -0
- package/dist/src/commands/re-export-removed.js +49 -0
- package/dist/src/commands/re-export-single.js +4 -1
- package/dist/src/commands/re-export.js +8 -2
- package/dist/src/commands/rebase/confirm.d.ts +12 -1
- package/dist/src/commands/rebase/confirm.js +6 -3
- package/dist/src/commands/rebase/index.js +77 -13
- package/dist/src/commands/run.js +43 -7
- package/dist/src/commands/status.js +5 -3
- package/dist/src/commands/test-browser-preflight.d.ts +28 -0
- package/dist/src/commands/test-browser-preflight.js +181 -0
- package/dist/src/commands/test-build-phase.js +30 -7
- package/dist/src/commands/test-checkjs.d.ts +3 -0
- package/dist/src/commands/test-checkjs.js +27 -0
- package/dist/src/commands/test-diagnose.js +3 -0
- package/dist/src/commands/test-harness-env.d.ts +5 -6
- package/dist/src/commands/test-harness-env.js +27 -7
- package/dist/src/commands/test-modes.d.ts +2 -4
- package/dist/src/commands/test-register.js +11 -5
- package/dist/src/commands/test-run.d.ts +2 -0
- package/dist/src/commands/test-run.js +21 -1
- package/dist/src/commands/test-verdict.d.ts +2 -2
- package/dist/src/commands/test.js +83 -211
- package/dist/src/commands/typecheck.d.ts +5 -0
- package/dist/src/commands/typecheck.js +7 -1
- package/dist/src/core/build-baseline-types.d.ts +12 -14
- package/dist/src/core/build-baseline-validate.d.ts +3 -0
- package/dist/src/core/build-baseline-validate.js +53 -0
- package/dist/src/core/build-baseline.d.ts +4 -3
- package/dist/src/core/build-baseline.js +23 -11
- package/dist/src/core/build-prepare.d.ts +2 -0
- package/dist/src/core/build-prepare.js +67 -35
- package/dist/src/core/component-source-guard.d.ts +20 -0
- package/dist/src/core/component-source-guard.js +68 -0
- package/dist/src/core/config-paths.d.ts +1 -1
- package/dist/src/core/config-paths.js +1 -0
- package/dist/src/core/config-validate.js +7 -0
- package/dist/src/core/coverage-extend.d.ts +5 -34
- package/dist/src/core/coverage-extend.js +26 -37
- package/dist/src/core/discard-baseline.js +2 -1
- package/dist/src/core/engine-write-boundary.d.ts +5 -0
- package/dist/src/core/engine-write-boundary.js +41 -0
- package/dist/src/core/forward-registrations.d.ts +33 -0
- package/dist/src/core/forward-registrations.js +16 -0
- package/dist/src/core/furnace-apply-helpers.js +8 -2
- package/dist/src/core/furnace-apply.js +7 -0
- package/dist/src/core/furnace-validate-accessibility.js +9 -12
- package/dist/src/core/furnace-validate.js +0 -43
- package/dist/src/core/gecko-dom-types.d.ts +2 -0
- package/dist/src/core/gecko-dom-types.js +17 -0
- package/dist/src/core/git-file-ops.d.ts +21 -0
- package/dist/src/core/git-file-ops.js +75 -0
- package/dist/src/core/git-status.d.ts +14 -0
- package/dist/src/core/git-status.js +41 -2
- package/dist/src/core/lit-template-regions.d.ts +4 -0
- package/dist/src/core/lit-template-regions.js +50 -0
- package/dist/src/core/mach-build-artifacts.d.ts +6 -6
- package/dist/src/core/mach-build-artifacts.js +13 -7
- package/dist/src/core/mach-mozconfig.d.ts +2 -1
- package/dist/src/core/mach-mozconfig.js +2 -0
- package/dist/src/core/mach-resource-shim.d.ts +1 -1
- package/dist/src/core/mach-resource-shim.js +2 -0
- package/dist/src/core/marionette-port.d.ts +2 -0
- package/dist/src/core/marionette-port.js +38 -53
- package/dist/src/core/mochitest-server-port.d.ts +1 -1
- package/dist/src/core/mochitest-server-port.js +20 -17
- package/dist/src/core/objdir-maintenance.d.ts +10 -0
- package/dist/src/core/objdir-maintenance.js +151 -0
- package/dist/src/core/patch-apply-fuzz.d.ts +12 -0
- package/dist/src/core/patch-apply-fuzz.js +27 -6
- package/dist/src/core/patch-apply.d.ts +13 -0
- package/dist/src/core/patch-apply.js +27 -2
- package/dist/src/core/patch-lint-checkjs-cache.d.ts +18 -0
- package/dist/src/core/patch-lint-checkjs-cache.js +45 -0
- package/dist/src/core/patch-lint-checkjs.d.ts +6 -0
- package/dist/src/core/patch-lint-checkjs.js +10 -2
- package/dist/src/core/patch-lint-cross.d.ts +7 -1
- package/dist/src/core/patch-lint-cross.js +13 -12
- package/dist/src/core/patch-lint-forward-registration-extended.d.ts +38 -0
- package/dist/src/core/patch-lint-forward-registration-extended.js +240 -0
- package/dist/src/core/patch-lint-forward-registration.d.ts +18 -5
- package/dist/src/core/patch-lint-forward-registration.js +83 -37
- package/dist/src/core/patch-lint.d.ts +11 -0
- package/dist/src/core/patch-lint.js +20 -2
- package/dist/src/core/patch-transform.d.ts +14 -13
- package/dist/src/core/patch-transform.js +29 -17
- package/dist/src/core/preflight-wait.d.ts +2 -0
- package/dist/src/core/preflight-wait.js +24 -0
- package/dist/src/core/process-owner.d.ts +9 -0
- package/dist/src/core/process-owner.js +43 -0
- package/dist/src/core/rebase-dry-run.d.ts +48 -0
- package/dist/src/core/rebase-dry-run.js +105 -0
- package/dist/src/core/run-profile.d.ts +50 -0
- package/dist/src/core/run-profile.js +131 -0
- package/dist/src/core/status-binary.d.ts +1 -1
- package/dist/src/core/status-binary.js +3 -1
- package/dist/src/core/status-classify.js +29 -2
- package/dist/src/core/test-harness-crash.d.ts +1 -1
- package/dist/src/core/test-harness-crash.js +5 -5
- package/dist/src/core/test-host-state.d.ts +14 -0
- package/dist/src/core/test-host-state.js +80 -0
- package/dist/src/core/test-profile-files.d.ts +17 -0
- package/dist/src/core/test-profile-files.js +75 -0
- package/dist/src/core/test-stale-check.d.ts +6 -7
- package/dist/src/core/test-stale-check.js +9 -32
- package/dist/src/core/token-category.d.ts +7 -26
- package/dist/src/core/token-category.js +68 -121
- package/dist/src/core/token-inventory.js +12 -7
- package/dist/src/core/toolchain-preflight.d.ts +6 -0
- package/dist/src/core/toolchain-preflight.js +1 -1
- package/dist/src/core/typecheck.d.ts +11 -1
- package/dist/src/core/typecheck.js +86 -12
- package/dist/src/errors/rebase.d.ts +9 -0
- package/dist/src/errors/rebase.js +20 -0
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.js +2 -1
- package/dist/src/types/commands/options.d.ts +29 -3
- package/dist/src/types/config.d.ts +10 -0
- package/dist/src/utils/assert.js +2 -7
- package/dist/src/utils/child-env.d.ts +0 -3
- package/dist/src/utils/child-env.js +0 -3
- package/dist/src/utils/concurrency.d.ts +2 -2
- package/dist/src/utils/concurrency.js +15 -4
- package/dist/src/utils/fs.js +11 -1
- package/dist/src/utils/options.d.ts +16 -1
- package/dist/src/utils/options.js +5 -3
- package/dist/src/utils/paths.js +3 -2
- package/dist/src/utils/process-group.d.ts +1 -2
- package/dist/src/utils/process-group.js +1 -2
- package/dist/src/utils/process-input.d.ts +27 -0
- package/dist/src/utils/process-input.js +46 -0
- package/dist/src/utils/process.d.ts +5 -2
- package/dist/src/utils/process.js +21 -8
- package/dist/src/utils/validation.d.ts +11 -0
- package/dist/src/utils/validation.js +43 -0
- package/docs/configuration.md +82 -0
- package/docs/exit-codes.md +1 -1
- package/docs/lifecycle-invariants.md +13 -8
- package/docs/machine-output.md +36 -19
- package/docs/patch-workflow.md +50 -1
- package/docs/testing.md +77 -12
- package/package.json +10 -5
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// SPDX-License-Identifier: EUPL-1.2
|
|
2
|
+
import { tokenizer } from 'acorn';
|
|
3
|
+
/** Collects Lit html quasis, masking expressions while preserving nested templates. */
|
|
4
|
+
export function collectLitTemplateRegions(content) {
|
|
5
|
+
const regions = [];
|
|
6
|
+
const stack = [];
|
|
7
|
+
let previous = '';
|
|
8
|
+
let previousValue;
|
|
9
|
+
try {
|
|
10
|
+
for (const token of tokenizer(content, { ecmaVersion: 'latest', sourceType: 'module' })) {
|
|
11
|
+
const label = token.type.label;
|
|
12
|
+
if (label === '`') {
|
|
13
|
+
if (previous === 'template' || previous === 'invalidTemplate') {
|
|
14
|
+
const closed = stack.pop();
|
|
15
|
+
if (closed?.html)
|
|
16
|
+
regions.push(closed.text);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
stack.push({ html: previous === 'name' && previousValue === 'html', text: '' });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
else if (label === 'template') {
|
|
23
|
+
const current = stack.at(-1);
|
|
24
|
+
if (current)
|
|
25
|
+
current.text += content.slice(token.start, token.end);
|
|
26
|
+
}
|
|
27
|
+
else if (label === '${') {
|
|
28
|
+
const current = stack.at(-1);
|
|
29
|
+
if (current)
|
|
30
|
+
current.text += '${expression}';
|
|
31
|
+
}
|
|
32
|
+
previous = label;
|
|
33
|
+
previousValue = content.slice(token.start, token.end);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
// Keep completed regions from a file being edited; never re-pair backticks.
|
|
38
|
+
}
|
|
39
|
+
return regions;
|
|
40
|
+
}
|
|
41
|
+
/** Reads actual attribute names and values, skipping quoted values as indivisible tokens. */
|
|
42
|
+
export function parseTemplateAttributes(source) {
|
|
43
|
+
const attributes = new Map();
|
|
44
|
+
const pattern = /([^\s=/>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s>]+)))?/g;
|
|
45
|
+
for (const match of source.matchAll(pattern)) {
|
|
46
|
+
if (match[1])
|
|
47
|
+
attributes.set(match[1].toLowerCase(), match[2] ?? match[3] ?? match[4]);
|
|
48
|
+
}
|
|
49
|
+
return attributes;
|
|
50
|
+
}
|
|
@@ -92,6 +92,8 @@ export interface MozinfoRewriteResult {
|
|
|
92
92
|
reason?: string;
|
|
93
93
|
/** New `topsrcdir` value written to disk (populated on success). */
|
|
94
94
|
newTopsrcdir?: string;
|
|
95
|
+
/** Original source prefix checked after configure. */
|
|
96
|
+
oldTopsrcdir?: string;
|
|
95
97
|
/** New `topobjdir` value written to disk (populated on success). */
|
|
96
98
|
newTopobjdir?: string;
|
|
97
99
|
/** New `mozconfig` value written to disk (populated on success when it lived inside topsrcdir). */
|
|
@@ -100,12 +102,10 @@ export interface MozinfoRewriteResult {
|
|
|
100
102
|
/**
|
|
101
103
|
* Safe-relocation rewriter for mozinfo.json under the active obj-* tree.
|
|
102
104
|
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* reads to learn where its checkout actually lives. Once mozinfo.json
|
|
108
|
-
* agrees with the on-disk layout, `mach configure` regenerates the rest.
|
|
105
|
+
* Copied objdirs retain source symlinks, dependency metadata and tracking
|
|
106
|
+
* inventories as well as mozinfo.json. Repair them before configure so
|
|
107
|
+
* config.track cannot write nulls into the primary checkout. Configure then
|
|
108
|
+
* regenerates the root config files; callers verify old-source links too.
|
|
109
109
|
*
|
|
110
110
|
* Safety rules. The rewrite is refused when any of them are violated:
|
|
111
111
|
* - `topsrcdir` and `topobjdir` must both be present and non-empty.
|
|
@@ -9,6 +9,7 @@ import { verbose } from '../utils/logger.js';
|
|
|
9
9
|
import { getPlatform } from '../utils/platform.js';
|
|
10
10
|
import { isObject, isString } from '../utils/validation.js';
|
|
11
11
|
import { extractMozObjdirName } from './mach-mozconfig.js';
|
|
12
|
+
import { findStaleObjdirLink, relocateObjdirProducts } from './objdir-maintenance.js';
|
|
12
13
|
function validateBuildMozinfo(data) {
|
|
13
14
|
if (!isObject(data)) {
|
|
14
15
|
throw new Error('mozinfo metadata must be an object');
|
|
@@ -239,12 +240,10 @@ export function buildArtifactMismatchMessage(engineDir, buildCheck, commandName)
|
|
|
239
240
|
/**
|
|
240
241
|
* Safe-relocation rewriter for mozinfo.json under the active obj-* tree.
|
|
241
242
|
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* reads to learn where its checkout actually lives. Once mozinfo.json
|
|
247
|
-
* agrees with the on-disk layout, `mach configure` regenerates the rest.
|
|
243
|
+
* Copied objdirs retain source symlinks, dependency metadata and tracking
|
|
244
|
+
* inventories as well as mozinfo.json. Repair them before configure so
|
|
245
|
+
* config.track cannot write nulls into the primary checkout. Configure then
|
|
246
|
+
* regenerates the root config files; callers verify old-source links too.
|
|
248
247
|
*
|
|
249
248
|
* Safety rules. The rewrite is refused when any of them are violated:
|
|
250
249
|
* - `topsrcdir` and `topobjdir` must both be present and non-empty.
|
|
@@ -328,10 +327,17 @@ export async function attemptMozinfoRewrite(engineDir, objDir) {
|
|
|
328
327
|
// its mozconfig will still fail configure. The operator can re-point
|
|
329
328
|
// with `MOZCONFIG=…` or run a full clean rebuild.
|
|
330
329
|
}
|
|
330
|
+
try {
|
|
331
|
+
await relocateObjdirProducts(engineDir, objDir, oldSrcResolved);
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
return { rewritten: false, reason: `objdir relocation refused: ${toError(error).message}` };
|
|
335
|
+
}
|
|
331
336
|
await writeJson(mozinfoPath, patched);
|
|
332
337
|
return {
|
|
333
338
|
rewritten: true,
|
|
334
339
|
newTopsrcdir: newSrc,
|
|
340
|
+
oldTopsrcdir: oldSrcResolved,
|
|
335
341
|
newTopobjdir: newObj,
|
|
336
342
|
...(newMozconfig ? { newMozconfig } : {}),
|
|
337
343
|
};
|
|
@@ -426,5 +432,5 @@ export async function findObjdirRelocationViolation(args) {
|
|
|
426
432
|
return `${objDir}/${name} still contains the primary engine path ${forbidden}`;
|
|
427
433
|
}
|
|
428
434
|
}
|
|
429
|
-
return
|
|
435
|
+
return await findStaleObjdirLink(engineDir, objDir, forbidden);
|
|
430
436
|
}
|
|
@@ -52,5 +52,6 @@ export declare function assertBrandingMozconfigAgreement(engineDir: string, mozc
|
|
|
52
52
|
* @param configsDir - Path to the configs directory
|
|
53
53
|
* @param engineDir - Path to the engine directory
|
|
54
54
|
* @param config - FireForge configuration
|
|
55
|
+
* @returns Whether engine/mozconfig was rewritten (its content changed)
|
|
55
56
|
*/
|
|
56
|
-
export declare function generateMozconfig(configsDir: string, engineDir: string, config: FireForgeConfig): Promise<
|
|
57
|
+
export declare function generateMozconfig(configsDir: string, engineDir: string, config: FireForgeConfig): Promise<boolean>;
|
|
@@ -114,6 +114,7 @@ export async function assertBrandingMozconfigAgreement(engineDir, mozconfigPath,
|
|
|
114
114
|
* @param configsDir - Path to the configs directory
|
|
115
115
|
* @param engineDir - Path to the engine directory
|
|
116
116
|
* @param config - FireForge configuration
|
|
117
|
+
* @returns Whether engine/mozconfig was rewritten (its content changed)
|
|
117
118
|
*/
|
|
118
119
|
export async function generateMozconfig(configsDir, engineDir, config) {
|
|
119
120
|
const platform = getPlatform();
|
|
@@ -161,4 +162,5 @@ export async function generateMozconfig(configsDir, engineDir, config) {
|
|
|
161
162
|
// write, before anything consumes mozconfig) keeps `generateMozconfig`
|
|
162
163
|
// the single source of truth for both the render and the sanity-check.
|
|
163
164
|
await assertBrandingMozconfigAgreement(engineDir, outputPath, config);
|
|
165
|
+
return wrote;
|
|
164
166
|
}
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
* regression test (mach-resource-shim.python.test.ts), which asserts the
|
|
72
72
|
* degraded readings' runtime behavior rather than string-matching the source.
|
|
73
73
|
*/
|
|
74
|
-
export declare const GUARD_PYTHON_SOURCE = "# Generated by FireForge - do not edit.\n# Degrades the broken host resource monitor family (psutil vs Darwin) from\n# fatal startup errors into non-fatal warnings, so mach builds and tests\n# proceed. Installed into the mach virtualenv site-packages via a .pth file\n# (survives mach's venv re-exec, which drops PYTHONPATH).\nimport builtins\nimport collections\nimport inspect\nimport sys\nimport warnings\n\n# Any psutil degradation observed in this process. Once set, monitors are\n# kept inert (start never spawns the collector child) \u2014 on a flapping host a\n# healthy-looking moment must not be trusted to start a sample stream that a\n# later degraded reading could malform.\n_fireforge_host_degraded = [False]\n\n\ndef _fireforge_degraded_notice(exc):\n _fireforge_host_degraded[0] = True\n warnings.warn(\n \"FireForge: host resource monitor degraded (%s); continuing without resource monitoring.\" % exc\n )\n\n\nclass _DegradedReading(object):\n # Last-resort duck type of psutil's namedtuple results (svmem field\n # order): mozsystemmonitor subscripts, iterates, and unpacks readings,\n # so the degraded fallback must survive r[0], list(r), len(r), r._fields\n # and r._asdict(), not just attribute access. The parent also reconstructs\n # readings via type(reading)(*values), so the constructor must tolerate\n # the full positional field list. Normal\n # degraded paths use the per-function namedtuple fallbacks below; this\n # class only backstops a fallback that itself raised.\n _fields = (\"total\", \"available\", \"percent\", \"used\", \"free\", \"active\", \"inactive\", \"wired\")\n total = available = used = free = active = inactive = wired = 0\n percent = 0.0\n\n def __init__(self, *_args, **_kwargs):\n pass\n\n def __getattr__(self, _name):\n return 0\n\n def _values(self):\n return tuple(getattr(self, _field) for _field in self._fields)\n\n def __getitem__(self, index):\n return self._values()[index]\n\n def __iter__(self):\n return iter(self._values())\n\n def __len__(self):\n return len(self._fields)\n\n def _asdict(self):\n return dict(zip(self._fields, self._values()))\n\n\n# Per-function fallback result classes with the exact macOS field\n# orders/arities. Module-level on purpose: readings cross the\n# mozsystemmonitor collector pipe via pickle (by reference), and the parent\n# rebuilds each sample with type(reading)(*values) \u2014 so a swap fallback must\n# be 6-field sswap-shaped in every position. An svmem-shaped fallback there\n# makes the parent reject every sample, fill the pipe, block the collector\n# child in send(), and wedge mach's atexit join forever.\n_FallbackVmem = collections.namedtuple(\n \"_FallbackVmem\",\n (\"total\", \"available\", \"percent\", \"used\", \"free\", \"active\", \"inactive\", \"wired\"),\n)\n_FallbackSwap = collections.namedtuple(\n \"_FallbackSwap\", (\"total\", \"used\", \"free\", \"percent\", \"sin\", \"sout\")\n)\n_FallbackCpuTimes = collections.namedtuple(\"_FallbackCpuTimes\", (\"user\", \"nice\", \"system\", \"idle\"))\n_FallbackDiskIO = collections.namedtuple(\n \"_FallbackDiskIO\",\n (\"read_count\", \"write_count\", \"read_bytes\", \"write_bytes\", \"read_time\", \"write_time\"),\n)\n\n_FALLBACK_CLASSES = {\n \"virtual_memory\": _FallbackVmem,\n \"swap_memory\": _FallbackSwap,\n \"cpu_times\": _FallbackCpuTimes,\n \"disk_io_counters\": _FallbackDiskIO,\n}\n\n# psutil result namedtuple per wrapped function, resolvable via getattr from\n# the platform module / _common without invoking the failing syscall.\n_PSUTIL_RESULT_CLASSES = {\n \"virtual_memory\": \"svmem\",\n \"swap_memory\": \"sswap\",\n \"cpu_times\": \"scputimes\",\n \"disk_io_counters\": \"sdiskio\",\n}\n\n\ndef _zeroed(cls):\n return cls(*([0] * len(cls._fields)))\n\n\ndef _percpu_requested(args, kwargs, positional_index):\n if \"percpu\" in kwargs:\n return bool(kwargs[\"percpu\"])\n return len(args) > positional_index and bool(args[positional_index])\n\n\ndef _degraded_result_factory(psutil, name):\n # cpu_percent returns a plain float (or a per-CPU list) callers do\n # arithmetic on.\n if name == \"cpu_percent\":\n def _cpu_percent_fallback(*args, **kwargs):\n # cpu_percent(interval=None, percpu=False)\n if _percpu_requested(args, kwargs, 1):\n return []\n return 0.0\n\n return _cpu_percent_fallback\n # The guard-owned class is always shape-correct for this function;\n # psutil's own result namedtuple is still preferred when resolvable so\n # type() captures stay identical to real readings on a flapping host.\n _cls = _FALLBACK_CLASSES.get(name, _DegradedReading)\n _cls_name = _PSUTIL_RESULT_CLASSES.get(name)\n if _cls_name is not None:\n for _mod in (getattr(psutil, \"_psplatform\", None), getattr(psutil, \"_common\", None)):\n _real = getattr(_mod, _cls_name, None) if _mod is not None else None\n if _real is not None and getattr(_real, \"_fields\", None):\n _cls = _real\n break\n if name == \"cpu_times\":\n def _cpu_times_fallback(*args, **kwargs):\n # cpu_times(percpu=False); the collector child samples per-CPU.\n if _percpu_requested(args, kwargs, 0):\n return []\n return _zeroed(_cls)\n\n return _cpu_times_fallback\n\n def _reading_fallback(*_args, **_kwargs):\n return _zeroed(_cls)\n\n return _reading_fallback\n\n\ndef _guard_psutil():\n try:\n import psutil\n except Exception:\n return\n\n def _wrap(orig, fallback):\n def wrapper(*args, **kwargs):\n try:\n return orig(*args, **kwargs)\n except Exception as exc: # noqa: BLE001\n _fireforge_degraded_notice(exc)\n try:\n return fallback(*args, **kwargs)\n except Exception: # noqa: BLE001\n return _DegradedReading()\n\n return wrapper\n\n for _name in (\"virtual_memory\", \"swap_memory\", \"cpu_percent\", \"cpu_times\", \"disk_io_counters\"):\n _orig = getattr(psutil, _name, None)\n if _orig is not None and not getattr(_orig, \"_fireforge_guarded\", False):\n try:\n _fallback = _degraded_result_factory(psutil, _name)\n except Exception: # noqa: BLE001\n _fallback = _DegradedReading\n _wrapped = _wrap(_orig, _fallback)\n _wrapped._fireforge_guarded = True\n setattr(psutil, _name, _wrapped)\n\n\n_MONITOR_METHOD_NAMES = (\n \"start\",\n \"stop\",\n \"record_event\",\n \"record_marker\",\n \"begin_phase\",\n \"finish_phase\",\n \"aggregate_cpu_percent\",\n \"aggregate_cpu_times\",\n \"aggregate_io\",\n \"min_memory_available\",\n \"as_dict\",\n)\n\n\ndef _fireforge_stop_collector(monitor):\n # Best-effort: terminate a live collector child and drain the parent end\n # of the pipe, so a child blocked in send() on a full pipe can never\n # keep mach's atexit join (os.waitpid) waiting forever. Attribute names\n # follow upstream resourcemonitor.py (self._process / self._pipe); a\n # miss on a refactored upstream is harmless \u2014 the shape-correct\n # fallbacks above still keep the sample stream well-formed.\n try:\n proc = getattr(monitor, \"_process\", None)\n if proc is not None and getattr(proc, \"is_alive\", lambda: False)():\n proc.terminate()\n proc.join(1)\n except Exception: # noqa: BLE001\n pass\n try:\n pipe = getattr(monitor, \"_pipe\", None)\n if pipe is not None:\n while pipe.poll(0):\n pipe.recv()\n except Exception: # noqa: BLE001\n pass\n\n\ndef _monitor_per_cpu_requested(args, kwargs):\n # aggregate_cpu_percent/aggregate_cpu_times(start=None, end=None,\n # phase=None, per_cpu=True) \u2014 self already stripped from args.\n if \"per_cpu\" in kwargs:\n return bool(kwargs[\"per_cpu\"])\n if len(args) > 3:\n return bool(args[3])\n return True\n\n\ndef _monitor_degraded_result(name, args, kwargs):\n # Degraded aggregate methods return zeroed shapes, not None, where\n # callers subscript the result: mozbuild's log_resource_usage does\n # usage[\"io\"].read_bytes after a successful compile, so None there fails\n # the build with complete artifacts already on disk.\n if name == \"aggregate_io\":\n return _zeroed(_FallbackDiskIO)\n if name == \"aggregate_cpu_percent\":\n return [] if _monitor_per_cpu_requested(args, kwargs) else 0.0\n if name == \"aggregate_cpu_times\":\n return [] if _monitor_per_cpu_requested(args, kwargs) else _zeroed(_FallbackCpuTimes)\n return None\n\n\ndef _patch_monitor_class(cls):\n if cls is None or getattr(cls, \"_fireforge_guarded\", False):\n return\n orig_init = cls.__init__\n\n def guarded_init(self, *args, **kwargs):\n # Pre-populate the attributes a partially-constructed instance is\n # later polled for, so a failing __init__ can never surface\n # AttributeError: poll_interval.\n self.poll_interval = kwargs.get(\"poll_interval\") or 1.0\n self._fireforge_degraded = False\n try:\n orig_init(self, *args, **kwargs)\n except Exception as exc: # noqa: BLE001\n self._fireforge_degraded = True\n _fireforge_degraded_notice(exc)\n if _fireforge_host_degraded[0] and not self._fireforge_degraded:\n # Host already degraded once: keep the monitor inert so the\n # collector child never spawns on a flapping host.\n self._fireforge_degraded = True\n if self._fireforge_degraded:\n _fireforge_stop_collector(self)\n\n cls.__init__ = guarded_init\n\n def _wrap_method(name, orig):\n def wrapper(self, *args, **kwargs):\n if (\n name == \"start\"\n and _fireforge_host_degraded[0]\n and not getattr(self, \"_fireforge_degraded\", False)\n ):\n self._fireforge_degraded = True\n _fireforge_stop_collector(self)\n if getattr(self, \"_fireforge_degraded\", False):\n if name == \"stop\":\n _fireforge_stop_collector(self)\n return _monitor_degraded_result(name, args, kwargs)\n try:\n return orig(self, *args, **kwargs)\n except Exception as exc: # noqa: BLE001\n self._fireforge_degraded = True\n _fireforge_degraded_notice(exc)\n # A raising stop (or any degraded transition) must still\n # take the collector child down, or the parent's atexit\n # join hangs on a child blocked writing to a full pipe.\n _fireforge_stop_collector(self)\n return _monitor_degraded_result(name, args, kwargs)\n\n return wrapper\n\n def _wrap_unbound(name, func):\n # Upstream declares record_event/record_marker as @staticmethods (and\n # may declare others as @classmethods). Those take no receiver, so\n # they cannot use the per-instance degraded flag and must not have one\n # threaded in \u2014 see the descriptor dispatch below.\n def wrapper(*args, **kwargs):\n if _fireforge_host_degraded[0]:\n return _monitor_degraded_result(name, args, kwargs)\n try:\n return func(*args, **kwargs)\n except Exception as exc: # noqa: BLE001\n _fireforge_degraded_notice(exc)\n return _monitor_degraded_result(name, args, kwargs)\n\n return wrapper\n\n for _name in _MONITOR_METHOD_NAMES:\n # getattr_static returns the raw class attribute, so a staticmethod\n # is still a staticmethod object here. Plain getattr would hand back\n # the underlying function, and re-setting THAT on the class rebinds\n # it as an instance method: the receiver is then passed through as an\n # extra leading positional and every call raises\n # \"takes N positional arguments but N+1 were given\". Re-apply the\n # descriptor the attribute actually had.\n try:\n _raw = inspect.getattr_static(cls, _name)\n except AttributeError:\n continue\n if isinstance(_raw, staticmethod):\n setattr(cls, _name, staticmethod(_wrap_unbound(_name, _raw.__func__)))\n continue\n if isinstance(_raw, classmethod):\n setattr(cls, _name, classmethod(_wrap_unbound(_name, _raw.__func__)))\n continue\n _orig = getattr(cls, _name, None)\n if _orig is not None:\n setattr(cls, _name, _wrap_method(_name, _orig))\n cls._fireforge_guarded = True\n\n\ndef _patch_build_monitor_class(cls):\n # mozbuild's BuildMonitor is not a SystemResourceMonitor; only\n # log_resource_usage needs guarding \u2014 end-of-build resource reporting\n # must never fail a build whose artifacts are complete. No __init__\n # replacement, no degraded-instance gating.\n if cls is None or getattr(cls, \"_fireforge_guarded\", False):\n return\n _orig = getattr(cls, \"log_resource_usage\", None)\n if _orig is not None:\n def _guarded_log_resource_usage(self, *args, **kwargs):\n try:\n return _orig(self, *args, **kwargs)\n except Exception as exc: # noqa: BLE001\n _fireforge_degraded_notice(exc)\n return None\n\n cls.log_resource_usage = _guarded_log_resource_usage\n cls._fireforge_guarded = True\n\n\n_MONITOR_CLASS_PATCHES = (\n (\"mozsystemmonitor.resourcemonitor\", \"SystemResourceMonitor\", _patch_monitor_class),\n (\"mozbuild.resources\", \"SystemResourceMonitor\", _patch_monitor_class),\n (\"mozbuild.controller.building\", \"BuildMonitor\", _patch_build_monitor_class),\n)\n\n\ndef _patch_loaded_monitor_modules():\n for _mod_name, _cls_name, _patcher in _MONITOR_CLASS_PATCHES:\n _mod = sys.modules.get(_mod_name)\n if _mod is not None:\n try:\n _patcher(getattr(_mod, _cls_name, None))\n except Exception: # noqa: BLE001\n pass\n\n\ndef _install_import_hook():\n _orig_import = builtins.__import__\n if getattr(_orig_import, \"_fireforge_guarded\", False):\n return\n\n def _guarding_import(name, *args, **kwargs):\n _module = _orig_import(name, *args, **kwargs)\n try:\n _patch_loaded_monitor_modules()\n except Exception: # noqa: BLE001\n pass\n return _module\n\n _guarding_import._fireforge_guarded = True\n builtins.__import__ = _guarding_import\n\n\ntry:\n _guard_psutil()\n _patch_loaded_monitor_modules()\n _install_import_hook()\nexcept Exception: # noqa: BLE001\n pass\n";
|
|
74
|
+
export declare const GUARD_PYTHON_SOURCE = "# Generated by FireForge - do not edit.\n# Degrades the broken host resource monitor family (psutil vs Darwin) from\n# fatal startup errors into non-fatal warnings, so mach builds and tests\n# proceed. Installed into the mach virtualenv site-packages via a .pth file\n# (survives mach's venv re-exec, which drops PYTHONPATH).\nimport builtins\nimport collections\nimport inspect\nimport sys\nimport warnings\n\nimport os\nimport shutil\n_fireforge_profile_root = os.environ.get(\"FIREFORGE_PROFILE_MERGE_ROOT\")\nif _fireforge_profile_root and not getattr(shutil.copytree, \"_fireforge_profile_merge\", False):\n _fireforge_copytree = shutil.copytree\n def _fireforge_merge_copytree(src, dst, *args, **kwargs):\n if os.path.realpath(src).startswith(os.path.realpath(_fireforge_profile_root) + os.sep):\n # Python's recursive calls may supply dirs_exist_ok positionally.\n if len(args) >= 5:\n args = args[:4] + (True,) + args[5:]\n else:\n kwargs[\"dirs_exist_ok\"] = True\n return _fireforge_copytree(src, dst, *args, **kwargs)\n _fireforge_merge_copytree._fireforge_profile_merge = True\n shutil.copytree = _fireforge_merge_copytree\n\n\n# Any psutil degradation observed in this process. Once set, monitors are\n# kept inert (start never spawns the collector child) \u2014 on a flapping host a\n# healthy-looking moment must not be trusted to start a sample stream that a\n# later degraded reading could malform.\n_fireforge_host_degraded = [False]\n\n\ndef _fireforge_degraded_notice(exc):\n _fireforge_host_degraded[0] = True\n warnings.warn(\n \"FireForge: host resource monitor degraded (%s); continuing without resource monitoring.\" % exc\n )\n\n\nclass _DegradedReading(object):\n # Last-resort duck type of psutil's namedtuple results (svmem field\n # order): mozsystemmonitor subscripts, iterates, and unpacks readings,\n # so the degraded fallback must survive r[0], list(r), len(r), r._fields\n # and r._asdict(), not just attribute access. The parent also reconstructs\n # readings via type(reading)(*values), so the constructor must tolerate\n # the full positional field list. Normal\n # degraded paths use the per-function namedtuple fallbacks below; this\n # class only backstops a fallback that itself raised.\n _fields = (\"total\", \"available\", \"percent\", \"used\", \"free\", \"active\", \"inactive\", \"wired\")\n total = available = used = free = active = inactive = wired = 0\n percent = 0.0\n\n def __init__(self, *_args, **_kwargs):\n pass\n\n def __getattr__(self, _name):\n return 0\n\n def _values(self):\n return tuple(getattr(self, _field) for _field in self._fields)\n\n def __getitem__(self, index):\n return self._values()[index]\n\n def __iter__(self):\n return iter(self._values())\n\n def __len__(self):\n return len(self._fields)\n\n def _asdict(self):\n return dict(zip(self._fields, self._values()))\n\n\n# Per-function fallback result classes with the exact macOS field\n# orders/arities. Module-level on purpose: readings cross the\n# mozsystemmonitor collector pipe via pickle (by reference), and the parent\n# rebuilds each sample with type(reading)(*values) \u2014 so a swap fallback must\n# be 6-field sswap-shaped in every position. An svmem-shaped fallback there\n# makes the parent reject every sample, fill the pipe, block the collector\n# child in send(), and wedge mach's atexit join forever.\n_FallbackVmem = collections.namedtuple(\n \"_FallbackVmem\",\n (\"total\", \"available\", \"percent\", \"used\", \"free\", \"active\", \"inactive\", \"wired\"),\n)\n_FallbackSwap = collections.namedtuple(\n \"_FallbackSwap\", (\"total\", \"used\", \"free\", \"percent\", \"sin\", \"sout\")\n)\n_FallbackCpuTimes = collections.namedtuple(\"_FallbackCpuTimes\", (\"user\", \"nice\", \"system\", \"idle\"))\n_FallbackDiskIO = collections.namedtuple(\n \"_FallbackDiskIO\",\n (\"read_count\", \"write_count\", \"read_bytes\", \"write_bytes\", \"read_time\", \"write_time\"),\n)\n\n_FALLBACK_CLASSES = {\n \"virtual_memory\": _FallbackVmem,\n \"swap_memory\": _FallbackSwap,\n \"cpu_times\": _FallbackCpuTimes,\n \"disk_io_counters\": _FallbackDiskIO,\n}\n\n# psutil result namedtuple per wrapped function, resolvable via getattr from\n# the platform module / _common without invoking the failing syscall.\n_PSUTIL_RESULT_CLASSES = {\n \"virtual_memory\": \"svmem\",\n \"swap_memory\": \"sswap\",\n \"cpu_times\": \"scputimes\",\n \"disk_io_counters\": \"sdiskio\",\n}\n\n\ndef _zeroed(cls):\n return cls(*([0] * len(cls._fields)))\n\n\ndef _percpu_requested(args, kwargs, positional_index):\n if \"percpu\" in kwargs:\n return bool(kwargs[\"percpu\"])\n return len(args) > positional_index and bool(args[positional_index])\n\n\ndef _degraded_result_factory(psutil, name):\n # cpu_percent returns a plain float (or a per-CPU list) callers do\n # arithmetic on.\n if name == \"cpu_percent\":\n def _cpu_percent_fallback(*args, **kwargs):\n # cpu_percent(interval=None, percpu=False)\n if _percpu_requested(args, kwargs, 1):\n return []\n return 0.0\n\n return _cpu_percent_fallback\n # The guard-owned class is always shape-correct for this function;\n # psutil's own result namedtuple is still preferred when resolvable so\n # type() captures stay identical to real readings on a flapping host.\n _cls = _FALLBACK_CLASSES.get(name, _DegradedReading)\n _cls_name = _PSUTIL_RESULT_CLASSES.get(name)\n if _cls_name is not None:\n for _mod in (getattr(psutil, \"_psplatform\", None), getattr(psutil, \"_common\", None)):\n _real = getattr(_mod, _cls_name, None) if _mod is not None else None\n if _real is not None and getattr(_real, \"_fields\", None):\n _cls = _real\n break\n if name == \"cpu_times\":\n def _cpu_times_fallback(*args, **kwargs):\n # cpu_times(percpu=False); the collector child samples per-CPU.\n if _percpu_requested(args, kwargs, 0):\n return []\n return _zeroed(_cls)\n\n return _cpu_times_fallback\n\n def _reading_fallback(*_args, **_kwargs):\n return _zeroed(_cls)\n\n return _reading_fallback\n\n\ndef _guard_psutil():\n try:\n import psutil\n except Exception:\n return\n\n def _wrap(orig, fallback):\n def wrapper(*args, **kwargs):\n try:\n return orig(*args, **kwargs)\n except Exception as exc: # noqa: BLE001\n _fireforge_degraded_notice(exc)\n try:\n return fallback(*args, **kwargs)\n except Exception: # noqa: BLE001\n return _DegradedReading()\n\n return wrapper\n\n for _name in (\"virtual_memory\", \"swap_memory\", \"cpu_percent\", \"cpu_times\", \"disk_io_counters\"):\n _orig = getattr(psutil, _name, None)\n if _orig is not None and not getattr(_orig, \"_fireforge_guarded\", False):\n try:\n _fallback = _degraded_result_factory(psutil, _name)\n except Exception: # noqa: BLE001\n _fallback = _DegradedReading\n _wrapped = _wrap(_orig, _fallback)\n _wrapped._fireforge_guarded = True\n setattr(psutil, _name, _wrapped)\n\n\n_MONITOR_METHOD_NAMES = (\n \"start\",\n \"stop\",\n \"record_event\",\n \"record_marker\",\n \"begin_phase\",\n \"finish_phase\",\n \"aggregate_cpu_percent\",\n \"aggregate_cpu_times\",\n \"aggregate_io\",\n \"min_memory_available\",\n \"as_dict\",\n)\n\n\ndef _fireforge_stop_collector(monitor):\n # Best-effort: terminate a live collector child and drain the parent end\n # of the pipe, so a child blocked in send() on a full pipe can never\n # keep mach's atexit join (os.waitpid) waiting forever. Attribute names\n # follow upstream resourcemonitor.py (self._process / self._pipe); a\n # miss on a refactored upstream is harmless \u2014 the shape-correct\n # fallbacks above still keep the sample stream well-formed.\n try:\n proc = getattr(monitor, \"_process\", None)\n if proc is not None and getattr(proc, \"is_alive\", lambda: False)():\n proc.terminate()\n proc.join(1)\n except Exception: # noqa: BLE001\n pass\n try:\n pipe = getattr(monitor, \"_pipe\", None)\n if pipe is not None:\n while pipe.poll(0):\n pipe.recv()\n except Exception: # noqa: BLE001\n pass\n\n\ndef _monitor_per_cpu_requested(args, kwargs):\n # aggregate_cpu_percent/aggregate_cpu_times(start=None, end=None,\n # phase=None, per_cpu=True) \u2014 self already stripped from args.\n if \"per_cpu\" in kwargs:\n return bool(kwargs[\"per_cpu\"])\n if len(args) > 3:\n return bool(args[3])\n return True\n\n\ndef _monitor_degraded_result(name, args, kwargs):\n # Degraded aggregate methods return zeroed shapes, not None, where\n # callers subscript the result: mozbuild's log_resource_usage does\n # usage[\"io\"].read_bytes after a successful compile, so None there fails\n # the build with complete artifacts already on disk.\n if name == \"aggregate_io\":\n return _zeroed(_FallbackDiskIO)\n if name == \"aggregate_cpu_percent\":\n return [] if _monitor_per_cpu_requested(args, kwargs) else 0.0\n if name == \"aggregate_cpu_times\":\n return [] if _monitor_per_cpu_requested(args, kwargs) else _zeroed(_FallbackCpuTimes)\n return None\n\n\ndef _patch_monitor_class(cls):\n if cls is None or getattr(cls, \"_fireforge_guarded\", False):\n return\n orig_init = cls.__init__\n\n def guarded_init(self, *args, **kwargs):\n # Pre-populate the attributes a partially-constructed instance is\n # later polled for, so a failing __init__ can never surface\n # AttributeError: poll_interval.\n self.poll_interval = kwargs.get(\"poll_interval\") or 1.0\n self._fireforge_degraded = False\n try:\n orig_init(self, *args, **kwargs)\n except Exception as exc: # noqa: BLE001\n self._fireforge_degraded = True\n _fireforge_degraded_notice(exc)\n if _fireforge_host_degraded[0] and not self._fireforge_degraded:\n # Host already degraded once: keep the monitor inert so the\n # collector child never spawns on a flapping host.\n self._fireforge_degraded = True\n if self._fireforge_degraded:\n _fireforge_stop_collector(self)\n\n cls.__init__ = guarded_init\n\n def _wrap_method(name, orig):\n def wrapper(self, *args, **kwargs):\n if (\n name == \"start\"\n and _fireforge_host_degraded[0]\n and not getattr(self, \"_fireforge_degraded\", False)\n ):\n self._fireforge_degraded = True\n _fireforge_stop_collector(self)\n if getattr(self, \"_fireforge_degraded\", False):\n if name == \"stop\":\n _fireforge_stop_collector(self)\n return _monitor_degraded_result(name, args, kwargs)\n try:\n return orig(self, *args, **kwargs)\n except Exception as exc: # noqa: BLE001\n self._fireforge_degraded = True\n _fireforge_degraded_notice(exc)\n # A raising stop (or any degraded transition) must still\n # take the collector child down, or the parent's atexit\n # join hangs on a child blocked writing to a full pipe.\n _fireforge_stop_collector(self)\n return _monitor_degraded_result(name, args, kwargs)\n\n return wrapper\n\n def _wrap_unbound(name, func):\n # Upstream declares record_event/record_marker as @staticmethods (and\n # may declare others as @classmethods). Those take no receiver, so\n # they cannot use the per-instance degraded flag and must not have one\n # threaded in \u2014 see the descriptor dispatch below.\n def wrapper(*args, **kwargs):\n if _fireforge_host_degraded[0]:\n return _monitor_degraded_result(name, args, kwargs)\n try:\n return func(*args, **kwargs)\n except Exception as exc: # noqa: BLE001\n _fireforge_degraded_notice(exc)\n return _monitor_degraded_result(name, args, kwargs)\n\n return wrapper\n\n for _name in _MONITOR_METHOD_NAMES:\n # getattr_static returns the raw class attribute, so a staticmethod\n # is still a staticmethod object here. Plain getattr would hand back\n # the underlying function, and re-setting THAT on the class rebinds\n # it as an instance method: the receiver is then passed through as an\n # extra leading positional and every call raises\n # \"takes N positional arguments but N+1 were given\". Re-apply the\n # descriptor the attribute actually had.\n try:\n _raw = inspect.getattr_static(cls, _name)\n except AttributeError:\n continue\n if isinstance(_raw, staticmethod):\n setattr(cls, _name, staticmethod(_wrap_unbound(_name, _raw.__func__)))\n continue\n if isinstance(_raw, classmethod):\n setattr(cls, _name, classmethod(_wrap_unbound(_name, _raw.__func__)))\n continue\n _orig = getattr(cls, _name, None)\n if _orig is not None:\n setattr(cls, _name, _wrap_method(_name, _orig))\n cls._fireforge_guarded = True\n\n\ndef _patch_build_monitor_class(cls):\n # mozbuild's BuildMonitor is not a SystemResourceMonitor; only\n # log_resource_usage needs guarding \u2014 end-of-build resource reporting\n # must never fail a build whose artifacts are complete. No __init__\n # replacement, no degraded-instance gating.\n if cls is None or getattr(cls, \"_fireforge_guarded\", False):\n return\n _orig = getattr(cls, \"log_resource_usage\", None)\n if _orig is not None:\n def _guarded_log_resource_usage(self, *args, **kwargs):\n try:\n return _orig(self, *args, **kwargs)\n except Exception as exc: # noqa: BLE001\n _fireforge_degraded_notice(exc)\n return None\n\n cls.log_resource_usage = _guarded_log_resource_usage\n cls._fireforge_guarded = True\n\n\n_MONITOR_CLASS_PATCHES = (\n (\"mozsystemmonitor.resourcemonitor\", \"SystemResourceMonitor\", _patch_monitor_class),\n (\"mozbuild.resources\", \"SystemResourceMonitor\", _patch_monitor_class),\n (\"mozbuild.controller.building\", \"BuildMonitor\", _patch_build_monitor_class),\n)\n\n\ndef _patch_loaded_monitor_modules():\n for _mod_name, _cls_name, _patcher in _MONITOR_CLASS_PATCHES:\n _mod = sys.modules.get(_mod_name)\n if _mod is not None:\n try:\n _patcher(getattr(_mod, _cls_name, None))\n except Exception: # noqa: BLE001\n pass\n\n\ndef _install_import_hook():\n _orig_import = builtins.__import__\n if getattr(_orig_import, \"_fireforge_guarded\", False):\n return\n\n def _guarding_import(name, *args, **kwargs):\n _module = _orig_import(name, *args, **kwargs)\n try:\n _patch_loaded_monitor_modules()\n except Exception: # noqa: BLE001\n pass\n return _module\n\n _guarding_import._fireforge_guarded = True\n builtins.__import__ = _guarding_import\n\n\ntry:\n _guard_psutil()\n _patch_loaded_monitor_modules()\n _install_import_hook()\nexcept Exception: # noqa: BLE001\n pass\n";
|
|
75
75
|
/** Result of installing the mach resource guard before a dispatch. */
|
|
76
76
|
export interface MachResourceGuardInstallation {
|
|
77
77
|
/**
|
|
@@ -70,6 +70,7 @@ import { BuildError } from '../errors/build.js';
|
|
|
70
70
|
import { toError } from '../utils/errors.js';
|
|
71
71
|
import { pathExists, writeText } from '../utils/fs.js';
|
|
72
72
|
import { verbose } from '../utils/logger.js';
|
|
73
|
+
import { PROFILE_MERGE_PYTHON } from './test-profile-files.js';
|
|
73
74
|
/**
|
|
74
75
|
* Prefix of the per-user directory (under the OS temp dir) holding the
|
|
75
76
|
* PYTHONPATH fallback. The uid (or, on Windows, the username) is appended:
|
|
@@ -99,6 +100,7 @@ import collections
|
|
|
99
100
|
import inspect
|
|
100
101
|
import sys
|
|
101
102
|
import warnings
|
|
103
|
+
${PROFILE_MERGE_PYTHON}
|
|
102
104
|
|
|
103
105
|
# Any psutil degradation observed in this process. Once set, monitors are
|
|
104
106
|
# kept inert (start never spawns the collector child) — on a flapping host a
|
|
@@ -38,6 +38,8 @@ export interface RunningBundleProcess {
|
|
|
38
38
|
* exactly the wrong attribution for a peer session's long-lived browser.
|
|
39
39
|
*/
|
|
40
40
|
elapsedSeconds: number;
|
|
41
|
+
parentPid?: number;
|
|
42
|
+
owner?: string;
|
|
41
43
|
}
|
|
42
44
|
/**
|
|
43
45
|
* Pure parser kept exported so platform output handling is
|
|
@@ -30,6 +30,8 @@ import { toError } from '../utils/errors.js';
|
|
|
30
30
|
import { getPlatform } from '../utils/platform.js';
|
|
31
31
|
import { exec } from '../utils/process.js';
|
|
32
32
|
import { formatPsDuration, parsePsDuration } from '../utils/ps-duration.js';
|
|
33
|
+
import { waitForPreflight } from './preflight-wait.js';
|
|
34
|
+
import { isOrphanedHarness, isStillOrphanedProcess, readProcessOwner } from './process-owner.js';
|
|
33
35
|
/** Default Marionette control port set by `-marionette`. */
|
|
34
36
|
export const DEFAULT_MARIONETTE_PORT = 2828;
|
|
35
37
|
/** Basenames of browser binaries that ship a Marionette listener. */
|
|
@@ -98,7 +100,9 @@ export function parseProcessList(stdout, launchableBinary) {
|
|
|
98
100
|
const commandLine = Number.isNaN(elapsedSeconds)
|
|
99
101
|
? `${maybeElapsed} ${match[3] ?? ''}`.trim()
|
|
100
102
|
: (match[3] ?? '');
|
|
101
|
-
if (!commandLine
|
|
103
|
+
if (!(commandLine === launchableBinary ||
|
|
104
|
+
commandLine.startsWith(launchableBinary + ' ') ||
|
|
105
|
+
commandLine.startsWith('"' + launchableBinary + '" ')))
|
|
102
106
|
continue;
|
|
103
107
|
matches.push({ pid, commandLine, elapsedSeconds });
|
|
104
108
|
}
|
|
@@ -118,40 +122,24 @@ export async function ensureLaunchableBrowserNotRunning(launchableBinary, option
|
|
|
118
122
|
const holder = parent ?? processes[0];
|
|
119
123
|
if (!holder)
|
|
120
124
|
return;
|
|
121
|
-
|
|
125
|
+
const ownership = await readProcessOwner(holder.pid);
|
|
126
|
+
if (ownership)
|
|
127
|
+
Object.assign(holder, ownership);
|
|
128
|
+
const orphan = isOrphanedHarness(holder.commandLine, holder.parentPid);
|
|
129
|
+
if (options.killStaleBrowser && orphan) {
|
|
130
|
+
if (!(await isStillOrphanedProcess(holder.pid, holder.commandLine))) {
|
|
131
|
+
throw new PreflightRefusalError(`Browser PID ${holder.pid} changed identity or ownership; re-run the preflight before requesting cleanup.`, 'browser-busy');
|
|
132
|
+
}
|
|
122
133
|
try {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
'-NoProfile',
|
|
126
|
-
'-NonInteractive',
|
|
127
|
-
'-Command',
|
|
128
|
-
`Stop-Process -Id ${holder.pid} -Force`,
|
|
129
|
-
]);
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
process.kill(holder.pid, 'SIGTERM');
|
|
133
|
-
}
|
|
134
|
+
process.kill(holder.pid, 'SIGTERM');
|
|
135
|
+
await waitForPreflight(() => ensureLaunchableBrowserNotRunning(launchableBinary), 2);
|
|
134
136
|
return;
|
|
135
137
|
}
|
|
136
138
|
catch (error) {
|
|
137
139
|
throw new PreflightRefusalError(`A browser from this objdir is still running (PID ${holder.pid}), but FireForge could not terminate it: ${toError(error).message}`, 'stale-browser-kill-failed');
|
|
138
140
|
}
|
|
139
141
|
}
|
|
140
|
-
throw new PreflightRefusalError(describeRunningBundleRefusal(holder), 'stale-browser');
|
|
141
|
-
}
|
|
142
|
-
/**
|
|
143
|
-
* True when the process was launched by a test harness, i.e. its command
|
|
144
|
-
* line carries `-marionette` (the control channel every `fireforge test`
|
|
145
|
-
* browser is started with) or a harness `-profile`.
|
|
146
|
-
*
|
|
147
|
-
* This is the distinction the refusal turns on. The objdir-bundle probe
|
|
148
|
-
* matches any process running this project's binary, which on a shared
|
|
149
|
-
* checkout includes a peer session's live browser and a developer's own
|
|
150
|
-
* interactive one. Only a marionette-driven browser is safely
|
|
151
|
-
* attributable to an interrupted run.
|
|
152
|
-
*/
|
|
153
|
-
function isHarnessDrivenBrowser(commandLine) {
|
|
154
|
-
return /\s-marionette(?:\s|$)/.test(commandLine) || /\s-profile(?:\s|=)/.test(commandLine);
|
|
142
|
+
throw new PreflightRefusalError(describeRunningBundleRefusal(holder), orphan ? 'stale-browser' : 'browser-busy');
|
|
155
143
|
}
|
|
156
144
|
/**
|
|
157
145
|
* Builds the refusal for a running objdir browser.
|
|
@@ -177,9 +165,10 @@ export function describeRunningBundleRefusal(holder) {
|
|
|
177
165
|
: `kill ${holder.pid} # or "kill -9 ${holder.pid}" if it doesn't exit`;
|
|
178
166
|
const elapsed = formatPsDuration(holder.elapsedSeconds);
|
|
179
167
|
const age = elapsed !== undefined ? ` running for ${elapsed}` : '';
|
|
180
|
-
const harnessDriven =
|
|
168
|
+
const harnessDriven = isOrphanedHarness(holder.commandLine, holder.parentPid);
|
|
181
169
|
const evidence = `A browser from this project's objdir is already running (PID ${holder.pid}${age}).\n` +
|
|
182
|
-
` command: ${holder.commandLine}\n
|
|
170
|
+
` command: ${holder.commandLine}\n` +
|
|
171
|
+
` owner: parent ${holder.parentPid ?? 'unknown'} (${holder.owner ?? 'unavailable'})\n`;
|
|
183
172
|
if (harnessDriven) {
|
|
184
173
|
return (evidence +
|
|
185
174
|
' This one carries harness arguments (-marionette/-profile), so it is very likely an orphan ' +
|
|
@@ -187,11 +176,8 @@ export function describeRunningBundleRefusal(holder) {
|
|
|
187
176
|
` Stop it with "${killHint}", or retry with "--kill-stale-marionette".`);
|
|
188
177
|
}
|
|
189
178
|
return (evidence +
|
|
190
|
-
' This
|
|
191
|
-
|
|
192
|
-
' "--kill-stale-marionette" is deliberately NOT suggested here: FireForge cannot tell an orphan ' +
|
|
193
|
-
"from someone else's live browser. Confirm the owner (start time above, and the command line), " +
|
|
194
|
-
`then stop it yourself with "${killHint}" if it is in fact yours.`);
|
|
179
|
+
' This browser is busy; its ownership does not prove a harness orphan. ' +
|
|
180
|
+
'FireForge will not terminate it. Use --wait-browser [seconds] to queue behind the owner.');
|
|
195
181
|
}
|
|
196
182
|
/**
|
|
197
183
|
* Returns `true` when the holder's command basename or command-line
|
|
@@ -347,14 +333,14 @@ export async function assertMarionettePortAvailable(port = DEFAULT_MARIONETTE_PO
|
|
|
347
333
|
return;
|
|
348
334
|
const holder = probe.holder;
|
|
349
335
|
if (isBrowserMarionettePortHolder(holder, options.binaryName)) {
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
: `kill ${holder.pid} # or "kill -9 ${holder.pid}" if it doesn't exit`;
|
|
336
|
+
const owner = await readProcessOwner(holder.pid);
|
|
337
|
+
const orphan = isOrphanedHarness(holder.commandLine, owner?.parentPid);
|
|
353
338
|
throw new PreflightRefusalError(`Marionette port ${port} is already in use by ${holder.command} (PID ${holder.pid}). ` +
|
|
354
|
-
`
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
339
|
+
`Owner: parent ${owner?.parentPid ?? 'unknown'} (${owner?.owner ?? 'unavailable'}). ` +
|
|
340
|
+
(orphan
|
|
341
|
+
? 'The harness parent exited; retry with --kill-stale-marionette to reap this orphan. '
|
|
342
|
+
: 'A live peer may own it; FireForge will not suggest terminating it. ') +
|
|
343
|
+
'Use --wait-port [seconds] to queue, or --marionette-port <port> to use a different port.', 'marionette-port-busy');
|
|
358
344
|
}
|
|
359
345
|
// Non-browser holder: mach test will still fail to bind, but the
|
|
360
346
|
// cause is not a stale FireForge-launched browser. Flag it
|
|
@@ -377,23 +363,22 @@ export async function ensureMarionettePortAvailable(port = DEFAULT_MARIONETTE_PO
|
|
|
377
363
|
await assertMarionettePortAvailable(port, options);
|
|
378
364
|
return;
|
|
379
365
|
}
|
|
366
|
+
const owner = await readProcessOwner(holder.pid);
|
|
367
|
+
if (!isOrphanedHarness(holder.commandLine, owner?.parentPid)) {
|
|
368
|
+
await assertMarionettePortAvailable(port, options);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (!(await isStillOrphanedProcess(holder.pid, holder.commandLine))) {
|
|
372
|
+
throw new PreflightRefusalError(`Marionette listener PID ${holder.pid} changed identity or ownership; re-run the preflight before requesting cleanup.`, 'marionette-port-busy');
|
|
373
|
+
}
|
|
380
374
|
try {
|
|
381
|
-
|
|
382
|
-
await exec('powershell.exe', [
|
|
383
|
-
'-NoProfile',
|
|
384
|
-
'-NonInteractive',
|
|
385
|
-
'-Command',
|
|
386
|
-
`Stop-Process -Id ${holder.pid} -Force`,
|
|
387
|
-
]);
|
|
388
|
-
}
|
|
389
|
-
else {
|
|
390
|
-
process.kill(holder.pid, 'SIGTERM');
|
|
391
|
-
}
|
|
375
|
+
process.kill(holder.pid, 'SIGTERM');
|
|
392
376
|
}
|
|
393
377
|
catch (error) {
|
|
394
378
|
throw new PreflightRefusalError(`Marionette port ${port} is held by stale browser ${holder.command} (PID ${holder.pid}), ` +
|
|
395
379
|
`but FireForge could not terminate it: ${toError(error).message}`, 'stale-browser-kill-failed');
|
|
396
380
|
}
|
|
381
|
+
await waitForPreflight(() => assertMarionettePortAvailable(port, options), 2);
|
|
397
382
|
}
|
|
398
383
|
/**
|
|
399
384
|
* Extracts a `--marionette-port=N` (or `--marionette-port N`) argument from
|
|
@@ -78,4 +78,4 @@ export declare function ensureMochitestServerPortAvailable(port: number | undefi
|
|
|
78
78
|
* @param holder - The listener found on it
|
|
79
79
|
* @param holderClass - Whose harness `server.js` it is, if it is one at all
|
|
80
80
|
*/
|
|
81
|
-
export declare function describeMochitestServerRefusal(port: number, holder: MarionettePortHolder, holderClass: MochitestServerHolderClass): string;
|
|
81
|
+
export declare function describeMochitestServerRefusal(port: number, holder: MarionettePortHolder, holderClass: MochitestServerHolderClass, parentPid?: number): string;
|
|
@@ -22,10 +22,11 @@
|
|
|
22
22
|
* the missing half, and it reuses {@link probeMarionettePort}, which is
|
|
23
23
|
* port-generic, rather than growing a second probe.
|
|
24
24
|
*/
|
|
25
|
-
import {
|
|
25
|
+
import { PreflightRefusalError } from '../errors/base.js';
|
|
26
26
|
import { isExplicitAbsolutePath, isPathInsideRoot } from '../utils/paths.js';
|
|
27
|
-
import { exec } from '../utils/process.js';
|
|
28
27
|
import { probeMarionettePort } from './marionette-port.js';
|
|
28
|
+
import { waitForPreflight } from './preflight-wait.js';
|
|
29
|
+
import { isStillOrphanedProcess, readProcessOwner } from './process-owner.js';
|
|
29
30
|
/** Default port the mochitest harness binds its httpd to. */
|
|
30
31
|
export const DEFAULT_MOCHITEST_SERVER_PORT = 8888;
|
|
31
32
|
/**
|
|
@@ -109,19 +110,17 @@ export async function ensureMochitestServerPortAvailable(port = DEFAULT_MOCHITES
|
|
|
109
110
|
return;
|
|
110
111
|
const holder = probe.holder;
|
|
111
112
|
const holderClass = classifyMochitestServerHolder(holder, options.engineDir);
|
|
112
|
-
|
|
113
|
+
const owner = await readProcessOwner(holder.pid);
|
|
114
|
+
if (holderClass === 'this-checkout' &&
|
|
115
|
+
owner?.parentPid === 1 &&
|
|
116
|
+
options.killStaleServer === true &&
|
|
117
|
+
(await isStillOrphanedProcess(holder.pid, holder.commandLine))) {
|
|
113
118
|
try {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
'-
|
|
118
|
-
|
|
119
|
-
`Stop-Process -Id ${holder.pid} -Force`,
|
|
120
|
-
]);
|
|
121
|
-
}
|
|
122
|
-
else {
|
|
123
|
-
process.kill(holder.pid, 'SIGTERM');
|
|
124
|
-
}
|
|
119
|
+
process.kill(holder.pid, 'SIGTERM');
|
|
120
|
+
await waitForPreflight(async () => {
|
|
121
|
+
if ((await probeMarionettePort(port)).inUse)
|
|
122
|
+
throw new PreflightRefusalError(`Mochitest port ${port} remains busy after orphan cleanup; wait for the holder to exit.`, 'mochitest-port-busy');
|
|
123
|
+
}, 2);
|
|
125
124
|
return;
|
|
126
125
|
}
|
|
127
126
|
catch {
|
|
@@ -130,7 +129,8 @@ export async function ensureMochitestServerPortAvailable(port = DEFAULT_MOCHITES
|
|
|
130
129
|
// pretending the port is now free.
|
|
131
130
|
}
|
|
132
131
|
}
|
|
133
|
-
throw new
|
|
132
|
+
throw new PreflightRefusalError(describeMochitestServerRefusal(port, holder, holderClass, owner?.parentPid) +
|
|
133
|
+
`\n Parent: ${owner?.parentPid ?? 'unknown'} (${owner?.owner ?? 'unavailable'}). Use --wait-port [seconds] to queue.`, 'mochitest-port-busy');
|
|
134
134
|
}
|
|
135
135
|
/**
|
|
136
136
|
* Builds the operator-facing refusal. Exported for direct unit testing.
|
|
@@ -139,7 +139,7 @@ export async function ensureMochitestServerPortAvailable(port = DEFAULT_MOCHITES
|
|
|
139
139
|
* @param holder - The listener found on it
|
|
140
140
|
* @param holderClass - Whose harness `server.js` it is, if it is one at all
|
|
141
141
|
*/
|
|
142
|
-
export function describeMochitestServerRefusal(port, holder, holderClass) {
|
|
142
|
+
export function describeMochitestServerRefusal(port, holder, holderClass, parentPid) {
|
|
143
143
|
const killHint = process.platform === 'win32'
|
|
144
144
|
? `Stop-Process -Id ${holder.pid} -Force`
|
|
145
145
|
: `kill -9 ${holder.pid}`;
|
|
@@ -150,9 +150,12 @@ export function describeMochitestServerRefusal(port, holder, holderClass) {
|
|
|
150
150
|
'manifest — the run would stall before TEST_START and die on the no-output timeout with ' +
|
|
151
151
|
'"Ran 0 checks".\n';
|
|
152
152
|
if (holderClass === 'this-checkout') {
|
|
153
|
+
if (parentPid !== 1)
|
|
154
|
+
return (evidence +
|
|
155
|
+
' This checkout has a live or unattributed harness server. Wait for its owner to finish; --wait-port [seconds] queues behind it.');
|
|
153
156
|
return (evidence +
|
|
154
157
|
" This is the mochitest harness's own server.js from THIS checkout's objdir, so it is " +
|
|
155
|
-
'debris from an interrupted run.
|
|
158
|
+
'possibly debris from an interrupted run. For a dead parent, retry with "--kill-stale-marionette" to have FireForge ' +
|
|
156
159
|
`stop it, or stop it yourself with "${killHint}" (a wedged httpd can ignore SIGTERM).`);
|
|
157
160
|
}
|
|
158
161
|
if (holderClass === 'foreign-checkout') {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Refuses an objdir that writes through a directory alias into a different checkout. */
|
|
2
|
+
export declare function assertLocalObjdir(engineDir: string, objDir: string): Promise<void>;
|
|
3
|
+
/** Repairs copied links and dependency metadata BEFORE configure can consume config.track. */
|
|
4
|
+
export declare function relocateObjdirProducts(engineDir: string, objDir: string, oldRoot: string): Promise<void>;
|
|
5
|
+
/** Finds a link still consulting the previous source tree after relocation/configure. */
|
|
6
|
+
export declare function findStaleObjdirLink(engineDir: string, objDir: string, oldRoot: string): Promise<string | undefined>;
|
|
7
|
+
/** Prunes only dangling _tests symlinks; never descends through a symlink. */
|
|
8
|
+
export declare function pruneDanglingTestLinks(engineDir: string, objDir: string): Promise<number>;
|
|
9
|
+
/** Names a partial-config group whose entire persisted value set was nulled. */
|
|
10
|
+
export declare function assertHealthyPartialConfig(engineDir: string, objDir: string): Promise<void>;
|