How to get notified when spawned subprocess dies?

I am spawning a long-living subprocess, and currently storing the child process handle after spawning it, in case I want to kill it in the future. However, I want to include log messages for when the child process dies (I am not actively monitoring it currently), but I don't know how I can get notified when this happens. Maybe by using a callback somehow? As a bonus, getting its stderr/stdout/exit status would also be great. I think I can do all of these things with process::Command.output(), but that would be busy waiting, which I don't want to do if possible (at least not in the main thread).

Is the subprocess detached? If not the parent should receive a SIGCHLD when the child exits. You can install a signal handler for that.

1 Like

How do I know if the subprocess is detached? I'm doing a simple

match process::Command::new(args[0]).args(&args[1..]).spawn() {

if that answers the question.
How would such a signal handler look?

Check if Command in std::process - Rust and ExitStatus in std::process - Rust helps.

But .status() would busily wait for the process to finish (in the main thread), which I want to avoid.

How do I know if the subprocess is detached?

By default .spawn() doesn't detach the process.

As far signal handlers go, there's the signal_hook crate, you'll probably need to signal_hook::flag::register it.

Unfortunately, I've only ever done it in C, and it vaguely resembles:

void handle_sigchld(void) { /* logic */ }

int main(void) {
    signal (SIGCHLD, handle_sigchld);
    switch (fork()) {
        /* forking logic */
    }
    return 0;
}

Maybe someone who's done this in rust can chime in?

1 Like

I'm using tokio anyways, so I'd probably try to use their signal handler.
Do you think using the tokio version of Command can be used to implement my requirements, such as Command in tokio::process - Rust?

In principle, async should be simple with wait().

let child = Command::new()....spawn().unwrap();
tokio::spawn(async move {
    let result = child.wait().await;
    do_something_about_exit(result);
});

But wait() takes &mut self so you can't simultaneously have the Child available to call kill(), and I'd guess this is probably necessary to avoid a PID race condition (not accidentally killing an unrelated process).

As a workaround, you could read the child process's output and wait for an EOF/error to occur — which doesn't guarantee that the child has exited but usually correlates. You can then try_wait() to collect the exit status, and if it hasn't yet, run a polling loop (say, once per second) to check again.

1 Like

If you already have access to async code, then either waiting or cancelling is just a case of select().

/// Start a process in the background and return a channel that can be used to
/// kill it.
fn spawn_in_background() -> oneshot::Sender<()> {
    let (sender, receiver) = oneshot::channel();
    let child = Command::new("cargo").spawn().unwrap();

    tokio::spawn(async move {
        futures::select! {
            result = child.wait() => match result {
                Ok(exit_status) => todo!("Handle {exit_status:?}"),
                Err(e) => panic!("Error occurred while waiting for result: {e}"),
            }
            _ = receiver => child.kill().expect("Unable to kill the child"),

        }
    });

    sender
}

Tokio actually uses this as the example for Command::kill().

4 Likes

Ah I see, so then we would store just the sender, and if we want to kill it we just send something to that channel with the sender. Thanks!

1 Like

Would I not have to join the tokio process that I started with join at some point? I read that for the std::Command not joining may lead to system resources not being cleaned up, is this also the case for tokio?

Cleaning up zombie processes is done by wait()ing on them and .kill() will call .wait() after sending the kill signal.

This has already mostly solved my requirements, the only thing I am still unsure about: Is it possible to somehow mutate the self value in the tokio::spawn block? I know just passing it wouldn't work, maybe wrapping in a Arc(Mutex)) would?
If that is impossible, what about not spawning a tokio thread at all? I could make spawn_in_background inself async. Would that help in being able to pass in self as well?
The reason I want to mutate self is that I am currently storing

videocapture_process: Option<oneshot::Sender<KillRequest>>,

in self. So I would like to add a self.videocapture_process = None after the select! block is exited (because the process was either killed or died by itself).

If this mutation is impossible, any suggestions on other ways to update the internal state (self) to reflect that the process has died/been killed?

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.