Skip to content

Commit 7989f73

Browse files
andreyneringmarco-m-pix4daliculPix4D
committed
Fix behavior of interrupt (SIGINT, SIGTERM) signals
Task will now give time for the processes running to do cleanup work Ref #458 Ref #479 Fixes #728 Co-authored-by: Marco Molteni <[email protected]> Co-authored-by: aliculPix4D <[email protected]>
1 parent c9a582f commit 7989f73

File tree

9 files changed

+480
-18
lines changed

9 files changed

+480
-18
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ dist/
2626
# exuberant ctags
2727
tags
2828

29-
/bin
29+
/bin/*
30+
!/bin/.keep
3031
/testdata/vars/v1
3132
/tmp

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
## Unreleased
44

5+
- Fix behavior of interrupt (SIGINT, SIGTERM) signals. Task will now give time
6+
for the processes running to do cleanup work
7+
([#458](https://github.com/go-task/task/issues/458), [#479](https://github.com/go-task/task/pull/479), [#728](https://github.com/go-task/task/issues/728)).
58
- Add new `--exit-code` (`-x`) flag that will pass-through the exit form the
69
command being ran
710
([#755](https://github.com/go-task/task/pull/755)).

Taskfile.yml

+21-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ tasks:
2323

2424
install:
2525
desc: Installs Task
26+
sources:
27+
- './**/*.go'
2628
cmds:
2729
- go install -v -ldflags="-w -s -X main.version={{.GIT_COMMIT}}" ./cmd/task
2830

@@ -40,12 +42,30 @@ tasks:
4042

4143
lint:
4244
desc: Runs golangci-lint
45+
sources:
46+
- './**/*.go'
4347
cmds:
4448
- golangci-lint run
4549

50+
sleepit:build:
51+
desc: Builds the sleepit test helper
52+
sources:
53+
- ./cmd/sleepit/**/*.go
54+
generates:
55+
- ./bin/sleepit
56+
cmds:
57+
- go build -o ./bin/sleepit{{exeExt}} ./cmd/sleepit
58+
59+
sleepit:run:
60+
desc: Builds the sleepit test helper
61+
deps: [sleepit:build]
62+
cmds:
63+
- ./bin/sleepit {{.CLI_ARGS}}
64+
silent: true
65+
4666
test:
4767
desc: Runs test suite
48-
deps: [install]
68+
deps: [install, sleepit:build]
4969
cmds:
5070
- go test {{catLines .GO_PACKAGES}}
5171

bin/.keep

Whitespace-only changes.

cmd/sleepit/sleepit.go

