From 1b83db3489a917f59e1e8fb54489e3e423926fc9 Mon Sep 17 00:00:00 2001 From: Romano <6548898+romanornr@users.noreply.github.com> Date: Wed, 10 Dec 2025 06:44:48 +0100 Subject: [PATCH] signaler: improve cross-platform signal compatibility (#1952) * signaler: improve cross-platform signal compatibility - Add runtime-based platform detection for signal handling - Include os.Kill only on Windows (cannot be caught/ignored on Unix) - Refactor signal initialization into separate getPlatformSignals function * signaler: add dependency injection and improve docs - Introduce SignalNotifier interface for testability - Add osSignalNotifier implementation and factory function - Enhance package and function documentation - Maintain backward compatibility with existing API * [signaler] simplify shutdown handling; drop SIGABRT/os.Kill * skip signaler tests on windows * support signaler interrupt tests and improve signal handling tests * removing getPlatformSignal function * remove signaler readme * Signaler: Return a channel from WaitForInterrupt * Signaler: fix comment Co-authored-by: Ryan O'Hara-Reid * Signaler: require NoError to NoErrorf Co-authored-by: Ryan O'Hara-Reid --------- Co-authored-by: Ryan O'Hara-Reid --- backtester/btcli/main.go | 2 +- backtester/main.go | 4 ++-- cmd/gctcli/main.go | 2 +- main.go | 2 +- signaler/signaler.go | 22 ++++++---------------- signaler/signaler_test.go | 37 +++++++++++++++++++++++++++++++++++++ 6 files changed, 48 insertions(+), 21 deletions(-) create mode 100644 signaler/signaler_test.go diff --git a/backtester/btcli/main.go b/backtester/btcli/main.go index 2a221f53..a08bdf65 100644 --- a/backtester/btcli/main.go +++ b/backtester/btcli/main.go @@ -125,7 +125,7 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) go func() { // Capture cancel for interrupt - signaler.WaitForInterrupt() + <-signaler.WaitForInterrupt() cancel() fmt.Println("rpc process interrupted") os.Exit(1) diff --git a/backtester/main.go b/backtester/main.go index 45d64e4d..7a625da4 100644 --- a/backtester/main.go +++ b/backtester/main.go @@ -195,7 +195,7 @@ func main() { fmt.Printf("Could not stop task %v %v. Error: %v\n", bt.MetaData.ID, bt.MetaData.Strategy, err) os.Exit(1) } - interrupt := signaler.WaitForInterrupt() + interrupt := <-signaler.WaitForInterrupt() log.Infof(log.Global, "Captured %v, shutdown requested\n", interrupt) log.Infoln(log.Global, "Exiting.") err = bt.Stop() @@ -230,7 +230,7 @@ func main() { } log.Infoln(log.GRPCSys, "Ready to receive commands") }(btCfg) - interrupt := signaler.WaitForInterrupt() + interrupt := <-signaler.WaitForInterrupt() log.Infof(log.Global, "Captured %v, shutdown requested\n", interrupt) if btCfg.StopAllTasksOnClose { log.Infoln(log.Global, "Stopping all running tasks on close") diff --git a/cmd/gctcli/main.go b/cmd/gctcli/main.go index ab5cca14..a26588b9 100644 --- a/cmd/gctcli/main.go +++ b/cmd/gctcli/main.go @@ -225,7 +225,7 @@ func main() { ctx, cancel := context.WithCancel(context.Background()) go func() { // Capture cancel for interrupt - signaler.WaitForInterrupt() + <-signaler.WaitForInterrupt() cancel() fmt.Println("rpc process interrupted") os.Exit(1) diff --git a/main.go b/main.go index 77a99418..0501980a 100644 --- a/main.go +++ b/main.go @@ -157,7 +157,7 @@ func main() { } func waitForInterrupt(waiter chan<- struct{}) { - interrupt := signaler.WaitForInterrupt() + interrupt := <-signaler.WaitForInterrupt() gctlog.Infof(gctlog.Global, "Captured %v, shutdown requested.\n", interrupt) waiter <- struct{}{} } diff --git a/signaler/signaler.go b/signaler/signaler.go index 04d19b1a..9b6d0496 100644 --- a/signaler/signaler.go +++ b/signaler/signaler.go @@ -1,3 +1,4 @@ +// Package signaler provides cross-platform signal handling for graceful application shutdown package signaler import ( @@ -6,20 +7,9 @@ import ( "syscall" ) -var s = make(chan os.Signal, 1) - -func init() { - sigs := []os.Signal{ - os.Interrupt, - os.Kill, - syscall.SIGTERM, - syscall.SIGABRT, - } - signal.Notify(s, sigs...) -} - -// WaitForInterrupt waits until a os.Signal is -// received and returns the result -func WaitForInterrupt() os.Signal { - return <-s +// WaitForInterrupt returns a channel to receive termination signals +func WaitForInterrupt() chan os.Signal { + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + return c } diff --git a/signaler/signaler_test.go b/signaler/signaler_test.go new file mode 100644 index 00000000..196f7bf8 --- /dev/null +++ b/signaler/signaler_test.go @@ -0,0 +1,37 @@ +package signaler + +import ( + "os" + "runtime" + "syscall" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWaitForInterrupt(t *testing.T) { + t.Parallel() + for _, sig := range []os.Signal{syscall.SIGTERM, os.Interrupt} { + sigC := WaitForInterrupt() + proc, err := os.FindProcess(os.Getpid()) + require.NoError(t, err, "os.FindProcess must not error") + + if err := proc.Signal(sig); err != nil { + if runtime.GOOS == "windows" { + t.Skipf("proc.Signal(%s) not supported on Windows: %v", sig, err) + } + require.NoErrorf(t, err, "proc.Signal(%s) must not error", sig) + } + + assert.Eventuallyf(t, func() bool { + select { + case got := <-sigC: + return got == sig + default: + return false + } + }, 2*time.Second, 10*time.Millisecond, "Signal %s should be received within timeout", sig) + } +}