Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions headroom/install/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,26 @@ def _read_pid(profile: str) -> int | None:
return None


def _pid_alive(pid: int) -> bool:
"""Return True if ``pid`` names a live process."""

if pid <= 0:
return False
try:
import psutil

return bool(psutil.pid_exists(pid))
except Exception:
pass
try:
os.kill(pid, 0)
except PermissionError:
return True
except (ProcessLookupError, OSError, SystemError):
return False
return True


def _clear_pid(profile: str) -> None:
path = pid_path(profile)
if path.exists():
Expand Down
38 changes: 37 additions & 1 deletion tests/test_install/test_runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
_clear_pid,
_deployment_env,
_mount_source,
_pid_alive,
_read_pid,
_runtime_env,
_write_pid,
Expand Down Expand Up @@ -253,6 +254,34 @@ def test_write_read_and_clear_pid(monkeypatch, tmp_path: Path) -> None:
assert _read_pid("default") is None


def test_pid_alive_uses_psutil_without_signal(monkeypatch) -> None:
fake_psutil = types.ModuleType("psutil")
fake_psutil.pid_exists = lambda pid: True if pid > 0 else False # type: ignore[attr-defined]
monkeypatch.setitem(sys.modules, "psutil", fake_psutil)
monkeypatch.setattr(
"headroom.install.runtime.os.kill",
lambda pid, sig: (_ for _ in ()).throw(AssertionError("os.kill should not run")),
)

assert _pid_alive(123) is True


def test_pid_alive_falls_back_and_handles_systemerror(monkeypatch) -> None:
fake_psutil = types.ModuleType("psutil")

def _raise_pid_exists(pid: int) -> bool:
raise RuntimeError("boom")

fake_psutil.pid_exists = _raise_pid_exists # type: ignore[attr-defined]
monkeypatch.setitem(sys.modules, "psutil", fake_psutil)
monkeypatch.setattr(
"headroom.install.runtime.os.kill",
lambda pid, sig: (_ for _ in ()).throw(SystemError("WinError 87")),
)

assert _pid_alive(123) is False


def test_runtime_start_lock_is_nonblocking(monkeypatch, tmp_path: Path) -> None:
monkeypatch.setattr(Path, "home", lambda: tmp_path)

Expand All @@ -272,7 +301,12 @@ def test_runtime_start_lock_blocks_another_process(monkeypatch, tmp_path: Path)
"with acquire_runtime_start_lock('default') as acquired:\n"
" print(acquired)\n"
)
env = {**os.environ, "HOME": str(tmp_path), "PYTHONPATH": str(Path.cwd())}
env = {
**os.environ,
"HOME": str(tmp_path),
"USERPROFILE": str(tmp_path),
"PYTHONPATH": str(Path.cwd()),
}

with acquire_runtime_start_lock("default") as acquired:
assert acquired is True
Expand Down Expand Up @@ -519,6 +553,8 @@ def __init__(self, stdout: str = "") -> None:
assert runtime_status(python_manifest) == "stopped"

_write_pid("default", 125)
monkeypatch.setattr("headroom.install.runtime.pid_alive", lambda pid: True)
assert runtime_status(python_manifest) == "running"
monkeypatch.setattr("headroom.install.runtime.pid_alive", lambda pid: False)
assert runtime_status(python_manifest) == "stopped"

Expand Down
Loading