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

  1. Guarantee `into_parts` runs exactly once per subshell lifecycle (own the master; do not share the Arc)
  2. Return an error instead of panicking: `take_writer().ok_or_else(|| Error::WriterTaken)`
  3. 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

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


AI-assisted analysis of atuinsh/atuin@15fe1318f1 (2026-08-19). Data as JSON: /api/errors/323e3646081970e6. Report an issue: GitHub.