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?;