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/ 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 ```bash
ssh checklist-ingest 'zsh ~/status-panel-install/install.sh' 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"> <Profile majorVersion="2" minorVersion="1" product="ATEM 4 M/E Constellation 4K Plus">
<MixEffectBlocks> <MixEffectBlocks>
<MixEffectBlock index="0"> <MixEffectBlock index="0">
<Program input="18"/> <Program input="17"/>
<Preview input="3"/> <Preview input="18"/>
<NextTransition selection="Background" nextSelection="Background"/> <NextTransition selection="Background" nextSelection="Background"/>
<TransitionStyle style="Mix" nextStyle="Mix" previewTransition="False" transitionPosition="0"> <TransitionStyle style="Mix" nextStyle="Mix" previewTransition="False" transitionPosition="0">
<MixParameters rate="7"/> <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"/> <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> </TransitionStyle>
<Keys> <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"/> <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"/> <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"/> <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"/> <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> </TransitionStyle>
<Keys> <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"/> <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"/> <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"/> <PatternParameters style="DiamondIris" inverse="False" size="50" symmetry="50" softness="0" xPosition="0.5" yPosition="0.5"/>
@@ -90,7 +90,7 @@
<ForceTally tally=""/> <ForceTally tally=""/>
</MixEffectBlock> </MixEffectBlock>
<MixEffectBlock index="2"> <MixEffectBlock index="2">
<Program input="1"/> <Program input="16"/>
<Preview input="3"/> <Preview input="3"/>
<NextTransition selection="Background" nextSelection="Background"/> <NextTransition selection="Background" nextSelection="Background"/>
<TransitionStyle style="Mix" nextStyle="Mix" previewTransition="False" transitionPosition="0"> <TransitionStyle style="Mix" nextStyle="Mix" previewTransition="False" transitionPosition="0">
@@ -134,8 +134,8 @@
<ForceTally tally=""/> <ForceTally tally=""/>
</MixEffectBlock> </MixEffectBlock>
<MixEffectBlock index="3"> <MixEffectBlock index="3">
<Program input="14"/> <Program input="17"/>
<Preview input="6"/> <Preview input="17"/>
<NextTransition selection="Background" nextSelection="Background"/> <NextTransition selection="Background" nextSelection="Background"/>
<TransitionStyle style="Mix" nextStyle="Mix" previewTransition="False" transitionPosition="0"> <TransitionStyle style="Mix" nextStyle="Mix" previewTransition="False" transitionPosition="0">
<MixParameters rate="7"/> <MixParameters rate="7"/>
@@ -4459,9 +4459,9 @@
</SuperSources> </SuperSources>
<MediaPool> <MediaPool>
<Stills> <Stills>
<Still index="0" name="Welcome 4" path="ATEM Media Pool/Welcome 4.png"/> <Still index="0" name="Welcome 5" path="ATEM Media Pool/Welcome 5.png"/>
<Still index="1" name="2501_ThankYou 5" path="ATEM Media Pool/2501_ThankYou 5.png"/> <Still index="1" name="2501_ThankYou 6" path="ATEM Media Pool/2501_ThankYou 6.png"/>
<Still index="2" name="2408_HourOfPrayer 5" path="ATEM Media Pool/2408_HourOfPrayer 5.png"/> <Still index="2" name="2408_HourOfPrayer 6" path="ATEM Media Pool/2408_HourOfPrayer 6.png"/>
</Stills> </Stills>
</MediaPool> </MediaPool>
<CameraControl> <CameraControl>
@@ -4481,7 +4481,7 @@
<Parameter device="1" category="ColorCorrection" parameter="ColorAdjust" hue="0" saturation="1"/> <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="ApertureFstop" value="5"/>
<Parameter device="2" category="Lens" parameter="ApertureNormalised" value="0"/> <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="Exposure" value="16667"/>
<Parameter device="2" category="Video" parameter="DetailLevel" value="1"/> <Parameter device="2" category="Video" parameter="DetailLevel" value="1"/>
<Parameter device="2" category="Video" parameter="SensorGainInDecibels" value="2"/> <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)) 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): def exit_timeout(cfg):
"""How long launchd must let the watchdog finish a running fix before killing it: the longest """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).""" 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 longest = 0
for fix in cfg.get("fixes", {}).values(): for fix in cfg.get("fixes", {}).values():
longest = max(longest, fix.get("timeout", 60) + (fix.get("verify_seconds", 20) 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)) % (prefix, alert_after))
alerted = True alerted = True
stop.wait(0.25) stop.wait(0.25)
# A fix that is running finishes before the process goes (launchd waits exit_timeout for us). # A fix that is running finishes before the process goes (launchd waits up to the exit timeout,
budget = time.time() + max(5, exit_timeout(cfg) - 5) # 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: for t in engine.fix_threads:
t.join(timeout=max(0.1, budget - time.time())) t.join(timeout=max(0.1, budget - time.time()))
if any(t.is_alive() for t in engine.fix_threads): if any(t.is_alive() for t in engine.fix_threads):