jdx/mise · error

piped stdout

Error message

piped stdout

What it means

`read_isolated` (in src/cmd/bounded.rs) runs a command in its own process group with a bounded output budget, which requires both pipes. This `.expect("piped stdout")` (and its stderr sibling) panics if the child was not spawned with piped stdio — a spawn-configuration invariant for the task-output capture path used by mise tasks.

Source

Thrown at src/cmd/bounded.rs:27

    /// Capture a finite response, with one deadline for the process and pipes.
    /// This command owns its child tree even when mise itself is nested.
    pub(crate) async fn read_isolated(mut self, limit: usize) -> Result<String> {
        let _read_lock = RAW_LOCK.read().await;
        let timeout = self.timeout.unwrap_or(Duration::from_secs(5));
        self.cmd.kill_on_drop(true);
        self.cmd
            .stdin(Stdio::null())
            .stdout(Stdio::piped())
            .stderr(Stdio::piped());
        #[cfg(unix)]
        {
            self.cmd.env(TASK_PGID_MANAGED_ENV, "1");
            self.cmd.process_group(0);
        }
        let mut child = self.spawn_async_with_etxtbsy_retry().await?;
        let _running = RunningPidGuard::new(child.id());
        let tree = ChildTree::new(&mut child)?;
        let stdout = child.stdout.take().expect("piped stdout");
        let stderr = child.stderr.take().expect("piped stderr");
        // One budget for both pipes: judging them only once both reach EOF
        // would let each hold the whole limit first, so a command could
        // allocate twice what was asked before anyone objected.
        let budget = AtomicUsize::new(limit);
        let result = tokio::time::timeout(timeout, async {
            tokio::try_join!(
                child.wait(),
                capture(stdout, &budget, limit),
                capture(stderr, &budget, limit)
            )
        })
        .await;
        let (status, stdout, _stderr) = match result {
            Ok(Ok(output)) => output,
            Ok(Err(err)) => {
                end(&mut child, tree).await;
                return Err(err.into());

View on GitHub (pinned to afd2eddd3a)

Solutions

  1. Ensure the command sets `.stdout(Stdio::piped()).stderr(Stdio::piped())` before `spawn_async_with_etxtbsy_retry()`
  2. Keep interactive (non-captured) task execution on a separate code path from `read_isolated`
  3. Report with `MISE_DEBUG=1` and the failing task definition if seen on an unmodified build
  4. Add a debug_assert or early error right after spawn verifying both pipes exist

Example fix

// before
let mut cmd = build_task_cmd(task); // stdio unset -> inherits
// after
let mut cmd = build_task_cmd(task);
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
Defensive patterns

Strategy: validation

Validate before calling

// In task spawn helpers, before read_isolated:
assert!(matches!(cmd.get_stdout(), Stdio::piped()) && matches!(cmd.get_stderr(), Stdio::piped()));

Prevention

When it happens

Trigger: A task command spawned for `read_isolated` with inherited or null stdio; a wrapper (e.g. one setting process groups or env) that also overrides stdio; refactors in task spawn configuration that drop the pipes.

Common situations: Changes to `src/cmd/bounded.rs` or task spawn helpers that reconfigure stdio; custom task run modes (e.g. interactive tasks) accidentally routed through the bounded/isolated reader.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09). Data as JSON: /api/errors/86c5bd949bb6a216. Report an issue: GitHub.