File tree Expand file tree Collapse file tree
Expand file tree Collapse file tree Original file line number Diff line number Diff line change 1+ package lifecycle
2+
3+ import (
4+ "io"
5+ "os"
6+ "os/signal"
7+ "sync"
8+ )
9+
10+ const ParentStdinWatchEnv = "HORNETS_PARENT_STDIN_WATCH"
11+
12+ // StopChannel closes when an OS shutdown signal arrives or, when explicitly
13+ // enabled, when the supervising parent's stdin pipe reaches EOF. Services and
14+ // standalone invocations retain their existing signal-only lifecycle.
15+ func StopChannel (parent io.Reader , watchParent bool , shutdownSignals ... os.Signal ) <- chan struct {} {
16+ stop := make (chan struct {})
17+ var once sync.Once
18+ requestStop := func () {
19+ once .Do (func () { close (stop ) })
20+ }
21+
22+ signals := make (chan os.Signal , 1 )
23+ signal .Notify (signals , shutdownSignals ... )
24+ go func () {
25+ select {
26+ case <- signals :
27+ requestStop ()
28+ case <- stop :
29+ }
30+ signal .Stop (signals )
31+ }()
32+
33+ if watchParent {
34+ go func () {
35+ _ , _ = io .Copy (io .Discard , parent )
36+ requestStop ()
37+ }()
38+ }
39+
40+ return stop
41+ }
42+
43+ // WatchParentStdin reports whether this console process was launched under the
44+ // Nosis session supervisor. It is deliberately opt-in so an ordinary closed
45+ // terminal stdin never stops a service or standalone relay.
46+ func WatchParentStdin () bool {
47+ return os .Getenv (ParentStdinWatchEnv ) == "1"
48+ }
Original file line number Diff line number Diff line change 1+ package lifecycle
2+
3+ import (
4+ "os"
5+ "testing"
6+ "time"
7+ )
8+
9+ func TestStopChannelClosesWhenParentPipeCloses (t * testing.T ) {
10+ reader , writer , err := os .Pipe ()
11+ if err != nil {
12+ t .Fatal (err )
13+ }
14+ defer reader .Close ()
15+
16+ stop := StopChannel (reader , true , os .Interrupt )
17+ if err := writer .Close (); err != nil {
18+ t .Fatal (err )
19+ }
20+
21+ select {
22+ case <- stop :
23+ case <- time .After (time .Second ):
24+ t .Fatal ("parent pipe EOF did not close stop channel" )
25+ }
26+ }
27+
28+ func TestStopChannelIgnoresClosedParentWhenWatchDisabled (t * testing.T ) {
29+ reader , writer , err := os .Pipe ()
30+ if err != nil {
31+ t .Fatal (err )
32+ }
33+ defer reader .Close ()
34+
35+ stop := StopChannel (reader , false , os .Interrupt )
36+ if err := writer .Close (); err != nil {
37+ t .Fatal (err )
38+ }
39+
40+ select {
41+ case <- stop :
42+ t .Fatal ("closed parent pipe stopped an unsupervised process" )
43+ case <- time .After (50 * time .Millisecond ):
44+ }
45+ }
Original file line number Diff line number Diff line change @@ -4,11 +4,11 @@ import (
44 "context"
55 "flag"
66 "os"
7- "os/signal"
87 "syscall"
98
109 "github.com/HORNET-Storage/hornet-storage/lib/logging"
1110 "github.com/HORNET-Storage/hornet-storage/services/server/core"
11+ "github.com/HORNET-Storage/hornet-storage/services/server/lifecycle"
1212)
1313
1414var (
@@ -30,15 +30,12 @@ func init() {
3030}
3131
3232func main () {
33- // Convert OS kill signals into the core stop channel
34- sigs := make (chan os.Signal , 1 )
35- signal .Notify (sigs , syscall .SIGINT , syscall .SIGTERM )
36-
37- stop := make (chan struct {})
38- go func () {
39- <- sigs
40- close (stop )
41- }()
33+ stop := lifecycle .StopChannel (
34+ os .Stdin ,
35+ lifecycle .WatchParentStdin (),
36+ syscall .SIGINT ,
37+ syscall .SIGTERM ,
38+ )
4239
4340 if err := core .Run (context .Background (), core.Options {
4441 CompactDB : * compactDB ,
Original file line number Diff line number Diff line change 77 "flag"
88 "fmt"
99 "os"
10- "os/signal"
1110 "path/filepath"
1211 "strings"
1312 "syscall"
@@ -16,6 +15,7 @@ import (
1615
1716 "github.com/HORNET-Storage/hornet-storage/lib/logging"
1817 "github.com/HORNET-Storage/hornet-storage/services/server/core"
18+ "github.com/HORNET-Storage/hornet-storage/services/server/lifecycle"
1919)
2020
2121// serviceName is the Windows service identity shared by the installer
@@ -77,15 +77,12 @@ func runConsole() {
7777 // Initialize config and logging before entering the shared run lifecycle
7878 core .Initialize ()
7979
80- // Convert OS kill signals into the core stop channel
81- sigs := make (chan os.Signal , 1 )
82- signal .Notify (sigs , syscall .SIGINT , syscall .SIGTERM )
83-
84- stop := make (chan struct {})
85- go func () {
86- <- sigs
87- close (stop )
88- }()
80+ stop := lifecycle .StopChannel (
81+ os .Stdin ,
82+ lifecycle .WatchParentStdin (),
83+ syscall .SIGINT ,
84+ syscall .SIGTERM ,
85+ )
8986
9087 if err := core .Run (context .Background (), options (stop )); err != nil {
9188 logging .Fatalf ("Relay exited with error: %v" , err )
You can’t perform that action at this time.
0 commit comments