Prevent program from exiting on child SIGINT


#1

I’m writing a shell in rust and I’m trying to make my shell not exit if I ^C a program the started.

I’m starting the child process with this code. The readline library I’m using, rustyline, typically handles exit signals but if ^C is press while a program is running the shell exits. Any ideas on how to prevent this?

match Command::new(&args[0])
    .stdout(Stdio::inherit())
    .stderr(Stdio::inherit())
    .spawn() {
    Ok(mut cmd) => {
        match cmd.wait() {
            Ok(status) => {
                status.success()
            },
            Err(_) => {
                println!("failed to wait for child");
                false
            },
        }
    },
    Err(_) => {
        println!("Failed to execute");
        false
    },
}

My code


#2

I assume you’re on UNIX.

What you’re encountering is not specific to Rust, but to UNIX. You have encountered process groups.

Relevant Stack Overflow question

To direct signals from the controlling terminal to your child process only, you must create a process group and make it foreground.


#3

Thank you very much for the suggestion. Any ideas on how to implement that in Rust? Any suggestions would be much appreciated.


#4

I can’t give you many Rust specifics except that you’ll have to stop using spawn and switch to POSIX API. Here’s a discussion of doing it in C, which should mostly translate.


#5

Actually, you should be able to do this using Command::spawn. What you have to do is create the new process group in a before_exec hook. This method is added by the CommandExt trait only available on unix.

In the before_exec callback, you’ll need to use the C functions to actually create a new process group. These are available through the libc crate.


#6

Nice. Is that hook executed synchronously before spawn returns? (The documentation implies this but doesn’t state it explicitly.)

If that’s the case, you can likely avoid the double-process-group-setting trick that we usually do in C to avoid certain race conditions. The Wikipedia page notes that handling the process group transition synchronously (i.e. by writing to a pipe after it’s complete) can avoid the race… you’d want to be careful though.


#7

I tried to implement what you guys suggested with no luck. Any suggestions on what I’m doing wrong?

match Command::new(&args[0])
            .args(&args[1..])
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit())
            .before_exec(|| {
                unsafe {
                    let pid = libc::getpid();
                    libc::setgid(pid as u32);
                }
                Result::Ok(())
            })
            .spawn() {
            Ok(mut cmd) => {
                match cmd.wait() {
                    Ok(status) => {
                        status.success()
                    },
                    Err(_) => {
                        println!("failed to wait for child");
                        false
                    },
                }
            },
            Err(_) => {
                println!("Failed to execute");
                false
            },
        }

#8

I believe you want libc::setpgid, not libc::setgid. :grimacing:


#9

I don’t think so because its executed after forking, but I’m not sure at all.


#10

Since failures in the hook are conveyed back to the parent process, it seems like it must be creating a pipe and pushing the result back – which would mean it’s synchronous, and could avoid the race condition in setpgid. But I should probably try it out.


#11

It would seem that I also need to execute the tcsetpgrp function to put the child process in the foreground but I can’t find a rust binding for it. Anyone know of one? posix.rs is no longer mainted and nix doesn’t seem to have it.


#12

Oops. Thanks :sweat_smile:


#13

In case anyone cares I found a working solution.

Full code in my repo https://github.com/zethra/rush

match Command::new(&args[0])
            .args(&args[1..])
            .stdout(Stdio::inherit())
            .stderr(Stdio::inherit())
            .before_exec(move || {
                let pid = nix::unistd::getpid();
                nix::unistd::setpgid(pid, pid);
                unsafe {
                    libc::signal(libc::SIGINT, libc::SIG_DFL);
                    libc::signal(libc::SIGQUIT, libc::SIG_DFL);
                    libc::signal(libc::SIGTSTP, libc::SIG_DFL);
                    libc::signal(libc::SIGTTIN, libc::SIG_DFL);
                    libc::signal(libc::SIGTTOU, libc::SIG_DFL);
                }
                Result::Ok(())
            })
            .spawn() {
            Ok(mut child) => {
                let child_pgid = child.id() as i32;
                nix::unistd::tcsetpgrp(0, child_pgid);
                match child.wait() {
                    Ok(status) => {
                        nix::unistd::tcsetpgrp(0, nix::unistd::getpid());
                        status.success()
                    },
                    Err(_) => {
                        nix::unistd::tcsetpgrp(0, nix::unistd::getpid());
                        println!("failed to wait for child");
                        false
                    },
                }
            },
            Err(_) => {
                println!("Failed to execute");
                false
            },
        }

Job control on Windows