elkowar/eww · error

Failed to start command-execution-thread

Error message

Failed to start command-execution-thread

What it means

run_command spawns a thread per executed widget command; the expect panics if std::thread::Builder::spawn fails. Like all spawn failures this is an OS resource error (thread limit, memory), not a problem with the command string itself.

Solutions

  1. Raise limits: `ulimit -u`, systemd TasksMax, container --pids-limit.
  2. Restart the eww daemon to clear leaked threads, then investigate why threads accumulated.
  3. Avoid spawning per command: reuse a worker/thread-pool or an async runtime for widget commands.
  4. Propagate/log the io::Error instead of panicking so a failed command click doesn't kill the whole daemon.

Example fix

// before
std::thread::Builder::new().name(...).spawn(move || { ... }).expect("Failed to start command-execution-thread");
// after
if let Err(e) = std::thread::Builder::new().name("command-execution".to_string()).spawn(move || { ... }) {
    log::error!("Failed to start command-execution-thread: {}", e);
}
Defensive patterns

Strategy: try-catch

Validate before calling

let ok = rlimit::getrlimit(rlimit::Resource::NPROC).map(|l| l.0 > 32).unwrap_or(true);
if !ok { log::warn!("thread limit low; widget commands may fail to spawn"); }

Try / catch

if let Err(e) = std::thread::Builder::new().name("cmd-exec".into()).spawn(work) {
    log::error!("Failed to start command-execution-thread: {}", e);
}

Prevention

When it happens

Trigger: Clicking a widget button (or any command-triggering widget) while the process is at its thread limit: RLIMIT_NPROC exhausted, pids cgroup cap, or out-of-memory at pthread_create.

Common situations: Configs with many frequently-clicked widgets leaking threads over a long session, containers with tiny pids limits, or heavily loaded shared machines.

Related errors


AI-assisted analysis of elkowar/eww@48f5aa8b37 (2026-09-08). Data as JSON: /api/errors/60cf5503210dbb12. Report an issue: GitHub.

Appendix: source

Thrown at crates/eww/src/widgets/mod.rs:41

        .name("command-execution-thread".to_string())
        .spawn(move || {
            log::debug!("Running command from widget [timeout: {}ms]: {}", timeout.as_millis(), cmd);
            let child = Command::new("/bin/sh").arg("-c").arg(&cmd).spawn();
            match child {
                Ok(mut child) => match child.wait_timeout(timeout) {
                    // child timed out
                    Ok(None) => {
                        log::error!("WARNING: command {} timed out", &cmd);
                        let _ = child.kill();
                        let _ = child.wait();
                    }
                    Err(err) => log::error!("Failed to execute command {}: {}", cmd, err),
                    Ok(Some(_)) => {}
                },
                Err(err) => log::error!("Failed to launch child process: {}", err),
            }
        })
        .expect("Failed to start command-execution-thread");
}

fn replace_placeholders<T>(cmd: &str, args: &[T]) -> String
where
    T: 'static + std::fmt::Display + Send + Sync + Clone,
{
    if !args.is_empty() {
        let cmd = cmd.replace("{}", &format!("{}", args[0]));
        args.iter().enumerate().fold(cmd, |acc, (i, arg)| acc.replace(&format!("{{{}}}", i), &format!("{}", arg)))
    } else {
        cmd.to_string()
    }
}

#[cfg(test)]
mod test {
    use super::*;
    #[test]

View on GitHub (pinned to 48f5aa8b37)