summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
Diffstat (limited to 'internal')
-rw-r--r--internal/server.go16
-rw-r--r--internal/server_test.go66
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")
+}