Conversation
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
8ae2796 to
5390fab
Compare
A client that stays connected but stops draining its socket, e.g. an IDE terminal that was orphaned when its window lost the remote connection, used to park the shell->client thread in write() forever. Once the pty filled up the shell froze, and the session stayed attached: detach and attach -f both time out handing the disconnect to the blocked thread, so the session could only be recovered by finding and killing the stale attach process. Give writes to the client a 10 second timeout. When a write makes no progress for that long, treat the client as gone and shut its socket down, so the client->shell thread and the attach process see EOF, the session becomes disconnected, and the shell keeps running into the output spool. Stop the session-restore dump at the first failed chunk too, since each further chunk could otherwise block for another timeout. force_attach_stalled_client floods a session whose attach client never reads its stdout and checks that the session is released and the shell is still usable. It fails without this change.
5390fab to
8c73450
Compare
ethanpailes
left a comment
There was a problem hiding this comment.
Thanks for finding the bug, this seems like the right approach, just a few things to address.
| /// Reports whether a client write failed because it hit | ||
| /// CLIENT_WRITE_TIMEOUT. Depending on the platform, an expired | ||
| /// socket write timeout surfaces as either kind. | ||
| fn is_client_write_timeout(err: &io::Error) -> bool { |
There was a problem hiding this comment.
This seems like generic functionality, so let's name it accordingly. is_socket_write_timeout would describe what it does better.
| .attach("sh1", AttachArgs { force: true, ..Default::default() }) | ||
| .context("attaching from tty2")?; | ||
| let mut line_matcher2 = tty2.line_matcher()?; | ||
| tty2.run_raw(vec![3])?; // ^C the flood |
There was a problem hiding this comment.
nit: can you give a useful name to this magic number with a constant?
| // output) forever. That takes the daemon's client write timeout, | ||
| // and the exponential backoff in wait_until_list_matches polls too | ||
| // sparsely around that point, so poll at a fixed interval instead. | ||
| let deadline = time::Instant::now() + time::Duration::from_secs(20); |
There was a problem hiding this comment.
Having the test block for 10s is not great. The test suite already takes a while to run because of all the process juggling, and this guy will immediately become a long pole. To address this, and also make this feature more configurable, let's make this write timeout an option in the config file, and then set it to something short like 1s in this test.
| // a stalled ssh window) would otherwise park the shell->client thread in | ||
| // write() forever, freezing the shell once the pty fills up and keeping | ||
| // the session attached so that neither detach nor attach -f can take it. | ||
| const CLIENT_WRITE_TIMEOUT: time::Duration = time::Duration::from_secs(10); |
There was a problem hiding this comment.
10s seems like a solid default, but let's make this optionally configurable by adding an entry to config.rs. That will also allow the test to run in a more reasonable timeframe.
|
Oh, also, in the future, please don't delete the PR template or use AI to generate a PR description. AI is fine for code, but human communication should still be hand-written in the shpool project. (Though it would be fine to include a block with some prefix like "this is what my agent says about the change") |
|
The macos CI env is kinda flaky, so I kicked off another test run. |
Problem
If an attach client stays connected but stops reading, the daemon's shell->client thread blocks in
write()with no timeout. The write timeout set inwrite_replyis cleared before streaming starts. Two things follow:attached.shpool detachandshpool attach -fboth time out when they try to hand the disconnect to the blocked thread.attach -fthen reportssession '...' already has a terminal which remains attached even after attempting to detach it. The only way out is to find and kill the staleshpool attachprocess by hand.We hit this with an IDE terminal (Cursor Remote-SSH) that was left behind when the window lost its connection. The IDE's server kept the terminal alive but stopped draining it, so the attach process inside it blocked writing its stdout. The daemon log showed
sending client detach to shell->client for <session>: "SendTimeoutError(..)"for every detach attempt. The comment above the detach handler inserver.rsalready describes this client ("a stalled ssh window, a suspended laptop"): the handler no longer wedges the daemon, but the session itself stays stuck.Fix
CLIENT_WRITE_TIMEOUT). A slow client is not affected, because any progress restarts the timeout. Only a client that accepts nothing for 10 seconds is dropped.bidi_streamreturns, the session becomesdisconnected, and the shell keeps writing into the output spool.unexpected IO errorarm, which ends the shell->client thread. It is now handled like a hangup.10 seconds is a judgment call. It is long enough to ride out a network hiccup and short enough that a frozen shell or a failing
attach -fdoesn't last. I'm happy to change it or make it configurable.Testing
force_attach_stalled_client: floods a session whose attach client never reads its stdout, waits for the session to becomedisconnected, then force-attaches and checks that it is the same shell and that it still responds. It fails on master (the stalled client was never dropped) and passes with this change in about 11 s.cargo teston master and on this branch. The only failures are the same 4 on both, and they need programs that aren't on my test host:forward_env_live_reload_{zsh,fish}andprompt_prefix_zshneed zsh or fish,output_floodneeds hexdump.cargo +nightly fmt -- --checkandcargo clippy --all-targets -- -D warningsare clean.AI disclosure
This change was made with Claude Code: tracing the hang on a live system, the patch and the test. It was verified by running the tests described above.
🤖 Generated with Claude Code