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:
+34
-2
@@ -1,12 +1,19 @@
|
|||||||
use anyhow::{Context, Result};
|
use anyhow::{Context, Result, bail};
|
||||||
use iroh::Endpoint;
|
use iroh::Endpoint;
|
||||||
use iroh::endpoint::presets;
|
use iroh::endpoint::presets;
|
||||||
use iroh_tickets::endpoint::EndpointTicket;
|
use iroh_tickets::endpoint::EndpointTicket;
|
||||||
|
use std::time::Duration;
|
||||||
use tokio::net::TcpListener;
|
use tokio::net::TcpListener;
|
||||||
|
|
||||||
use crate::cli::ViewerOpts;
|
use crate::cli::ViewerOpts;
|
||||||
use crate::common::{alpn::ALPN, output, signal};
|
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<()> {
|
pub async fn run(ticket: EndpointTicket, opts: ViewerOpts) -> Result<()> {
|
||||||
let cancel = signal::install_ctrl_c();
|
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();
|
let addr = ticket.endpoint_addr().clone();
|
||||||
tracing::info!(remote = %addr.id, "connecting to host");
|
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 (quic_send, quic_recv) = conn.open_bi().await?;
|
||||||
|
|
||||||
let listener = TcpListener::bind(("127.0.0.1", opts.port)).await?;
|
let listener = TcpListener::bind(("127.0.0.1", opts.port)).await?;
|
||||||
|
|||||||
Reference in New Issue
Block a user