Search code examples
linuxrustforkparent-childspawn

Rust: How to spawn child process that continues to live after parent receives SIGINT/SIGTERM


I am currently writing an application that also starts other applications (e.g. firefox). I want these child applications to outlive the parent (e.g. they should continue to run when the parent exits). This is working (see my code below) as long as the parent exits itself (end of main, process:exit()) but if the parent receives SIGINT (ctrl + c), SIGTERM all child processes instantly die with it as well. How can I avoid this? Note: My main process is intended to be long-lived so all the examples below which exit themselves right after spawning the child are not suitable for my case, I just listed them for completeness to show what I've tried, etc.

For now I only care for Linux support if there is no clean cross-plattform solution for this.

So far, I have tried the following methods, none of them working to my satisfaction:

use std::{
    process::{self, Child, Command, Stdio},
    thread,
};

const EXECUTABLE: &str = "/usr/bin/firefox";

fn main() {
    // The child continues to live after our process has finished
    spawn_and_exit();

    // The child continues to live after our process has cleanly finished
    //spawn_and_continue()

    // The child gets killed as well if our process gets killed
    //spawn_and_force_shutdown()

    // Child continues to live (if our process shuts down cleanly)
    //threaded_clean_spawn()

    // Child gets killed as well
    //threaded_and_force_shutdown()

    // child gets killed as well
    //double_threaded_and_force_shutdown()
}

fn wait() {
    std::thread::sleep(std::time::Duration::from_millis(250));
}

fn hang() {
    println!("You can now kill the process (e.g. Ctrl+C)");
    loop { wait(); }
}

/// The child continues to live after our process has finished
fn spawn_and_exit() {
    println!("Spawn and exit");
    let _child = Command::new(EXECUTABLE)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();

    // give the process some time to actually start
    wait();
    wait();
    process::exit(0);
}


/// The child continues to live after our process has finished
fn spawn_and_continue() {
    println!("Spawn and clean shutdown");
    let _child = Command::new(EXECUTABLE)
        //.stdin(Stdio::null())
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();

    // give the process some time to actually start
    wait();
}


/// The child gets killed as well if our process gets killed
fn spawn_and_force_shutdown() {
    println!("Spawn and force shutdown");
    let _child = Command::new(EXECUTABLE)
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()
        .unwrap();

    wait();
    hang();
}


/// Child continues to live (if our process shuts down cleanly)
fn threaded_clean_spawn() {
    println!("threaded_clean_spawn");
    let _joinhandle = thread::Builder::new().spawn(|| {
        spawn_and_continue();
    });

    wait();
}


/// Child gets killed as well
fn threaded_and_force_shutdown() {
    println!("threaded_and_force_shutdown");
    let _joinhandle = thread::Builder::new().spawn(|| {
        spawn_and_continue();
    });

    hang();
}


/// child gets killed as well
fn double_threaded_and_force_shutdown() {
    println!("double_threaded_and_force_shutdown");
    let _joinhandle = thread::Builder::new().spawn(|| {
        let joinhandle = thread::Builder::new().spawn(move || {
            spawn_and_continue();
        }).unwrap();

        let _ = joinhandle.join();
        println!("inner thing returned");
    });


    hang();
}

Side-note: Originally, I expected that thread::Builder::new().spawn() would solve my problem because the documentation (https://doc.rust-lang.org/std/thread/struct.Builder.html#method.spawn) states:

The spawned thread may outlive the caller (unless the caller thread is the main thread; the whole process is terminated when the main thread finishes).

Due to the addition in the brackets, I also tried the double_threaded_and_force_shutdown method, without success.

This is basically the same question as How to Spawn Child Processes that Don't Die with Parent? but for Rust instead of c++.


Solution

  • You need to double-fork in order to prevent the child from being terminated when the parent process is killed. This is linux specific and is not related to rust.

    I'm using the nix crate to call the linux APIs (proper error handling is omitted):

    use std::{
        process::{exit, Command},
        thread::sleep,
        time::Duration,
    };
    
    use nix::{
        sys::wait::waitpid,
        unistd::{fork, ForkResult},
    };
    
    fn main() {
        match fork().expect("Failed to fork process") {
            ForkResult::Parent { child } => {
                println!("Try to kill me to check if the target process will be killed");
    
                // Do not forget to wait for the fork in order to prevent it from becoming a zombie!!!
                waitpid(Some(child), None).unwrap();
    
                // You have 120 seconds to kill the process :)
                sleep(Duration::from_secs(120));
            }
    
            ForkResult::Child => {
                // replace with your executable
                Command::new("/usr/bin/file-roller")
                    .spawn()
                    .expect("failed to spawn the target process");
                exit(0);
            }
        }
    }
    

    You must not forget to call waitpid on the first fork while you have its PID, otherwise it will become a zombie process. The only wait to get rid of the zombies is to either call waitpid in order for the OS to release any associated resources or kill their parent - i.e. your app, so just call waitpid and save yourself the trouble.