ramensoftware/windhawk · critical
spawn the event pump thread
Error message
spawn the event pump thread
What it means
This is a deliberate panic via .expect() when std::thread::Builder::spawn fails to create the "wh-event-pump" thread in the Tauri UI's run(). The library treats a refused thread spawn as fatal because nothing reaches the front-end without the event pump and setup has no error path that leaves a usable window, so unwinding with a usable app is impossible.
Solutions
- Raise the thread/process limit: increase ulimit -u (RLIMIT_NPROC), systemd TasksMax, or cgroup pids.max for the user running the UI.
- Free memory or increase available memory/swap so the thread stack allocation succeeds.
- Check for runaway thread leaks in the process (count threads via /proc/<pid>/status Threads) and fix the leak before startup.
- If the crash is expected to be graceful, replace expect with propagation of an error instead of a panic (upstream change).
Example fix
// shell, before ulimit -u 256 // after ulimit -u 4096 # or for a systemd unit: # TasksMax=infinity
Defensive patterns
Strategy: fallback
Validate before calling
fn can_spawn_thread() -> bool {
std::thread::Builder::new().name("probe").spawn(|| {}).map(|h| h.join().is_ok()).unwrap_or(false)
}
if !can_spawn_thread() { eprintln!("thread quota exhausted; aborting startup"); } Try / catch
// spawn returns Result, so guard before expect:
let handle = std::thread::Builder::new()
.name("wh-event-pump".to_owned())
.spawn(move || pump::run(pump_ctx, pump_messages));
match handle {
Ok(h) => h,
Err(e) => { log::error!("event pump spawn failed: {e}"); std::process::exit(1); }
} Prevention
- Run the UI with an adequate RLIMIT_NPROC / systemd TasksMax / cgroup pids.max.
- Monitor thread counts of the process in production to catch leaks before startup quota is exhausted.
- Keep sufficient free memory/swap for thread stack allocation.
When it happens
Trigger: OS-level refusal of std::thread::Builder::new().name("wh-event-pump").spawn(...) during windhawk-core UI startup: EAGAIN from pthread_create due to thread/process limit (RLIMIT_NPROC, cgroup pids.max) exhaustion, out of memory for the new stack, or spawning after the runtime is being torn down.
Common situations: Running the UI inside a container or systemd service with a low TasksMax/pids limit; hitting ulimit -u under heavy load; extremely low-memory machines where the thread stack cannot be allocated.
Related errors
- spawn the background startup thread
- error while running the Windhawk UI
- Failed to load metadata for mod
- the mod cannot be stored in an archive, so it was not…
- the mod source could not be parsed, so its settings were…
AI-assisted analysis of ramensoftware/windhawk@61d99ed8e1 (2026-09-12).
Data as JSON: /api/errors/f917b99c65a15c11.
Report an issue: GitHub.
Appendix: source
Thrown at src/windhawk-core/ui/src/lib.rs:729
// from managed state; the banner commands reach the link the same way.
app.manage(ctx.clone());
app.manage(link.clone());
// Now that there is a window, the link can report what it is doing.
link.attach(app.handle().clone());
// The pump thread owns the message channel: it routes each operation
// event to its op through the bridge (off the core callback thread, so
// a composite follow-up may re-enter the session), and it runs the
// session swaps, which need the same seams and the same thread.
let pump_ctx = ctx.clone();
// Nothing reaches the front-end without the pump, and setup has no error
// path that leaves a usable window, so a refused thread is a panic.
#[allow(clippy::expect_used)]
std::thread::Builder::new()
.name("wh-event-pump".to_owned())
.spawn(move || pump::run(pump_ctx, pump_messages))
.expect("spawn the event pump thread");
// Everything the UI starts for ITSELF waits for the session to settle -
// the swap to the broker's session, or degraded mode, whichever comes
// first. The startup catalog refresh is why: its terminal writes the
// user profile, so issued against the local session in the window
// before the broker arrives it would either fail unelevated or be
// drained by the swap, on every single launch. Deferring it also leaves
// the swap-point drain empty in the normal case, which is what keeps
// that path a rare-path concern rather than a per-launch one.
//
// Off the setup thread, as the seed and the sweep already were: none of
// it may delay the window.
let background_ctx = ctx.clone();
let background_link = link.clone();
// As with the pump: setup has nowhere to report a refused thread.
#[allow(clippy::expect_used)]
std::thread::Builder::new()
.name("wh-ui-background".to_owned())View on GitHub (pinned to 61d99ed8e1)