Skip to content

Commit 2a6696d

Browse files
committed
feat: create syscall wrapper for termination signals
1 parent db4854f commit 2a6696d

File tree

5 files changed

+239
-1
lines changed

5 files changed

+239
-1
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,6 @@ go.work.sum
2323

2424
# env file
2525
.env
26+
27+
# Editor
28+
.idea

README.md

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,17 @@
1-
# sigterm
1+
# sigterm
2+
3+
A subset of the `syscall` package for terminating processes. This package provides a `Signal` type that implements
4+
the `encoding.TextMarshaller` and `encoding.TextUnmarshaler` interfaces for text-based serialization and deserialization.
5+
It can be used for configuration management using environment variables[^env_caarlos0] or flags [^flag].
6+
7+
List of termination signals can be found in the GNU libc manual [^gnu_manual].
8+
9+
## Installation
10+
11+
```bash
12+
go get -u github.com/josestg/sigterm
13+
```
14+
15+
[^flag]: https://golang.org/pkg/flag/#TextVar
16+
[^env_caarlos0]: https://github.com/caarlos0/env
17+
[^gnu_manual]: https://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module github.com/josestg/sigterm
2+
3+
go 1.23.0

sigterm.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package sigterm
2+
3+
import (
4+
"encoding"
5+
"fmt"
6+
"strings"
7+
"syscall"
8+
)
9+
10+
// Signal is a termination signal that is used for initiating the shutdown procedure.
11+
// This type implements the encoding.TextMarshaller and encoding.TextUnmarshaler interfaces
12+
// for text-based serialization and deserialization. So, it can be used for configuration
13+
// management using environment variables or flag.TextVar().
14+
//
15+
// List of termination signals can be found in the GNU libc manual:
16+
// - https://www.gnu.org/software/libc/manual/html_node/Termination-Signals.html
17+
type Signal syscall.Signal
18+
19+
// Supported shutdown signals.
20+
const (
21+
SIGINT = Signal(syscall.SIGINT)
22+
SIGHUP = Signal(syscall.SIGHUP)
23+
SIGTERM = Signal(syscall.SIGTERM)
24+
SIGQUIT = Signal(syscall.SIGQUIT)
25+
SIGKILL = Signal(syscall.SIGKILL)
26+
)
27+
28+
var _signalNames = map[Signal]string{
29+
SIGINT: "SIGINT",
30+
SIGHUP: "SIGHUP",
31+
SIGTERM: "SIGTERM",
32+
SIGQUIT: "SIGQUIT",
33+
SIGKILL: "SIGKILL",
34+
}
35+
36+
var (
37+
_ encoding.TextMarshaler = (*Signal)(nil)
38+
_ encoding.TextUnmarshaler = (*Signal)(nil)
39+
)
40+
41+
// Unwrap returns the syscall.Signal value of the sigterm.Signal.
42+
func (s Signal) Unwrap() syscall.Signal {
43+
return syscall.Signal(s)
44+
}
45+
46+
// String returns the string representation of the signal.
47+
func (s Signal) String() string {
48+
if name, ok := _signalNames[s]; ok {
49+
return name
50+
}
51+
return fmt.Sprintf("sigterm.Signal(%d): unknown signal", s)
52+
}
53+
54+
func (s Signal) MarshalText() ([]byte, error) {
55+
return []byte(s.String()), nil
56+
}
57+
58+
func (s *Signal) UnmarshalText(b []byte) error {
59+
text := string(b)
60+
for sig, name := range _signalNames {
61+
if strings.EqualFold(text, name) {
62+
*s = sig
63+
return nil
64+
}
65+
}
66+
return fmt.Errorf("unknown termination signal: %s", text)
67+
}
68+
69+
type signalValue interface {
70+
syscall.Signal | Signal
71+
}
72+
73+
// IsTermination returns true if the given sigterm.Signal or syscall.Signal is a termination signal.
74+
func IsTermination[T signalValue](sig T) bool {
75+
switch t := any(sig).(type) {
76+
case Signal:
77+
return IsTermination(t.Unwrap())
78+
case syscall.Signal:
79+
switch t {
80+
case
81+
syscall.SIGINT,
82+
syscall.SIGHUP,
83+
syscall.SIGTERM,
84+
syscall.SIGQUIT,
85+
syscall.SIGKILL:
86+
return true
87+
}
88+
}
89+
return false
90+
}

