@@ -3,7 +3,12 @@ package command
33import (
44 "context"
55 "log"
6+ "os"
7+ "os/exec"
8+ "os/signal"
69 "regexp"
10+ "syscall"
11+ "time"
712
813 "github.com/google/go-github/github"
914 secretvalue "github.com/zimbatm/go-secretvalue"
@@ -61,3 +66,87 @@ func refString(str string) *string {
6166func refStringList (l []string ) * []string {
6267 return & l
6368}
69+
70+ var DefaultKillDelay = 5 * time .Minute
71+
72+ // waitOrStop waits for the already-started command cmd by calling its Wait method.
73+ //
74+ // If cmd does not return before ctx is done, waitOrStop sends it the given interrupt signal.
75+ // waitOrStop waits DefaultKillDelay for Wait to return before sending os.Kill.
76+ //
77+ // This function is copied from the one added to x/playground/internal in
78+ // http://golang.org/cl/228438.
79+ func waitOrStop (ctx context.Context , cmd * exec.Cmd ) error {
80+ if cmd .Process == nil {
81+ panic ("waitOrStop called with a nil cmd.Process — missing Start call?" )
82+ }
83+
84+ errc := make (chan error )
85+ go func () {
86+ select {
87+ case errc <- nil :
88+ return
89+ case <- ctx .Done ():
90+ }
91+
92+ err := cmd .Process .Signal (os .Interrupt )
93+ if err == nil {
94+ err = ctx .Err () // Report ctx.Err() as the reason we interrupted.
95+ } else if err .Error () == "os: process already finished" {
96+ errc <- nil
97+
98+ return
99+ }
100+
101+ if DefaultKillDelay > 0 {
102+ timer := time .NewTimer (DefaultKillDelay )
103+ select {
104+ // Report ctx.Err() as the reason we interrupted the process...
105+ case errc <- ctx .Err ():
106+ timer .Stop ()
107+
108+ return
109+ // ...but after killDelay has elapsed, fall back to a stronger signal.
110+ case <- timer .C :
111+ }
112+
113+ // Wait still hasn't returned.
114+ // Kill the process harder to make sure that it exits.
115+ //
116+ // Ignore any error: if cmd.Process has already terminated, we still
117+ // want to send ctx.Err() (or the error from the Interrupt call)
118+ // to properly attribute the signal that may have terminated it.
119+ _ = cmd .Process .Kill ()
120+ }
121+
122+ errc <- err
123+ }()
124+
125+ waitErr := cmd .Wait ()
126+
127+ interruptErr := <- errc
128+ if interruptErr != nil {
129+ return interruptErr
130+ }
131+
132+ return waitErr
133+ }
134+
135+ // contextWithHandler returns a context that is canceled when the program receives a SIGINT or SIGTERM.
136+ //
137+ // !! Only call this function once per program.
138+ func contextWithHandler () context.Context {
139+ ctx , cancel := context .WithCancel (context .Background ())
140+
141+ signalChan := make (chan os.Signal , 1 )
142+
143+ signal .Notify (signalChan , syscall .SIGINT , syscall .SIGTERM )
144+
145+ go func () {
146+ sig := <- signalChan
147+ log .Printf ("Received signal %s, stopping" , sig )
148+ cancel ()
149+ }()
150+
151+ return ctx
152+ }
0 commit comments