jdx/mise · error

command wait must complete

Error message

command wait must complete

What it means

In the hashed-output process runner (src/cmd.rs), a tokio::select loop drives both `cp.wait()` and consumption of stdout/stderr chunks; every exit path of the loop assigns `status = Some(...)` (either the wait branch completes or the channel closes and `wait.await` finishes, lines 1314-1337). The trailing `status.expect("command wait must complete")` asserts that invariant. It cannot be None in the current code — the loop only breaks after setting it — so hitting it means the loop was refactored to exit by another means.

Source

Thrown at src/cmd.rs:1371

                    signal_process_tree(id, nix::sys::signal::Signal::SIGKILL);
                    #[cfg(windows)]
                    kill_process_tree(id);
                    bail!("command output pipes did not close within {pipe_drain_timeout:?}");
                }
            };
            if let Err(err) = consume(output) {
                #[cfg(unix)]
                signal_process_tree(id, nix::sys::signal::Signal::SIGKILL);
                #[cfg(windows)]
                kill_process_tree(id);
                return Err(err);
            }
        }

        if let Some(guard) = &timeout_guard {
            guard.cancel();
        }
        let status = status.expect("command wait must complete");
        if !status.success() {
            if let Some(timeout) = timeout_guard.as_ref().and_then(|guard| guard.timed_out()) {
                bail!("timed out after {timeout:?}");
            }
            bail!("exited with non-zero status: {status}");
        }
        Ok((
            stdout_hasher.finalize().to_hex().to_string(),
            stderr_hasher.finalize().to_hex().to_string(),
        ))
    }

    /// Run the command and return stdout, even when raw mode is enabled.
    pub(crate) async fn read(mut self) -> Result<String> {
        let _read_lock = RAW_LOCK.read().await;
        debug!("$ {self}");
        self.cmd.kill_on_drop(true);
        #[cfg(unix)]

View on GitHub (pinned to 6f52dcdf99)

Solutions

  1. If hit as a user: update mise — internal regression in the process runner; include the panic backtrace in a report
  2. As a contributor: ensure every `break` in the select loop is preceded by `status = Some(...)` (or await the wait future before breaking)
  3. Consider restructuring so the compiler enforces it: compute status via a dedicated function returning Result<ExitStatus>
  4. Report at https://github.com/jdx/mise/issues

Example fix

// before: new branch breaks without recording status
loop {
    tokio::select! {
        r = &mut wait, if status.is_none() => { status = Some(r?); break; }
        output = rx.recv() => {
            let Some(o) = output else { status = Some(wait.await?); break; };
            if too_big(o) { break; } // BUG: status stays None -> panic
        }
    }
}

// after: every break completes the wait first
if too_big(o) { status = Some(wait.await?); break; }
Defensive patterns

Strategy: validation

Prevention

When it happens

Trigger: Only an internal change to the select loop that adds a break without assigning status (e.g. a new output-limit or cancellation branch). Timeouts, output-byte caps, and pipe-drain failures all take explicit bail!/return paths that never reach this expect; the OS-level wait result is always stored before the loop exits.

Common situations: Contributors editing the process-pumping logic in src/cmd.rs (e.g. adding a new early-exit condition); not triggerable by command output, environment, or the child process itself in shipped builds.

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@6f52dcdf99 (2026-08-22). Data as JSON: /api/errors/d800abd206095686. Report an issue: GitHub.