fix(remote): restore blocking bridge streams (#2485)

refs #2478

Co-authored-by: akbash-bot <300245827+akbash-bot@users.noreply.github.com>
This commit is contained in:
akbash 2026-08-07 18:59:04 +03:00 committed by GitHub
parent 79a953e91e
commit 374d3eddc5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 54 additions and 0 deletions

View File

@ -8,6 +8,7 @@
- The plugin marketplace now discovers valid manifests at repository roots and subdirectories, groups multiple plugins under each repository, and publishes their versions and exact default-branch commits.
### Fixed
- macOS `herdr --remote` clients now keep the accepted bridge socket blocking, preventing an immediate disconnect after the protocol handshake. (#2478, thanks @mathijshenquet)
- Stable direct installs, self-updates, and remote helper downloads now require and verify the SHA-256 digest published for each GitHub release asset.
- Configs containing the retired Herdr-written `ui.agent_panel_scope` setting no longer report it as an unknown key after upgrades. (#2292)
- Claude Code confirmation prompts using `Enter to confirm · Esc to cancel` now report `blocked` instead of `idle`. (#2268)

View File

@ -1696,6 +1696,16 @@ impl SshStdioBridge {
while !thread_stop.load(Ordering::Acquire) {
match listener.accept() {
Ok(stream) => {
let stream = match prepare_remote_bridge_stream(stream) {
Ok(stream) => stream,
Err(err) => {
tracing::error!(
error = %err,
"remote bridge failed to prepare client socket"
);
continue;
}
};
if let Err(err) = bridge_connection(
stream,
&target,
@ -1727,6 +1737,13 @@ impl SshStdioBridge {
}
}
fn prepare_remote_bridge_stream(
mut stream: crate::ipc::LocalStream,
) -> io::Result<crate::ipc::LocalStream> {
crate::ipc::set_local_stream_polling(&mut stream, false)?;
Ok(stream)
}
impl Drop for SshStdioBridge {
fn drop(&mut self) {
self.should_stop.store(true, Ordering::Release);
@ -2201,6 +2218,42 @@ mod tests {
let _ = std::fs::remove_file(socket);
}
#[cfg(unix)]
#[test]
fn accepted_bridge_stream_is_reset_to_blocking() {
use std::os::fd::AsRawFd as _;
fn is_nonblocking(stream: &crate::ipc::LocalStream) -> bool {
let fd = match stream {
crate::ipc::LocalStream::UdSocket(stream) => stream.inner().as_raw_fd(),
};
// SAFETY: F_GETFL only reads flags from the live descriptor owned by `stream`.
let flags = unsafe { libc::fcntl(fd, libc::F_GETFL) };
assert!(flags >= 0, "fcntl(F_GETFL): {}", io::Error::last_os_error());
flags & libc::O_NONBLOCK != 0
}
let socket = std::env::temp_dir().join(format!(
"herdr-bridge-blocking-test-{}.sock",
std::process::id()
));
let _ = std::fs::remove_file(&socket);
let listener = crate::ipc::bind_private_local_listener(&socket).expect("bind listener");
let client = crate::ipc::connect_local_stream(&socket).expect("connect client");
let mut server = listener.accept().expect("accept client");
crate::ipc::set_local_stream_polling(&mut server, true)
.expect("force the macOS accepted-stream state");
assert!(is_nonblocking(&server));
let server = prepare_remote_bridge_stream(server).expect("prepare bridge stream");
assert!(!is_nonblocking(&server));
drop(server);
drop(client);
drop(listener);
let _ = std::fs::remove_file(socket);
}
#[cfg(windows)]
#[test]
fn windows_bridge_drop_while_waiting_for_client_is_bounded() {