generated from coulomb/repo-seed
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:
@@ -2,7 +2,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import subprocess
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional
|
||||
from urllib.parse import urlparse, urlunparse
|
||||
@@ -215,6 +214,27 @@ def cleanup_tunnel(
|
||||
return CleanupAction(name, "error", str(exc))
|
||||
|
||||
|
||||
def restart_tunnel(
|
||||
cfg: TunnelConfig,
|
||||
state_mgr: StateManager,
|
||||
) -> CleanupAction:
|
||||
"""Restart one tunnel with blank-slate recovery for reverse tunnels."""
|
||||
if cfg.direction == "local":
|
||||
mgr = TunnelManager(cfg, state_dir=state_mgr._dir)
|
||||
mgr.stop()
|
||||
mgr.start()
|
||||
return CleanupAction(cfg.name, "restarted", "local tunnel stop/start")
|
||||
return cleanup_tunnel(cfg, state_mgr, restart=True)
|
||||
|
||||
|
||||
def restart_all_tunnels(
|
||||
cfg,
|
||||
state_mgr: StateManager,
|
||||
) -> list[CleanupAction]:
|
||||
"""Restart every inline tunnel (reverse via cleanup path, local via stop/start)."""
|
||||
return [restart_tunnel(tcfg, state_mgr) for tcfg in cfg.tunnels.values()]
|
||||
|
||||
|
||||
def cleanup_all_tunnels(
|
||||
cfg,
|
||||
state_mgr: StateManager,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -169,19 +169,22 @@ def bridge_down(tunnel: Optional[str] = None) -> dict:
|
||||
def bridge_restart(tunnel: Optional[str] = None) -> dict:
|
||||
"""Restart one or all configured tunnels.
|
||||
|
||||
Reverse tunnels run conditional remote stale-forward cleanup before
|
||||
reconnecting; healthy forwards are left running.
|
||||
|
||||
Args:
|
||||
tunnel: Tunnel name to restart. If omitted, restarts all inline tunnels.
|
||||
|
||||
Returns:
|
||||
{"restarted": [...]} or {"error": "..."}
|
||||
{"actions": [{"tunnel", "action", "detail"}, ...]} or {"error": "..."}
|
||||
"""
|
||||
cfg, err = _load_cfg_or_error()
|
||||
if err:
|
||||
return err
|
||||
|
||||
from bridge.manager import TunnelManager
|
||||
from bridge.cleanup import restart_all_tunnels, restart_tunnel
|
||||
sd = _state_dir()
|
||||
restarted = []
|
||||
state_mgr = StateManager(state_dir=sd)
|
||||
|
||||
if tunnel:
|
||||
from bridge.catalog.loader import load_catalog
|
||||
@@ -196,18 +199,19 @@ def bridge_restart(tunnel: Optional[str] = None) -> dict:
|
||||
tcfg = resolve(tunnel, catalog=catalog, inline_tunnels=cfg.tunnels)
|
||||
except BridgeNotFound:
|
||||
return {"error": f"Tunnel '{tunnel}' not found in config or catalog"}
|
||||
mgr = TunnelManager(tcfg, state_dir=sd)
|
||||
mgr.stop()
|
||||
mgr.start()
|
||||
restarted.append(tunnel)
|
||||
actions = [restart_tunnel(tcfg, state_mgr)]
|
||||
else:
|
||||
for name, tcfg in cfg.tunnels.items():
|
||||
mgr = TunnelManager(tcfg, state_dir=sd)
|
||||
mgr.stop()
|
||||
mgr.start()
|
||||
restarted.append(name)
|
||||
actions = restart_all_tunnels(cfg, state_mgr)
|
||||
|
||||
return {"restarted": restarted}
|
||||
payload = {
|
||||
"actions": [
|
||||
{"tunnel": a.tunnel, "action": a.action, "detail": a.detail}
|
||||
for a in actions
|
||||
],
|
||||
}
|
||||
if any(a.action == "error" for a in actions):
|
||||
payload["error"] = "one or more tunnels failed to restart"
|
||||
return payload
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
|
||||
Reference in New Issue
Block a user