{"record":{"id":"4ad3fcb36fc50b4d","repo":"jdx/mise","slug":"a-process-it-started-kept-its-output-open","errorCode":null,"errorMessage":"a process it started kept its output open","messagePattern":"a process it started kept its output open","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/history/describe_command.rs","lineNumber":176,"sourceCode":"    let status = loop {\n        if let Some(status) = child.try_wait()? {\n            break status;\n        }\n        if started.elapsed() >= timeout {\n            // the shell and whatever it started; the reader thread ends\n            // with the last writer of the pipe, so it is not waited for\n            active.0.kill();\n            let _ = child.kill();\n            let _ = child.wait();\n            bail!(\"took longer than {}s\", timeout.as_secs());\n        }\n        std::thread::sleep(Duration::from_millis(100));\n    };\n    // a descendant that outlived the shell and kept the pipe is not the\n    // shell's answer: the output is waited for a moment, not forever\n    let Ok(output) = receiver.recv_timeout(output_grace) else {\n        active.0.kill();\n        bail!(\"a process it started kept its output open\");\n    };\n    if output.len() > DIFF_LIMIT {\n        bail!(\"description output exceeded {} bytes\", DIFF_LIMIT);\n    }\n    if !status.success() {\n        bail!(\"exited with {status}\");\n    }\n    let Some(line) = first_line(&output) else {\n        return Ok(None);\n    };\n    annotate(\n        store,\n        entry,\n        Annotation {\n            description: Some(line.clone()),\n            description_source: Some(DescriptionSource::Command),\n            labels: None,\n            updated_at: store::now_rfc3339(),","sourceCodeStart":158,"sourceCodeEnd":194,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/history/describe_command.rs#L158-L194","documentation":"After the shell exits, run_with_limits waits only output_grace for the output pipe to close. A descendant process that outlived the shell and still holds the stdout pipe is not considered the shell's answer; mise kills it and bails with 'a process it started kept its output open'. This guards against background children hijacking or indefinitely extending the command's output.","triggerScenarios":"The describe command's shell exits but spawns a background/detached child that inherits stdout and keeps it open longer than the output grace period (e.g. `cmd &`, daemons, watchers).","commonSituations":"Describe scripts that background long-running helpers (log tailers, dev servers); `nohup`-style launches inside the describe command; shells leaving stray children on abnormal exit.","solutions":["Redirect background processes' stdout/stderr away from the describe command's output (e.g. `> /dev/null 2>&1` or to a log file)","Ensure the describe command waits for or terminates its children before exiting","If a legitimately slow writer is expected, raise the output grace period in the history settings"],"exampleFix":"# before: child keeps the pipe open\nsome-daemon &\necho done\n\n# after: detach the child's output\nsome-daemon > /dev/null 2>&1 &\necho done","handlingStrategy":"validation","validationCode":"# check the script does not leave stdout-holding children behind\ndescribe-command; sleep 2; ls /proc/*/fd 2>/dev/null | grep -c pipe || true  # or run: timeout 5 describe-command >/dev/null && pgrep -f child-daemon","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Redirect every background process's output to a file or /dev/null","Have describe scripts clean up or wait for their children","Avoid launching daemons/watchers from describe commands"],"tags":["process","history","pipe"],"backgroundTag":"request-timeout","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}