feat(viewer): time out the initial connect instead of hanging forever

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 <noreply@anthropic.com>
This commit is contained in:
2026-05-25 02:25:17 -04:00
parent 90e0dc8621
commit 0187bc9bcf
+34 -2
View File
@@ -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?;