Skip to content

Commit 7c7ad95

Browse files
committed
daemon: monitor SIGTERM and Windows events
1 parent 5cb7d0e commit 7c7ad95

File tree

5 files changed

+268
-34
lines changed

5 files changed

+268
-34
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ require (
5656
github.com/libp2p/go-buffer-pool v0.1.0 // indirect
5757
github.com/libp2p/go-libp2p v0.27.7 // indirect
5858
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
59+
github.com/lxn/win v0.0.0-20210218163916-a377121e959e
5960
github.com/mattn/go-isatty v0.0.19 // indirect
6061
github.com/mattn/go-runewidth v0.0.14 // indirect
6162
github.com/microcosm-cc/bluemonday v1.0.21 // indirect

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ github.com/libp2p/go-nat v0.1.0 h1:MfVsH6DLcpa04Xr+p8hmVRG4juse0s3J8HyNWYHffXg=
150150
github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU=
151151
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
152152
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
153+
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
154+
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
153155
github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA=
154156
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
155157
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
@@ -340,6 +342,7 @@ golang.org/x/sys v0.0.0-20190219092855-153ac476189d/go.mod h1:STP8DvDyc/dI5b8T5h
340342
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
341343
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
342344
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
345+
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
343346
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
344347
golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
345348
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

internal/commands/daemon.go

Lines changed: 12 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import (
1010
golog "log"
1111
"net"
1212
"os"
13-
"os/signal"
1413
"reflect"
1514
"strings"
1615
"sync"
@@ -280,18 +279,16 @@ func watchService(ctx context.Context,
280279
}
281280

282281
func makeStoppers(ctx context.Context) (wgShutdown, <-chan shutdownDisposition) {
283-
var (
284-
shutdownChan = newWaitGroupChan[shutdownDisposition](int(maximumShutdown))
285-
shutdownLevels = make(chan shutdownDisposition)
286-
)
287-
shutdownChan.Add(2)
288-
go stopOnSignal(os.Interrupt, shutdownChan)
289-
go stopOnDone(ctx, shutdownChan)
282+
shutdownSend := newWaitGroupChan[shutdownDisposition](int(maximumShutdown))
283+
registerSystemStoppers(ctx, shutdownSend)
284+
shutdownSend.Add(1)
285+
go stopOnDone(ctx, shutdownSend)
286+
shutdownReceive := make(chan shutdownDisposition)
290287
go func() {
291-
sequentialLeveling(shutdownChan.ch, shutdownLevels)
292-
close(shutdownLevels)
288+
sequentialLeveling(shutdownSend.ch, shutdownReceive)
289+
close(shutdownReceive)
293290
}()
294-
return shutdownChan, shutdownLevels
291+
return shutdownSend, shutdownReceive
295292
}
296293

297294
func makeServer(fsys p9.Attacher, log ulog.Logger) *p9net.Server {
@@ -826,31 +823,12 @@ func unmountAll(system mountSubsystem,
826823
}
827824
}
828825

829-
func stopOnSignal(sig os.Signal, stopCh wgShutdown) {
830-
signals := make(chan os.Signal, generic.Max(1, cap(stopCh.ch)))
831-
signal.Notify(signals, sig)
832-
defer func() {
833-
signal.Stop(signals)
834-
stopCh.Done()
835-
}()
836-
for count := minimumShutdown; count <= maximumShutdown; count++ {
837-
select {
838-
case <-signals:
839-
if !stopCh.send(count) {
840-
return
841-
}
842-
case <-stopCh.Closing():
843-
return
844-
}
845-
}
846-
}
847-
848-
func stopOnDone(ctx context.Context, stopCh wgShutdown) {
849-
defer stopCh.Done()
826+
func stopOnDone(ctx context.Context, shutdownSend wgShutdown) {
827+
defer shutdownSend.Done()
850828
select {
851829
case <-ctx.Done():
852-
stopCh.send(immediateShutdown)
853-
case <-stopCh.closing:
830+
shutdownSend.send(immediateShutdown)
831+
case <-shutdownSend.closing:
854832
}
855833
}
856834

internal/commands/daemon_unix.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//go:build unix
2+
3+
package commands
4+
5+
import (
6+
"context"
7+
"os"
8+
"os/signal"
9+
"syscall"
10+
)
11+
12+
func registerSystemStoppers(_ context.Context, shutdownSend wgShutdown) {
13+
shutdownSend.Add(2)
14+
go stopOnSignalLinear(shutdownSend, os.Interrupt)
15+
go stopOnSignalLinear(shutdownSend, syscall.SIGTERM)
16+
}
17+
18+
func stopOnSignalLinear(stopCh wgShutdown, sig os.Signal) {
19+
signals := make(chan os.Signal, 1)
20+
signal.Notify(signals, sig)
21+
defer func() {
22+
signal.Stop(signals)
23+
stopCh.Done()
24+
}()
25+
for count := minimumShutdown; count <= maximumShutdown; count++ {
26+
select {
27+
case <-signals:
28+
if !stopCh.send(count) {
29+
return
30+
}
31+
case <-stopCh.Closing():
32+
return
33+
}
34+
}
35+
}
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
package commands
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/signal"
8+
"runtime"
9+
"syscall"
10+
"unsafe"
11+
12+
"github.com/djdv/go-filesystem-utils/internal/generic"
13+
"github.com/lxn/win"
14+
"golang.org/x/sys/windows"
15+
)
16+
17+
// NOTE: On signal handling.
18+
// Go's default signal handler translates some messages
19+
// into `SIGTERM` (see [os/signal] documentation).
20+
// Windows has at least 3 forms of messaging that apply to us.
21+
// 1) Control signals.
22+
// 2) Window messages.
23+
// 3) Service events.
24+
// 1 applies to SIGTERM, with exceptions.
25+
// `HandlerRoutine` will not receive `CTRL_LOGOFF_EVENT` nor
26+
// `CTRL_SHUTDOWN_EVENT` for "interactive applications"
27+
// (applications which link with `user32`;
28+
// see `SetConsoleCtrlHandler` documentation).
29+
//
30+
// We utilize `user32` (indirectly through the [xdg] package)
31+
// which flags us as interactive. As such, we need to
32+
// initialize a window message queue (2) and monitor it for
33+
// these events. The console handler (1) can (and must) be
34+
// registered simultaneously, to handle the other signals
35+
// such as interrupt, break, close, etc.
36+
//
37+
// We do not yet handle case 3.
38+
39+
type wndProcFunc func(win.HWND, uint32, uintptr, uintptr) uintptr
40+
41+
func registerSystemStoppers(ctx context.Context, shutdownSend wgShutdown) {
42+
shutdownSend.Add(2)
43+
// NOTE: [Go 1.20] This must be `syscall.SIGTERM`
44+
// not `windows.SIGTERM`, otherwise the runtime
45+
// will not set up the console control handler.
46+
go stopOnSignalLinear(shutdownSend, syscall.SIGTERM)
47+
go stopOnSignalLinear(shutdownSend, os.Interrupt)
48+
if err := createAndWatchWindow(ctx, shutdownSend); err != nil {
49+
panic(err)
50+
}
51+
}
52+
53+
func stopOnSignalLinear(shutdownSend wgShutdown, sig os.Signal) {
54+
signals := make(chan os.Signal, 1)
55+
signal.Notify(signals, sig)
56+
defer func() {
57+
signal.Stop(signals)
58+
shutdownSend.Done()
59+
}()
60+
for count := minimumShutdown; count <= maximumShutdown; count++ {
61+
select {
62+
case <-signals:
63+
if !shutdownSend.send(count) {
64+
return
65+
}
66+
case <-shutdownSend.Closing():
67+
return
68+
}
69+
}
70+
}
71+
72+
func createAndWatchWindow(ctx context.Context, shutdownSend wgShutdown) error {
73+
errs := make(chan error, 1)
74+
go func() {
75+
defer shutdownSend.Done()
76+
runtime.LockOSThread() // The window and message processor
77+
defer runtime.UnlockOSThread() // must be on the same thread.
78+
hWnd, err := createEventWindow("go-fs", shutdownSend)
79+
errs <- err
80+
if err != nil {
81+
return
82+
}
83+
closeWindowWhenDone := func() {
84+
select {
85+
case <-ctx.Done():
86+
case <-shutdownSend.Closing():
87+
}
88+
const (
89+
NULL = 0
90+
wParam = NULL
91+
lParam = NULL
92+
)
93+
win.SendMessage(hWnd, win.WM_CLOSE, wParam, lParam)
94+
}
95+
go closeWindowWhenDone()
96+
const (
97+
// Ignore C's `BOOL` declaration for `GetMessage`
98+
// it actually returns a trinary value. See MSDN docs.
99+
failed = -1
100+
wmQuit = 0
101+
success = 1
102+
NULL = 0
103+
msgFilterMin = NULL
104+
msgFilterMax = NULL
105+
)
106+
for {
107+
var msg win.MSG
108+
switch win.GetMessage(&msg, hWnd, msgFilterMin, msgFilterMax) {
109+
case failed, wmQuit:
110+
// NOTE: If we fail here the error
111+
// (`GetLastError`) is dropped.
112+
// Given our parameter set, failure
113+
// implies the window handle was (somehow)
114+
// invalidated, so we can't continue.
115+
// This is very unlikely to happen on accident.
116+
// Especially outside of development.
117+
return
118+
case success:
119+
win.TranslateMessage(&msg)
120+
win.DispatchMessage(&msg)
121+
}
122+
}
123+
}()
124+
return <-errs
125+
}
126+
127+
func createEventWindow(name string, shutdownSend wgShutdown) (win.HWND, error) {
128+
const INVALID_HANDLE_VALUE win.HWND = ^win.HWND(0)
129+
lpClassName, err := windows.UTF16PtrFromString(name)
130+
if err != nil {
131+
return INVALID_HANDLE_VALUE, err
132+
}
133+
var (
134+
hInstance = win.GetModuleHandle(nil)
135+
windowClass = win.WNDCLASSEX{
136+
LpfnWndProc: newWndProc(shutdownSend),
137+
HInstance: hInstance,
138+
LpszClassName: lpClassName,
139+
}
140+
)
141+
windowClass.CbSize = uint32(unsafe.Sizeof(windowClass))
142+
_ = win.RegisterClassEx(&windowClass)
143+
const (
144+
NULL = 0
145+
dwExStyle = NULL
146+
dwStyle = NULL
147+
x = NULL
148+
y = NULL
149+
nWidth = NULL
150+
nHeight = NULL
151+
hWndParent = NULL
152+
hMenu = NULL
153+
)
154+
var (
155+
lpWindowName *uint16 = nil
156+
lpParam unsafe.Pointer = nil
157+
hWnd = win.CreateWindowEx(
158+
dwExStyle,
159+
lpClassName, lpWindowName,
160+
dwStyle,
161+
x, y,
162+
nWidth, nHeight,
163+
hWndParent, hMenu,
164+
hInstance, lpParam,
165+
)
166+
)
167+
if hWnd == NULL {
168+
var err error = generic.ConstError(
169+
"CreateWindowEx failed",
170+
)
171+
if lErr := windows.GetLastError(); lErr != nil {
172+
err = fmt.Errorf("%w: %w", err, lErr)
173+
}
174+
return INVALID_HANDLE_VALUE, err
175+
}
176+
return hWnd, nil
177+
}
178+
179+
func newWndProc(shutdownSend wgShutdown) uintptr {
180+
return windows.NewCallback(
181+
func(hWnd win.HWND, uMsg uint32, wParam uintptr, lParam uintptr) uintptr {
182+
switch uMsg {
183+
case win.WM_QUERYENDSESSION:
184+
const shutdownOrRestart = 0
185+
var disposition shutdownDisposition
186+
switch {
187+
case lParam == shutdownOrRestart:
188+
disposition = immediateShutdown
189+
case lParam&win.ENDSESSION_LOGOFF != 0:
190+
disposition = shortShutdown
191+
case lParam&win.ENDSESSION_CRITICAL != 0:
192+
disposition = immediateShutdown
193+
default:
194+
disposition = immediateShutdown
195+
}
196+
shutdownSend.send(disposition)
197+
const (
198+
FALSE = 0
199+
canClose = FALSE
200+
)
201+
return canClose
202+
case win.WM_CLOSE:
203+
const processedToken = 0
204+
shutdownSend.send(immediateShutdown)
205+
win.DestroyWindow(hWnd)
206+
return processedToken
207+
case win.WM_DESTROY:
208+
const processedToken = 0
209+
shutdownSend.send(immediateShutdown)
210+
const toCallingThread = 0
211+
win.PostQuitMessage(toCallingThread)
212+
return processedToken
213+
default:
214+
return win.DefWindowProc(hWnd, uMsg, wParam, lParam)
215+
}
216+
})
217+
}

0 commit comments

Comments
 (0)