The source content for blog.juliobiason.me
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2.4 KiB

+++ title = "Timeout With Command in Tokio" date = 2023-08-31

[taxonomies] tags = ["random", "rust", "tokio", "spawn", "process", "command", "timeout"] +++

How to spawn an external command and give it a timeout in Rust, with Tokio

The entry point for running external applications in Rust is the Command structure, in the process module. This whole structure is duplicated on Tokio, with async.

But there is one thing that exist in other languages (like Python) that Rust doesn't have: Having a timeout for the command (and killing it if it runs over the timeout). The usual solution is to run the command on a specialized thread and, with another thread, make sure to kill the first if the second finishes first.

But Tokio have a funcionality that saves a lot of code when dealing with this: timeout. While it doesn't apply to the Command itself, it applies to Futures, and waiting for a command is an async function, which means it is wrapped around a Future, and we can leverage this.

use std::time::Duration;

use tokio::process::Command;
use tokio::time::timeout;

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let sleep = "sleep";

    println!("Run 3 secs");
    let mut cmd = Command::new(&sleep).arg("3s").spawn().unwrap();
    if let Err(_) = timeout(Duration::from_secs(4), cmd.wait()).await {
        println!("Got timeout!");
        cmd.kill().await.unwrap();
    } else {
        println!("No timeout");
    }

    println!("Run 25 secs");
    let mut cmd = Command::new(&sleep).arg("25s").spawn().unwrap();
    if let Err(_) = timeout(Duration::from_secs(4), cmd.wait()).await {
        println!("Got timeout");
        cmd.kill().await.unwrap();
    } else {
        println!("No timeout");
    }
}

The thing here is .wait(). That's when Tokio wraps the command call into a Future. But, because the task is dead, it doesn't actually kill the command, and that's why we need to call .kill() in case of timeout -- otherwise the command will still run (you can check this by removing the .kill() call on the 25s block, and calling ps after the application finishes).

Just note that the if let Err(_) is for timeout; .wait() also returns a Result, and that's the one that needs to be checked for the actual success of the execution.