slint-ui/slint · error

Child has no stdout

Error message

Child has no stdout

What it means

The editor-preview LSP spawns its preview child process with `stdout(Stdio::piped())`, then takes ownership of the stdout pipe. `child.stdout.take()` returning None means the pipe was not configured or already taken — an internal invariant break in the spawn setup. The panic aborts the LSP server's preview thread.

Solutions

  1. Ensure the spawn call keeps `.stdout(std::process::Stdio::piped())`
  2. Take stdout exactly once and keep the returned reader alive
  3. Rebuild/reinstall the slint-lsp binary from a clean checkout
  4. Report a bug with version info if reproducible in an unmodified build

Example fix

// before (stdout taken twice)
let a = child.stdout.take().expect("Child has no stdout");
let b = child.stdout.take().expect("Child has no stdout");

// after
let a = child.stdout.take().expect("Child has no stdout");
// do not take again; use `a`
Defensive patterns

Strategy: validation

Validate before calling

let Some(stdout) = child.stdout.take() else { return Err("preview child stdout missing"); };

Type guard

fn has_pipes(child: &std::process::Child) -> bool { child.stdout.is_some() && child.stdin.is_some() }

Try / catch

// wrap start_preview
match start_preview().await {
    Ok(h) => h,
    Err(e) => tracing::error!("preview failed to start: {e}"),
}

Prevention

When it happens

Trigger: `start_preview` (called from `send`) spawning the preview binary when the `Stdio::piped()` configuration did not take effect or stdout was already consumed — practically only from a code regression in spawn options or an already-taken handle.

Common situations: Custom/patched builds where spawn options were modified; reusing a `Child` whose stdout was already taken; extremely unlikely in stock 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 slint-ui/slint@bb937076de (2026-09-16). Data as JSON: /api/errors/df4512cc3fa51b70. Report an issue: GitHub.

Appendix: source

Thrown at internal/editor-preview/child_process.rs:54

    fn preview_is_running(&self) -> bool {
        self.inner.borrow().as_ref().map(|i| !i.communication_handle.is_finished()).unwrap_or(false)
    }

    fn start_preview(&self) -> crate::Result<()> {
        if let Some(inner) = self.inner.borrow_mut().take() {
            inner.communication_handle.abort();
        }

        let mut child = tokio::process::Command::new(&self.executable)
            .args(&self.arguments)
            .stdin(std::process::Stdio::piped())
            .stdout(std::process::Stdio::piped())
            .spawn()?;

        tracing::debug!("Preview process spawned (PID {:?})", child.id());

        let from_child = child.stdout.take().expect("Child has no stdout");
        let mut to_child = child.stdin.take().expect("Child has no stdin");

        let channel = self.preview_to_lsp_channel.clone();

        let communication_handle = tokio::spawn(async move {
            let _exited_guard = scopeguard::guard(channel.clone(), |channel| {
                channel.send(PreviewToLspMessage::Exited).ok();
            });
            let reader = tokio::io::BufReader::new(from_child);
            let mut lines = reader.lines();
            while let Some(line) = lines.next_line().await.map_err(|e| e.to_string())? {
                if let Ok(message) = serde_json::from_str(&line) {
                    channel.send(message).map_err(|e| e.to_string())?;
                }
            }

            let exit_status = child.wait().await.map_err(|e| e.to_string());

View on GitHub (pinned to bb937076de)