aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-16 00:20:53 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-16 00:20:53 +0300
commite9cbcf2f6f8654dd41b10826ef6aecd526f6b0b0 (patch)
tree4a25668626326c4a678109183f7bf21ef4dc9fba /src
parent600753b47b75898c311ecfb625bd1887031b3475 (diff)
fix: handle pane graphics stream disconnect race
Diffstat (limited to 'src')
-rw-r--r--src/api/server/pane_graphics_stream.rs47
1 files changed, 35 insertions, 12 deletions
diff --git a/src/api/server/pane_graphics_stream.rs b/src/api/server/pane_graphics_stream.rs
index a58c0f08..747c0205 100644
--- a/src/api/server/pane_graphics_stream.rs
+++ b/src/api/server/pane_graphics_stream.rs
@@ -496,30 +496,41 @@ impl PollBackoff {
fn with_timed_reads<T>(
stream: &mut LocalStream,
- read: impl FnOnce(&mut LocalStream, ReadWait) -> std::io::Result<T>,
-) -> std::io::Result<T> {
+ read: impl FnOnce(&mut LocalStream, ReadWait) -> std::io::Result<Option<T>>,
+) -> std::io::Result<Option<T>> {
match stream.set_recv_timeout(Some(CONNECTION_POLL_INTERVAL)) {
Ok(()) => {
let result = read(stream, ReadWait::SocketTimeout);
- merge_read_reset_result(result, stream.set_recv_timeout(None))
+ finish_timed_read(result, || stream.set_recv_timeout(None))
}
Err(err) if err.kind() == io::ErrorKind::Unsupported => {
stream.set_nonblocking(true)?;
let result = read(stream, ReadWait::Poll(PollBackoff::new()));
- merge_read_reset_result(result, stream.set_nonblocking(false))
+ finish_timed_read(result, || stream.set_nonblocking(false))
}
+ // A peer can disconnect after the caller's running check but before
+ // setsockopt. macOS reports that closed-socket race as EINVAL.
+ Err(err) if err.kind() == io::ErrorKind::InvalidInput => Ok(None),
Err(err) => Err(err),
}
}
-fn merge_read_reset_result<T>(
- result: std::io::Result<T>,
- reset_result: std::io::Result<()>,
-) -> std::io::Result<T> {
- match (result, reset_result) {
- (Ok(value), Ok(())) => Ok(value),
- (Ok(_), Err(err)) => Err(err),
- (Err(err), _) => Err(err),
+fn finish_timed_read<T>(
+ result: std::io::Result<Option<T>>,
+ reset: impl FnOnce() -> std::io::Result<()>,
+) -> std::io::Result<Option<T>> {
+ match result {
+ // None is terminal for this dedicated stream. macOS returns EINVAL when
+ // socket options are restored after the peer has already disconnected.
+ Ok(None) => Ok(None),
+ Ok(value) => {
+ reset()?;
+ Ok(value)
+ }
+ Err(err) => {
+ let _ = reset();
+ Err(err)
+ }
}
}
@@ -599,6 +610,18 @@ mod tests {
assert!(owner.starts_with("pane.graphics.stream:"));
}
+ #[test]
+ fn timed_read_skips_reset_after_stream_ends() {
+ let mut reset_called = false;
+ let result = finish_timed_read::<()>(Ok(None), || {
+ reset_called = true;
+ Ok(())
+ });
+
+ assert!(result.unwrap().is_none());
+ assert!(!reset_called);
+ }
+
#[cfg(unix)]
#[test]
fn pane_graphics_stream_dispatches_binary_frames() {