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 <noreply@anthropic.com>
This commit is contained in:
2026-10-13 16:03:24 -04:00
co-authored by Claude Fable 5.1
parent 5d66805687
commit b05ff408d5
3 changed files with 21 additions and 16 deletions
+1 -1
View File
@@ -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'
+11 -11
View File
@@ -2,8 +2,8 @@
<Profile majorVersion="2" minorVersion="1" product="ATEM 4 M/E Constellation 4K Plus">
<MixEffectBlocks>
<MixEffectBlock index="0">
<Program input="18"/>
<Preview input="3"/>
<Program input="17"/>
<Preview input="18"/>
<NextTransition selection="Background" nextSelection="Background"/>
<TransitionStyle style="Mix" nextStyle="Mix" previewTransition="False" transitionPosition="0">
<MixParameters rate="7"/>
@@ -13,7 +13,7 @@
<DVEParameters rate="30" logoRate="30" reverseDirection="False" flipFlop="False" effect="PushRight" fillSource="3010" keySource="3011" enableKey="True" preMultipliedKey="True" clip="50" gain="70" invertKey="False"/>
</TransitionStyle>
<Keys>
<Key index="0" type="Luma" inputFill="13" inputCut="12" onAir="False" masked="False" maskTop="9" maskBottom="-9" maskLeft="-16" maskRight="16">
<Key index="0" type="Luma" inputFill="13" inputCut="12" onAir="True" masked="False" maskTop="9" maskBottom="-9" maskLeft="-16" maskRight="16">
<LumaParameters preMultiplied="True" clip="50" gain="70" inverse="False"/>
<AdvancedChromaParameters foregroundLevel="0" backgroundLevel="0" keyEdge="50" spillSuppress="0" flareSuppress="0" brightness="0" contrast="0" saturation="100" red="0" green="0" blue="0" cursorXPosition="-15.41" cursorYPosition="8.41" cursorSize="6.25" sampledY="0.6748" sampledCb="0.1621" sampledCr="0.1015"/>
<PatternParameters style="DiamondIris" inverse="False" size="50" symmetry="50" softness="0" xPosition="0.5" yPosition="0.5"/>
@@ -57,7 +57,7 @@
<DVEParameters rate="30" logoRate="30" reverseDirection="False" flipFlop="False" effect="PushRight" fillSource="3010" keySource="3011" enableKey="True" preMultipliedKey="True" clip="50" gain="70" invertKey="False"/>
</TransitionStyle>
<Keys>
<Key index="0" type="Luma" inputFill="13" inputCut="12" onAir="False" masked="False" maskTop="9" maskBottom="-9" maskLeft="-16" maskRight="16">
<Key index="0" type="Luma" inputFill="13" inputCut="12" onAir="True" masked="False" maskTop="9" maskBottom="-9" maskLeft="-16" maskRight="16">
<LumaParameters preMultiplied="True" clip="50" gain="70" inverse="False"/>
<AdvancedChromaParameters foregroundLevel="0" backgroundLevel="0" keyEdge="50" spillSuppress="0" flareSuppress="0" brightness="0" contrast="0" saturation="100" red="0" green="0" blue="0" cursorXPosition="-15.41" cursorYPosition="8.41" cursorSize="6.25" sampledY="0.6748" sampledCb="0.1621" sampledCr="0.1015"/>
<PatternParameters style="DiamondIris" inverse="False" size="50" symmetry="50" softness="0" xPosition="0.5" yPosition="0.5"/>
@@ -90,7 +90,7 @@
<ForceTally tally=""/>
</MixEffectBlock>
<MixEffectBlock index="2">
<Program input="1"/>
<Program input="16"/>
<Preview input="3"/>
<NextTransition selection="Background" nextSelection="Background"/>
<TransitionStyle style="Mix" nextStyle="Mix" previewTransition="False" transitionPosition="0">
@@ -134,8 +134,8 @@
<ForceTally tally=""/>
</MixEffectBlock>
<MixEffectBlock index="3">
<Program input="14"/>
<Preview input="6"/>
<Program input="17"/>
<Preview input="17"/>
<NextTransition selection="Background" nextSelection="Background"/>
<TransitionStyle style="Mix" nextStyle="Mix" previewTransition="False" transitionPosition="0">
<MixParameters rate="7"/>
@@ -4459,9 +4459,9 @@
</SuperSources>
<MediaPool>
<Stills>
<Still index="0" name="Welcome 4" path="ATEM Media Pool/Welcome 4.png"/>
<Still index="1" name="2501_ThankYou 5" path="ATEM Media Pool/2501_ThankYou 5.png"/>
<Still index="2" name="2408_HourOfPrayer 5" path="ATEM Media Pool/2408_HourOfPrayer 5.png"/>
<Still index="0" name="Welcome 5" path="ATEM Media Pool/Welcome 5.png"/>
<Still index="1" name="2501_ThankYou 6" path="ATEM Media Pool/2501_ThankYou 6.png"/>
<Still index="2" name="2408_HourOfPrayer 6" path="ATEM Media Pool/2408_HourOfPrayer 6.png"/>
</Stills>
</MediaPool>
<CameraControl>
@@ -4481,7 +4481,7 @@
<Parameter device="1" category="ColorCorrection" parameter="ColorAdjust" hue="0" saturation="1"/>
<Parameter device="2" category="Lens" parameter="ApertureFstop" value="5"/>
<Parameter device="2" category="Lens" parameter="ApertureNormalised" value="0"/>
<Parameter device="2" category="Video" parameter="ManualWhiteBalance" temperature="4500" tint="0"/>
<Parameter device="2" category="Video" parameter="ManualWhiteBalance" temperature="4500" tint="15"/>
<Parameter device="2" category="Video" parameter="Exposure" value="16667"/>
<Parameter device="2" category="Video" parameter="DetailLevel" value="1"/>
<Parameter device="2" category="Video" parameter="SensorGainInDecibels" value="2"/>
+9 -4
View File
@@ -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):