2

i noticed that subprocesses created using Start() will be terminated after program exit, for example:

package main

import "os/exec"

func main() {
    cmd := exec.Command("sh", "test.sh")
    cmd.Start()
}

when main() exits, test.sh will stop running

jm33_m0
  • 595
  • 2
  • 9
  • 17
  • There seems to be a problem with how "when main() exits, test.sh will stop running" is being asserted and/or why the result isn't what you expect. How long of a run time is `test.sh` expected to have? How long after executing the go program have you checked to see if the `test.sh` process is still running? Take the following `test.sh`: `for _ in $(seq 20); do sleep 1; done`. Now, run the go program and immediately run `pgrep -f test.sh`. It should return a process ID for approx. 19-20 sec. after the go program has exited. This is the expected behavior of `cmd.Start()`. – John B Feb 26 '17 at 19:45

4 Answers4

4

The subprocess should continue to run after your process ends, as long as it ends cleanly, which won't happen if you hit ^C. What you can do is intercept the signals sent to your process so you can end cleanly.

sigchan := make(chan os.Signal, 1)
signal.Notify(sigchan,
    syscall.SIGINT,
    syscall.SIGKILL,
    syscall.SIGTERM,
    syscall.SIGQUIT)
go func() {
    s := <-sigchan
    // do anything you need to end program cleanly
}()
mat007
  • 905
  • 8
  • 16
MahlerFive
  • 5,159
  • 5
  • 30
  • 40
  • 1
    Yeah now I understand that subprocess doesn't exit with main program, I am going to add `&` to the shell command, ( dunno if `^C` kills subprocess too – jm33_m0 Feb 26 '17 at 17:30
  • Thanks for this answer! I spent hours investigating this without realizing that `^C` was the problem. – Wayne Bloss Dec 08 '22 at 01:07
3

A subprocess (if not waited on within the go program) will continue to run once the go program has finished (unless the subprocess naturally finishes before the parent go program).

The problem the original poster is likely encountering is that they are probably terminating their go program early (e.g. using <Ctrl-c>), and because the go program is not exiting cleanly the subprocess it spawned is also terminated.

Below is a reduced test case that helps validate this behaviour...

First I create a bash shell script I want to run (e.g. test.sh, don't forget to chmod +x ./test.sh so the script is considered 'executable'). The script is very simple. It sleeps for 10 seconds and then either creates a new file called testfile (if it doesn't exist) or if the file already exists it will update the 'last modified' timestamp. This is important because this is how I confirm the bash script is still running once my go program finishes (which I expect to finish long before the bash script finishes due to the 10 second sleep).

#!/usr/local/bin/bash

sleep 10
touch testfile

Next, I have a simple go program, which spawns a subprocess that runs the bash script above but importantly doesn't wait for it to complete. You'll see I've also added a 2 second sleep to my go program which gives me some time to press <Ctrl-c>. Now, even though I have a 2 second sleep, this program (if left to run without me pressing <Ctrl-c>) will finish before the subprocess bash script does (which is sleeping for 10 seconds):

package main

import (
    "fmt"
    "log"
    "os/exec"
    "time"
)

func main() {
    cmd := exec.Command("./test.sh")
    err := cmd.Start()
    if err != nil {
        log.Fatal(err)
    }
    time.Sleep(2 * time.Second)
    fmt.Println("program finished, but what about the subprocess?")
}

If I run the go program and just let it finish naturally, I can ls -l testfile and check the timestamp on it. I'll then wait 10 seconds and run the ls -l testfile again and I will see the timestamp update (which shows the subprocess finished successfully).

Now if I was to re-run the go program and this time press <Ctrl-c> before the program finishes (this is why I add the 2 second sleep), then not only will the go program exit early, but the subprocess will be terminated also. So I can wait 10 seconds or 10 hours or longer, doesn't matter. The timestamp on the testfile will not update, proving the subprocess was terminated.

Integralist
  • 5,899
  • 5
  • 25
  • 42
0

Try modding you program a to use Run instead of start. In that way the Go program will wait for the sh script to finish before exiting.

