From b05ff408d5613630a86f51f230b6e263b659b7e1 Mon Sep 17 00:00:00 2001 From: christian Date: Tue, 13 Oct 2026 16:03:24 -0400 Subject: [PATCH] ATEM: restore file saved 2026-10-13 15:54 (camera 2 tint 15); shutdown budget capped at launchd's 60 s Co-Authored-By: Claude Fable 5.1 --- README.md | 2 +- atem-reference.xml | 22 +++++++++++----------- status_panel.py | 13 +++++++++---- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 9244137..fef2202 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ This folder is the source of truth. Edit `config.json` here, then copy the folde scp status_panel.py coremidi.py netgear.py make_page.py push_page.py nav_keys.py nav_templates.json config.json install.sh atem_watch.js atem_state.js atem-reference.xml checklist-ingest:status-panel-install/ ``` -Then run the installer there. It checks the config with the new code first (a bad config never gets as far as the watchdog), keeps the code that was running in `prev/`, backs up the old config, writes the LaunchAgent with the exit timeout this config needs (the longest fix plus its verify time, so a restart never cuts a fix in half) and restarts the watchdog: +Then run the installer there. It checks the config with the new code first (a bad config never gets as far as the watchdog), keeps the code that was running in `prev/`, backs up the old config, writes the LaunchAgent with the exit timeout this config needs (the longest fix plus its verify time, so a restart never cuts a fix in half; launchd honours at most 60 s of it) and restarts the watchdog: ```bash ssh checklist-ingest 'zsh ~/status-panel-install/install.sh' diff --git a/atem-reference.xml b/atem-reference.xml index 8e14a49..86fad14 100644 --- a/atem-reference.xml +++ b/atem-reference.xml @@ -2,8 +2,8 @@ - - + + @@ -13,7 +13,7 @@ - + @@ -57,7 +57,7 @@ - + @@ -90,7 +90,7 @@ - + @@ -134,8 +134,8 @@ - - + + @@ -4459,9 +4459,9 @@ - - - + + + @@ -4481,7 +4481,7 @@ - + diff --git a/status_panel.py b/status_panel.py index 43aa803..0c6a10d 100644 --- a/status_panel.py +++ b/status_panel.py @@ -2501,9 +2501,13 @@ def check_host(cfg, host, where): raise ValueError("%s: host %s has no ssh address" % (where, host)) +LAUNCHD_EXIT_CAP = 60 # launchd on macOS 26 applies at most 60 s of ExitTimeOut, whatever the plist says + + def exit_timeout(cfg): - """How long launchd must let the watchdog finish a running fix before killing it: the longest - fix plus its verify time plus a margin, never under a minute (install.sh writes it into the plist).""" + """How long launchd should let the watchdog finish a running fix before killing it: the longest + fix plus its verify time plus a margin, never under a minute (install.sh writes it into the plist; + launchd caps what it honours at LAUNCHD_EXIT_CAP, so the shutdown budget is capped too).""" longest = 0 for fix in cfg.get("fixes", {}).values(): longest = max(longest, fix.get("timeout", 60) + (fix.get("verify_seconds", 20) @@ -2767,8 +2771,9 @@ def run(cfg, data_dir, dry_run=False, stop=None, config_sha=None): % (prefix, alert_after)) alerted = True stop.wait(0.25) - # A fix that is running finishes before the process goes (launchd waits exit_timeout for us). - budget = time.time() + max(5, exit_timeout(cfg) - 5) + # A fix that is running finishes before the process goes (launchd waits up to the exit timeout, + # capped at LAUNCHD_EXIT_CAP, for us). + budget = time.time() + max(5, min(exit_timeout(cfg), LAUNCHD_EXIT_CAP) - 5) for t in engine.fix_threads: t.join(timeout=max(0.1, budget - time.time())) if any(t.is_alive() for t in engine.fix_threads):