+173
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// This code is released under the MIT License
2+
// Copyright (c) 2020 Marco Molteni and the timeit contributors.
3+
4+
package main
5+
6+
import (
7+
"flag"
8+
"fmt"
9+
"os"
10+
"os/signal"
11+
"time"
12+
)
13+
14+
const usage = `sleepit: sleep for the specified duration, optionally handling signals
15+
When the line "sleepit: ready" is printed, it means that it is safe to send signals to it
16+
Usage: sleepit <command> [<args>]
17+
Commands
18+
default Use default action: on reception of SIGINT terminate abruptly
19+
handle Handle signals: on reception of SIGINT perform cleanup before exiting
20+
version Show the sleepit version`
21+
22+
var (
23+
// Filled by the linker.
24+
fullVersion = "unknown" // example: v0.0.9-8-g941583d027-dirty
25+
)
26+
27+
func main() {
28+
os.Exit(run(os.Args[1:]))
29+
}
30+
31+
func run(args []string) int {
32+
if len(args) < 1 {
33+
fmt.Fprintln(os.Stderr, usage)
34+
return 2
35+
}
36+
37+
defaultCmd := flag.NewFlagSet("default", flag.ExitOnError)
38+
defaultSleep := defaultCmd.Duration("sleep", 5*time.Second, "Sleep duration")
39+
40+
handleCmd := flag.NewFlagSet("handle", flag.ExitOnError)
41+
handleSleep := handleCmd.Duration("sleep", 5*time.Second, "Sleep duration")
42+
handleCleanup := handleCmd.Duration("cleanup", 5*time.Second, "Cleanup duration")
43+
handleTermAfter := handleCmd.Int("term-after", 0,
44+
"Terminate immediately after `N` signals.\n"+
45+
"Default is to terminate only when the cleanup phase has completed.")
46+
47+
versionCmd := flag.NewFlagSet("version", flag.ExitOnError)
48+
49+
switch args[0] {
50+
51+
case "default":
52+
_ = defaultCmd.Parse(args[1:])
53+
if len(defaultCmd.Args()) > 0 {
54+
fmt.Fprintf(os.Stderr, "default: unexpected arguments: %v\n", defaultCmd.Args())
55+
return 2
56+
}
57+
return supervisor(*defaultSleep, 0, 0, nil)
58+
59+
case "handle":
60+
_ = handleCmd.Parse(args[1:])
61+
if *handleTermAfter == 1 {
62+
fmt.Fprintf(os.Stderr, "handle: term-after cannot be 1\n")
63+
return 2
64+
}
65+
if len(handleCmd.Args()) > 0 {
66+
fmt.Fprintf(os.Stderr, "handle: unexpected arguments: %v\n", handleCmd.Args())
67+
return 2
68+
}
69+
sigCh := make(chan os.Signal, 1)
70+
signal.Notify(sigCh, os.Interrupt) // Ctrl-C -> SIGINT
71+
return supervisor(*handleSleep, *handleCleanup, *handleTermAfter, sigCh)
72+
73+
case "version":
74+
_ = versionCmd.Parse(args[1:])
75+
if len(versionCmd.Args()) > 0 {
76+
fmt.Fprintf(os.Stderr, "version: unexpected arguments: %v\n", versionCmd.Args())
77+
return 2
78+
}
79+
fmt.Printf("sleepit version %s\n", fullVersion)
80+
return 0
81+
82+
default:
83+
fmt.Fprintln(os.Stderr, usage)
84+
return 2
85+
}
86+
}
87+
88+
func supervisor(
89+
sleep time.Duration,
90+
cleanup time.Duration,
91+
termAfter int,
92+
sigCh <-chan os.Signal,
93+
) int {
94+
fmt.Printf("sleepit: ready\n")
95+
fmt.Printf("sleepit: PID=%d sleep=%v cleanup=%v\n",
96+
os.Getpid(), sleep, cleanup)
97+
98+
cancelWork := make(chan struct{})
99+
workerDone := worker(cancelWork, sleep, "work")
100+
101+
cancelCleaner := make(chan struct{})
102+
var cleanerDone <-chan struct{}
103+
104+
sigCount := 0
105+
for {
106+
select {
107+
case sig := <-sigCh:
108+
sigCount++
109+
fmt.Printf("sleepit: got signal=%s count=%d\n", sig, sigCount)
110+
if sigCount == 1 {
111+
// since `cancelWork` is unbuffered, sending will be synchronous:
112+
// we are ensured that the worker has terminated before starting cleanup.
113+
// This is important in some real-life situations.
114+
cancelWork <- struct{}{}
115+
cleanerDone = worker(cancelCleaner, cleanup, "cleanup")
116+
}
117+
if sigCount == termAfter {
118+
cancelCleaner <- struct{}{}
119+
return 4
120+
}
121+
case <-workerDone:
122+
return 0
123+
case <-cleanerDone:
124+
return 3
125+
}
126+
}
127+
}
128+
129+
// Start a worker goroutine and return immediately a `workerDone` channel.
130+
// The goroutine will prepend its prints with the prefix `name`.
131+
// The goroutine will simulate some work and will terminate when one of the following
132+
// conditions happens:
133+
// 1. When `howlong` is elapsed. This case will be signaled on the `workerDone` channel.
134+
// 2. When something happens on channel `canceled`. Note that this simulates real-life,
135+
// so cancellation is not instantaneous: if the caller wants a synchronous cancel,
136+
// it should send a message; if instead it wants an asynchronous cancel, it should
137+
// close the channel.
138+
func worker(
139+
canceled <-chan struct{},
140+
howlong time.Duration,
141+
name string,
142+
) <-chan struct{} {
143+
workerDone := make(chan struct{})
144+
deadline := time.Now().Add(howlong)
145+
go func() {
146+
fmt.Printf("sleepit: %s started\n", name)
147+
for {
148+
select {
149+
case <-canceled:
150+
fmt.Printf("sleepit: %s canceled\n", name)
151+
return
152+
default:
153+
if doSomeWork(deadline) {
154+
fmt.Printf("sleepit: %s done\n", name) // <== NOTE THIS LINE
155+
workerDone <- struct{}{}
156+
return
157+
}
158+
}
159+
}
160+
}()
161+
return workerDone
162+
}
163+
164+
// Do some work and then return, so that the caller can decide wether to continue or not.
165+
// Return true when all work is done.
166+
func doSomeWork(deadline time.Time) bool {
167+
if time.Now().After(deadline) {
168+
return true
169+
}
170+
timeout := 100 * time.Millisecond
171+
time.Sleep(timeout)
172+
return false
173+
}

