authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 14:55:33-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-10-02 15:03:17-07:00
logac922b129d8b93bb2ff2565a0bcd42758be4a87a
tree59f3ec798f1881f91873b23b9632122904219566
parent7a9721e96ff69cc09d9bceaca1504d938b96b699
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU

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

3 files changed, 54 insertions(+), 11 deletions(-)

crates/notebook/src/smb/directory.rs+1-1
...@@ -140,7 +140,7 @@ impl Client {...@@ -140,7 +140,7 @@ impl Client {
140 {140 {
141 io::Error::new(io::ErrorKind::Unsupported, error)141 io::Error::new(io::ErrorKind::Unsupported, error)
142 }142 }
143 _ => io::Error::other(error),143 _ => super::io_error(error),
144 })?;144 })?;
145 // However the task ends, the connection failing or the client retiring its runtime,145 // However the task ends, the connection failing or the client retiring its runtime,
146 // `changed` hears that the watch did.146 // `changed` hears that the watch did.
crates/notebook/src/smb/mod.rs+22-10
...@@ -77,7 +77,7 @@ impl Client {...@@ -77,7 +77,7 @@ impl Client {
77 let mut connection = sign_in(address, &credentials, timeout).await?;77 let mut connection = sign_in(address, &credentials, timeout).await?;
78 let tree = Tree::connect(&mut connection, share)78 let tree = Tree::connect(&mut connection, share)
79 .await79 .await
80 .map_err(io::Error::other)?;80 .map_err(io_error)?;
81 Ok::<_, io::Error>((connection, tree))81 Ok::<_, io::Error>((connection, tree))
82 })82 })
83 .await83 .await
...@@ -150,7 +150,7 @@ impl Client {...@@ -150,7 +150,7 @@ impl Client {
150 };150 };
151 let frame = frame151 let frame = frame
152 .map_err(|_| io::Error::from(io::ErrorKind::TimedOut))152 .map_err(|_| io::Error::from(io::ErrorKind::TimedOut))
153 .and_then(|result| result.map_err(io::Error::other))153 .and_then(|result| result.map_err(io_error))
154 .inspect_err(|_| self.retire())?;154 .inspect_err(|_| self.retire())?;
155 self.unpack(&self.body(command, frame)?)155 self.unpack(&self.body(command, frame)?)
156 }156 }
...@@ -184,15 +184,13 @@ impl Client {...@@ -184,15 +184,13 @@ impl Client {
184 };184 };
185 let frames = frames185 let frames = frames
186 .map_err(|_| io::Error::from(io::ErrorKind::TimedOut))186 .map_err(|_| io::Error::from(io::ErrorKind::TimedOut))
187 .and_then(|result| result.map_err(io::Error::other))187 .and_then(|result| result.map_err(io_error))
188 .inspect_err(|_| self.retire())?;188 .inspect_err(|_| self.retire())?;
189 Ok(frames189 Ok(frames
190 .into_iter()190 .into_iter()
191 .zip(requests)191 .zip(requests)
192 .map(|(frame, (command, _))| {192 .map(|(frame, (command, _))| {
193 let frame = frame193 let frame = frame.map_err(io_error).inspect_err(|_| self.retire())?;
194 .map_err(io::Error::other)
195 .inspect_err(|_| self.retire())?;
196 self.body(*command, frame)194 self.body(*command, frame)
197 })195 })
198 .collect())196 .collect())
...@@ -568,7 +566,7 @@ pub fn shares(...@@ -568,7 +566,7 @@ pub fn shares(
568 let mut connection = sign_in(address, &credentials, timeout).await?;566 let mut connection = sign_in(address, &credentials, timeout).await?;
569 let shares = smb2::client::list_shares(&mut connection)567 let shares = smb2::client::list_shares(&mut connection)
570 .await568 .await
571 .map_err(io::Error::other);569 .map_err(io_error);
572 connection.mark_dead();570 connection.mark_dead();
573 shares571 shares
574 })572 })
...@@ -578,6 +576,20 @@ pub fn shares(...@@ -578,6 +576,20 @@ pub fn shares(
578 Ok(shares.into_iter().map(|share| share.name).collect())576 Ok(shares.into_iter().map(|share| share.name).collect())
579}577}
580578
579/// `error` inside an `io::Error` whose kind says whether the server was reached: a lost or
580/// unanswered connection is `NotConnected` or `TimedOut`, and a socket's failure keeps its kind.
581fn io_error(error: smb2::Error) -> io::Error {
582 let kind = match &error {
583 smb2::Error::Io(source) => source.kind(),
584 error => match error.kind() {
585 smb2::ErrorKind::ConnectionLost => io::ErrorKind::NotConnected,
586 smb2::ErrorKind::TimedOut => io::ErrorKind::TimedOut,
587 _ => io::ErrorKind::Other,
588 },
589 };
590 io::Error::new(kind, error)
591}
592
581/// A negotiated session with the server at `address`, signed in with `credentials`.593/// A negotiated session with the server at `address`, signed in with `credentials`.
582async fn sign_in(594async fn sign_in(
583 address: &str,595 address: &str,
...@@ -586,14 +598,14 @@ async fn sign_in(...@@ -586,14 +598,14 @@ async fn sign_in(
586) -> io::Result<Connection> {598) -> io::Result<Connection> {
587 let mut connection = Connection::connect(address, timeout)599 let mut connection = Connection::connect(address, timeout)
588 .await600 .await
589 .map_err(io::Error::other)?;601 .map_err(io_error)?;
590 connection.set_compression_requested(false);602 connection.set_compression_requested(false);
591 if let Err(error) = connection.negotiate().await {603 if let Err(error) = connection.negotiate().await {
592 connection.mark_dead();604 connection.mark_dead();
593 return Err(if speaks_only_smb1(address, timeout).await {605 return Err(if speaks_only_smb1(address, timeout).await {
594 io::Error::new(io::ErrorKind::Unsupported, Refusal::Smb1)606 io::Error::new(io::ErrorKind::Unsupported, Refusal::Smb1)
595 } else {607 } else {
596 io::Error::other(error)608 io_error(error)
597 });609 });
598 }610 }
599 Session::setup(611 Session::setup(
...@@ -603,7 +615,7 @@ async fn sign_in(...@@ -603,7 +615,7 @@ async fn sign_in(
603 credentials.domain,615 credentials.domain,
604 )616 )
605 .await617 .await
606 .map_err(io::Error::other)?;618 .map_err(io_error)?;
607 Ok(connection)619 Ok(connection)
608}620}
609621
crates/notebook/src/smb/tests.rs+31
...@@ -1047,3 +1047,34 @@ fn a_torn_read_is_contention() {...@@ -1047,3 +1047,34 @@ fn a_torn_read_is_contention() {
1047 let error = snapshot(torn, 1 << 20).unwrap_err();1047 let error = snapshot(torn, 1 << 20).unwrap_err();
1048 assert_eq!(error.kind(), io::ErrorKind::WouldBlock);1048 assert_eq!(error.kind(), io::ErrorKind::WouldBlock);
1049}1049}
1050
1051#[test]
1052fn unreachable_server_is_not_connected() {
1053 use crate::session::{SyncState, SyncStatus};
1054 let state = |error: smb2::Error| {
1055 SyncStatus {
1056 synced: Some(1),
1057 error: Some(io_error(error)),
1058 queued: 1,
1059 }
1060 .state()
1061 };
1062 for kind in [
1063 io::ErrorKind::ConnectionRefused,
1064 io::ErrorKind::HostUnreachable,
1065 io::ErrorKind::TimedOut,
1066 ] {
1067 assert_eq!(
1068 state(smb2::Error::Io(kind.into())),
1069 SyncState::NotConnected,
1070 "{kind}"
1071 );
1072 }
1073 assert_eq!(state(smb2::Error::Disconnected), SyncState::NotConnected);
1074 assert_eq!(state(smb2::Error::Timeout), SyncState::NotConnected);
1075 let denied = smb2::Error::Protocol {
1076 status: smb2::types::status::NtStatus(0xc0000022),
1077 command: Command::Create,
1078 };
1079 assert_eq!(io_error(denied).kind(), io::ErrorKind::Other);
1080}