From 6b7d7d04419ef8ac14a286f491b7d485ff00248d Mon Sep 17 00:00:00 2001 From: Michael Hanselmann Date: Mon, 5 Jul 2021 23:51:12 +0200 Subject: [PATCH] atexit: Terminate with non-zero status after receiving signal When rclone received a SIGINT (Ctrl+C) or SIGTERM signal while an atexit function is registered it always terminated with status code 0. Unix convention is to exit with a non-zero status code. Often it's `128 + int(signum), but at least not zero. With this change fatal signals handled by the `atexit` package cause a non-zero exit code. On Unix systems it's `128 + int(signum)` while on other systems, such as Windows, it's always 2 ("error not otherwise categorised"). Resolves #5437. Signed-off-by: Michael Hanselmann --- lib/atexit/atexit.go | 2 +- lib/atexit/atexit_other.go | 6 ++++++ lib/atexit/atexit_test.go | 41 ++++++++++++++++++++++++++++++++++++++ lib/atexit/atexit_unix.go | 14 +++++++++++++ 4 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 lib/atexit/atexit_test.go diff --git a/lib/atexit/atexit.go b/lib/atexit/atexit.go index c7b37238a..bcc851847 100644 --- a/lib/atexit/atexit.go +++ b/lib/atexit/atexit.go @@ -51,7 +51,7 @@ func Register(fn func()) FnHandle { fs.Infof(nil, "Signal received: %s", sig) Run() fs.Infof(nil, "Exiting...") - os.Exit(0) + os.Exit(exitCode(sig)) }() }) diff --git a/lib/atexit/atexit_other.go b/lib/atexit/atexit_other.go index 15faa7448..bd9f238bf 100644 --- a/lib/atexit/atexit_other.go +++ b/lib/atexit/atexit_other.go @@ -4,6 +4,12 @@ package atexit import ( "os" + + "github.com/rclone/rclone/lib/exitcode" ) var exitSignals = []os.Signal{os.Interrupt} + +func exitCode(_ os.Signal) int { + return exitcode.UncategorizedError +} diff --git a/lib/atexit/atexit_test.go b/lib/atexit/atexit_test.go new file mode 100644 index 000000000..6e0315ffe --- /dev/null +++ b/lib/atexit/atexit_test.go @@ -0,0 +1,41 @@ +package atexit + +import ( + "os" + "runtime" + "testing" + + "github.com/rclone/rclone/lib/exitcode" + "github.com/stretchr/testify/assert" +) + +type fakeSignal struct{} + +func (*fakeSignal) String() string { + return "fake" +} + +func (*fakeSignal) Signal() { +} + +var _ os.Signal = (*fakeSignal)(nil) + +func TestExitCode(t *testing.T) { + switch runtime.GOOS { + case "windows", "plan9": + for _, i := range []os.Signal{ + os.Interrupt, + os.Kill, + } { + assert.Equal(t, exitCode(i), exitcode.UncategorizedError) + } + + default: + // SIGINT (2) and SIGKILL (9) are portable numbers specified by POSIX. + assert.Equal(t, exitCode(os.Interrupt), 128+2) + assert.Equal(t, exitCode(os.Kill), 128+9) + } + + // Never a real signal + assert.Equal(t, exitCode(&fakeSignal{}), exitcode.UncategorizedError) +} diff --git a/lib/atexit/atexit_unix.go b/lib/atexit/atexit_unix.go index acebfaf1c..2df372a1a 100644 --- a/lib/atexit/atexit_unix.go +++ b/lib/atexit/atexit_unix.go @@ -5,6 +5,20 @@ package atexit import ( "os" "syscall" + + "github.com/rclone/rclone/lib/exitcode" ) var exitSignals = []os.Signal{syscall.SIGINT, syscall.SIGTERM} // Not syscall.SIGQUIT as we want the default behaviour + +// exitCode calculates the exit code for the given signal. Many Unix programs +// exit with 128+signum if they handle signals. Most shell also implement the +// same convention if a program is terminated by an uncaught and/or fatal +// signal. +func exitCode(sig os.Signal) int { + if real, ok := sig.(syscall.Signal); ok && int(real) > 0 { + return 128 + int(real) + } + + return exitcode.UncategorizedError +}