Files
hermes-agent/scripts/bundles/dmgbuild_diagnostics.py

101 lines
4.3 KiB
Python

"""Run the supplier's dmgbuild with failure-time, read-only handle diagnostics."""
from __future__ import annotations
import functools
import json
import subprocess
import sys
import time
def report_detach_failure(target, attachment, output, *, run=subprocess.run, stream=None):
stream = sys.stderr if stream is None else stream
def log(text):
print(f"[dmg-detach] {text}", file=stream, flush=True)
def invoke(argv):
log("command: " + json.dumps(argv))
try:
result = run(argv, stdin=subprocess.DEVNULL, capture_output=True,
text=True, encoding="utf-8", errors="replace", timeout=15)
except subprocess.TimeoutExpired as error:
for partial in (error.stdout, error.stderr):
if partial:
log(partial.decode("utf-8", errors="replace") if isinstance(partial, bytes) else partial)
log("diagnostic timed out after 15s. Snapshot is incomplete.")
return None
except OSError as error:
log(f"diagnostic unavailable: {error}")
return None
if result.stdout:
log(result.stdout.rstrip())
if result.stderr:
log(result.stderr.rstrip())
log(f"diagnostic exit status: {result.returncode}")
return result
log(f"detach failed for {target}: {output}")
log("Open-handle snapshot before dmgbuild retries or cleanup. No processes are stopped.")
elevated = invoke(["/usr/bin/sudo", "-n", "/usr/bin/true"])
prefix = ["/usr/bin/sudo", "-n"] if elevated is not None and elevated.returncode == 0 else []
if not prefix:
log("Using an unprivileged snapshot; system-process handles may be hidden.")
entities = attachment.get("entities", []) if attachment else []
mounts = list(dict.fromkeys(e["mount-point"] for e in entities if e.get("mount-point")))
devices = list(dict.fromkeys([target, *(e["dev-entry"] for e in entities if e.get("dev-entry"))]))
raw_devices = ["/dev/r" + device[5:] for device in devices if device.startswith("/dev/disk")]
image = attachment.get("image") if attachment else None
log("attachment: " + json.dumps({"image": image, "entities": entities}))
command = [*prefix, "/usr/sbin/lsof", "-nP", "+c", "0", "-R"]
for mount in mounts:
# Select the filesystem, not a recursive stat of every bundled file.
invoke([*command, "+f", "--", mount])
invoke([*command, "-f", "--", *([image] if image else []), *devices, *raw_devices])
log("End of handle snapshot. Errors or empty output do not prove the absence of holders.")
def wrap_hdiutil(native, *, report=report_detach_failure, stream=None):
attachments = {}
stream = sys.stderr if stream is None else stream
@functools.wraps(native)
def observed(command, *args, **kwargs):
started = time.monotonic()
print(f"[dmg-hdiutil] start {command} {json.dumps(args)}", file=stream, flush=True)
result = native(command, *args, **kwargs)
code, output = result
print(f"[dmg-hdiutil] end {command} status={code} elapsed={time.monotonic() - started:.2f}s",
file=stream, flush=True)
if code != 0:
print(f"[dmg-hdiutil] output: {output!r}", file=stream, flush=True)
if command == "attach" and code == 0 and isinstance(output, dict):
attachment = {"image": args[-1], "entities": output.get("system-entities", [])}
for entity in attachment["entities"]:
for key in ("dev-entry", "mount-point"):
if entity.get(key):
attachments[entity[key]] = attachment
elif command in ("detach", "unmount") and code != 0:
target = next((arg for arg in reversed(args) if not arg.startswith("-")), "")
try:
report(target, attachments.get(target), output)
except Exception as error:
# Diagnostics must not replace the original detach failure.
print(f"[dmg-detach] diagnostic failed: {error}",
file=sys.stderr if stream is None else stream, flush=True)
return result
return observed
def main():
from dmgbuild import core
from dmgbuild.__main__ import main as build
core.hdiutil = wrap_hdiutil(core.hdiutil)
build()
if __name__ == "__main__":
main()