diff options
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/server.go | 16 | ||||
| -rw-r--r-- | internal/server_test.go | 66 |
2 files changed, 82 insertions, 0 deletions
diff --git a/internal/server.go b/internal/server.go index 46b95ef..3ae9be2 100644 --- a/internal/server.go +++ b/internal/server.go @@ -2,7 +2,9 @@ package internal import ( "context" + "fmt" "net" + "runtime/debug" "strings" "sync" @@ -84,6 +86,7 @@ func (s *Server) Serve() error { go func() { defer s.Wg.Done() defer func() { _ = conn.Close() }() + defer s.recoverPanic("connection", "remote", conn.RemoteAddr().String()) s.HandleConn(conn) }() } @@ -129,12 +132,25 @@ func (s *Server) propagateClose() { s.Wg.Add(1) go func() { defer s.Wg.Done() + defer s.recoverPanic("close propagation") <-s.Ctx.Done() _ = s.Listener.Close() }() } +func (s *Server) recoverPanic(goroutine string, keyvals ...any) { + if r := recover(); r != nil { + keyvals = append([]any{ + "msg", "panic in server goroutine", + "goroutine", goroutine, + "panic", fmt.Sprintf("%v", r), + "stack", string(debug.Stack()), + }, keyvals...) + _ = s.ErrorLog.Log(keyvals...) + } +} + // JoinDefaultPort appends ":<port>" iff the address does not already contain a port. func JoinDefaultPort(address string, port string) string { if len(address) > 0 && address[0] == '[' { diff --git a/internal/server_test.go b/internal/server_test.go new file mode 100644 index 0000000..62d8681 --- /dev/null +++ b/internal/server_test.go @@ -0,0 +1,66 @@ +package internal_test + +import ( + "context" + "fmt" + "net" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "tildegit.org/tjp/sliderule/internal" +) + +type captureLogger struct { + mu sync.Mutex + logs []string +} + +func (cl *captureLogger) Log(keyvals ...any) error { + cl.mu.Lock() + defer cl.mu.Unlock() + + for _, val := range keyvals { + cl.logs = append(cl.logs, fmt.Sprint(val)) + } + return nil +} + +func (cl *captureLogger) String() string { + cl.mu.Lock() + defer cl.mu.Unlock() + + return strings.Join(cl.logs, " ") +} + +func TestServerRecoversConnectionPanic(t *testing.T) { + var logs captureLogger + server, err := internal.NewServer( + context.Background(), + "localhost", + "tcp", + "127.0.0.1:0", + &logs, + func(net.Conn) { panic("boom") }, + ) + require.Nil(t, err) + + done := make(chan error, 1) + go func() { done <- server.Serve() }() + + conn, err := net.Dial(server.Network(), server.Address()) + require.Nil(t, err) + _ = conn.Close() + + require.Eventually(t, func() bool { + return strings.Contains(logs.String(), "panic in server goroutine") + }, time.Second, 10*time.Millisecond) + + server.Close() + assert.Nil(t, <-done) + assert.Contains(t, logs.String(), "boom") +} |
