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 <oharareid.ryan@gmail.com>

* Signaler: require NoError to NoErrorf

Co-authored-by: Ryan O'Hara-Reid <oharareid.ryan@gmail.com>

---------

Co-authored-by: Ryan O'Hara-Reid <oharareid.ryan@gmail.com>
This commit is contained in:
Romano
2025-12-10 06:44:48 +01:00
committed by GitHub
parent 78382afb14
commit 1b83db3489
6 changed files with 48 additions and 21 deletions

View File

@@ -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)

View File

@@ -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")

View File

@@ -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)

View File

@@ -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{}{}
}

View File

@@ -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
}

37
signaler/signaler_test.go Normal file
View File

@@ -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)
}
}