diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index d02f97d..4f60259 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -12,7 +12,17 @@ jobs: runs-on: ubuntu-latest container: rust:1 steps: - - uses: actions/checkout@v5 + # Not the checkout action: it runs on node, which the rust:1 job + # container doesn't have and this runner doesn't inject (the v5 + # run died with 'exec: "node": not found'). Plain git needs + # neither, and the token URL works against the private repo. + - name: checkout + run: | + git init -q . + git fetch --depth 1 \ + "https://gitea:${{ secrets.GITHUB_TOKEN }}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git" \ + "$GITHUB_SHA" + git checkout -q FETCH_HEAD - name: rustfmt run: | rustup component add rustfmt diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 1f79170..c477836 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -19,7 +19,17 @@ jobs: matrix: target: [x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu] steps: - - uses: actions/checkout@v5 + # Not the checkout action: it runs on node, which the rust:1 job + # container doesn't have and this runner doesn't inject (the v5 + # run died with 'exec: "node": not found'). Plain git needs + # neither, and the token URL works against the private repo. + - name: checkout + run: | + git init -q . + git fetch --depth 1 \ + "https://gitea:${{ secrets.GITHUB_TOKEN }}@${GITHUB_SERVER_URL#https://}/${GITHUB_REPOSITORY}.git" \ + "$GITHUB_SHA" + git checkout -q FETCH_HEAD - name: install tooling run: | diff --git a/varde-daemon/src/transfer.rs b/varde-daemon/src/transfer.rs index 78b7494..e977d27 100644 --- a/varde-daemon/src/transfer.rs +++ b/varde-daemon/src/transfer.rs @@ -7,6 +7,7 @@ use std::path::Path; use std::sync::Arc; +use std::time::Duration; use anyhow::{Context, Result}; use iroh::address_lookup::memory::MemoryLookup; @@ -170,11 +171,16 @@ impl Transfer { !self.metered.is_metered(), "metered connection: refusing to push" ); - let conn = self - .endpoint - .connect(peer, iroh_blobs::ALPN) - .await - .with_context(|| format!("connecting to {peer}"))?; + // Every await here is bounded: this runs inside a socket-API + // request, and a wedged peer must produce a structured error, + // not a hung client connection. + let conn = tokio::time::timeout( + Duration::from_secs(30), + self.endpoint.connect(peer, iroh_blobs::ALPN), + ) + .await + .map_err(|_| anyhow::anyhow!("connecting to {peer} timed out"))? + .with_context(|| format!("connecting to {peer}"))?; let request = match content.format { BlobFormat::Raw => { iroh_blobs::protocol::PushRequest::from(GetRequest::blob(content.hash)) @@ -240,7 +246,8 @@ impl Transfer { } /// Watch `hash` on the remote end of `conn` until its bitfield - /// reports the blob complete. + /// reports the blob complete. Bounded: the peer has already received + /// the bytes, so verification is bookkeeping, not transfer. async fn wait_remote_complete( &self, conn: &iroh::endpoint::Connection, @@ -251,13 +258,22 @@ impl Transfer { iroh_blobs::protocol::ObserveRequest::new(hash), ); let mut observe = std::pin::pin!(observe); - while let Some(bitfield) = observe.next().await { - let bitfield = bitfield.context("observing pushed content on the peer")?; - if bitfield.is_complete() { - return Ok(()); + loop { + let next = tokio::time::timeout(Duration::from_secs(60), observe.next()) + .await + .map_err(|_| anyhow::anyhow!("verifying pushed content on the peer timed out"))?; + match next { + Some(bitfield) => { + let bitfield = bitfield.context("observing pushed content on the peer")?; + if bitfield.is_complete() { + return Ok(()); + } + } + None => { + anyhow::bail!("peer stopped reporting before the pushed content completed") + } } } - anyhow::bail!("peer stopped reporting before the pushed content completed"); } /// Produce a ticket for out-of-band sharing of `content`.