{"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":175,"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":157,"sourceCodeEnd":193,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/history/describe_command.rs#L157-L193","documentation":"Thrown by `run_with_limits` in describe_command.rs when the command's output channel produces nothing within `output_grace` after the shell exits. This indicates a descendant process the shell spawned kept the inherited stdout/stderr pipe open, so the collector cannot distinguish 'shell finished' from 'shell still writing'. The library intentionally refuses to wait forever on orphaned descendants and kills the remaining active process instead.","triggerScenarios":"Running a description command whose shell spawns a background child (daemon, `tail -f`, `nohup ... &`, detached process) that inherits the stdout pipe and outlives the shell; `receiver.recv_timeout(output_grace)` then times out even though the shell itself exited.","commonSituations":"A dotfile hook or annotation command that starts a background agent or logging process; scripts that leave `&` background jobs attached to the terminal; watchdog processes spawned by shell profile files executed during command startup.","solutions":["Rewrite the command so background children do not inherit stdout/stderr: redirect them to a file or /dev/null (e.g. `my-daemon &>/dev/null &`).","Use `setsid`/`disown` with full redirection so the descendant detaches from the inherited pipe.","Increase the grace window if the command legitimately produces output slowly, via the run_with_limits configuration.","Ensure the description command itself exits promptly instead of blocking on children it spawns."],"exampleFix":"// before\nlogging-agent --follow &\necho done\n// after\nlogging-agent --follow >/dev/null 2>&1 &\ndisown\necho done","handlingStrategy":"try-catch","validationCode":"// shell script audit: no background jobs inheriting stdout\n// grep -nE '&([^>]|$)' my-hook.sh  — ensure each is redirected","typeGuard":null,"tryCatchPattern":"// match run_with_limits(...) {\n//     Err(e) if e.message().contains(\"kept its output open\") => {\n//         // command spawned a daemon; treat as inconclusive, not fatal\n//     }\n//     other => other?,\n// }","preventionTips":["Always redirect background children (`>/dev/null 2>&1 & disown`).","Keep description commands single-purpose and fast-exiting.","Avoid sourcing profile files that spawn daemons during commands."],"tags":["process","timeout","pipe","background-process"],"backgroundTag":"request-timeout","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}