feat: add tunnel start/stop with PID file tracking

Implements build_autossh_cmd, start_tunnel, and stop_tunnel in process.py.
Adds _reap() helper to handle zombie processes in test environments where
the parent process is long-lived (SIGKILL leaves zombies unless waitpid is called).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-21 12:19:06 +08:00
parent 8a2e4b6cc0
commit 5218431349
2 changed files with 187 additions and 0 deletions

View File

@@ -101,3 +101,88 @@ def test_read_pid_file_corrupt(config_dir):
pid_path.write_text("only-one-line\n")
with pytest.raises(click.ClickException, match="Corrupt PID file"):
read_pid_file(config_dir, "web-service")
# ---------------------------------------------------------------------------
# Task 4: build_autossh_cmd, start_tunnel, stop_tunnel
# ---------------------------------------------------------------------------
import shutil
import signal
import subprocess
import time
from autossh_mgr.config import TunnelConfig
from autossh_mgr.process import start_tunnel, stop_tunnel, build_autossh_cmd
def test_build_autossh_cmd_basic(sample_tunnel):
cmd = build_autossh_cmd(sample_tunnel)
assert cmd[0] == "autossh"
assert "-M" in cmd and "0" in cmd
assert "-N" in cmd
assert "-i" in cmd
assert os.path.expanduser(sample_tunnel.identity_file) in cmd
assert "-p" in cmd and str(sample_tunnel.port) in cmd
assert "-R" in cmd
r_idx = cmd.index("-R")
assert cmd[r_idx + 1] == (
f"{sample_tunnel.remote_host}:{sample_tunnel.remote_port}"
f":{sample_tunnel.local_host}:{sample_tunnel.local_port}"
)
assert f"{sample_tunnel.user}@{sample_tunnel.host}" in cmd
def test_build_autossh_cmd_with_ssh_options():
t = TunnelConfig(
name="test", host="h.com", user="u",
local_port=80, remote_port=8080,
ssh_options="-o StrictHostKeyChecking=no",
)
cmd = build_autossh_cmd(t)
assert "-o" in cmd
assert "StrictHostKeyChecking=no" in cmd
def test_start_tunnel_no_autossh(config_dir, sample_tunnel, monkeypatch):
monkeypatch.setattr(shutil, "which", lambda _: None)
import click
with pytest.raises(click.ClickException, match="autossh not found"):
start_tunnel(config_dir, sample_tunnel)
def test_start_creates_pid_file(config_dir, sample_tunnel, monkeypatch, tmp_path):
# Replace autossh with a real long-lived process for testing
fake = tmp_path / "autossh"
fake.write_text("#!/bin/sh\nexec sleep 60\n")
fake.chmod(0o755)
monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
pid = start_tunnel(config_dir, sample_tunnel)
try:
assert pid > 0
result = read_pid_file(config_dir, "web-service")
assert result is not None
assert result[0] == pid
assert is_process_alive(pid)
finally:
os.kill(pid, signal.SIGKILL)
def test_stop_tunnel(config_dir, sample_tunnel, monkeypatch, tmp_path):
fake = tmp_path / "autossh"
fake.write_text("#!/bin/sh\nexec sleep 60\n")
fake.chmod(0o755)
monkeypatch.setenv("PATH", f"{tmp_path}:{os.environ['PATH']}")
pid = start_tunnel(config_dir, sample_tunnel)
assert is_process_alive(pid)
stop_tunnel(config_dir, "web-service")
time.sleep(0.2)
assert not is_process_alive(pid)
assert read_pid_file(config_dir, "web-service") is None
def test_stop_tunnel_stale_pid(config_dir):
write_pid_file(config_dir, "web-service", 9999999, datetime.now(timezone.utc))
stop_tunnel(config_dir, "web-service") # must not raise
assert read_pid_file(config_dir, "web-service") is None