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

  1. Raise the thread/process limit: increase ulimit -u (RLIMIT_NPROC), systemd TasksMax, or cgroup pids.max for the user running the UI.
  2. Free memory or increase available memory/swap so the thread stack allocation succeeds.
  3. Check for runaway thread leaks in the process (count threads via /proc/<pid>/status Threads) and fix the leak before startup.
  4. 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

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


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)