cmd/task/task.go

+3-16
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,9 @@ import (
55
"fmt"
66
"log"
77
"os"
8-
"os/signal"
98
"path/filepath"
109
"runtime/debug"
1110
"strings"
12-
"syscall"
1311

1412
"github.com/spf13/pflag"
1513
"mvdan.cc/sh/v3/syntax"
@@ -204,11 +202,12 @@ func main() {
204202
globals.Set("CLI_ARGS", taskfile.Var{Static: cliArgs})
205203
e.Taskfile.Vars.Merge(globals)
206204

207-
ctx := context.Background()
208205
if !watch {
209-
ctx = getSignalContext()
206+
e.InterceptInterruptSignals()
210207
}
211208

209+
ctx := context.Background()
210+
212211
if status {
213212
if err := e.Status(ctx, calls...); err != nil {
214213
log.Fatal(err)
@@ -249,18 +248,6 @@ func getArgs() ([]string, string, error) {
249248
return args[:doubleDashPos], strings.Join(quotedCliArgs, " "), nil
250249
}
251250

252-
func getSignalContext() context.Context {
253-
ch := make(chan os.Signal, 1)
254-
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
255-
ctx, cancel := context.WithCancel(context.Background())
256-
go func() {
257-
sig := <-ch
258-
log.Printf("task: signal received: %s", sig)
259-
cancel()
260-
}()
261-
return ctx
262-
}
263-
264251
func getVersion() string {
265252
if version != "" {
266253
return version

internal/execext/exec.go

+2
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"path/filepath"
99
"strings"
10+
"time"
1011

1112
"mvdan.cc/sh/v3/expand"
1213
"mvdan.cc/sh/v3/interp"
@@ -48,6 +49,7 @@ func RunCommand(ctx context.Context, opts *RunCommandOptions) error {
4849
r, err := interp.New(
4950
interp.Params("-e"),
5051
interp.Env(expand.ListEnviron(environ...)),
52+
interp.ExecHandler(interp.DefaultExecHandler(15*time.Second)),
5153
interp.OpenHandler(openHandler),
5254
interp.StdIO(opts.Stdin, opts.Stdout, opts.Stderr),
5355
dirOption(opts.Dir),

signals.go

+31
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package task
2+
3+
import (
4+
"os"
5+
"os/signal"
6+
"syscall"
7+
8+
"github.com/go-task/task/v3/internal/logger"
9+
)
10+
11+
// NOTE(@andreynering): This function intercepts SIGINT and SIGTERM signals
12+
// so the Task process is not killed immediatelly and processes running have
13+
// time to do cleanup work.
14+
func (e *Executor) InterceptInterruptSignals() {
15+
ch := make(chan os.Signal, 3)
16+
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
17+
18+
go func() {
19+
for i := 1; i <= 3; i++ {
20+
sig := <-ch
21+
22+
if i < 3 {
23+
e.Logger.Outf(logger.Yellow, `task: Signal received: "%s"`, sig)
24+
continue
25+
}
26+
27+
e.Logger.Errf(logger.Red, `task: Signal received for the third time: "%s". Forcing shutdown`, sig)
28+
os.Exit(1)
29+
}
30+
}()
31+
}

0 commit comments

Comments
 (0)