From 0187bc9bcf610bbfd3333bc0ba7683521a7eda4a Mon Sep 17 00:00:00 2001 From: Mollusk Date: Mon, 25 May 2026 02:25:17 -0400 Subject: [PATCH] feat(viewer): time out the initial connect instead of hanging forever MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit endpoint.connect() has no built-in deadline, so an offline host, a stale share code, or an unreachable relay left the viewer spinning silently with no feedback — surfacing in the GUI as a permanent "Connecting…" with no error. Wrap the connect in a 15s tokio::time::timeout (matching the host's online() cap) and race it against ctrl-c, bailing with an actionable message. The error reaches stderr, so the GUI's ChildProc stderr-tail path renders it on the viewer screen. Co-Authored-By: Claude Opus 4.7 --- src/viewer/mod.rs | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/src/viewer/mod.rs b/src/viewer/mod.rs index bba5720..d6bc081 100644 --- a/src/viewer/mod.rs +++ b/src/viewer/mod.rs @@ -1,12 +1,19 @@ -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; use iroh::Endpoint; use iroh::endpoint::presets; use iroh_tickets::endpoint::EndpointTicket; +use std::time::Duration; use tokio::net::TcpListener; use crate::cli::ViewerOpts; use crate::common::{alpn::ALPN, output, signal}; +/// Cap on the initial QUIC connect. `endpoint.connect()` has no built-in +/// deadline, so an offline host / stale code / unreachable relay otherwise +/// hangs forever with no feedback (the silent "connecting…" failure mode). +/// Matches the host's 15s `online()` cap. +const CONNECT_TIMEOUT: Duration = Duration::from_secs(15); + pub async fn run(ticket: EndpointTicket, opts: ViewerOpts) -> Result<()> { let cancel = signal::install_ctrl_c(); @@ -17,7 +24,32 @@ pub async fn run(ticket: EndpointTicket, opts: ViewerOpts) -> Result<()> { let addr = ticket.endpoint_addr().clone(); tracing::info!(remote = %addr.id, "connecting to host"); - let conn = endpoint.connect(addr, ALPN).await?; + + // Bound the connect attempt and let ctrl-c abort it, so the viewer fails + // loud (and the GUI surfaces the error) instead of spinning indefinitely. + let conn = tokio::select! { + _ = cancel.cancelled() => { + tracing::info!("ctrl-c received before the connection was established"); + endpoint.close().await; + return Ok(()); + } + result = tokio::time::timeout(CONNECT_TIMEOUT, endpoint.connect(addr, ALPN)) => match result { + Ok(Ok(conn)) => conn, + Ok(Err(e)) => { + endpoint.close().await; + bail!("failed to connect to the host: {e:#}"); + } + Err(_) => { + endpoint.close().await; + bail!( + "couldn't reach the host within {}s — it may be offline, the share \ + code may be stale, or the relay may be unreachable. Check that the \ + host is running, then re-copy the code and try again.", + CONNECT_TIMEOUT.as_secs() + ); + } + }, + }; let (quic_send, quic_recv) = conn.open_bi().await?; let listener = TcpListener::bind(("127.0.0.1", opts.port)).await?;