From db6ca6ee57c30843e24245d62fc26dfbe4c3dc58 Mon Sep 17 00:00:00 2001 From: curo1305 Date: Tue, 19 May 2026 15:54:56 +0200 Subject: [PATCH] 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 --- src/pyra/daemon/core.py | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/src/pyra/daemon/core.py b/src/pyra/daemon/core.py index 894ea6f..d516d14 100644 --- a/src/pyra/daemon/core.py +++ b/src/pyra/daemon/core.py @@ -68,6 +68,22 @@ class PluginSupervisor: if tasks: 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]: return [ { @@ -131,8 +147,8 @@ def _make_ipc_handler(supervisor: PluginSupervisor): supervisor.request_shutdown() return {"ok": True, "data": {}} case "reload": - _log.info("Reload requested via IPC.") - return {"ok": True, "data": {}} + await supervisor.reload() + return {"ok": True, "data": {"tasks_reloaded": len(supervisor._records)}} case _: return {"ok": False, "data": {"error": f"unknown command: {cmd}"}} @@ -196,13 +212,17 @@ def run_foreground() -> None: _start_time = time.monotonic() - with pid_file: - _log.info("Pyra daemon starting (PID %d).", os.getpid()) - try: - asyncio.run(_run_daemon(cfg, supervisor)) - except KeyboardInterrupt: - pass - _log.info("Pyra daemon stopped.") + try: + with pid_file: + _log.info("Pyra daemon starting (PID %d).", os.getpid()) + try: + asyncio.run(_run_daemon(cfg, supervisor)) + except KeyboardInterrupt: + 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) ─────────────────────────────────────