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...))
}