feat: add PID file utilities and tunnel status detection
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
87
src/autossh_mgr/process.py
Normal file
87
src/autossh_mgr/process.py
Normal file
@@ -0,0 +1,87 @@
|
||||
import os
|
||||
import signal
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import click
|
||||
|
||||
|
||||
@dataclass
|
||||
class TunnelStatus:
|
||||
state: str # "running", "stopped", "stale"
|
||||
pid: Optional[int] = None
|
||||
uptime: Optional[str] = None
|
||||
|
||||
|
||||
def _pid_path(config_dir: Path, name: str) -> Path:
|
||||
return config_dir / "pids" / f"{name}.pid"
|
||||
|
||||
|
||||
def write_pid_file(config_dir: Path, name: str, pid: int, started_at: datetime) -> None:
|
||||
path = _pid_path(config_dir, name)
|
||||
path.write_text(f"{pid}\n{started_at.isoformat()}\n")
|
||||
with open(path, "a") as f:
|
||||
os.fsync(f.fileno())
|
||||
|
||||
|
||||
def read_pid_file(config_dir: Path, name: str) -> Optional[tuple[int, datetime]]:
|
||||
path = _pid_path(config_dir, name)
|
||||
if not path.exists():
|
||||
return None
|
||||
lines = path.read_text().splitlines()
|
||||
if len(lines) < 2:
|
||||
raise click.ClickException(
|
||||
f"Corrupt PID file for '{name}', try: autossh-mgr stop {name}"
|
||||
)
|
||||
return int(lines[0]), datetime.fromisoformat(lines[1])
|
||||
|
||||
|
||||
def delete_pid_file(config_dir: Path, name: str) -> None:
|
||||
path = _pid_path(config_dir, name)
|
||||
path.unlink(missing_ok=True)
|
||||
|
||||
|
||||
def is_process_alive(pid: int) -> bool:
|
||||
try:
|
||||
os.kill(pid, 0)
|
||||
return True
|
||||
except (ProcessLookupError, PermissionError):
|
||||
return False
|
||||
|
||||
|
||||
def check_stale(config_dir: Path, name: str) -> bool:
|
||||
result = read_pid_file(config_dir, name)
|
||||
if result is None:
|
||||
return False
|
||||
pid, _ = result
|
||||
return not is_process_alive(pid)
|
||||
|
||||
|
||||
def format_uptime(started_at: datetime) -> str:
|
||||
delta = datetime.now(timezone.utc) - started_at
|
||||
total = int(delta.total_seconds())
|
||||
days, rem = divmod(total, 86400)
|
||||
hours, rem = divmod(rem, 3600)
|
||||
minutes, seconds = divmod(rem, 60)
|
||||
parts = []
|
||||
if days:
|
||||
parts.append(f"{days}d")
|
||||
if hours:
|
||||
parts.append(f"{hours}h")
|
||||
if minutes:
|
||||
parts.append(f"{minutes}m")
|
||||
if not parts:
|
||||
parts.append(f"{seconds}s")
|
||||
return " ".join(parts)
|
||||
|
||||
|
||||
def get_status(config_dir: Path, name: str) -> TunnelStatus:
|
||||
result = read_pid_file(config_dir, name)
|
||||
if result is None:
|
||||
return TunnelStatus(state="stopped")
|
||||
pid, started_at = result
|
||||
if not is_process_alive(pid):
|
||||
return TunnelStatus(state="stale", pid=pid)
|
||||
return TunnelStatus(state="running", pid=pid, uptime=format_uptime(started_at))
|
||||
Reference in New Issue
Block a user