libnyanpasu/clash-nyanpasu · error · anyhow::Error

Failed to wait for widget process: {}

Error message

Failed to wait for widget process: {}

What it means

In WidgetManager::start, the `child.wait()` future itself failed (not the child exiting normally) — tokio reported an OS-level error waiting for the spawned widget process (e.g. waitpid failure or the child was already reaped). The manager returns this wrapped error instead of an IPC sender.

Source

Thrown at backend/tauri/src/widget.rs:135

            .stdout(os_pipe::dup_stdout()?)
            .stderr(os_pipe::dup_stderr()?)
            .spawn()
            .context("Failed to spawn widget process")?;
        tracing::debug!("Waiting for widget process to start...");
        let tx = tokio::select! {
            res = tokio::task::spawn_blocking(move || {
                ipc_server
                    .connect()
                    .context("Failed to connect to widget")?;
                ipc_server.into_tx().context("Failed to get ipc sender")
            }) => res.context("Failed to get ipc sender")??,
            res = child.wait() => {
                match res {
                    Ok(status) => {
                        return Err(anyhow::anyhow!("Widget process exited: {}", status));
                    }
                    Err(e) => {
                        return Err(anyhow::anyhow!("Failed to wait for widget process: {}", e));
                    }
                }
            }
        };
        instance.replace(WidgetManagerInstance { tx, process: child });
        Ok(())
    }

    pub async fn stop(&self) -> anyhow::Result<()> {
        let Some(mut instance) = self.instance.lock().await.take() else {
            tracing::debug!("Widget instance is not exists, skipping...");
            return Ok(());
        };
        if !instance.is_alive() {
            tracing::debug!("Widget instance is not alive, skipping...");
            return Ok(());
        }
        // first try to stop the process gracefully

View on GitHub (pinned to f7dbce2997)

Solutions

  1. Ensure start()/stop() are serialized — the manager locks self.instance but avoid calling start concurrently from multiple tasks
  2. Check the inner error `e` for the concrete OS cause (e.g. 'No child processes') indicating double-wait or SIGCHLD handling issues
  3. Verify no other code awaits or takes the same tokio::process::Child (only one owner allowed)
  4. Retry start() once the environment is stable; the instance slot remains empty on failure

Example fix

// before
Err(anyhow::anyhow!("Failed to wait for widget process: {}", e))
// after
Err(anyhow::anyhow!("Failed to wait for widget process: {e:#}"))
Defensive patterns

Strategy: try-catch

Validate before calling

// serialize start/stop through a mutex and ensure no concurrent waiter exists
if start_in_flight.swap(true, Ordering::SeqCst) {
    anyhow::bail!("widget start already in progress");
}

Try / catch

match manager.start(variant).await {
    Err(e) if e.to_string().contains("Failed to wait for widget process") => {
        log::error!("tokio wait failure: {e:#}; check for double-wait/concurrent start");
    }
    Err(e) => return Err(e),
    Ok(()) => {}
}

Prevention

When it happens

Trigger: tokio::process::Child::wait() returns Err during the select! race in start(), e.g. the child's stdin/stdout handles are invalid, the process was already awaited elsewhere, or an OS waitpid error occurs.

Common situations: Calling start() twice concurrently so two wait() calls race on the same Child; platform quirks after fork/exec failure; killing the process externally in a way tokio surfaces as a wait error rather than a status.

Related errors


AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08). Data as JSON: /api/errors/7bd7754ad627302c. Report an issue: GitHub.