From ac922b129d8b93bb2ff2565a0bcd42758be4a87a Mon Sep 17 00:00:00 2001 From: clover caruso Date: Fri, 2 Oct 2026 14:55:33 -0700 Subject: [PATCH] fix: A server that drops or refuses the connection reads as Not connected, not Unable to sync SMB errors were wrapped as io::ErrorKind::Other, so a server that went away (smb2's Disconnected, a refused or unreachable socket, a timeout) showed as a failed sync instead of not connected, and the background sync did not take it for a lost connection. They now keep a kind that says so. Assisted-by: claude-opus-5.5 --- crates/notebook/src/smb/directory.rs | 2 +- crates/notebook/src/smb/mod.rs | 32 +++++++++++++++++++--------- crates/notebook/src/smb/tests.rs | 31 +++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 11 deletions(-) diff --git a/crates/notebook/src/smb/directory.rs b/crates/notebook/src/smb/directory.rs index 7c125c4aa98f6bbfa89237cdfccccd1cec6ef2e3..69c0f2f7635ee71670c976fd74f8586110975580 100644 --- a/crates/notebook/src/smb/directory.rs +++ b/crates/notebook/src/smb/directory.rs @@ -140,7 +140,7 @@ impl Client { { io::Error::new(io::ErrorKind::Unsupported, error) } - _ => io::Error::other(error), + _ => super::io_error(error), })?; // However the task ends, the connection failing or the client retiring its runtime, // `changed` hears that the watch did. diff --git a/crates/notebook/src/smb/mod.rs b/crates/notebook/src/smb/mod.rs index a5ca48a740bd257b1905793625813b357f67186a..c7376e942a4eba856ea34b0ce1c5e62836eca439 100644 --- a/crates/notebook/src/smb/mod.rs +++ b/crates/notebook/src/smb/mod.rs @@ -77,7 +77,7 @@ impl Client { let mut connection = sign_in(address, &credentials, timeout).await?; let tree = Tree::connect(&mut connection, share) .await - .map_err(io::Error::other)?; + .map_err(io_error)?; Ok::<_, io::Error>((connection, tree)) }) .await @@ -150,7 +150,7 @@ impl Client { }; let frame = frame .map_err(|_| io::Error::from(io::ErrorKind::TimedOut)) - .and_then(|result| result.map_err(io::Error::other)) + .and_then(|result| result.map_err(io_error)) .inspect_err(|_| self.retire())?; self.unpack(&self.body(command, frame)?) } @@ -184,15 +184,13 @@ impl Client { }; let frames = frames .map_err(|_| io::Error::from(io::ErrorKind::TimedOut)) - .and_then(|result| result.map_err(io::Error::other)) + .and_then(|result| result.map_err(io_error)) .inspect_err(|_| self.retire())?; Ok(frames .into_iter() .zip(requests) .map(|(frame, (command, _))| { - let frame = frame - .map_err(io::Error::other) - .inspect_err(|_| self.retire())?; + let frame = frame.map_err(io_error).inspect_err(|_| self.retire())?; self.body(*command, frame) }) .collect()) @@ -568,7 +566,7 @@ pub fn shares( let mut connection = sign_in(address, &credentials, timeout).await?; let shares = smb2::client::list_shares(&mut connection) .await - .map_err(io::Error::other); + .map_err(io_error); connection.mark_dead(); shares }) @@ -578,6 +576,20 @@ pub fn shares( Ok(shares.into_iter().map(|share| share.name).collect()) } +/// `error` inside an `io::Error` whose kind says whether the server was reached: a lost or +/// unanswered connection is `NotConnected` or `TimedOut`, and a socket's failure keeps its kind. +fn io_error(error: smb2::Error) -> io::Error { + let kind = match &error { + smb2::Error::Io(source) => source.kind(), + error => match error.kind() { + smb2::ErrorKind::ConnectionLost => io::ErrorKind::NotConnected, + smb2::ErrorKind::TimedOut => io::ErrorKind::TimedOut, + _ => io::ErrorKind::Other, + }, + }; + io::Error::new(kind, error) +} + /// A negotiated session with the server at `address`, signed in with `credentials`. async fn sign_in( address: &str, @@ -586,14 +598,14 @@ async fn sign_in( ) -> io::Result { let mut connection = Connection::connect(address, timeout) .await - .map_err(io::Error::other)?; + .map_err(io_error)?; connection.set_compression_requested(false); if let Err(error) = connection.negotiate().await { connection.mark_dead(); return Err(if speaks_only_smb1(address, timeout).await { io::Error::new(io::ErrorKind::Unsupported, Refusal::Smb1) } else { - io::Error::other(error) + io_error(error) }); } Session::setup( @@ -603,7 +615,7 @@ async fn sign_in( credentials.domain, ) .await - .map_err(io::Error::other)?; + .map_err(io_error)?; Ok(connection) } diff --git a/crates/notebook/src/smb/tests.rs b/crates/notebook/src/smb/tests.rs index d2bb9a6a4bcdfcc2666e183476a5aa7c1d593228..519ee28aa4819d8dfc57cab84863f280256b3872 100644 --- a/crates/notebook/src/smb/tests.rs +++ b/crates/notebook/src/smb/tests.rs @@ -1047,3 +1047,34 @@ fn a_torn_read_is_contention() { let error = snapshot(torn, 1 << 20).unwrap_err(); assert_eq!(error.kind(), io::ErrorKind::WouldBlock); } + +#[test] +fn unreachable_server_is_not_connected() { + use crate::session::{SyncState, SyncStatus}; + let state = |error: smb2::Error| { + SyncStatus { + synced: Some(1), + error: Some(io_error(error)), + queued: 1, + } + .state() + }; + for kind in [ + io::ErrorKind::ConnectionRefused, + io::ErrorKind::HostUnreachable, + io::ErrorKind::TimedOut, + ] { + assert_eq!( + state(smb2::Error::Io(kind.into())), + SyncState::NotConnected, + "{kind}" + ); + } + assert_eq!(state(smb2::Error::Disconnected), SyncState::NotConnected); + assert_eq!(state(smb2::Error::Timeout), SyncState::NotConnected); + let denied = smb2::Error::Protocol { + status: smb2::types::status::NtStatus(0xc0000022), + command: Command::Create, + }; + assert_eq!(io_error(denied).kind(), io::ErrorKind::Other); +} -- 2.54.0