Skip to content

Daemon PID file: remove hard dependency on pgrep - #12

Merged
pzfreo merged 2 commits into
mainfrom
feat/daemon-pid-file
May 28, 2026
Merged

Daemon PID file: remove hard dependency on pgrep#12
pzfreo merged 2 commits into
mainfrom
feat/daemon-pid-file

Conversation

@pzfreo

@pzfreo pzfreo commented May 28, 2026

Copy link
Copy Markdown
Contributor

Summary

PR #9 added _kill_local_daemon as the force-kill path for a wedged boocloud-bridge daemon, using pgrep -f to find the PID. That works on hosts with procps installed but fails silently on slim container images (e.g., python:3.12-slim, alpine, …) where pgrep isn't available — exactly the environment the Teleclaude agent container runs in.

This PR makes the force-kill path work without pgrep by recording the PID at spawn time.

Changes

src/boocloud/bridge.py

  • New _pid_file_path() — returns $XDG_RUNTIME_DIR/boocloud-bridge.pid when set (per-user, auto-cleared on logout), otherwise a per-user file in the system temp dir (/tmp/boocloud-bridge-{uid}.pid) so concurrent users on the same host don't collide.
  • New _write_pid_file / _read_pid_file / _clear_pid_file helpers — best-effort I/O, all failures logged at DEBUG and non-fatal.
  • _start_daemon captures proc.pid from subprocess.Popen and writes the PID file before the readiness-poll loop.
  • _kill_local_daemon now:
    1. Reads the PID file. If the recorded PID is still alive, target it. If it's gone (stale file), clear the file.
    2. Falls back to pgrep -f boocloud-bridge.*\bdaemon\b for daemons started outside _start_daemon (e.g., boocloud daemon CLI on a developer host with procps).
    3. If neither yields a PID, returns False cleanly. Previously this same case wrongly required pgrep and silently no-op'd.
    4. Skips the current process PID (defence-in-depth).
    5. Clears the PID file after a successful kill.

Why a file, not an in-process variable

The Python boocloud package is invoked as separate processes (CLI, MCP server, sometimes both). State has to outlive the invocation that spawned the daemon.

Test plan

  • pytest tests/test_bridge.py::TestKillLocalDaemon tests/test_bridge.py::TestPidFilePath — 7 passed.
  • pytest tests/test_bridge.py — 60 passed, 1 pre-existing failure unrelated to this PR (TestFindLocalBridge::test_returns_none_when_not_found doesn't patch /usr/local/bin/boocloud-bridge and fails on dev hosts where the binary is installed system-wide).
  • ruff check + ruff format --check clean.
  • Smoke: install in a container without pgrep, force-restart the daemon, confirm a fresh process appears.

🤖 Generated with Claude Code

PR #9 added _kill_local_daemon as the force-kill path for a wedged
boocloud-bridge daemon, using `pgrep -f` to find the PID. That works
on hosts with procps installed but fails silently on slim container
images (python:3.12-slim, alpine, …) where pgrep isn't available —
exactly the environment the Teleclaude agent runs in.

Wire _start_daemon to capture proc.pid and write it to a PID file
(XDG_RUNTIME_DIR if set, otherwise a per-user file in the system
temp dir so concurrent users don't collide). _kill_local_daemon
reads the PID file first, validates the process is still alive,
and falls back to pgrep for daemons started outside _start_daemon
(e.g., the `boocloud daemon` CLI on a developer host with procps).
Stale PID files are detected via os.kill(pid, 0) and cleared.

Tests cover: PID-file kill happy path; stale PID-file cleanup;
pgrep-fallback when no PID file; both-mechanisms-unavailable returns
False cleanly; own-PID is never targeted by the pgrep path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PR adds new tests that exercise _kill_local_daemon end-to-end on a
real PID; Windows CI surfaced three issues, two of which were
pre-existing portability bugs in the PR #9 code:

- `signal.SIGKILL` doesn't exist on Windows (only SIGTERM).
- `os.kill(pid, 0)` raises OSError on Windows, not ProcessLookupError.
- `os.getuid()` doesn't exist on Windows (used by _pid_file_path).

Fixes:
- _kill_local_daemon now returns False immediately on Windows. The
  README documents that Windows always uses Docker (no native daemon
  to kill), so this isn't a regression — just an honest no-op.
- On Unix, SIGKILL is looked up via getattr so the SIGTERM-only loop
  still works on platforms that lack SIGKILL.
- The liveness check (`os.kill(pid, 0)`) catches OSError too, so the
  parent kill loop doesn't crash if Windows ever reaches it.
- _pid_file_path drops the `-{uid}` suffix when os.getuid isn't
  available, so the helper still works on Windows (PID file write
  is platform-agnostic even if the kill path isn't).

Tests:
- TestKillLocalDaemon is skipif Windows (the path is documented as
  Unix-only and the unit tests rely on `sleep` + Unix process
  semantics that don't translate).
- TestPidFilePath gains test_falls_back_to_temp_without_getuid which
  monkeypatches getuid off to simulate the Windows path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@pzfreo
pzfreo merged commit d249201 into main May 28, 2026
12 checks passed
@pzfreo
pzfreo deleted the feat/daemon-pid-file branch May 28, 2026 16:47
@pzfreo pzfreo mentioned this pull request May 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant