Skip to content

Commit 37fe2bc

Browse files
committed
fixed: shut down the server when stdin is closed
1 parent 50006a6 commit 37fe2bc

File tree

7 files changed

+139
-5
lines changed

7 files changed

+139
-5
lines changed

.github/workflows/test.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,5 @@ jobs:
1919
- run: cd examples/k8s_contexts_resources && go test
2020
- run: cd examples/list_current_dir_files_tool && go test
2121
- run: cd examples/list_k8s_contexts_tool && go test
22+
- run: cd tests/lifecycle && go test
2223
- run: go test ./...

pkg/app/foxy_app.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,8 +218,10 @@ func (f *Builder) provideServerLifecycle(transport server.Transport) fx.Option {
218218
)
219219
if err != nil {
220220
f.transportError = err
221-
_ = shutdowner.Shutdown()
222221
}
222+
223+
// shutdown the server when transport is done
224+
_ = shutdowner.Shutdown()
223225
}()
224226
return nil
225227
},

pkg/stdio/stdio_transport.go

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"context"
66
"errors"
77
"io"
8+
"log"
89
"os"
910

1011
foxyevent "github.com/strowk/foxy-contexts/pkg/foxy_event"
@@ -92,6 +93,7 @@ func (s *stdioTransport) run(
9293
}
9394
}
9495
}
96+
log.Printf("stopped reading responses")
9597
close(s.stoppedReadingResponses)
9698
}()
9799

@@ -115,22 +117,45 @@ func (s *stdioTransport) run(
115117
close(s.stoppedReadingInput)
116118
}()
117119

118-
<-s.shuttingDown
120+
// wait for either shutting down or stopped reading input
121+
select {
122+
case <-s.shuttingDown:
123+
// if we got shutting down signal,
124+
// we need to wait until we stop reading input,
125+
// which waits for shutdown by itself
126+
<-s.stoppedReadingInput
127+
case <-s.stoppedReadingInput:
128+
// if we stopped reading input, we can now initiate shutdown
129+
close(s.shuttingDown)
130+
}
131+
132+
// wait until we stop reading responses,
133+
// before signaling that we are stopped
119134
<-s.stoppedReadingResponses
120-
<-s.stoppedReadingInput
121135

122136
close(s.stopped)
123-
124137
return nil
125138
}
126139

127140
func (s *stdioTransport) Shutdown(ctx context.Context) error {
128-
close(s.shuttingDown)
141+
safeClose(s.shuttingDown)
129142

143+
// this waits either till we are stopped or we cannot wait anymore
130144
select {
131145
case <-s.stopped:
132146
return nil
133147
case <-ctx.Done():
134148
return ctx.Err()
135149
}
136150
}
151+
152+
func safeClose(ch chan struct{}) {
153+
defer func() {
154+
if r := recover(); r != nil {
155+
// it is possible to panic if channel is already closed
156+
// in which case we just go on, as it is possible that
157+
// Shutdown would be called soon after transport is stopped
158+
}
159+
}()
160+
close(ch)
161+
}

tests/lifecycle/go.mod

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
module github.com/strowk/foxy-contexts/tests/lifecycle
2+
3+
replace github.com/strowk/foxy-contexts => ../../
4+
5+
go 1.23.3
6+
7+
require (
8+
github.com/strowk/foxy-contexts v0.0.0-00010101000000-000000000000
9+
go.uber.org/fx v1.23.0
10+
go.uber.org/zap v1.27.0
11+
)
12+
13+
require (
14+
go.uber.org/dig v1.18.0 // indirect
15+
go.uber.org/multierr v1.10.0 // indirect
16+
golang.org/x/sys v0.21.0 // indirect
17+
)

tests/lifecycle/go.sum

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
2+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
4+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
5+
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
6+
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
7+
go.uber.org/dig v1.18.0 h1:imUL1UiY0Mg4bqbFfsRQO5G4CGRBec/ZujWTvSVp3pw=
8+
go.uber.org/dig v1.18.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
9+
go.uber.org/fx v1.23.0 h1:lIr/gYWQGfTwGcSXWXu4vP5Ws6iqnNEIY+F/aFzCKTg=
10+
go.uber.org/fx v1.23.0/go.mod h1:o/D9n+2mLP6v1EG+qsdT1O8wKopYAsqZasju97SDFCU=
11+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
12+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
13+
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
14+
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
15+
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
16+
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
17+
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
18+
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
19+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
20+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

tests/lifecycle/lifecycle.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
package main
2+
3+
import (
4+
"github.com/strowk/foxy-contexts/pkg/app"
5+
"github.com/strowk/foxy-contexts/pkg/stdio"
6+
"go.uber.org/fx"
7+
"go.uber.org/fx/fxevent"
8+
"go.uber.org/zap"
9+
)
10+
11+
func main() {
12+
app.
13+
NewBuilder().
14+
// setting up server
15+
WithName("great-tool-server").
16+
WithVersion("0.0.1").
17+
WithTransport(stdio.NewTransport()).
18+
// Configuring fx logging to only show errors
19+
WithFxOptions(
20+
fx.Provide(func() *zap.Logger {
21+
cfg := zap.NewDevelopmentConfig()
22+
cfg.Level.SetLevel(zap.ErrorLevel)
23+
logger, _ := cfg.Build()
24+
return logger
25+
}),
26+
fx.Option(fx.WithLogger(
27+
func(logger *zap.Logger) fxevent.Logger {
28+
return &fxevent.ZapLogger{Logger: logger}
29+
},
30+
)),
31+
).Run()
32+
}

tests/lifecycle/lifecycle_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package main
2+
3+
import (
4+
"os"
5+
"os/exec"
6+
"testing"
7+
"time"
8+
)
9+
10+
func TestStartStop(t *testing.T) {
11+
cmd := exec.Command("go", "run", "lifecycle.go")
12+
stdin, err := cmd.StdinPipe()
13+
if err != nil {
14+
t.Fatal(err)
15+
}
16+
cmd.Stderr = os.Stderr
17+
processStopped := make(chan struct{})
18+
go func() {
19+
defer close(processStopped)
20+
cmd.Run()
21+
}()
22+
23+
// this should cause process to exit
24+
err = stdin.Close()
25+
if err != nil {
26+
t.Error(err)
27+
return
28+
}
29+
30+
timer := time.NewTimer(10 * time.Second)
31+
select {
32+
case <-processStopped:
33+
// process exited
34+
case <-timer.C:
35+
t.Error("process did not exit in time")
36+
}
37+
}

0 commit comments

Comments
 (0)