Skip to content

Commit 66fc2f4

Browse files
authored
RSDK-12332 - Do not ignore SIGPIPEs for child processes (#514)
1 parent 01acb54 commit 66fc2f4

File tree

2 files changed

+50
-11
lines changed

2 files changed

+50
-11
lines changed

runtime.go

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,40 @@ import (
1515
)
1616

1717
// ContextualMain calls a main entry point function with a cancellable
18-
// context via SIGTERM. This should be called once per process so as
18+
// context via SIGTERM and ignores SIGPIPEs. This should be called once per process so as
1919
// to not clobber the signals from Notify.
2020
func ContextualMain[L ILogger](main func(ctx context.Context, args []string, logger L) error, logger L) {
21-
contextualMain(main, false, logger)
21+
// RSDK-11244: SIGPIPE errors happen when a program writes to a pipe that has no readers. This
22+
// can, for example, happen when:
23+
// - Piping a program (e.g: viam-server) to `tee` and hitting Ctrl-C.
24+
// - Writing a gRPC request over a unix socket to a program that has crashed.
25+
//
26+
// We want to ignore SIGPIPE errors such that, in the SIGINT case, clean shutdown can proceed.
27+
contextualMain(main, false, logger, syscall.SIGPIPE)
2228
}
2329

2430
// ContextualMainQuit is the same as ContextualMain but catches quit signals into the provided
2531
// context accessed via ContextMainQuitSignal.
2632
func ContextualMainQuit[L ILogger](main func(ctx context.Context, args []string, logger L) error, logger L) {
27-
contextualMain(main, true, logger)
33+
contextualMain(main, true, logger, syscall.SIGPIPE)
34+
}
35+
36+
// ContextualMainWithSIGPIPE calls a main entry point function with a cancellable
37+
// context via SIGTERM and does not ignore SIGPIPEs. This should be called once per process so as
38+
// to not clobber the signals from Notify.
39+
// This should be used with processes where SIGPIPEs indicate that it should stop (e.g. child processes which should
40+
// not live beyond its parent's lifetime).
41+
func ContextualMainWithSIGPIPE[L ILogger](main func(ctx context.Context, args []string, logger L) error, logger L) {
42+
contextualMain(main, false, logger)
2843
}
2944

30-
func contextualMain[L ILogger](main func(ctx context.Context, args []string, logger L) error, quitSignal bool, logger L) {
45+
func contextualMain[L ILogger](
46+
main func(ctx context.Context, args []string, logger L) error, quitSignal bool, logger L, ignoreSignals ...os.Signal,
47+
) {
3148
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
32-
// RSDK-11244: SIGPIPE errors happen when a program writes to a pipe that has no readers. This
33-
// can, for example, happen when:
34-
// - Piping a program (e.g: viam-server) to `tee` and hitting Ctrl-C.
35-
// - Writing a gRPC request over a unix socket to a program that has crashed.
36-
//
37-
// We want to ignore SIGPIPE errors such that, in the SIGINT case, clean shutdown can proceed.
38-
signal.Ignore(syscall.SIGPIPE)
49+
if len(ignoreSignals) > 0 {
50+
signal.Ignore(ignoreSignals...)
51+
}
3952
if quitSignal {
4053
quitC := make(chan os.Signal, 1)
4154
signal.Notify(quitC, syscall.SIGQUIT)

runtime_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,32 @@ func TestContextualMain(t *testing.T) {
3838
test.That(t, captured, test.ShouldResemble, []interface{}{err1})
3939
}
4040

41+
//nolint:dupl
42+
func TestContextualMainWithSIGPIPE(t *testing.T) {
43+
var captured []interface{}
44+
fatal = func(logger ILogger, args ...interface{}) {
45+
captured = args
46+
}
47+
err1 := errors.New("whoops")
48+
mainWithArgs := func(ctx context.Context, args []string, logger ZapCompatibleLogger) error {
49+
return err1
50+
}
51+
var logger ZapCompatibleLogger = golog.NewTestLogger(t)
52+
ContextualMainWithSIGPIPE(mainWithArgs, logger)
53+
test.That(t, captured, test.ShouldResemble, []interface{}{err1})
54+
captured = nil
55+
mainWithArgs = func(ctx context.Context, args []string, logger ZapCompatibleLogger) error {
56+
return context.Canceled
57+
}
58+
ContextualMainWithSIGPIPE(mainWithArgs, logger)
59+
test.That(t, captured, test.ShouldBeNil)
60+
mainWithArgs = func(ctx context.Context, args []string, logger ZapCompatibleLogger) error {
61+
return multierr.Combine(context.Canceled, err1)
62+
}
63+
ContextualMainWithSIGPIPE(mainWithArgs, logger)
64+
test.That(t, captured, test.ShouldResemble, []interface{}{err1})
65+
}
66+
4167
//nolint:dupl
4268
func TestContextualMainQuit(t *testing.T) {
4369
var captured []interface{}

0 commit comments

Comments
 (0)