[Fix]: Make PTY reader thread joinable so old-session output cannot bleed into the next tab#178
Open
matiaspalmac wants to merge 1 commit intoTrixtyAI:mainfrom
Open
[Fix]: Make PTY reader thread joinable so old-session output cannot bleed into the next tab#178matiaspalmac wants to merge 1 commit intoTrixtyAI:mainfrom
matiaspalmac wants to merge 1 commit intoTrixtyAI:mainfrom
Conversation
…eed into the next tab `spawn_pty` started a reader thread with `thread::spawn` and dropped the `JoinHandle`. When `PtyState` was replaced (either via `kill_pty` or a second `spawn_pty`), the old reader kept running on its cloned reader until it hit EOF. Anything the old shell had already pushed to the PTY — or any prompt re-drawn by an editor like `vim` as the slave side flushed — surfaced as `pty-output` events inside the new session's tab. Fix: - `PtyState` now owns an `Arc<AtomicBool>` shutdown flag and an `Option<JoinHandle<()>>`. The reader thread checks the flag after every read and bails before emitting if it's set. - `PtyState::shutdown_and_join(self)` flips the flag, drops the `master` (which closes the native handle and unblocks the reader's blocking `read`), then joins the thread. - `spawn_pty` and `kill_pty` both take the old state *out of the mutex first* and only then run `shutdown_and_join`. Joining inside the lock would serialize PTY teardown with every other command and could deadlock on a reader re-entering via `AppHandle::emit`. Deliberately no `Drop` impl: dropping by `&mut self` cannot move `master` out before `handle.join()`, which would deadlock (the reader is parked inside `read` and only master closure wakes it up). Teardown is instead a single explicit path that both callers always go through. No new unit tests — the change is wiring that only manifests against a real PTY. Verified manually: spawn → type → kill_pty → spawn → no stray output in the new session; also with the `vim` / prompt-redraw repro from the issue. Fixes TrixtyAI#79
|
Thanks for the contribution! I'll review it as soon as possible. If you have still changes, please mark this PR as draft and all reviews will be cancelled. Tests reviews will be re-run only when the PR is marked as ready for review. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
spawn_ptystarted the reader thread withthread::spawnand threw away theJoinHandle. WhenPtyStatewas replaced (either viakill_ptyor a secondspawn_ptyon the same slot), the old reader kept running on its cloned reader until it hit EOF. Any bytes the previous shell had already written to the PTY — including a prompt re-drawn by e.g.vimorlessduring teardown — then surfaced aspty-outputevents inside the new session's tab.Related Issue
Fixes #79
Context / Problem
Minimal repro:
vim.kill_pty— e.g. closing the terminal panel).Root cause: the old reader thread is still parked inside
reader.read()when the new session starts. It eventually wakes up and callsapp.emit("pty-output", ...), which the frontend routes to whichever terminal is currently open.Change
PtyStatenow owns anArc<AtomicBool>shutdown flag and anOption<JoinHandle<()>>.PtyState::shutdown_and_join(self)flips the flag, drops themaster(closing the native handle — this is what actually unblocks the blockingread), then joins the thread.spawn_ptyandkill_ptyboth take the old state out of the mutex first and only then runshutdown_and_join. Joining inside the lock would serialize teardown with every other PTY command and could deadlock on a reader re-entering viaAppHandle::emit.Trade-offs
Dropimpl onPtyState. ADropwould run on&mut self, which cannot movemasterout beforehandle.join(). Joining with the master still alive deadlocks (the reader is parked insidereadand only master closure wakes it up). Teardown is therefore a single explicit path (shutdown_and_join) that both callers always go through.Verification
cargo check/cargo clippy --lib --no-deps -- -D warnings/cargo fmt --checkclean.cargo test --lib→ 30 passed (no regressions).vim→ kill_pty → spawn → no stray output.Checklist