Skip to content

Commit 4c1dd5a

Browse files
julienldclaude
andauthored
fix: exit on HA container death + daily reset before CI check (#1295)
* fix(internal): exit on HA container death + daily reset before CI check - test_env_manager: poll container status every 30s in --no-interactive mode; exit(1) when container stops so systemd Restart=on-failure fires and starts a fresh HA instance automatically - setup-ha-mcp.sh: add hamcp-demo-reset.timer (19:00 UTC daily) to restart the service 2h before the check-demo-env CI workflow (21:00 UTC); explicit UTC in OnCalendar= to avoid timezone ambiguity - README: document reset timer and update service description Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(internal): catch watchdog Docker/IO errors and exit with code 1 Ensures systemd Restart=on-failure triggers when reload() fails (e.g. Docker daemon unreachable, container removed by OOM killer). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(internal): also catch docker.errors.DockerException in watchdog docker.errors.DockerException (base class) and subclasses like InvalidVersion inherit from Exception, not OSError, so they slipped past the previous handler and exited 0. Adding it explicitly ensures all Docker SDK errors trigger a systemd restart. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 8bdd0fc commit 4c1dd5a

3 files changed

Lines changed: 51 additions & 6 deletions

File tree

tests/lab-setup/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ The setup script is **idempotent** (safe to re-run) and performs:
3737
3. **Docker** — Via official get.docker.com script
3838
4. **uv** — Python package manager for running ha-mcp
3939
5. **ha-mcp repo** — Clones to `~/ha-mcp` (or pulls if it already exists)
40-
6. **Systemd service** — Creates `hamcp-demo.service` (starts on boot, restarts on failure) and `hamcp-demo-update.timer` (daily at 3am: git pull, docker image prune, service restart)
40+
6. **Systemd service** — Creates `hamcp-demo.service` (starts on boot, exits+restarts if HA container dies) and two timers: `hamcp-demo-reset.timer` (daily at 19:00 UTC: fresh HA restart, 2h before nightly CI check) and `hamcp-demo-update.timer` (daily at 03:00 UTC: git pull, docker image prune, service restart)
4141
7. **Caddy** — Reverse proxy with automatic Let's Encrypt TLS for your domain
4242
8. **Unattended upgrades** — Auto-updates OS packages, reboots at 4am if needed
4343
9. **Container cleanup** — Removes stale HA containers and any leaked processes
@@ -65,9 +65,13 @@ sudo systemctl restart hamcp-demo
6565
# Live logs
6666
sudo journalctl -u hamcp-demo -f
6767

68-
# Weekly update timer — next run and last result
68+
# Daily update timer (03:00 UTC) — next run and last result
6969
sudo systemctl list-timers hamcp-demo-update.timer
7070
sudo journalctl -u hamcp-demo-update --no-pager -n 20
71+
72+
# Daily reset timer (19:00 UTC) — next run and last result
73+
sudo systemctl list-timers hamcp-demo-reset.timer
74+
sudo journalctl -u hamcp-demo-reset --no-pager -n 10
7175
```
7276

7377
## Logs

tests/lab-setup/setup-ha-mcp.sh

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,24 +129,51 @@ SVCEOF
129129

130130
cat > /etc/systemd/system/hamcp-demo-update.timer << SVCEOF
131131
[Unit]
132-
Description=HA-MCP Demo Weekly Update Timer
132+
Description=HA-MCP Demo Daily Update Timer
133133
134134
[Timer]
135-
OnCalendar=*-*-* 03:00:00
135+
OnCalendar=*-*-* 03:00:00 UTC
136136
AccuracySec=1h
137137
Persistent=true
138138
139139
[Install]
140140
WantedBy=timers.target
141141
SVCEOF
142142

143+
# Daily reset at 19:00 UTC — 2 hours before the CI check-demo-env workflow (21:00 UTC).
144+
# Restarts the service so HA comes up fresh for the nightly health check.
145+
cat > /etc/systemd/system/hamcp-demo-reset.service << SVCEOF
146+
[Unit]
147+
Description=HA-MCP Demo Daily Reset
148+
After=hamcp-demo.service
149+
150+
[Service]
151+
Type=oneshot
152+
ExecStart=/usr/bin/systemctl restart hamcp-demo
153+
SVCEOF
154+
155+
cat > /etc/systemd/system/hamcp-demo-reset.timer << SVCEOF
156+
[Unit]
157+
Description=HA-MCP Demo Daily Reset Timer
158+
159+
[Timer]
160+
OnCalendar=*-*-* 19:00:00 UTC
161+
AccuracySec=5m
162+
Persistent=true
163+
164+
[Install]
165+
WantedBy=timers.target
166+
SVCEOF
167+
143168
# Remove sudoers rule if it exists from a previous install (no longer needed)
144169
rm -f /etc/sudoers.d/hamcp-demo
145170

146171
systemctl daemon-reload
147172
systemctl enable hamcp-demo.service
148173
systemctl enable hamcp-demo-update.timer
149174
systemctl start hamcp-demo-update.timer
175+
systemctl enable hamcp-demo-reset.timer
176+
systemctl start hamcp-demo-reset.timer
150177

151178
#=============================================================================
152179
# 7. CADDY

tests/test_env_manager.py

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import time
1818
from pathlib import Path
1919

20+
import docker.errors
2021
import requests
2122
from testcontainers.core.container import DockerContainer
2223

@@ -302,13 +303,26 @@ def main():
302303
env.print_status()
303304

304305
if args.no_interactive:
305-
# Non-interactive mode: just wait for interrupt
306+
# Non-interactive mode: wait for interrupt, and exit if the container dies
307+
# so systemd's Restart=on-failure kicks in with a fresh instance.
306308
logger.info("🔄 Running in non-interactive mode. Press Ctrl+C to stop.")
307309
try:
308310
while True:
309-
time.sleep(1)
311+
time.sleep(30)
312+
if env.container:
313+
wrapped = env.container.get_wrapped_container()
314+
wrapped.reload()
315+
if wrapped.status not in ("running",):
316+
logger.error(
317+
f"❌ Container stopped unexpectedly "
318+
f"(status: {wrapped.status}), exiting for restart..."
319+
)
320+
sys.exit(1)
310321
except KeyboardInterrupt:
311322
logger.info("\n🛑 Received interrupt signal")
323+
except (OSError, RuntimeError, docker.errors.DockerException) as e:
324+
logger.error(f"❌ Watchdog failure: {e}, exiting for restart...")
325+
sys.exit(1)
312326
else:
313327
# Interactive menu loop
314328
while True:

0 commit comments

Comments
 (0)