package main

import (
    "log"
    "os/exec"
)

func main() {
    cmd := exec.Command("sh", "test.sh")
    err := cmd.Run()
    if err != nil {
        log.Fatalln(err)
    }
}

Likewise, you could always use a wait group but I think that's overkill here.

You could also just a go routine with or without a wait group. Depends on if you want go to wait for the program the sh program to complete

package main

import (
    "os/exec"
)

func runOffMainProgram() {
    cmd := exec.Command("sh", "test.sh")
    cmd.Start()
}

func main() {
    // This will start a go routine, but without a waitgroup this program will exit as soon as it runs
    // regardless the sh program will be running in the background. Until the sh program completes
    go runOffMainProgram()
}
reticentroot
  • 3,612
  • 2
  • 22
  • 39
  • Thanks for your answer, but I want `test.sh` to keep running even if when the main program is dead, in which case the subprocess is totally independent of the go program – jm33_m0 Feb 26 '17 at 17:06
  • what do you mean? Are you killing the main program. In the example above the main program doesn't die until the sh program completes, likewise you are creating a subprocess above. If you run the program above and then use something like ps aux | grep test.sh in a terminal, you'll see the script is running independently of the go program. – reticentroot Feb 26 '17 at 17:08
  • What if I add `&` to my command?... I think it might be a solution – jm33_m0 Feb 26 '17 at 17:12
  • Just want to make sure the subprocess is always running even when the main program is not, yes, `Run()` and `Wait()` will tell Golang to wait for my process to finish, but I don't need that – jm33_m0 Feb 26 '17 at 17:18
  • This does not change how `crtl-c` is processed. See my answer. However, running the main program on the shell with an ampersand (send to background) does make a difference! – Ярослав Рахматуллин Oct 23 '21 at 17:47
0

The accepted answer is vague about where the signal should be handled. I think some more sophisticated techniques must be used to prevent sending interrupts to children, if at all possible.

TLDR;

So the only way to deal with ctrl-c is to anticipate the SIGINT and process that signal in the children.


I did some experimentation of my own.

go build -o ctrl-c ctrl-c.go

If the program is sent to the background, The only way to kill the main process is with kill -9 (SIGKILL).

SIGTERM (15) will not do.

$ ./ctrl-c & cmd=$! ; sleep 1 && echo kill $cmd && kill $cmd 
[1] 1165918
1165918
bashed 1165926
bashed 1165927
bashed 1165928
main()
go SIGN 23 urgent I/O condition
go SIGN 23 urgent I/O condition
main()
kill 1165918
go SIGN 15 terminated
main()
$ main()
main()
main()
main()
main()
main() done.
Bash _ 1165926 EXITs
Bash q 1165927 EXITs
Bash c 1165928 EXITs

[1]+  Done                    ./ctrl-c

SIGINT (2) will not do.

$ ./ctrl-c & cmd=$! ; sleep 1 && echo kill $cmd &&  kill -INT  $cmd 
[1] 1167675
1167675
bashed 1167683
bashed 1167684
bashed 1167685
main()
main()
kill 1167675
go SIGN 2 interrupt
main()
balmora: ~/src/my/go/doodles/sub-process [master]
$ main()
main()
main()
main()
main()
main() done.
Bash _ 1167683 EXITs
Bash q 1167684 EXITs
Bash c 1167685 EXITs

SIGKILL kills the main process but not the bash sub-commands.


$ ./ctrl-c & cmd=$! ; sleep 1 && echo kill $cmd &&  kill -KILL  $cmd 
[1] 1170721
1170721
bashed 1170729
bashed 1170730
bashed 1170731
main()
main()
kill 1170721
[1]+  Killed                  ./ctrl-c

Bash _ 1170729 EXITs
Bash q 1170730 EXITs
Bash c 1170731 EXITs

However, if the go binary is running in the foreground then only children who do deal with SIGINT will be kept running. This feels like almost the opposite of the above findings

$ ./ctrl-c 
1186531
bashed 1186538
bashed 1186539
bashed 1186540
main()
main()
main()
main()
main()
main()
^C

