fix: improve tunnel diagnostics and add --no-autoupdate
- Remove --rm from docker run so failed containers persist for inspection - Add --no-autoupdate flag to prevent cloudflared from exiting on auto-update - Capture both stdout and stderr from docker logs - Check container exit code during wait loop; fail fast with logs if container exits early - Include exit code in timeout error message for easier debugging
This commit is contained in:
@@ -380,6 +380,3 @@ def find_free_port(start: int = 10000, end: int = 20000) -> int:
|
||||
return port
|
||||
|
||||
raise RuntimeError(f"No free port found in range {start}-{end}")
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -39,7 +39,48 @@ def _ensure_image() -> None:
|
||||
logger.warning("Failed to pull %s: %s", TUNNEL_IMAGE, pull.stderr)
|
||||
|
||||
|
||||
def start_tunnel(instance_name: str, published_port: int, timeout: int = 30) -> dict[str, str]:
|
||||
def _cleanup_stale_tunnel(tunnel_name: str) -> None:
|
||||
"""Remove any existing tunnel container with this name."""
|
||||
subprocess.run(
|
||||
["docker", "stop", "-t", "3", tunnel_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["docker", "rm", "-f", tunnel_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
|
||||
def _get_container_logs(tunnel_name: str) -> tuple[str, str]:
|
||||
"""Get stdout and stderr logs from a container."""
|
||||
result = subprocess.run(
|
||||
["docker", "logs", tunnel_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.stdout, result.stderr
|
||||
|
||||
|
||||
def _get_container_exit_code(tunnel_name: str) -> int | None:
|
||||
"""Get exit code of a container if it has exited."""
|
||||
result = subprocess.run(
|
||||
["docker", "inspect", "-f", "{{.State.ExitCode}}", tunnel_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
try:
|
||||
return int(result.stdout.strip())
|
||||
except ValueError:
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
def start_tunnel(
|
||||
instance_name: str, published_port: int, timeout: int = 30
|
||||
) -> dict[str, str]:
|
||||
"""Start a temporary Cloudflare tunnel for an instance.
|
||||
|
||||
Args:
|
||||
@@ -53,60 +94,69 @@ def start_tunnel(instance_name: str, published_port: int, timeout: int = 30) ->
|
||||
_ensure_image()
|
||||
|
||||
tunnel_name = _tunnel_container_name(instance_name)
|
||||
|
||||
# Clean up any stale tunnel container with this name
|
||||
subprocess.run(
|
||||
["docker", "stop", "-t", "3", tunnel_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
subprocess.run(
|
||||
["docker", "rm", tunnel_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
_cleanup_stale_tunnel(tunnel_name)
|
||||
|
||||
cmd = [
|
||||
"docker", "run", "-d", "--rm",
|
||||
"--network", "host",
|
||||
"--name", tunnel_name,
|
||||
"docker",
|
||||
"run",
|
||||
"-d",
|
||||
"--network",
|
||||
"host",
|
||||
"--name",
|
||||
tunnel_name,
|
||||
TUNNEL_IMAGE,
|
||||
"tunnel", "--url", f"http://localhost:{published_port}",
|
||||
"tunnel",
|
||||
"--no-autoupdate",
|
||||
"--url",
|
||||
f"http://localhost:{published_port}",
|
||||
]
|
||||
|
||||
logger.debug("Running: %s", " ".join(cmd))
|
||||
proc = subprocess.run(cmd, capture_output=True, text=True)
|
||||
if proc.returncode != 0:
|
||||
raise RuntimeError(
|
||||
f"Failed to start tunnel container {tunnel_name}: {proc.stderr}"
|
||||
)
|
||||
|
||||
container_id = proc.stdout.strip()
|
||||
logger.debug("Tunnel container started: %s", container_id)
|
||||
|
||||
# Wait for URL to appear in logs
|
||||
url_pattern = re.compile(r"https://[a-z0-9-]+\.trycloudflare\.com")
|
||||
start_time = __import__("time").time()
|
||||
url: str | None = None
|
||||
logs_result = subprocess.run(
|
||||
["docker", "logs", tunnel_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
combined_logs = ""
|
||||
|
||||
while __import__("time").time() - start_time < timeout:
|
||||
match = url_pattern.search(logs_result.stdout)
|
||||
stdout, stderr = _get_container_logs(tunnel_name)
|
||||
combined_logs = stdout + "\n" + stderr
|
||||
|
||||
match = url_pattern.search(combined_logs)
|
||||
if match:
|
||||
url = match.group(0)
|
||||
break
|
||||
|
||||
# Check if container exited early
|
||||
exit_code = _get_container_exit_code(tunnel_name)
|
||||
if exit_code is not None and exit_code != 0:
|
||||
_cleanup_stale_tunnel(tunnel_name)
|
||||
raise RuntimeError(
|
||||
f"Tunnel container {tunnel_name} exited with code {exit_code}. "
|
||||
f"Logs:\n{combined_logs[-3000:]}"
|
||||
)
|
||||
|
||||
__import__("time").sleep(0.5)
|
||||
logs_result = subprocess.run(
|
||||
["docker", "logs", tunnel_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if not url:
|
||||
subprocess.run(["docker", "stop", "-t", "3", tunnel_name], capture_output=True)
|
||||
# Capture final state for debugging
|
||||
stdout, stderr = _get_container_logs(tunnel_name)
|
||||
combined_logs = stdout + "\n" + stderr
|
||||
exit_code = _get_container_exit_code(tunnel_name)
|
||||
|
||||
_cleanup_stale_tunnel(tunnel_name)
|
||||
raise RuntimeError(
|
||||
f"Tunnel {tunnel_name} did not produce a URL within {timeout}s. "
|
||||
f"cloudflared logs:\n{logs_result.stdout[-2000:]}"
|
||||
f"Exit code: {exit_code}. Logs:\n{combined_logs[-3000:]}"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
@@ -120,14 +170,10 @@ def start_tunnel(instance_name: str, published_port: int, timeout: int = 30) ->
|
||||
|
||||
|
||||
def stop_tunnel(instance_name: str) -> None:
|
||||
"""Stop the tunnel container for an instance."""
|
||||
"""Stop and remove the tunnel container for an instance."""
|
||||
tunnel_name = _tunnel_container_name(instance_name)
|
||||
subprocess.run(
|
||||
["docker", "stop", "-t", "5", tunnel_name],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
logger.debug("Stopped tunnel container %s", tunnel_name)
|
||||
_cleanup_stale_tunnel(tunnel_name)
|
||||
logger.debug("Stopped and removed tunnel container %s", tunnel_name)
|
||||
|
||||
|
||||
def recreate_tunnel(instance_name: str, published_port: int) -> dict[str, str]:
|
||||
|
||||
Reference in New Issue
Block a user