feat(restart): route reverse tunnels through stale-forward cleanup

bridge restart now means blank-slate recovery: reverse tunnels run
should_cleanup_tunnel and clear orphan remote listeners before reconnecting;
healthy forwards are left running. Local-direction tunnels keep stop/start
only. CLI and MCP report per-tunnel actions (healthy, cleaned_and_restarted,
restarted, error) and exit non-zero on cleanup failure.

Closes BRIDGE-WP-0005.
This commit is contained in:
tegwick 2026-06-21 20:12:13 +02:00
parent 8c11acc00c
commit 10c6fdaec9
8 changed files with 220 additions and 60 deletions

View file

@ -13,10 +13,13 @@ import typer
from bridge.audit import AuditLogger
from bridge.cleanup import (
CleanupAction,
build_cron_line,
cleanup_all_tunnels,
install_cleanup_cron,
read_installed_cron,
restart_all_tunnels,
restart_tunnel,
uninstall_cleanup_cron,
)
from bridge.config import ConfigError, load_config
@ -153,27 +156,37 @@ def down(
raise typer.Exit(2)
def _emit_restart_actions(actions: list[CleanupAction]) -> None:
any_error = False
for action in actions:
typer.echo(f"{action.tunnel}: {action.action}{action.detail}")
if action.action == "error":
any_error = True
if any_error:
raise typer.Exit(1)
@app.command()
def restart(
tunnel: Optional[str] = typer.Argument(None, help="Tunnel name (omit for all inline)"),
):
"""Restart one or all tunnels."""
"""Restart one or all tunnels.
Reverse tunnels run conditional remote stale-forward cleanup before
reconnecting; healthy forwards are left running. Local-direction tunnels
use local stop/start only.
"""
cfg = _load_or_exit()
sd = _state_dir()
state_mgr = StateManager(state_dir=sd)
if tunnel:
tcfg = _resolve_tunnel(cfg, tunnel)
mgr = TunnelManager(tcfg, state_dir=sd)
mgr.stop()
mgr.start()
typer.echo(f"Restarted tunnel '{tunnel}'.")
actions = [restart_tunnel(tcfg, state_mgr)]
else:
for name in _all_tunnel_names(cfg):
tcfg = cfg.tunnels[name]
mgr = TunnelManager(tcfg, state_dir=sd)
mgr.stop()
mgr.start()
typer.echo(f"Restarted tunnel '{name}'.")
actions = restart_all_tunnels(cfg, state_mgr)
_emit_restart_actions(actions)
@app.command()