Using tokio's child process across tasks

Is there a way to await for tokio's child process to finish in spawned task but still be able to use it after this task was spawned?

use std::{io, sync::Arc};
use tokio::{process::Command, sync::Mutex, task};

#[tokio::main]
async fn main() -> io::Result<()> {
    let process = Arc::new(Mutex::new(Command::new("ls").spawn()?));

    task::spawn({
        let process = process.clone();

        async move {
            let mut process = process.lock().await;
            process.wait_with_output().await;
        }
    });

    // use process here

    Ok(())
}

The current error is:

error[E0507]: cannot move out of dereference of `tokio::sync::MutexGuard<'_, tokio::process::Child>`
  --> src/main.rs:14:13
   |
14 |             process.wait_with_output().await;
   |             ^^^^^^^ move occurs because value has type `tokio::process::Child`, which does not implement the `Copy` trait

Playground: Rust Playground

You can use a enum in the Mutex then store the Output in it after the wait has been run.

I'm not sure I follow. Can you drop some code?

Anyway, I posted a dummy reduced example for readability but it doesn't really illustrates "why"s. Here's the full code that I'm trying to make work to clarify what exactly I'm trying to do:

pub(crate) async fn wait(self, stdio: Stdio) -> Result {
    let process = self.0;

    let exit_reason = {
        let process = process.clone();
        tokio::select! {
            result = {
              let mut process = process.lock().await;
              process.wait_with_output()
            } => ExitReason::ProcessFinished(result),
            _ = signal::ctrl_c() => ExitReason::CtrlC,
        }
    };

    match exit_reason {
        ExitReason::ProcessFinished(result) => {
            let output = result?;
            if output.status.success() {
                match stdio {
                    Stdio::Inherit | Stdio::Ignore => Ok(Done::Bye),
                    Stdio::Collect => Ok(Done::Output(output.into())),
                }
            } else {
                Err(output.into())
            }
        }
        ExitReason::CtrlC => {
            let res = {
                let process = process.clone();
                tokio::select! {
                    _ = {
                      let mut process = process.lock().await;
                      process.wait()
                    } => CtrlCResult::ProcessExited,
                    _ = time::sleep(Duration::from_secs(TIMEOUT)) => CtrlCResult::Timeout,
                }
            };

            match res {
                CtrlCResult::ProcessExited => Ok(Done::Bye),
                CtrlCResult::Timeout => {
                    let mut process = process.lock().await;
                    process.kill().await?;
                    Ok(Done::Bye)
                }
            }
        }
    }
}

Worked around it by

  • extracting pid before move
  • moving process + atomic bool to own task
  • if process exits, atomic bool gets updated
  • then it can be checked in the main task later on: if it wasn't flipped after timeout, it means process is still running -> killing it using libc::kill(pid, libc::SIGKILL).