sigterm_test.go

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
package sigterm
2+
3+
import (
4+
"syscall"
5+
"testing"
6+
)
7+
8+
func TestSignal_String(t *testing.T) {
9+
tests := []struct {
10+
name string
11+
s Signal
12+
want string
13+
}{
14+
{name: "Interrupt signal", s: SIGINT, want: "SIGINT"},
15+
{name: "Hangup signal", s: SIGHUP, want: "SIGHUP"},
16+
{name: "Terminate signal", s: SIGTERM, want: "SIGTERM"},
17+
{name: "Quit signal", s: SIGQUIT, want: "SIGQUIT"},
18+
{name: "Kill signal", s: SIGKILL, want: "SIGKILL"},
19+
{name: "Unknown signal", s: -1, want: "sigterm.Signal(-1): unknown signal"},
20+
}
21+
for _, tt := range tests {
22+
t.Run(tt.name, func(t *testing.T) {
23+
if got := tt.s.String(); got != tt.want {
24+
t.Errorf("String() = %v, want %v", got, tt.want)
25+
}
26+
})
27+
}
28+
}
29+
30+
func TestSignal_Unwrap(t *testing.T) {
31+
if SIGTERM.Unwrap() != syscall.SIGTERM {
32+
t.Errorf("Unwrap() = %v, want %v", SIGTERM.Unwrap(), syscall.SIGTERM)
33+
}
34+
}
35+
36+
func TestSignal_UnmarshalText(t *testing.T) {
37+
tests := []struct {
38+
name string
39+
text []byte
40+
wantErr bool
41+
}{
42+
{name: "Interrupt signal", text: []byte("SIGINT"), wantErr: false},
43+
{name: "Hangup signal", text: []byte("SIGHUP"), wantErr: false},
44+
{name: "Terminate signal", text: []byte("SIGTERM"), wantErr: false},
45+
{name: "Quit signal", text: []byte("SIGQUIT"), wantErr: false},
46+
{name: "Kill signal", text: []byte("SIGKILL"), wantErr: false},
47+
{name: "Empty signal", text: []byte(""), wantErr: true},
48+
{name: "Invalid signal", text: []byte("SIG_UNKNOWN"), wantErr: true},
49+
}
50+
for _, tt := range tests {
51+
t.Run(tt.name, func(t *testing.T) {
52+
var s Signal
53+
if err := s.UnmarshalText(tt.text); (err != nil) != tt.wantErr {
54+
t.Errorf("UnmarshalText() error = %v, wantErr %v", err, tt.wantErr)
55+
}
56+
})
57+
}
58+
}
59+
60+
func TestSignal_MarshalText(t *testing.T) {
61+
tests := []struct {
62+
name string
63+
s Signal
64+
wantErr bool
65+
}{
66+
{name: "Interrupt signal", s: SIGINT, wantErr: false},
67+
{name: "Hangup signal", s: SIGHUP, wantErr: false},
68+
{name: "Terminate signal", s: SIGTERM, wantErr: false},
69+
{name: "Quit signal", s: SIGQUIT, wantErr: false},
70+
{name: "Kill signal", s: SIGKILL, wantErr: false},
71+
}
72+
for _, tt := range tests {
73+
t.Run(tt.name, func(t *testing.T) {
74+
_, err := tt.s.MarshalText()
75+
if (err != nil) != tt.wantErr {
76+
t.Errorf("MarshalText() error = %v, wantErr %v", err, tt.wantErr)
77+
}
78+
})
79+
}
80+
}
81+
82+
func TestIsTermination(t *testing.T) {
83+
t.Run("sigterm.Signal", func(t *testing.T) {
84+
tests := []struct {
85+
name string
86+
sig Signal
87+
want bool
88+
}{
89+
{name: "Interrupt signal", sig: SIGINT, want: true},
90+
{name: "Hangup signal", sig: SIGHUP, want: true},
91+
{name: "Terminate signal", sig: SIGTERM, want: true},
92+
{name: "Quit signal", sig: SIGQUIT, want: true},
93+
{name: "Kill signal", sig: SIGKILL, want: true},
94+
{name: "Unknown signal", sig: -1, want: false},
95+
}
96+
for _, tt := range tests {
97+
t.Run(tt.name, func(t *testing.T) {
98+
if got := IsTermination(tt.sig); got != tt.want {
99+
t.Errorf("IsTermination() = %v, want %v", got, tt.want)
100+
}
101+
})
102+
}
103+
})
104+
105+
t.Run("syscall.Signal", func(t *testing.T) {
106+
tests := []struct {
107+
name string
108+
sig syscall.Signal
109+
want bool
110+
}{
111+
{name: "Interrupt signal", sig: syscall.SIGINT, want: true},
112+
{name: "Hangup signal", sig: syscall.SIGHUP, want: true},
113+
{name: "Terminate signal", sig: syscall.SIGTERM, want: true},
114+
{name: "Quit signal", sig: syscall.SIGQUIT, want: true},
115+
{name: "Kill signal", sig: syscall.SIGKILL, want: true},
116+
{name: "Unknown signal", sig: syscall.SIGSEGV, want: false},
117+
}
118+
for _, tt := range tests {
119+
t.Run(tt.name, func(t *testing.T) {
120+
if got := IsTermination(tt.sig); got != tt.want {
121+
t.Errorf("IsTermination() = %v, want %v", got, tt.want)
122+
}
123+
})
124+
}
125+
})
126+
}

0 commit comments

Comments
 (0)