@hominis/fireforge 0.34.1 → 0.34.2
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
CHANGED
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
- Added `furnace scan --track`, which persists every discovered untracked component into the `stock` section of furnace.json non-interactively (same locked, rollback-journaled write path as the interactive confirm flow). Scan's help and its non-interactive output now state that scan is report-only by default and where the inventory is consumed, ending the report-persist-nothing-explain-nothing behavior.
|
|
16
16
|
- Added `furnace chrome-doc create --browser-window`, a browser.xhtml-like scaffold for the document that ships as the fork's main browser window: `<html id="main-window">` root with the `windowtype="navigator:browser"`/`chromehidden`/geometry-`persist` attributes platform C++ reads before scripts run, while keeping the generic scaffold's bootstrap wiring, sentinel, and (already-correct) jar.mn registrations. When the target document matches a configured `tokenHostDocuments` entry and the flag was not passed, create warns that the browser-window variant is probably intended.
|
|
17
17
|
- Added `furnace create --test-dir <dir>` to redirect the `--with-tests` scaffold (browser-chrome and xpcshell styles) to any engine-relative directory under `browser/base/content/test/` — nested manifests register correctly — and made all test scaffolds collision-safe: existing `browser.toml`/`xpcshell.toml`/`chrome.toml` manifests are appended to (with a shared-manifest notice) instead of scaffolded over, and existing `head.js`/test implementation files are never overwritten.
|
|
18
|
+
- Fixed the degraded-psutil guard wedging `mach build` at exit on flapping hosts (0.34.1 post-upgrade field report: psutil's vm/swap syscalls flap between working and degraded, the fallback swap reading arrived svmem-shaped (8 fields) where mozsystemmonitor's parent reconstructs a 6-field `sswap` via `self._swap_type(*swap_mem)`, so every collector sample was rejected — `resourcemonitor.py:766: UserWarning: failed to read the received data` — upstream's handler breaks its drain loop, the pipe fills, the collector child blocks in `send()` and never reads the terminate sentinel, and mach hangs forever in its atexit `waitpid`). The guard's fallback readings are now **per-function, arity-correct, module-level namedtuples** (svmem 8 / sswap 6 / scputimes 4 / sdiskio 6; psutil's own result classes are still preferred when resolvable) that pickle by reference across the collector pipe and survive `type(reading)(*values)` reconstruction in **either** flapping direction (real type + degraded reading, or degraded type + real reading); `cpu_percent`/`cpu_times` fallbacks honour `percpu=True` (return `[]`) for the collector child's per-CPU sampling, and the last-resort `_DegradedReading` now tolerates positional-field construction. The report's "factory resolution bypassed in the collector child" observation is consistent with the two-copy load (`sitecustomize` via PYTHONPATH plus the in-venv `.pth`) or degraded-import-time `psutil._psplatform` resolution — the fix no longer depends on psutil internals resolving, so it is robust to both.
|
|
19
|
+
- Added **collector suppression on degradation** to the guard: any observed psutil degradation sets a process-wide flag, after which monitors are kept inert — `start` never spawns the mozsystemmonitor collector child — and a degraded transition (any wrapped monitor method raising) or a raising `stop` best-effort terminates a live collector child (`_process.terminate()` + bounded join) and drains the parent end of the pipe, so a malformed sample stream can never fail the command or wedge shutdown even if a future shape mismatch slips through. (The upstream enabler — mozsystemmonitor warning then `break`ing out of its drain loop without draining, leaving the pipe to fill — is worth reporting to Mozilla but is not FireForge-fixable.)
|
|
20
|
+
- Fixed the same degradation **failing `mach build` after a fully successful compile** ("Error running mach" with complete, valid `dist/` artifacts): degraded aggregate methods now return zeroed shapes instead of `None` where callers subscript the result (`aggregate_io` → zeroed io namedtuple, so mozbuild's `log_resource_usage` no longer dies on `usage["io"].read_bytes` at `mozbuild/controller/building.py:526`; `aggregate_cpu_percent`/`aggregate_cpu_times` return `[]`/`0.0`/zeroed shapes per their `per_cpu` mode), and the guard additionally wraps `mozbuild.controller.building.BuildMonitor.log_resource_usage` to warn-and-continue on any exception. Runtime-verified by new python3-executed regression tests covering per-function fallback arity, pickle/`type(*values)` round-trips in both flapping directions, collector suppression (stub monitor with upstream-named `_process`/`_pipe`), and the BuildMonitor wrap.
|
|
21
|
+
- Taught the harness-crash classifier the post-success `AttributeError: 'NoneType' object has no attribute 'read_bytes'` traceback (protected builds retry it as crash-not-failure — with the fixed guard the incremental retry is cheap and green; real test failures still take precedence), and exempted the upstream drain-loop `failed to read the received data` warning from crash evidence so runs that complete despite the rejected-sample stream never classify as crashes.
|
|
18
22
|
|
|
19
23
|
## 0.33.0
|
|
20
24
|
|
|
@@ -30,17 +30,52 @@
|
|
|
30
30
|
* reading instead of aborting. Downstream report (0.34.0 cycle):
|
|
31
31
|
* mozsystemmonitor subscripts (`_build_meta` does
|
|
32
32
|
* `psutil.virtual_memory()[0]`) and iterates/unpacks the reading, so the
|
|
33
|
-
* zeroed fallback is a full namedtuple duck type
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
33
|
+
* zeroed fallback is a full namedtuple duck type; `cpu_percent` degrades
|
|
34
|
+
* to `0.0` because callers do arithmetic on it. Field report (0.34.1,
|
|
35
|
+
* flapping host): the fallback must ALSO be per-function arity-correct
|
|
36
|
+
* and reconstructible — mozsystemmonitor's parent captures reading types
|
|
37
|
+
* at `__init__` (`self._swap_type = type(psutil.swap_memory())`) and
|
|
38
|
+
* rebuilds each collector sample via `self._swap_type(*swap_mem)`, and an
|
|
39
|
+
* svmem-shaped (8-field) fallback in the swap (6-field `sswap`) position
|
|
40
|
+
* made the parent reject every sample ("failed to read the received
|
|
41
|
+
* data") and BREAK its drain loop, so the pipe filled, the collector
|
|
42
|
+
* child blocked in `send()` and never read the terminate sentinel, and
|
|
43
|
+
* mach hung forever in its atexit `waitpid`. Each wrapped function now
|
|
44
|
+
* degrades to a zeroed instance of psutil's own result namedtuple where
|
|
45
|
+
* resolvable, else a module-level guard-owned namedtuple with the exact
|
|
46
|
+
* macOS field order/arity for THAT function (module-level so readings
|
|
47
|
+
* pickle by reference across the collector pipe); `_DegradedReading`
|
|
48
|
+
* remains only as a documented last resort and now tolerates
|
|
49
|
+
* `type(reading)(*values)` reconstruction;
|
|
38
50
|
* - guards `SystemResourceMonitor` CONSTRUCTION via an import hook:
|
|
39
51
|
* `poll_interval` is pre-populated before the real `__init__` runs, a
|
|
40
52
|
* failing `__init__` marks the instance degraded instead of raising,
|
|
41
53
|
* and every monitor method no-ops on a degraded instance — so a
|
|
42
54
|
* partially-constructed monitor can never surface the
|
|
43
|
-
* `AttributeError: poll_interval` variant.
|
|
55
|
+
* `AttributeError: poll_interval` variant. Degraded aggregate methods
|
|
56
|
+
* return zeroed shapes (not `None`) where callers subscript the result
|
|
57
|
+
* (`aggregate_io` → zeroed io reading, so mozbuild's
|
|
58
|
+
* `log_resource_usage` no longer dies on `usage["io"].read_bytes` after
|
|
59
|
+
* a fully successful compile);
|
|
60
|
+
* - suppresses the mozsystemmonitor collector child on degradation: once
|
|
61
|
+
* any psutil degradation is observed in the process, monitors are kept
|
|
62
|
+
* inert (`start` never spawns the collector), and a degraded transition
|
|
63
|
+
* or raising `stop` best-effort terminates a live collector child and
|
|
64
|
+
* drains the pipe — so a malformed sample stream can never wedge mach's
|
|
65
|
+
* shutdown even if a future shape mismatch slips through;
|
|
66
|
+
* - additionally wraps `mozbuild.controller.building.BuildMonitor
|
|
67
|
+
* .log_resource_usage` to warn-and-continue on any exception, so
|
|
68
|
+
* end-of-build resource reporting can never fail a build whose
|
|
69
|
+
* artifacts are complete.
|
|
70
|
+
*
|
|
71
|
+
* Why the 0.34.1 report saw `_DegradedReading` in the collector child even
|
|
72
|
+
* though the psutil result classes resolve when probed directly: two guard
|
|
73
|
+
* copies can load (PYTHONPATH `sitecustomize` in the bootstrap phase plus
|
|
74
|
+
* the in-venv `.pth`), each with its own factory/classes, and flapping at
|
|
75
|
+
* guard-import time can perturb `psutil._psplatform` attribute resolution
|
|
76
|
+
* in the spawn child specifically. The fix is robust to either mechanism:
|
|
77
|
+
* the guard-owned fallback classes never depend on psutil internals
|
|
78
|
+
* resolving and are arity-correct in every position.
|
|
44
79
|
*/
|
|
45
80
|
/**
|
|
46
81
|
* Python guard body, importable both as the in-venv `fireforge_mach_guard`
|
|
@@ -51,7 +86,7 @@
|
|
|
51
86
|
* regression test (mach-resource-shim.python.test.ts), which asserts the
|
|
52
87
|
* degraded readings' runtime behavior rather than string-matching the source.
|
|
53
88
|
*/
|
|
54
|
-
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 sys\nimport warnings\n\n\ndef _fireforge_degraded_notice(exc):\n warnings.warn(\n \"FireForge: host resource monitor degraded (%s); continuing without resource monitoring.\" % exc\n )\n\n\nclass _DegradedReading(object):\n # Duck type of psutil's namedtuple results (svmem field order):\n # mozsystemmonitor subscripts, iterates, and unpacks readings, so the\n # degraded fallback must survive r[0], list(r), len(r), r._fields and\n # r._asdict(), not just attribute access.\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 __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# 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 _degraded_result_factory(psutil, name):\n # cpu_percent returns a plain float callers do arithmetic on.\n if name == \"cpu_percent\":\n return lambda: 0.0\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 _cls = getattr(_mod, _cls_name, None) if _mod is not None else None\n _fields = getattr(_cls, \"_fields\", None)\n if _cls is not None and _fields:\n return lambda _cls=_cls, _n=len(_fields): _cls(*([0] * _n))\n return _DegradedReading\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()\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 _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\n cls.__init__ = guarded_init\n\n def _wrap_method(orig):\n def wrapper(self, *args, **kwargs):\n if getattr(self, \"_fireforge_degraded\", False):\n return None\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 return None\n\n return wrapper\n\n for _name in _MONITOR_METHOD_NAMES:\n _orig = getattr(cls, _name, None)\n if _orig is not None:\n setattr(cls, _name, _wrap_method(_orig))\n cls._fireforge_guarded = True\n\n\n_MONITOR_MODULES = (\"mozsystemmonitor.resourcemonitor\", \"mozbuild.resources\")\n\n\ndef _patch_loaded_monitor_modules():\n for _mod_name in _MONITOR_MODULES:\n _mod = sys.modules.get(_mod_name)\n if _mod is not None:\n try:\n _patch_monitor_class(getattr(_mod, \"SystemResourceMonitor\", 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";
|
|
89
|
+
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 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. Field report (0.34.1): the\n # parent also reconstructs readings via type(reading)(*values), so the\n # constructor must tolerate 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 (field report 0.34.1: an\n# svmem-shaped fallback in the swap position made the parent reject every\n# sample, fill the pipe, block the collector child in send(), and wedge\n# 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 (field report\n # 0.34.1: 'NoneType' object has no attribute 'read_bytes' failed the\n # build with complete artifacts).\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 for _name in _MONITOR_METHOD_NAMES:\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";
|
|
55
90
|
/** Result of installing the mach resource guard before a dispatch. */
|
|
56
91
|
export interface MachResourceGuardInstallation {
|
|
57
92
|
/**
|
|
@@ -31,17 +31,52 @@
|
|
|
31
31
|
* reading instead of aborting. Downstream report (0.34.0 cycle):
|
|
32
32
|
* mozsystemmonitor subscripts (`_build_meta` does
|
|
33
33
|
* `psutil.virtual_memory()[0]`) and iterates/unpacks the reading, so the
|
|
34
|
-
* zeroed fallback is a full namedtuple duck type
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
34
|
+
* zeroed fallback is a full namedtuple duck type; `cpu_percent` degrades
|
|
35
|
+
* to `0.0` because callers do arithmetic on it. Field report (0.34.1,
|
|
36
|
+
* flapping host): the fallback must ALSO be per-function arity-correct
|
|
37
|
+
* and reconstructible — mozsystemmonitor's parent captures reading types
|
|
38
|
+
* at `__init__` (`self._swap_type = type(psutil.swap_memory())`) and
|
|
39
|
+
* rebuilds each collector sample via `self._swap_type(*swap_mem)`, and an
|
|
40
|
+
* svmem-shaped (8-field) fallback in the swap (6-field `sswap`) position
|
|
41
|
+
* made the parent reject every sample ("failed to read the received
|
|
42
|
+
* data") and BREAK its drain loop, so the pipe filled, the collector
|
|
43
|
+
* child blocked in `send()` and never read the terminate sentinel, and
|
|
44
|
+
* mach hung forever in its atexit `waitpid`. Each wrapped function now
|
|
45
|
+
* degrades to a zeroed instance of psutil's own result namedtuple where
|
|
46
|
+
* resolvable, else a module-level guard-owned namedtuple with the exact
|
|
47
|
+
* macOS field order/arity for THAT function (module-level so readings
|
|
48
|
+
* pickle by reference across the collector pipe); `_DegradedReading`
|
|
49
|
+
* remains only as a documented last resort and now tolerates
|
|
50
|
+
* `type(reading)(*values)` reconstruction;
|
|
39
51
|
* - guards `SystemResourceMonitor` CONSTRUCTION via an import hook:
|
|
40
52
|
* `poll_interval` is pre-populated before the real `__init__` runs, a
|
|
41
53
|
* failing `__init__` marks the instance degraded instead of raising,
|
|
42
54
|
* and every monitor method no-ops on a degraded instance — so a
|
|
43
55
|
* partially-constructed monitor can never surface the
|
|
44
|
-
* `AttributeError: poll_interval` variant.
|
|
56
|
+
* `AttributeError: poll_interval` variant. Degraded aggregate methods
|
|
57
|
+
* return zeroed shapes (not `None`) where callers subscript the result
|
|
58
|
+
* (`aggregate_io` → zeroed io reading, so mozbuild's
|
|
59
|
+
* `log_resource_usage` no longer dies on `usage["io"].read_bytes` after
|
|
60
|
+
* a fully successful compile);
|
|
61
|
+
* - suppresses the mozsystemmonitor collector child on degradation: once
|
|
62
|
+
* any psutil degradation is observed in the process, monitors are kept
|
|
63
|
+
* inert (`start` never spawns the collector), and a degraded transition
|
|
64
|
+
* or raising `stop` best-effort terminates a live collector child and
|
|
65
|
+
* drains the pipe — so a malformed sample stream can never wedge mach's
|
|
66
|
+
* shutdown even if a future shape mismatch slips through;
|
|
67
|
+
* - additionally wraps `mozbuild.controller.building.BuildMonitor
|
|
68
|
+
* .log_resource_usage` to warn-and-continue on any exception, so
|
|
69
|
+
* end-of-build resource reporting can never fail a build whose
|
|
70
|
+
* artifacts are complete.
|
|
71
|
+
*
|
|
72
|
+
* Why the 0.34.1 report saw `_DegradedReading` in the collector child even
|
|
73
|
+
* though the psutil result classes resolve when probed directly: two guard
|
|
74
|
+
* copies can load (PYTHONPATH `sitecustomize` in the bootstrap phase plus
|
|
75
|
+
* the in-venv `.pth`), each with its own factory/classes, and flapping at
|
|
76
|
+
* guard-import time can perturb `psutil._psplatform` attribute resolution
|
|
77
|
+
* in the spawn child specifically. The fix is robust to either mechanism:
|
|
78
|
+
* the guard-owned fallback classes never depend on psutil internals
|
|
79
|
+
* resolving and are arity-correct in every position.
|
|
45
80
|
*/
|
|
46
81
|
import { readdir } from 'node:fs/promises';
|
|
47
82
|
import { homedir, tmpdir } from 'node:os';
|
|
@@ -68,25 +103,40 @@ export const GUARD_PYTHON_SOURCE = `# Generated by FireForge - do not edit.
|
|
|
68
103
|
# proceed. Installed into the mach virtualenv site-packages via a .pth file
|
|
69
104
|
# (survives mach's venv re-exec, which drops PYTHONPATH).
|
|
70
105
|
import builtins
|
|
106
|
+
import collections
|
|
71
107
|
import sys
|
|
72
108
|
import warnings
|
|
73
109
|
|
|
110
|
+
# Any psutil degradation observed in this process. Once set, monitors are
|
|
111
|
+
# kept inert (start never spawns the collector child) — on a flapping host a
|
|
112
|
+
# healthy-looking moment must not be trusted to start a sample stream that a
|
|
113
|
+
# later degraded reading could malform.
|
|
114
|
+
_fireforge_host_degraded = [False]
|
|
115
|
+
|
|
74
116
|
|
|
75
117
|
def _fireforge_degraded_notice(exc):
|
|
118
|
+
_fireforge_host_degraded[0] = True
|
|
76
119
|
warnings.warn(
|
|
77
120
|
"FireForge: host resource monitor degraded (%s); continuing without resource monitoring." % exc
|
|
78
121
|
)
|
|
79
122
|
|
|
80
123
|
|
|
81
124
|
class _DegradedReading(object):
|
|
82
|
-
#
|
|
83
|
-
# mozsystemmonitor subscripts, iterates, and unpacks readings,
|
|
84
|
-
# degraded fallback must survive r[0], list(r), len(r), r._fields
|
|
85
|
-
# r._asdict(), not just attribute access.
|
|
125
|
+
# Last-resort duck type of psutil's namedtuple results (svmem field
|
|
126
|
+
# order): mozsystemmonitor subscripts, iterates, and unpacks readings,
|
|
127
|
+
# so the degraded fallback must survive r[0], list(r), len(r), r._fields
|
|
128
|
+
# and r._asdict(), not just attribute access. Field report (0.34.1): the
|
|
129
|
+
# parent also reconstructs readings via type(reading)(*values), so the
|
|
130
|
+
# constructor must tolerate the full positional field list. Normal
|
|
131
|
+
# degraded paths use the per-function namedtuple fallbacks below; this
|
|
132
|
+
# class only backstops a fallback that itself raised.
|
|
86
133
|
_fields = ("total", "available", "percent", "used", "free", "active", "inactive", "wired")
|
|
87
134
|
total = available = used = free = active = inactive = wired = 0
|
|
88
135
|
percent = 0.0
|
|
89
136
|
|
|
137
|
+
def __init__(self, *_args, **_kwargs):
|
|
138
|
+
pass
|
|
139
|
+
|
|
90
140
|
def __getattr__(self, _name):
|
|
91
141
|
return 0
|
|
92
142
|
|
|
@@ -106,6 +156,34 @@ class _DegradedReading(object):
|
|
|
106
156
|
return dict(zip(self._fields, self._values()))
|
|
107
157
|
|
|
108
158
|
|
|
159
|
+
# Per-function fallback result classes with the exact macOS field
|
|
160
|
+
# orders/arities. Module-level on purpose: readings cross the
|
|
161
|
+
# mozsystemmonitor collector pipe via pickle (by reference), and the parent
|
|
162
|
+
# rebuilds each sample with type(reading)(*values) — so a swap fallback must
|
|
163
|
+
# be 6-field sswap-shaped in every position (field report 0.34.1: an
|
|
164
|
+
# svmem-shaped fallback in the swap position made the parent reject every
|
|
165
|
+
# sample, fill the pipe, block the collector child in send(), and wedge
|
|
166
|
+
# mach's atexit join forever).
|
|
167
|
+
_FallbackVmem = collections.namedtuple(
|
|
168
|
+
"_FallbackVmem",
|
|
169
|
+
("total", "available", "percent", "used", "free", "active", "inactive", "wired"),
|
|
170
|
+
)
|
|
171
|
+
_FallbackSwap = collections.namedtuple(
|
|
172
|
+
"_FallbackSwap", ("total", "used", "free", "percent", "sin", "sout")
|
|
173
|
+
)
|
|
174
|
+
_FallbackCpuTimes = collections.namedtuple("_FallbackCpuTimes", ("user", "nice", "system", "idle"))
|
|
175
|
+
_FallbackDiskIO = collections.namedtuple(
|
|
176
|
+
"_FallbackDiskIO",
|
|
177
|
+
("read_count", "write_count", "read_bytes", "write_bytes", "read_time", "write_time"),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
_FALLBACK_CLASSES = {
|
|
181
|
+
"virtual_memory": _FallbackVmem,
|
|
182
|
+
"swap_memory": _FallbackSwap,
|
|
183
|
+
"cpu_times": _FallbackCpuTimes,
|
|
184
|
+
"disk_io_counters": _FallbackDiskIO,
|
|
185
|
+
}
|
|
186
|
+
|
|
109
187
|
# psutil result namedtuple per wrapped function, resolvable via getattr from
|
|
110
188
|
# the platform module / _common without invoking the failing syscall.
|
|
111
189
|
_PSUTIL_RESULT_CLASSES = {
|
|
@@ -116,18 +194,51 @@ _PSUTIL_RESULT_CLASSES = {
|
|
|
116
194
|
}
|
|
117
195
|
|
|
118
196
|
|
|
197
|
+
def _zeroed(cls):
|
|
198
|
+
return cls(*([0] * len(cls._fields)))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _percpu_requested(args, kwargs, positional_index):
|
|
202
|
+
if "percpu" in kwargs:
|
|
203
|
+
return bool(kwargs["percpu"])
|
|
204
|
+
return len(args) > positional_index and bool(args[positional_index])
|
|
205
|
+
|
|
206
|
+
|
|
119
207
|
def _degraded_result_factory(psutil, name):
|
|
120
|
-
# cpu_percent returns a plain float callers do
|
|
208
|
+
# cpu_percent returns a plain float (or a per-CPU list) callers do
|
|
209
|
+
# arithmetic on.
|
|
121
210
|
if name == "cpu_percent":
|
|
122
|
-
|
|
211
|
+
def _cpu_percent_fallback(*args, **kwargs):
|
|
212
|
+
# cpu_percent(interval=None, percpu=False)
|
|
213
|
+
if _percpu_requested(args, kwargs, 1):
|
|
214
|
+
return []
|
|
215
|
+
return 0.0
|
|
216
|
+
|
|
217
|
+
return _cpu_percent_fallback
|
|
218
|
+
# The guard-owned class is always shape-correct for this function;
|
|
219
|
+
# psutil's own result namedtuple is still preferred when resolvable so
|
|
220
|
+
# type() captures stay identical to real readings on a flapping host.
|
|
221
|
+
_cls = _FALLBACK_CLASSES.get(name, _DegradedReading)
|
|
123
222
|
_cls_name = _PSUTIL_RESULT_CLASSES.get(name)
|
|
124
223
|
if _cls_name is not None:
|
|
125
224
|
for _mod in (getattr(psutil, "_psplatform", None), getattr(psutil, "_common", None)):
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
225
|
+
_real = getattr(_mod, _cls_name, None) if _mod is not None else None
|
|
226
|
+
if _real is not None and getattr(_real, "_fields", None):
|
|
227
|
+
_cls = _real
|
|
228
|
+
break
|
|
229
|
+
if name == "cpu_times":
|
|
230
|
+
def _cpu_times_fallback(*args, **kwargs):
|
|
231
|
+
# cpu_times(percpu=False); the collector child samples per-CPU.
|
|
232
|
+
if _percpu_requested(args, kwargs, 0):
|
|
233
|
+
return []
|
|
234
|
+
return _zeroed(_cls)
|
|
235
|
+
|
|
236
|
+
return _cpu_times_fallback
|
|
237
|
+
|
|
238
|
+
def _reading_fallback(*_args, **_kwargs):
|
|
239
|
+
return _zeroed(_cls)
|
|
240
|
+
|
|
241
|
+
return _reading_fallback
|
|
131
242
|
|
|
132
243
|
|
|
133
244
|
def _guard_psutil():
|
|
@@ -143,7 +254,7 @@ def _guard_psutil():
|
|
|
143
254
|
except Exception as exc: # noqa: BLE001
|
|
144
255
|
_fireforge_degraded_notice(exc)
|
|
145
256
|
try:
|
|
146
|
-
return fallback()
|
|
257
|
+
return fallback(*args, **kwargs)
|
|
147
258
|
except Exception: # noqa: BLE001
|
|
148
259
|
return _DegradedReading()
|
|
149
260
|
|
|
@@ -176,6 +287,54 @@ _MONITOR_METHOD_NAMES = (
|
|
|
176
287
|
)
|
|
177
288
|
|
|
178
289
|
|
|
290
|
+
def _fireforge_stop_collector(monitor):
|
|
291
|
+
# Best-effort: terminate a live collector child and drain the parent end
|
|
292
|
+
# of the pipe, so a child blocked in send() on a full pipe can never
|
|
293
|
+
# keep mach's atexit join (os.waitpid) waiting forever. Attribute names
|
|
294
|
+
# follow upstream resourcemonitor.py (self._process / self._pipe); a
|
|
295
|
+
# miss on a refactored upstream is harmless — the shape-correct
|
|
296
|
+
# fallbacks above still keep the sample stream well-formed.
|
|
297
|
+
try:
|
|
298
|
+
proc = getattr(monitor, "_process", None)
|
|
299
|
+
if proc is not None and getattr(proc, "is_alive", lambda: False)():
|
|
300
|
+
proc.terminate()
|
|
301
|
+
proc.join(1)
|
|
302
|
+
except Exception: # noqa: BLE001
|
|
303
|
+
pass
|
|
304
|
+
try:
|
|
305
|
+
pipe = getattr(monitor, "_pipe", None)
|
|
306
|
+
if pipe is not None:
|
|
307
|
+
while pipe.poll(0):
|
|
308
|
+
pipe.recv()
|
|
309
|
+
except Exception: # noqa: BLE001
|
|
310
|
+
pass
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _monitor_per_cpu_requested(args, kwargs):
|
|
314
|
+
# aggregate_cpu_percent/aggregate_cpu_times(start=None, end=None,
|
|
315
|
+
# phase=None, per_cpu=True) — self already stripped from args.
|
|
316
|
+
if "per_cpu" in kwargs:
|
|
317
|
+
return bool(kwargs["per_cpu"])
|
|
318
|
+
if len(args) > 3:
|
|
319
|
+
return bool(args[3])
|
|
320
|
+
return True
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _monitor_degraded_result(name, args, kwargs):
|
|
324
|
+
# Degraded aggregate methods return zeroed shapes, not None, where
|
|
325
|
+
# callers subscript the result: mozbuild's log_resource_usage does
|
|
326
|
+
# usage["io"].read_bytes after a successful compile (field report
|
|
327
|
+
# 0.34.1: 'NoneType' object has no attribute 'read_bytes' failed the
|
|
328
|
+
# build with complete artifacts).
|
|
329
|
+
if name == "aggregate_io":
|
|
330
|
+
return _zeroed(_FallbackDiskIO)
|
|
331
|
+
if name == "aggregate_cpu_percent":
|
|
332
|
+
return [] if _monitor_per_cpu_requested(args, kwargs) else 0.0
|
|
333
|
+
if name == "aggregate_cpu_times":
|
|
334
|
+
return [] if _monitor_per_cpu_requested(args, kwargs) else _zeroed(_FallbackCpuTimes)
|
|
335
|
+
return None
|
|
336
|
+
|
|
337
|
+
|
|
179
338
|
def _patch_monitor_class(cls):
|
|
180
339
|
if cls is None or getattr(cls, "_fireforge_guarded", False):
|
|
181
340
|
return
|
|
@@ -192,38 +351,81 @@ def _patch_monitor_class(cls):
|
|
|
192
351
|
except Exception as exc: # noqa: BLE001
|
|
193
352
|
self._fireforge_degraded = True
|
|
194
353
|
_fireforge_degraded_notice(exc)
|
|
354
|
+
if _fireforge_host_degraded[0] and not self._fireforge_degraded:
|
|
355
|
+
# Host already degraded once: keep the monitor inert so the
|
|
356
|
+
# collector child never spawns on a flapping host.
|
|
357
|
+
self._fireforge_degraded = True
|
|
358
|
+
if self._fireforge_degraded:
|
|
359
|
+
_fireforge_stop_collector(self)
|
|
195
360
|
|
|
196
361
|
cls.__init__ = guarded_init
|
|
197
362
|
|
|
198
|
-
def _wrap_method(orig):
|
|
363
|
+
def _wrap_method(name, orig):
|
|
199
364
|
def wrapper(self, *args, **kwargs):
|
|
365
|
+
if (
|
|
366
|
+
name == "start"
|
|
367
|
+
and _fireforge_host_degraded[0]
|
|
368
|
+
and not getattr(self, "_fireforge_degraded", False)
|
|
369
|
+
):
|
|
370
|
+
self._fireforge_degraded = True
|
|
371
|
+
_fireforge_stop_collector(self)
|
|
200
372
|
if getattr(self, "_fireforge_degraded", False):
|
|
201
|
-
|
|
373
|
+
if name == "stop":
|
|
374
|
+
_fireforge_stop_collector(self)
|
|
375
|
+
return _monitor_degraded_result(name, args, kwargs)
|
|
202
376
|
try:
|
|
203
377
|
return orig(self, *args, **kwargs)
|
|
204
378
|
except Exception as exc: # noqa: BLE001
|
|
205
379
|
self._fireforge_degraded = True
|
|
206
380
|
_fireforge_degraded_notice(exc)
|
|
207
|
-
|
|
381
|
+
# A raising stop (or any degraded transition) must still
|
|
382
|
+
# take the collector child down, or the parent's atexit
|
|
383
|
+
# join hangs on a child blocked writing to a full pipe.
|
|
384
|
+
_fireforge_stop_collector(self)
|
|
385
|
+
return _monitor_degraded_result(name, args, kwargs)
|
|
208
386
|
|
|
209
387
|
return wrapper
|
|
210
388
|
|
|
211
389
|
for _name in _MONITOR_METHOD_NAMES:
|
|
212
390
|
_orig = getattr(cls, _name, None)
|
|
213
391
|
if _orig is not None:
|
|
214
|
-
setattr(cls, _name, _wrap_method(_orig))
|
|
392
|
+
setattr(cls, _name, _wrap_method(_name, _orig))
|
|
215
393
|
cls._fireforge_guarded = True
|
|
216
394
|
|
|
217
395
|
|
|
218
|
-
|
|
396
|
+
def _patch_build_monitor_class(cls):
|
|
397
|
+
# mozbuild's BuildMonitor is not a SystemResourceMonitor; only
|
|
398
|
+
# log_resource_usage needs guarding — end-of-build resource reporting
|
|
399
|
+
# must never fail a build whose artifacts are complete. No __init__
|
|
400
|
+
# replacement, no degraded-instance gating.
|
|
401
|
+
if cls is None or getattr(cls, "_fireforge_guarded", False):
|
|
402
|
+
return
|
|
403
|
+
_orig = getattr(cls, "log_resource_usage", None)
|
|
404
|
+
if _orig is not None:
|
|
405
|
+
def _guarded_log_resource_usage(self, *args, **kwargs):
|
|
406
|
+
try:
|
|
407
|
+
return _orig(self, *args, **kwargs)
|
|
408
|
+
except Exception as exc: # noqa: BLE001
|
|
409
|
+
_fireforge_degraded_notice(exc)
|
|
410
|
+
return None
|
|
411
|
+
|
|
412
|
+
cls.log_resource_usage = _guarded_log_resource_usage
|
|
413
|
+
cls._fireforge_guarded = True
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
_MONITOR_CLASS_PATCHES = (
|
|
417
|
+
("mozsystemmonitor.resourcemonitor", "SystemResourceMonitor", _patch_monitor_class),
|
|
418
|
+
("mozbuild.resources", "SystemResourceMonitor", _patch_monitor_class),
|
|
419
|
+
("mozbuild.controller.building", "BuildMonitor", _patch_build_monitor_class),
|
|
420
|
+
)
|
|
219
421
|
|
|
220
422
|
|
|
221
423
|
def _patch_loaded_monitor_modules():
|
|
222
|
-
for _mod_name in
|
|
424
|
+
for _mod_name, _cls_name, _patcher in _MONITOR_CLASS_PATCHES:
|
|
223
425
|
_mod = sys.modules.get(_mod_name)
|
|
224
426
|
if _mod is not None:
|
|
225
427
|
try:
|
|
226
|
-
|
|
428
|
+
_patcher(getattr(_mod, _cls_name, None))
|
|
227
429
|
except Exception: # noqa: BLE001
|
|
228
430
|
pass
|
|
229
431
|
|
|
@@ -75,6 +75,13 @@ const STARTUP_TRACEBACK_SIGNALS = [
|
|
|
75
75
|
/HOST_VM_INFO64/,
|
|
76
76
|
/\(ipc\/mig\) array not large enough/,
|
|
77
77
|
/psutil\.[A-Za-z]*Error/,
|
|
78
|
+
// Field report (0.34.1): on a degraded host every collector sample is
|
|
79
|
+
// rejected, aggregation has nothing, and mozbuild's log_resource_usage
|
|
80
|
+
// dies on usage["io"].read_bytes AFTER a fully successful compile
|
|
81
|
+
// ("Error running mach" with complete artifacts). Environmental, not a
|
|
82
|
+
// build regression — the protected build retries it (incremental retry
|
|
83
|
+
// is cheap and the guard keeps the retry green).
|
|
84
|
+
/AttributeError: 'NoneType' object has no attribute 'read_bytes'/,
|
|
78
85
|
...DEGRADED_READING_CRASH_SIGNALS,
|
|
79
86
|
];
|
|
80
87
|
function findLine(output, patterns) {
|
|
@@ -102,6 +109,11 @@ const NOISE_LINE_PATTERNS = [
|
|
|
102
109
|
/_collect failed(?!.*_DegradedReading)/i,
|
|
103
110
|
/FireForge: host resource monitor degraded/i,
|
|
104
111
|
/warnings\.warn\(/,
|
|
112
|
+
// mozsystemmonitor's parent-side drain loop rejecting a malformed sample
|
|
113
|
+
// (field report 0.34.1); mach sometimes reprints the warning text without
|
|
114
|
+
// the `UserWarning:` token, and a run that completes despite the stream
|
|
115
|
+
// must never classify as crash on it.
|
|
116
|
+
/failed to read the received data/i,
|
|
105
117
|
];
|
|
106
118
|
/** Matches the caught/telemetry context that marks a traceback as benign. */
|
|
107
119
|
const BENIGN_TRACEBACK_CONTEXT = /telemetry|glean/i;
|