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.
Solutions
- Ensure start()/stop() are serialized — the manager locks self.instance but avoid calling start concurrently from multiple tasks
- Check the inner error `e` for the concrete OS cause (e.g. 'No child processes') indicating double-wait or SIGCHLD handling issues
- Verify no other code awaits or takes the same tokio::process::Child (only one owner allowed)
- 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
- Never call start() from multiple tasks concurrently; serialize via the instance lock or a supervisor task
- Ensure the tokio::process::Child has exactly one owner awaiting it
- Keep tokio runtime versions consistent to avoid waitpid quirks
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
- failed to create tokio runtime
- failed to join notify task
- Widget process exited
- application actor call timed out
- application failed to start
AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08).
Data as JSON: /api/errors/7bd7754ad627302c.
Report an issue: GitHub.
Appendix: 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 gracefullyView on GitHub (pinned to f7dbce2997)