The previous hotfix's disconnect()-on-comm-error path can now fire during server/brewpi.py's own final best-effort hardware-release cleanup (finally: block's heater.close()/stirrer.activate(False)), which runs after the event loop has already stopped - previously nothing about hardware teardown touched an AttributeChange-tracked field, so this path was dormant. asyncio.create_task() there raises RuntimeError: no running event loop, crashing the shutdown sequence. Adds fire_and_forget() (tasks/task.py) - asyncio.create_task() that silently drops the message instead of raising when there's no loop to schedule on - and uses it for the Connected/FirmwareVersion broadcasts. SudTask.check_connections() also now no-ops entirely without a running loop, since self.sud.stop() cascades into further unguarded create_task() calls (on_state_changed, heater_task.shutdown(), ...) that a stopped loop can't help with anyway. Reproduced and verified fixed directly against HeaterTask/HeaterHendi with no event loop running (mirrors the exact server/brewpi.py shutdown-time call path that crashed on the brewpi Pi). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YaPLuRPpyjWcwhMvCvpHCL
44 lines
1.1 KiB
Python
44 lines
1.1 KiB
Python
import abc
|
|
import asyncio
|
|
from utils.value import AttributeChange
|
|
|
|
|
|
def fire_and_forget(coro):
|
|
"""asyncio.create_task(), tolerant of there being no running event loop.
|
|
An actor's observable attribute (e.g. Connectable.connected) can change
|
|
as a side effect of the server's own final best-effort hardware-release
|
|
cleanup (server/brewpi.py's `finally:` block - heater.close()/
|
|
stirrer.activate(False)), which runs after the loop has already
|
|
stopped. There's nobody left to notify at that point anyway, so this
|
|
just drops the message instead of crashing the shutdown sequence."""
|
|
try:
|
|
return asyncio.create_task(coro)
|
|
except RuntimeError:
|
|
coro.close()
|
|
return None
|
|
|
|
|
|
class ATask(AttributeChange):
|
|
def __init__(self, interval):
|
|
AttributeChange.__init__(self)
|
|
self.interval = interval
|
|
|
|
@abc.abstractmethod
|
|
def on_process(self):
|
|
pass
|
|
|
|
|
|
class TaskManager:
|
|
def __init__(self):
|
|
self.tasks = []
|
|
|
|
def add(self, task: ATask):
|
|
self.tasks.append(task)
|
|
|
|
def start(self):
|
|
funcs = []
|
|
for task in self.tasks:
|
|
funcs.append(task.on_process())
|
|
|
|
return asyncio.gather(*funcs)
|