Bash c 1186540 INTs quit
Bash q 1186539 INTs ignored

Bash c 1186540 EXITs

Bash _ 1186538 INTs ignored
go SIGN 2 interrupt
go SIGN 17 child exited
6q ELAPSED 2

Bash q 1186539 EXITs
6_ ELAPSED 2

Bash _ 1186538 EXITs
go SIGN 17 child exited
main()
main()
main() done.

Anyway, the takeaway for me is that ctrl+c is forwarded to children when Cmd.Start() is used. The behavior is the same if Cmd.Run() is used, but Cmd.Run() will wait before each sub-command exits. Running the Cmd in a go routine (go func(){}()) does not change anything. If the sub-commands are started "in parallel" as a go-routine or with Cmd.Start(), the the interrupt signal will reach all of them at the same time.

To keep the sub-commands running on an interactive terminal after an interrupt, I think the sub-commands have to handle the signal and ignore it.


The code I experimented with:


package main

import (
    "fmt"
    "log"
    "os"
    "os/exec"
    "os/signal"
    "syscall"
    "time"
)

func signs(s ...os.Signal) chan os.Signal {
    signals := make(chan os.Signal, 1)
    signal.Notify(signals, s...)
    signal.Notify(signals,
        os.Interrupt, syscall.SIGINT, syscall.SIGQUIT, // keyboard
        syscall.SIGKILL, syscall.SIGHUP, syscall.SIGTERM, // os termination
        syscall.SIGUSR1, syscall.SIGUSR2, // user
        syscall.SIGPIPE, syscall.SIGCHLD, syscall.SIGSEGV, // os other
    )
    return signals
}

func interpret(signals chan os.Signal) chan os.Signal {
    go func() {
        for ;; {
            select {
            case sign := <-signals:
                elog("go SIGN %#v %s", sign, sign)
            }
        }
    }()
    return signals
}

func bash(script string) {
    cmd := exec.Command("/bin/bash", "-c", script )
    cmd.Stdout = os.Stderr
    err := cmd.Start()
    //err := cmd.Run()
    if err != nil {
        log.Fatal(err)
    }
    elog("bashed %d", cmd.Process.Pid)
}

func main() {
    fmt.Println(os.Getpid())

    signals := interpret(signs())
    signals = signals

    //go bash(`
    bash(`
        trap ' echo Bash _ $$  INTs ignored; ' SIGINT
        trap ' echo Bash _ $$ QUITs ignored; ' SIGQUIT
        trap ' echo Bash _ $$ EXITs'           EXIT
        sleep 6;
        echo 6_ $( ps -o etimes -p $$ )

        #for i in {1..60}; do echo -n _; sleep 0.1; done; echo
    `)

    // go bash(`
    bash(`
        trap ' echo Bash q $$  INTs ignored; ' SIGINT
        trap ' echo Bash q $$ QUITs; exit    ' SIGQUIT
        trap ' echo Bash q $$ EXITs;         ' EXIT
        sleep 6;
        echo 6q $( ps -o etimes -p $$ )
        #for i in {1..60}; do echo -n q; sleep 0.1; done; echo
    `)

    //go bash(`
    bash(`
        trap ' echo Bash c $$  INTs quit; exit   ' SIGINT
        trap ' echo Bash c $$ QUITs ignored; ' SIGQUIT
        trap ' echo Bash c $$ EXITs'           EXIT
        sleep 6;
        echo 6c $( ps -o etimes -p $$ )
        #for i in {1..60}; do echo -n c; sleep 0.1; done; echo
    `)

    go func() {
        for ;; {
            time.Sleep(time.Millisecond * 333)
            elog("main()")
        }
    }()

    time.Sleep(3 * time.Second)
    elog("main() done.")
}

func echo(a ...interface{}) {
    _, err := fmt.Println(a...)
    if err != nil {
        fmt.Println("ERR ", err.Error())
    }
}

func elog(form string, arg ...interface{}) {
    println(fmt.Sprintf(form, arg...))
}