{"record":{"id":"323e3646081970e6","repo":"atuinsh/atuin","slug":"take-pty-writer","errorCode":null,"errorMessage":"take pty writer","messagePattern":"take pty writer","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/atuin-lab-share/src/subshell.rs","lineNumber":122,"sourceCode":"    /// The subshell owns its child outright, so `stop` kills it, and `wait`\n    /// applies the exit-code mapping the session has always used: the child's\n    /// own code when the wait succeeds (non-`i32` codes clamp to 1), 0 when\n    /// it fails. Everything else is the subshell's defaults: no bootstrap (a\n    /// fresh shell starts blank), synthetic query answers (the compositor\n    /// swallows its output, so nothing else would reply), and hub resizes\n    /// applied to the child PTY.\n    ///\n    /// # Panics\n    ///\n    /// Panics if the reader cannot be cloned (the process is out of file\n    /// descriptors) or the writer was already taken — impossible on a freshly\n    /// spawned subshell, which is the only caller.\n    fn into_parts(self) -> crate::Result<SourceParts> {\n        let (reader, writer) = {\n            let master = self.master.lock().expect(\"master lock\");\n            (\n                master.try_clone_reader().expect(\"clone pty reader\"),\n                master.take_writer().expect(\"take pty writer\"),\n            )\n        };\n        let resizer = PtyResizer(self.master);\n        // Terminates the child without owning it, so the session can stop the\n        // child while `wait` runs on the blocking pool.\n        let mut killer = self.child.clone_killer();\n        let mut child = self.child;\n        Ok(SourceParts {\n            reader: Box::new(ByteReader(reader)),\n            writer,\n            resizer: Box::new(move |size| resizer.resize(size)),\n            stop: Box::new(move || {\n                // Best-effort, exactly as the session's kill switch always\n                // treated it: a failed kill still reaches `wait`'s mapping.\n                let _ = killer.kill();\n            }),\n            wait: Box::new(move || match child.wait() {\n                Ok(status) => i32::try_from(status.exit_code()).unwrap_or(1),","sourceCodeStart":104,"sourceCodeEnd":140,"githubUrl":"https://github.com/atuinsh/atuin/blob/15fe1318f1df51de604262eb50734c9883d48e7b/crates/atuin-lab-share/src/subshell.rs#L104-L140","documentation":"`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.","triggerScenarios":"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.","commonSituations":"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`.","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"],"exampleFix":"// before\nmaster.take_writer().expect(\"take pty writer\"),\n\n// after\nlet writer = master\n    .take_writer()\n    .ok_or(crate::Error::PtyWriterAlreadyTaken)?;","handlingStrategy":"validation","validationCode":"// Enforce single conversion of a subshell into its parts\nstruct SubshellOnce { inner: Option<Subshell> }\nimpl SubshellOnce {\n    fn into_parts(&mut self) -> Result<SourceParts> {\n        self.inner.take().ok_or(Error::AlreadySplit)?.into_parts()\n    }\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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"],"tags":["rust","pty","panic","ownership","invariant-violation"],"backgroundTag":"invalid-state-transition","analyzedSha":"15fe1318f1df51de604262eb50734c9883d48e7b","analyzedAt":"2026-08-19T08:56:57.719Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}