diff --git a/server/brewpi.py b/server/brewpi.py index 38afdd8..cf46c5b 100755 --- a/server/brewpi.py +++ b/server/brewpi.py @@ -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.")