{"record":{"id":"9895a7cdb18a195f","repo":"atuinsh/atuin","slug":"clone-pty-reader","errorCode":null,"errorMessage":"clone pty reader","messagePattern":"clone pty reader","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/atuin-lab-share/src/subshell.rs","lineNumber":123,"sourceCode":"    ///\n    /// 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() {","sourceCodeStart":105,"sourceCodeEnd":141,"githubUrl":"https://github.com/atuinsh/atuin/blob/202f6ad98ee0da165c35cdb2afbc5b13d6ab81a1/crates/atuin-lab-share/src/subshell.rs#L105-L141","documentation":"In atuin-lab-share, Subshell::into_parts() splits a freshly spawned PTY subshell into reader/writer/resizer. It expects portable-pty's master.try_clone_reader() and master.take_writer() to succeed; the doc comment states a clone failure means the process is out of file descriptors. Duplicating the PTY master fd fails with EMFILE/ENFILE and the expect panics with 'clone pty reader'.","triggerScenarios":"Calling into_parts() when the process is at its open-file limit (RLIMIT_NOFILE / ulimit -n) or the system file table is full; take_writer can only fail if into_parts is called twice on the same subshell, which the API contract forbids.","commonSituations":"Long-running hub/share sessions that leak descriptors; containers or systemd units with low LimitNOFILE; many concurrent PTY subshells exhausting the fd budget.","solutions":["Raise the open-file limit before starting atuin: ulimit -n 8192, systemd LimitNOFILE=8192, docker --ulimit nofile=8192","Watch /proc/<pid>/fd counts during lab/share sessions and fix any descriptor leak","Reduce the number of concurrent subshell sessions"],"exampleFix":"# before\n[Service]\nLimitNOFILE=1024\n\n# after\n[Service]\nLimitNOFILE=8192","handlingStrategy":"validation","validationCode":"fn fd_headroom(headroom: usize) -> bool {\n    let Ok(limits) = std::fs::read_to_string(\"/proc/self/limits\") else { return true };\n    let limit = limits\n        .lines()\n        .find(|l| l.starts_with(\"Max open files\"))\n        .and_then(|l| l.split_whitespace().nth(3))\n        .and_then(|v| v.parse::<usize>().ok())\n        .unwrap_or(1024);\n    let used = std::fs::read_dir(\"/proc/self/fd\").map(|d| d.count()).unwrap_or(0);\n    used + headroom < limit\n}\n\nassert!(fd_headroom(64), \"raise RLIMIT_NOFILE before starting PTY subshells\");","typeGuard":null,"tryCatchPattern":"let parts = std::panic::catch_unwind(AssertUnwindSafe(subshell.into_parts));\nmatch parts {\n    Ok(source) => source,\n    Err(_) => { /* terminate the session cleanly; treat fd exhaustion as fatal */ }\n}","preventionTips":["Set generous NOFILE limits for any process hosting PTY sessions","Call into_parts() exactly once per subshell","Monitor /proc/<pid>/fd growth in long-lived daemons"],"tags":["pty","portable-pty","file-descriptors","panic","resource-limits"],"backgroundTag":"file-descriptor-exhaustion","analyzedSha":"202f6ad98ee0da165c35cdb2afbc5b13d6ab81a1","analyzedAt":"2026-08-16T19:30:24.731Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}