fix(client): stop timeout setup from masking the daemon's refusal

On macOS, setsockopt against a socket whose peer has already closed fails with
EINVAL. The daemon answers a bad request by writing one Error frame and hanging
up at once, so PaneSession's `set_recv_timeout(...)?` would fail before the
refusal was ever read — turning "no such pane 42", already sitting in the
buffer, into "Invalid argument".

Bounding the reply wait is an optimisation, not a correctness requirement, so it
is now best effort in both attach/observe and spawn. Nothing can hang as a
result: a closed peer returns EOF immediately, and a live peer is exactly the
case where setsockopt succeeds.

This is what made client_lib's reattach test red.
This commit is contained in:
l0ng-ai
2026-07-31 13:04:51 +08:00
parent 400d7d76b6
commit fc01b3e31f
2 changed files with 25 additions and 7 deletions
+24 -5
View File
@@ -118,7 +118,15 @@ impl PaneClient {
owner: Option<String>,
workspace: Option<String>,
) -> io::Result<PaneSession> {
PaneSession::spawn_over(self.open()?, cwd, size, shell, owner, workspace, OPEN_REPLY_WAIT)
PaneSession::spawn_over(
self.open()?,
cwd,
size,
shell,
owner,
workspace,
OPEN_REPLY_WAIT,
)
}
pub fn attach(&self, pane_id: u64, size: WinSize) -> io::Result<PaneSession> {
@@ -155,9 +163,12 @@ impl PaneSession {
}
.encode(&mut stream)?;
let mut session = PaneSession::over(stream, 0)?;
session.set_recv_timeout(Some(reply_wait))?;
// Best effort, deliberately not `?`: see `checked` — a daemon that has
// already refused and hung up makes setsockopt fail, and that must not
// become the error the caller sees instead of the refusal itself.
let _ = session.set_recv_timeout(Some(reply_wait));
let first = session.recv();
session.set_recv_timeout(None)?;
let _ = session.set_recv_timeout(None);
match first {
Ok(DaemonMsg::Spawned { pane_id }) => {
session.input.pane_id = pane_id;
@@ -202,9 +213,17 @@ impl PaneSession {
reply_wait: Duration,
) -> io::Result<PaneSession> {
let mut session = PaneSession::over(stream, pane_id)?;
session.set_recv_timeout(Some(reply_wait))?;
// Best effort, deliberately not `?`. The daemon answers a bad request by
// writing one Error frame and closing immediately; on macOS setsockopt
// against a socket whose peer is already gone fails with EINVAL. Letting
// that propagate replaces "no such pane 42" — which is sitting in our
// buffer right now — with "Invalid argument", the least useful thing we
// could tell the caller. If the timeout does not take, the read below
// still cannot hang: a closed peer returns EOF at once, and a peer
// that is alive is exactly the case where setsockopt succeeds.
let _ = session.set_recv_timeout(Some(reply_wait));
let verdict = session.output.refusal_check(request, pane_id);
session.set_recv_timeout(None)?;
let _ = session.set_recv_timeout(None);
verdict?;
Ok(session)
}
+1 -2
View File
@@ -249,8 +249,7 @@ fn control_requests_build_the_tree_and_events_reach_the_other_client() {
);
match watcher.next_event(remaining) {
Some(ControlEvent::Layout { workspace, delta })
if workspace == key
&& matches!(delta, LayoutDelta::WorkspaceCreated { .. }) =>
if workspace == key && matches!(delta, LayoutDelta::WorkspaceCreated { .. }) =>
{
break;
}