linebender/druid · critical

failed to initialize the keyboard event loop!

Error message

failed to initialize the keyboard event loop!

What it means

The Wayland keyboard backend spawns a background thread running a calloop event loop for key-repeat/input handling. If `calloop::EventLoop::try_new()` fails (event loop infrastructure could not be created, typically an epoll/fd allocation failure), the thread panics with expect and this message, killing keyboard input for the session.

Solutions

  1. Check the process file-descriptor limit (`ulimit -n`) and raise it; look for fd leaks in the app.
  2. Verify the sandbox (Flatpak/Snap/seccomp profile) permits epoll syscalls (epoll_create1).
  3. Run the app outside the sandbox/container to confirm the cause, then adjust the profile.
  4. Update druid/druid-shell to a version that handles this failure gracefully instead of panicking, or patch the expect into a logged error and degraded keyboard mode.
  5. Check system memory; epoll allocation can fail under OOM pressure.

Example fix

// before
let mut eventloop = calloop::EventLoop::try_new()
    .expect("failed to initialize the keyboard event loop!");

// after
let mut eventloop = match calloop::EventLoop::try_new() {
    Ok(el) => el,
    Err(e) => { tracing::error!("keyboard event loop init failed: {e}"); return; }
};
Defensive patterns

Strategy: try-catch

Validate before calling

// preflight: ensure epoll and fd headroom before spawning the keyboard thread
let can_epoll = unsafe { libc::epoll_create1(0) } >= 0;
let fd_headroom = {
    let mut lim = libc::rlimit { rlim_cur: 0, rlim_max: 0 };
    unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut lim) };
    lim.rlim_cur > 256
};

Try / catch

// in patched builds, replace expect with graceful degradation:
match calloop::EventLoop::try_new() {
    Ok(el) => run_keyboard_loop(el),
    Err(e) => log::error!("keyboard loop unavailable: {e}"), // no key repeat
}

Prevention

When it happens

Trigger: EventLoop::try_new() returning Err due to epoll_create failure — exhausted file descriptors, restrictive seccomp/sandbox blocking epoll, or memory exhaustion — during keyboard backend initialization (Keyboard::default).

Common situations: Running under Flatpak/Snap or container sandboxes with tight fd or syscall limits; hitting RLIMIT_NOFILE in long-running sessions with many fd leaks; embedded/minimal Wayland setups lacking epoll support.

Related errors


AI-assisted analysis of linebender/druid@0f8b1195e4 (2026-09-10). Data as JSON: /api/errors/ee3b5f6a11ff27ab. Report an issue: GitHub.

Appendix: source

Thrown at druid-shell/src/backend/wayland/keyboard.rs:234

impl Default for State {
    fn default() -> Self {
        let (apptx, apprx) = calloop::channel::channel::<KeyEvent>();
        let (tx, rx) = calloop::channel::channel::<(
            u32,
            wl_keyboard::Event,
            calloop::channel::Sender<KeyEvent>,
        )>();
        let state = Self {
            apptx,
            apprx: std::cell::RefCell::new(Some(apprx)),
            tx,
        };

        std::thread::spawn(move || {
            let mut eventloop: calloop::EventLoop<(calloop::LoopSignal, Keyboard)> =
                calloop::EventLoop::try_new()
                    .expect("failed to initialize the keyboard event loop!");
            let signal = eventloop.get_signal();
            let handle = eventloop.handle();
            let repeat = calloop::timer::Timer::<CachedKeyPress>::new().unwrap();
            handle
                .insert_source(rx, {
                    let repeater = repeat.handle();
                    move |event, _ignored, state| {
                        let event = match event {
                            calloop::channel::Event::Closed => {
                                tracing::info!("keyboard event loop closed shutting down");
                                state.0.stop();
                                return;
                            }
                            calloop::channel::Event::Msg(keyevent) => keyevent,
                        };
                        state.1.consume(event.0, event.1, event.2);
                        match &state.1.last_key_press {
                            None => repeater.cancel_all_timeouts(),

View on GitHub (pinned to 0f8b1195e4)