fix: force heater/stirrer safe on SIGTERM, not just SIGINT

systemctl stop/restart send SIGTERM, whose default disposition kills
the process immediately - skipping the shutdown finally: block that
cancels tasks and forces both actors safe via their activate(False).
Route SIGTERM through the same KeyboardInterrupt path SIGINT already
uses, and add stirrer.activate(False) alongside the existing
heater.close() as a belt-and-suspenders fallback.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvgC7oy9MxaA4ZQxXqkNdS
This commit is contained in:
2026-07-01 09:37:24 +02:00
co-authored by Claude Sonnet 5
parent aeeecc4ddc
commit d3c8a47562
+16 -4
View File
@@ -3,6 +3,7 @@ import asyncio
import functools
import json
import os
import signal
import sys
import threading
import time
@@ -202,23 +203,34 @@ if __name__ == '__main__':
threading.Thread(target=http_server.serve_forever, daemon=True).start()
print("Serving {} on http://0.0.0.0:{}".format(web_dir, args.http_port))
# systemd's `stop`/`restart` send SIGTERM, whose default disposition is to
# kill the process immediately - skipping the finally: block below
# entirely, leaving the heater/stirrer at whatever they were last set to.
# Routing it through the same KeyboardInterrupt path Ctrl-C (SIGINT)
# already uses gets it the same graceful, actor-safing shutdown.
signal.signal(signal.SIGTERM, signal.default_int_handler)
loop = asyncio.get_event_loop()
try:
loop.run_forever()
except KeyboardInterrupt:
print("\nShutting down...")
finally:
# Cancel all tasks so their finally blocks (e.g. HeaterTask's
# `with device.open()`) get a chance to run before we close the port.
# Cancel all tasks so their finally blocks (e.g. HeaterTask's/
# StirrerTask's `with device.open()`) get a chance to run before we
# close the port - this is what actually forces the heater to 0 W
# (AHeater.activate(False)) and the stirrer to a stopped state
# (AStirrer.activate(False)) on any exit that reaches here.
pending = asyncio.all_tasks(loop)
for task in pending:
task.cancel()
if pending:
loop.run_until_complete(asyncio.gather(*pending, return_exceptions=True))
server_log_task.write()
# Belt-and-suspenders: explicitly close the heater connection even if
# the task cleanup above failed to reach HeaterHendi.activate(False).
# Belt-and-suspenders: explicitly deactivate both actors even if the
# task cleanup above failed to reach their own activate(False).
heater.close()
stirrer.activate(False)
http_server.shutdown()
loop.close()
print("Server stopped.")