feat: add PID file utilities and tunnel status detection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 12:10:37 +08:00
parent 62414399b1
commit 6416f8a8e2
2 changed files with 184 additions and 0 deletions

View 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))