libnyanpasu/clash-nyanpasu · error · anyhow::Error
Widget process exited
Error message
Widget process exited: {} What it means
WidgetManager::start races the IPC server connect against the child widget process exit via tokio::select!. If the spawned `statistic-widget` child process terminates before the IPC handshake completes, `child.wait()` resolves with a success status and the manager aborts startup, reporting the exit status in this error. It signals the widget subprocess crashed or exited early instead of connecting back.
Solutions
- Read the reported exit status: a non-zero code means the widget subprocess crashed — check stdout/stderr (they are inherited) for a panic message from the widget itself
- Verify the current executable supports the `statistic-widget <variant>` subcommand (the widget re-executes the same binary)
- Check/clear the widget state file `app_data_dir/widget_<variant>.state` which is passed via NYANPASU_EGUI_WINDOW_STATE_PATH
- Ensure a display/GPU environment is available where the egui widget can open a window
- Retry start() after fixing the environment; the manager replaces the instance only on success
Example fix
// before
Err(anyhow::anyhow!("Widget process exited: {}", status))
// after
Err(anyhow::anyhow!("Widget process exited before IPC handshake (status: {}); check widget stderr for a startup panic", status)) Defensive patterns
Strategy: try-catch
Validate before calling
// before start(): ensure the widget subcommand exists and state file is removable
let exe = tauri_utils::platform::current_exe()?;
let state_path = crate::utils::dirs::app_data_dir()?.join(format!("widget_{variant}.state"));
if !exe.exists() || std::env::var("DISPLAY").or_else(|_| std::env::var("WAYLAND_DISPLAY")).is_err() {
anyhow::bail!("widget prerequisites missing: display or executable unavailable");
} Type guard
fn widget_status_ok(status: std::process::ExitStatus) -> bool { status.success() } Try / catch
match manager.start(variant).await {
Err(e) if e.to_string().contains("Widget process exited") => {
log::error!("widget crashed at startup: {e:#}; check widget stderr");
}
Err(e) => return Err(e),
Ok(()) => {}
} Prevention
- Check stdout/stderr of the widget process (inherited) for the underlying crash cause
- Clear corrupted widget_<variant>.state files after updates
- Only spawn the widget in environments with a usable display
- Keep the executable and widget subcommand in sync across app updates
When it happens
Trigger: Calling WidgetManager::start when the spawned widget subprocess exits before completing the IPC handshake — e.g. the widget binary panics on startup, fails to parse env vars (NYANPASU_EGUI_IPC_SERVER / NYANPASU_EGUI_WINDOW_STATE_PATH), or the same executable lacks the `statistic-widget` subcommand.
Common situations: Broken or partially-updated install where the sidecar widget binary is corrupt; a missing GPU/display environment causing the egui widget to crash immediately; re-running `statistic-widget` manually and hitting an argument error; app updated while a stale widget state file confuses the widget.
Related errors
- Failed to wait for widget process
- Failed to write to socket
- IPC server is already initialized
- session state actor reply dropped
- application actor call timed out
AI-assisted analysis of libnyanpasu/clash-nyanpasu@f7dbce2997 (2026-09-08).
Data as JSON: /api/errors/acfa2c851654a2cf.
Report an issue: GitHub.
Appendix: source
Thrown at backend/tauri/src/widget.rs:132
.env("NYANPASU_EGUI_IPC_SERVER", server_name)
.env("NYANPASU_EGUI_WINDOW_STATE_PATH", widget_win_state_path)
.stdin(std::process::Stdio::inherit())
.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...");View on GitHub (pinned to f7dbce2997)