feat(daemon): implement reload, fix PID race condition

- PluginSupervisor.reload(): cancels all running plugin tasks, resets
  restart counters, and re-creates them with fresh coroutines
- IPC reload command now calls supervisor.reload() instead of being a stub
- run_foreground(): wrap PID file acquisition in try/except PidFileError
  to produce a clean error if two daemon starts race on the PID file

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
curo1305
2026-05-19 15:54:56 +02:00
parent cc24257ab0
commit db6ca6ee57
+29 -9
View File
@@ -68,6 +68,22 @@ class PluginSupervisor:
if tasks: if tasks:
await asyncio.gather(*tasks, return_exceptions=True) await asyncio.gather(*tasks, return_exceptions=True)
async def reload(self) -> None:
"""Cancel all running tasks and restart them with fresh coroutines."""
for record in self._records:
if record.task and not record.task.done():
record.task.cancel()
try:
await record.task
except (asyncio.CancelledError, Exception):
pass
record.restart_count = 0
record.last_error = None
record.task = asyncio.create_task(
self._supervise(record), name=record.name
)
_log.info("Reloaded %d plugin task(s).", len(self._records))
def status(self) -> list[dict]: def status(self) -> list[dict]:
return [ return [
{ {
@@ -131,8 +147,8 @@ def _make_ipc_handler(supervisor: PluginSupervisor):
supervisor.request_shutdown() supervisor.request_shutdown()
return {"ok": True, "data": {}} return {"ok": True, "data": {}}
case "reload": case "reload":
_log.info("Reload requested via IPC.") await supervisor.reload()
return {"ok": True, "data": {}} return {"ok": True, "data": {"tasks_reloaded": len(supervisor._records)}}
case _: case _:
return {"ok": False, "data": {"error": f"unknown command: {cmd}"}} return {"ok": False, "data": {"error": f"unknown command: {cmd}"}}
@@ -196,13 +212,17 @@ def run_foreground() -> None:
_start_time = time.monotonic() _start_time = time.monotonic()
with pid_file: try:
_log.info("Pyra daemon starting (PID %d).", os.getpid()) with pid_file:
try: _log.info("Pyra daemon starting (PID %d).", os.getpid())
asyncio.run(_run_daemon(cfg, supervisor)) try:
except KeyboardInterrupt: asyncio.run(_run_daemon(cfg, supervisor))
pass except KeyboardInterrupt:
_log.info("Pyra daemon stopped.") pass
_log.info("Pyra daemon stopped.")
except PidFileError as exc:
_log.error("Could not acquire PID file: %s", exc)
sys.exit(1)
# ── Background spawn (pyra daemon start) ───────────────────────────────────── # ── Background spawn (pyra daemon start) ─────────────────────────────────────