atuinsh/atuin · error
take pty writer
Error message
take pty writer
What it means
`into_parts` takes exclusive ownership of the PTY writer with `master.take_writer().expect("take pty writer")`. `take_writer` returns `None` once the writer has already been taken from that master, so this panic means the master was already split. The doc comment marks this 'impossible on a freshly spawned subshell, which is the only caller' - hitting it means that invariant was broken.
Source
Thrown at crates/atuin-lab-share/src/subshell.rs:122
/// The subshell owns its child outright, so `stop` kills it, and `wait`
/// applies the exit-code mapping the session has always used: the child's
/// own code when the wait succeeds (non-`i32` codes clamp to 1), 0 when
/// it fails. Everything else is the subshell's defaults: no bootstrap (a
/// fresh shell starts blank), synthetic query answers (the compositor
/// swallows its output, so nothing else would reply), and hub resizes
/// applied to the child PTY.
///
/// # Panics
///
/// Panics if the reader cannot be cloned (the process is out of file
/// descriptors) or the writer was already taken — impossible on a freshly
/// spawned subshell, which is the only caller.
fn into_parts(self) -> crate::Result<SourceParts> {
let (reader, writer) = {
let master = self.master.lock().expect("master lock");
(
master.try_clone_reader().expect("clone pty reader"),
master.take_writer().expect("take pty writer"),
)
};
let resizer = PtyResizer(self.master);
// Terminates the child without owning it, so the session can stop the
// child while `wait` runs on the blocking pool.
let mut killer = self.child.clone_killer();
let mut child = self.child;
Ok(SourceParts {
reader: Box::new(ByteReader(reader)),
writer,
resizer: Box::new(move |size| resizer.resize(size)),
stop: Box::new(move || {
// Best-effort, exactly as the session's kill switch always
// treated it: a failed kill still reaches `wait`'s mapping.
let _ = killer.kill();
}),
wait: Box::new(move || match child.wait() {
Ok(status) => i32::try_from(status.exit_code()).unwrap_or(1),View on GitHub (pinned to 15fe1318f1)
Solutions
- Guarantee `into_parts` runs exactly once per subshell lifecycle (own the master; do not share the Arc)
- Return an error instead of panicking: `take_writer().ok_or_else(|| Error::WriterTaken)`
- Add a debug assertion or log at every other `take_writer` call site to catch double takes early
Example fix
// before
master.take_writer().expect("take pty writer"),
// after
let writer = master
.take_writer()
.ok_or(crate::Error::PtyWriterAlreadyTaken)?; Defensive patterns
Strategy: validation
Validate before calling
// Enforce single conversion of a subshell into its parts
struct SubshellOnce { inner: Option<Subshell> }
impl SubshellOnce {
fn into_parts(&mut self) -> Result<SourceParts> {
self.inner.take().ok_or(Error::AlreadySplit)?.into_parts()
}
} Prevention
- Call into_parts exactly once per subshell; model it as a move (Option::take) so a second call fails to compile or errors
- Do not share the master's Arc<Mutex<..>> with other writers
- Add debug logs at every take_writer call site to catch double takes during development
When it happens
Trigger: Calling `into_parts` twice on the same `Subshell`; taking the writer from the shared `Arc<Mutex<Master>>` somewhere else (an injection or test harness) before `into_parts` runs; reusing a master from a recycled session.
Common situations: A refactor that shares the master with another component that also writes; a bug that re-converts a session twice; tests that drive the PTY manually and then call `into_parts`.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- master lock
- clone pty reader
- issue in stats average query
- issue in stats exits query
- issue in stats day of week query
AI-assisted analysis of atuinsh/atuin@15fe1318f1 (2026-08-19).
Data as JSON: /api/errors/323e3646081970e6.
Report an issue: GitHub.