orchestrata_ultimate.refactor.codemod.daemon
Reference › orchestrata_ultimate › refactor › codemod › daemon
Lifecycle management for the codemod daemon, which parses a workspace once, serves it over a Unix socket and watches for file changes. scripts.codemod uses CodemodDaemon for its start, stop and status commands; the process it launches runs _daemon_entry.
The daemon is started only through a systemd user unit template (ADR-0067: daemons execute deployed code only). start(repo_root) creates the state directory, takes a file lock so two starts cannot race, refuses if a daemon already runs for that repository, checks that the deployed launcher bin/codemod-daemon exists under the tools prefix (raising RuntimeError and spawning nothing if it does not), starts the unit instance named by _unit_instance (derived with systemd-escape), waits for its main process id and records it in the PID file. stop stops the unit, with a legacy path that sends SIGTERM and then SIGKILL after a timeout; restart combines the two; status reports whether the daemon runs. _pid_is_codemod_daemon checks that a PID really belongs to a codemod daemon for the repo before it is acted on, and stale PID files are removed.
At a glance
| Source | src/orchestrata_ultimate/refactor/codemod/daemon.py |
| Lines of code | 578 |
| Classes | 1 |
| Functions | 2 |
| Imports (internal) | orchestrata_ultimate.core.base.traces, orchestrata_ultimate.refactor.codemod._daemon_entry |
| Imported by | scripts.codemod |
| Tests that import it | tests/refactor/codemod/test_daemon_coverage.py, tests/refactor/codemod/test_daemon_lifecycle.py, tests/scripts/test_codemod_cli_daemon.py |
Class diagram
Dependency diagram
API
Classes
CodemodDaemon
Decorators: traces(spec='110', story='US-110-013', reqs=['REQ-110-133', 'REQ-110-134', 'REQ-110-135', 'REQ-110-136', 'REQ-110-137']) · Line 46
Manages the codemod daemon lifecycle.
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(*, tools_prefix: Path = DEFAULT_TOOLS_PREFIX, unit_name: str = DEFAULT_UNIT_NAME, shutdown_timeout: float = SHUTDOWN_TIMEOUT) -> None |
Initialize CodemodDaemon with no active process. |
_unit_instance |
_unit_instance(repo_root: Path) -> str |
The systemd instance serving repo_root: unit@<escaped path>.service. |
_unit_loaded |
_unit_loaded(instance: str) -> bool |
True when systemd has instance loaded (its template is installed). |
_systemctl staticmethod |
_systemctl(**args: str) -> subprocess.CompletedProcess[str] |
Run systemctl --user <args>; a missing binary reports rc 127. |
_unit_property |
_unit_property(instance: str, prop: str) -> str | None |
systemctl --user show -p <prop> --value for instance, or None. |
_unit_show |
_unit_show(instance: str, **props: str) -> dict[str, str] |
One systemctl --user show -p <prop>... call parsed to key/value. |
_wait_for_main_pid |
_wait_for_main_pid(instance: str) -> int |
Poll the unit's MainPID until it is non-zero (bounded). |
_settle_after_start |
_settle_after_start(instance: str, pid: int) -> None |
Hold START_SETTLE_SECONDS and fail if the unit leaves active. |
_refuse_if_running |
_refuse_if_running(instance: str, repo_root: Path) -> None |
Raise when a daemon already serves this repo; clear a stale PID file. |
_remove_stale_pid_file |
_remove_stale_pid_file() -> None |
Drop a PID file that names no live daemon. |
start |
start(repo_root: Path) -> None |
Start the codemod daemon through its systemd template instance. |
stop |
stop() -> None |
Stop the codemod daemon gracefully. |
status |
status() -> dict |
Return daemon status. |
restart |
restart() -> None |
Restart the daemon (stop then start). |
_read_pid |
_read_pid() -> int | None |
Read PID from the PID file. |
_write_pid |
_write_pid(pid: int) -> None |
Write PID to the PID file. |
_acquire_lock |
_acquire_lock() -> int | None |
Acquire the lock file. |
_release_lock |
_release_lock(fd: int) -> None |
Release the lock file. |
_cleanup_files |
_cleanup_files() -> None |
Remove PID and socket files. |
Functions
| Function | Signature | Summary |
|---|---|---|
_pid_is_codemod_daemon traces |
_pid_is_codemod_daemon(pid: int, repo_root: Path) -> bool |
True when pid's argv runs the codemod daemon entry module for repo_root. |
_process_alive traces |
_process_alive(pid: int) -> bool |
Check if a process with the given PID is alive. |
Source
"""SPEC-110 codemod engine — daemon lifecycle management.
Manages the long-running codemod daemon process that keeps the workspace
in memory between invocations. Handles start/stop/status/restart with
PID files, lock files, and graceful shutdown.
ADR-0067: the daemon executes DEPLOYED code only. It is never spawned from
this checkout's interpreter; ``start`` goes through the
``orchestrata-codemod-daemon@<escaped repo root>.service`` systemd template
whose ``ExecStart`` is ``<tools_prefix>/bin/codemod-daemon`` (written by
``deploy/install-tools.sh``), and refuses when that launcher is missing.
"""
from __future__ import annotations
import fcntl
import logging
import os
import signal
import subprocess
import time
from pathlib import Path
from orchestrata_ultimate.core.base.traces import traces
from orchestrata_ultimate.refactor.codemod._daemon_entry import read_deployed_commit
logger = logging.getLogger(__name__)
DAEMON_DIR = ".codemod_daemon"
SOCKET_NAME = "socket"
PID_NAME = "pid"
LOCK_NAME = "lock"
SHUTDOWN_TIMEOUT = 5.0
MAINPID_TIMEOUT = 10.0
START_SETTLE_SECONDS = 1.0
DEFAULT_TOOLS_PREFIX = Path("/opt/orchestrata-tools")
DEFAULT_UNIT_NAME = "orchestrata-codemod-daemon"
_DAEMON_ENTRY_MODULE = "orchestrata_ultimate.refactor.codemod._daemon_entry"
@traces(
spec="110",
story="US-110-013",
reqs=["REQ-110-133", "REQ-110-134", "REQ-110-135", "REQ-110-136", "REQ-110-137"],
)
class CodemodDaemon:
"""Manages the codemod daemon lifecycle.
The daemon parses the workspace once at startup, creates a Unix socket
for JSON-RPC communication, and watches for file changes via inotify.
"""
def __init__(
self,
*,
tools_prefix: Path = DEFAULT_TOOLS_PREFIX,
unit_name: str = DEFAULT_UNIT_NAME,
shutdown_timeout: float = SHUTDOWN_TIMEOUT,
) -> None:
"""Initialize CodemodDaemon with no active process.
Args:
tools_prefix: The deployed-tools prefix (``deploy/install-tools.sh``
writes ``bin/codemod-daemon`` and ``VERSION`` under it).
unit_name: The systemd template name (instance ``unit@<path>``).
shutdown_timeout: Seconds a legacy (non-unit) daemon gets after
SIGTERM before SIGKILL.
"""
self.shutdown_timeout = shutdown_timeout
self._repo_root: Path | None = None
self._daemon_dir: Path | None = None
self._pid_file: Path | None = None
self._lock_file: Path | None = None
self._socket_path: Path | None = None
self.tools_prefix = tools_prefix
self.unit_name = unit_name
self.launcher = tools_prefix / "bin" / "codemod-daemon"
@traces(spec="110", story="US-110-013", reqs=["REQ-110-135"])
def _unit_instance(self, repo_root: Path) -> str:
"""The systemd instance serving *repo_root*: ``unit@<escaped path>.service``."""
try:
escaped = subprocess.run(
["systemd-escape", "--path", str(repo_root)],
capture_output=True,
text=True,
check=True,
).stdout.strip()
except FileNotFoundError as e:
raise RuntimeError(
"systemd-escape is not installed; the codemod daemon is "
f"started through systemd units only ({e})"
) from e
except subprocess.CalledProcessError as e:
raise RuntimeError(
f"systemd-escape --path {repo_root} failed "
f"(rc {e.returncode}): {e.stderr.strip()}"
) from e
return f"{self.unit_name}@{escaped}.service"
@traces(spec="110", story="US-110-013", reqs=["REQ-110-135"])
def _unit_loaded(self, instance: str) -> bool:
"""True when systemd has *instance* loaded (its template is installed)."""
return self._unit_property(instance, "LoadState") == "loaded"
@staticmethod
@traces(spec="110", story="US-110-013", reqs=["REQ-110-135"])
def _systemctl(*args: str) -> subprocess.CompletedProcess[str]:
"""Run ``systemctl --user <args>``; a missing binary reports rc 127."""
try:
return subprocess.run(
["systemctl", "--user", *args],
capture_output=True,
text=True,
check=False,
)
except FileNotFoundError as e:
return subprocess.CompletedProcess(
["systemctl", "--user", *args], 127, "", str(e)
)
@traces(spec="110", story="US-110-013", reqs=["REQ-110-135"])
def _unit_property(self, instance: str, prop: str) -> str | None:
"""``systemctl --user show -p <prop> --value`` for *instance*, or None."""
result = self._systemctl("show", "-p", prop, "--value", instance)
value = result.stdout.strip()
return value if result.returncode == 0 and value else None
@traces(spec="110", story="US-110-013", reqs=["REQ-110-135"])
def _unit_show(self, instance: str, *props: str) -> dict[str, str]:
"""One ``systemctl --user show -p <prop>...`` call parsed to key/value."""
args: list[str] = []
for prop in props:
args.extend(["-p", prop])
result = self._systemctl("show", *args, instance)
parsed: dict[str, str] = {}
if result.returncode == 0:
for line in result.stdout.splitlines():
key, sep, val = line.partition("=")
if sep:
parsed[key] = val.strip()
return parsed
@traces(spec="110", story="US-110-013", reqs=["REQ-110-135"])
def _wait_for_main_pid(self, instance: str) -> int:
"""Poll the unit's MainPID until it is non-zero (bounded).
Each poll reads MainPID, ActiveState and SubState in one ``show`` call.
For a ``Type=simple`` unit ``systemctl start`` returns once the main
process is forked, so a zero MainPID with ActiveState ``failed`` or
``inactive``, or SubState ``auto-restart``, means the first process is
already gone: that is reported at once. A unit in ``auto-restart``
forks again after ``RestartSec``, but that is a second start, not this
one.
"""
deadline = time.monotonic() + MAINPID_TIMEOUT
while True:
props = self._unit_show(instance, "MainPID", "ActiveState", "SubState")
main_pid = int(props.get("MainPID") or 0)
if main_pid != 0:
return main_pid
state = props.get("ActiveState")
if (
state in {"failed", "inactive"}
or props.get("SubState") == "auto-restart"
):
raise RuntimeError(f"daemon {instance} failed before serving ({state})")
if time.monotonic() >= deadline:
raise RuntimeError(
f"daemon {instance} failed before serving "
f"(no MainPID within {MAINPID_TIMEOUT:.0f}s, ActiveState={state})"
)
time.sleep(0.1)
@traces(spec="110", story="US-110-013", reqs=["REQ-110-135"])
def _settle_after_start(self, instance: str, pid: int) -> None:
"""Hold ``START_SETTLE_SECONDS`` and fail if the unit leaves ``active``.
For a launcher that exits at once, the first ``show`` after
``systemctl start`` still reports the dead process's MainPID because
systemd has not reaped it yet, so a non-zero MainPID alone does not
prove the start. The settle has the same shape as the promoter's P6b
verify hold: poll ActiveState and SubState every 0.1 s and raise the
moment the unit is not ``active`` or is in ``auto-restart``.
"""
deadline = time.monotonic() + START_SETTLE_SECONDS
while True:
props = self._unit_show(instance, "ActiveState", "SubState")
state = props.get("ActiveState")
if state != "active" or props.get("SubState") == "auto-restart":
raise RuntimeError(f"daemon {instance} failed before serving ({state})")
if time.monotonic() >= deadline:
return
time.sleep(0.1)
@traces(spec="110", story="US-110-013", reqs=["REQ-110-135"])
def _refuse_if_running(self, instance: str, repo_root: Path) -> None:
"""Raise when a daemon already serves this repo; clear a stale PID file.
The same unit-first rule ``status`` applies: when the systemd instance
is loaded, its ``MainPID`` decides and the PID file is never consulted
(it can be stale after a reboot or a unit restart, pointing at an
unrelated live pid); a stale file under a loaded unit is removed. Only
an unloaded unit falls back to the PID file, and then only a pid that
really is a codemod daemon counts as running.
Raises:
RuntimeError: A daemon is already running for this repo.
"""
if self._unit_loaded(instance):
main_pid = self._unit_property(instance, "MainPID")
pid = int(main_pid) if main_pid and int(main_pid) != 0 else None
if pid is not None and _process_alive(pid):
raise RuntimeError(
f"Codemod daemon already running (PID {pid}) "
f"for {self._repo_root}"
)
self._remove_stale_pid_file()
return
existing_pid = self._read_pid()
if existing_pid is None:
return
if _pid_is_codemod_daemon(existing_pid, repo_root):
raise RuntimeError(
f"Codemod daemon already running (PID {existing_pid}) "
f"for {self._repo_root}"
)
self._remove_stale_pid_file()
@traces(spec="110", story="US-110-013", reqs=["REQ-110-135"])
def _remove_stale_pid_file(self) -> None:
"""Drop a PID file that names no live daemon."""
if self._pid_file is not None and self._pid_file.exists():
logger.info("Removing stale PID file %s", self._pid_file)
try:
self._pid_file.unlink()
except OSError as e:
logger.warning("Error removing %s: %s", self._pid_file, e)
@traces(spec="110", story="US-110-013", reqs=["REQ-110-135"])
def start(self, repo_root: Path) -> None:
"""Start the codemod daemon through its systemd template instance.
The deployed launcher parses the workspace, creates the Unix socket,
and starts the inotify watcher; this method records the unit's
MainPID in the PID file (the entry point does not write it).
Args:
repo_root: Repository root directory.
Raises:
RuntimeError: If a daemon is already running for this repo, the
deployed launcher is missing (nothing is spawned then), or
the unit fails to start.
"""
self._repo_root = repo_root.resolve()
self._daemon_dir = self._repo_root / DAEMON_DIR
self._pid_file = self._daemon_dir / PID_NAME
self._lock_file = self._daemon_dir / LOCK_NAME
self._socket_path = self._daemon_dir / SOCKET_NAME
self._daemon_dir.mkdir(parents=True, exist_ok=True)
lock_fd = self._acquire_lock()
if lock_fd is None:
raise RuntimeError(
f"Cannot acquire lock for {self._repo_root}; "
"another daemon may be starting"
)
try:
# The refusal runs UNDER the lock: two concurrent starts cannot
# both pass the check, and nothing is spawned (no `systemctl
# start`) before both the refusal and the launcher check pass.
instance = self._unit_instance(self._repo_root)
self._refuse_if_running(instance, self._repo_root)
if not self.launcher.exists():
raise RuntimeError(
f"codemod daemon is not deployed: {self.launcher} is "
"missing — run deploy/install-tools.sh (ADR-0067: daemons "
"execute deployed code only)"
)
started = self._systemctl("start", instance)
if started.returncode != 0:
raise RuntimeError(
f"systemctl --user start {instance} failed "
f"(rc {started.returncode}): {started.stderr.strip()}"
)
pid = self._wait_for_main_pid(instance)
self._settle_after_start(instance, pid)
self._write_pid(pid)
logger.info(
"Codemod daemon started (PID %d) as %s for %s",
pid,
instance,
self._repo_root,
)
finally:
self._release_lock(lock_fd)
@traces(spec="110", story="US-110-013", reqs=["REQ-110-135"])
def stop(self) -> None:
"""Stop the codemod daemon gracefully.
When the systemd instance is loaded, ``systemctl --user stop`` is the
only stop: after it succeeds the files are cleaned up and NO pid is
signalled (the PID file can be stale after a unit restart, and a
recycled pid is not ours to kill). Only when the unit is not loaded
does a live PID-file pid — a daemon started before the unit-only
launch — get SIGTERM, a wait of ``shutdown_timeout``, then SIGKILL.
Raises:
RuntimeError: ``systemctl stop`` failed for a loaded unit.
"""
if self._pid_file is None:
if self._repo_root is not None:
self._daemon_dir = self._repo_root / DAEMON_DIR
self._pid_file = self._daemon_dir / PID_NAME
self._socket_path = self._daemon_dir / SOCKET_NAME
else:
logger.warning("No repo root set; cannot stop daemon")
return
if self._repo_root is not None:
instance = self._unit_instance(self._repo_root)
if self._unit_loaded(instance):
stopped = self._systemctl("stop", instance)
if stopped.returncode != 0:
raise RuntimeError(
f"systemctl --user stop {instance} failed "
f"(rc {stopped.returncode}): {stopped.stderr.strip()}"
)
self._cleanup_files()
logger.info("Codemod daemon unit %s stopped", instance)
return
pid = self._read_pid()
if pid is None:
logger.info("No daemon PID file found")
return
if not _process_alive(pid):
logger.info("Daemon PID %d is not running; cleaning up", pid)
self._cleanup_files()
return
if self._repo_root is None or not _pid_is_codemod_daemon(pid, self._repo_root):
logger.warning(
"PID %d in the PID file is not a codemod daemon; "
"removing the stale file, signalling nothing",
pid,
)
self._cleanup_files()
return
logger.info("Sending SIGTERM to daemon PID %d", pid)
try:
os.kill(pid, signal.SIGTERM)
except ProcessLookupError:
logger.info("Daemon PID %d already exited", pid)
self._cleanup_files()
return
deadline = time.monotonic() + self.shutdown_timeout
while time.monotonic() < deadline:
if not _process_alive(pid):
break
time.sleep(0.2)
else:
logger.warning(
"Daemon PID %d did not exit in %.1fs; sending SIGKILL",
pid,
self.shutdown_timeout,
)
try:
os.kill(pid, signal.SIGKILL)
except ProcessLookupError:
pass
time.sleep(0.5)
self._cleanup_files()
logger.info("Codemod daemon stopped (PID %d)", pid)
@traces(spec="110", story="US-110-013", reqs=["REQ-110-135"])
def status(self) -> dict:
"""Return daemon status.
Returns:
Dict with pid, running, socket_path, the systemd ``unit`` instance
and its ``active_state``, the deployment ``identity`` (code root,
deployed commit, whether the launcher exists), and uptime if
available.
"""
identity = {
"code_root": str(self.tools_prefix / "lib"),
"deployed_commit": read_deployed_commit(self.tools_prefix / "VERSION"),
"deployed": self.launcher.exists(),
}
if self._pid_file is None:
if self._repo_root is not None:
self._daemon_dir = self._repo_root / DAEMON_DIR
self._pid_file = self._daemon_dir / PID_NAME
self._socket_path = self._daemon_dir / SOCKET_NAME
else:
return {
"running": False,
"pid": None,
"socket_path": None,
"unit": None,
"active_state": None,
"identity": identity,
}
instance = (
self._unit_instance(self._repo_root)
if self._repo_root is not None
else None
)
if instance is not None and self._unit_loaded(instance):
# The unit is the source of truth: a systemd restart changes the
# MainPID without touching the PID file.
main_pid = self._unit_property(instance, "MainPID")
pid = int(main_pid) if main_pid and int(main_pid) != 0 else None
else:
pid = self._read_pid()
running = pid is not None and _process_alive(pid)
socket_exists = self._socket_path is not None and self._socket_path.exists()
result: dict = {
"running": running,
"pid": pid,
"socket_path": str(self._socket_path) if self._socket_path else None,
"socket_exists": socket_exists,
"unit": instance,
"active_state": (
self._unit_property(instance, "ActiveState") if instance else None
),
"identity": identity,
}
if running and pid is not None:
try:
import psutil
proc = psutil.Process(pid)
create_time = proc.create_time()
result["uptime_seconds"] = time.time() - create_time
result["memory_rss"] = proc.memory_info().rss
except (ImportError, Exception) as e:
logger.debug("Could not get process info: %s", e)
return result
@traces(spec="110", story="US-110-013", reqs=["REQ-110-135"])
def restart(self) -> None:
"""Restart the daemon (stop then start)."""
if self._repo_root is None:
raise RuntimeError("No repo root set; cannot restart daemon")
repo_root = self._repo_root
self.stop()
self.start(repo_root)
@traces(spec="110", story="US-110-013", reqs=["REQ-110-134"])
def _read_pid(self) -> int | None:
"""Read PID from the PID file."""
if self._pid_file is None or not self._pid_file.exists():
return None
try:
text = self._pid_file.read_text(encoding="utf-8").strip()
return int(text)
except (ValueError, OSError) as e:
logger.warning("Invalid PID file: %s", e)
return None
@traces(spec="110", story="US-110-013", reqs=["REQ-110-134"])
def _write_pid(self, pid: int) -> None:
"""Write PID to the PID file."""
if self._pid_file is not None:
self._pid_file.write_text(str(pid), encoding="utf-8")
@traces(spec="110", story="US-110-013", reqs=["REQ-110-134"])
def _acquire_lock(self) -> int | None:
"""Acquire the lock file. Returns file descriptor or None on failure."""
if self._lock_file is None:
return None
try:
fd = os.open(str(self._lock_file), os.O_WRONLY | os.O_CREAT, 0o644)
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
return fd
except OSError as e:
logger.debug("Lock acquisition failed: %s", e)
return None
@traces(spec="110", story="US-110-013", reqs=["REQ-110-134"])
def _release_lock(self, fd: int) -> None:
"""Release the lock file."""
try:
fcntl.flock(fd, fcntl.LOCK_UN)
os.close(fd)
except OSError as e:
logger.warning("Error releasing lock: %s", e)
@traces(spec="110", story="US-110-013", reqs=["REQ-110-135"])
def _cleanup_files(self) -> None:
"""Remove PID and socket files."""
for path in (self._pid_file, self._socket_path, self._lock_file):
if path is not None and path.exists():
try:
path.unlink()
except OSError as e:
logger.warning("Error removing %s: %s", path, e)
@traces(spec="110", story="US-110-013", reqs=["REQ-110-135"])
def _pid_is_codemod_daemon(pid: int, repo_root: Path) -> bool:
"""True when *pid*'s argv runs the codemod daemon entry module for *repo_root*.
The deployed launcher execs ``python -m <_DAEMON_ENTRY_MODULE> <repo_root>
<socket> <pid_file>`` (the systemd template passes ``/%I`` first), so the
match is the argv ELEMENT run ``-m <module> <repo_root>``: the module name
directly after ``-m`` and the resolved repo root directly after it. A
substring match is not identity (any process that merely mentions the
module in an argument would qualify), and a daemon serving a DIFFERENT
repo is not ours to claim or signal; an unreadable ``/proc`` entry (the
pid is gone) is not a daemon. A RELATIVE repo root in the argv is the
daemon's own cwd's business, so it resolves against ``/proc/<pid>/cwd``,
never against the caller's cwd; the cwd link is read ONLY then (an
absolute root needs no cwd), and an unreadable cwd link on a relative
root is the same fail-safe ``False``.
"""
try:
raw = Path(f"/proc/{pid}/cmdline").read_bytes()
except OSError:
return False
argv = [a for a in raw.split(b"\0") if a]
module = _DAEMON_ENTRY_MODULE.encode()
want = repo_root.resolve()
for i in range(len(argv) - 2):
if argv[i] == b"-m" and argv[i + 1] == module:
root = Path(os.fsdecode(argv[i + 2]))
if not root.is_absolute():
try:
root = Path(os.readlink(f"/proc/{pid}/cwd")) / root
except OSError:
return False
return root.resolve() == want
return False
@traces(spec="110", story="US-110-013", reqs=["REQ-110-135"])
def _process_alive(pid: int) -> bool:
"""Check if a process with the given PID is alive.
A zombie (exited but not yet reaped) counts as dead: os.kill(pid, 0)
succeeds on zombies, which previously made stop() wait out the full
shutdown timeout and SIGKILL a process that had already exited
cleanly. If the zombie is our own child, reap it.
"""
try:
os.kill(pid, 0)
except ProcessLookupError, PermissionError:
return False
try:
stat = Path(f"/proc/{pid}/stat").read_text(encoding="utf-8")
# State is the first field after the closing paren of comm (which
# may itself contain spaces or parens).
state = stat[stat.rindex(")") + 2]
except OSError, ValueError, IndexError:
return True
if state == "Z":
try:
os.waitpid(pid, os.WNOHANG)
except ChildProcessError, OSError:
pass # Not our child; nothing to reap.
return False
return True