fix: add WebSocket ping/pong to detect dead connections in log viewer

Without ping/pong, silently dropped connections (network loss, phone
sleep) would leak goroutines and logger subscriptions until the OS-level
TCP timeout (minutes). Now pings every 54s; if no pong within 60s the
connection is closed and all resources (goroutines, subscriptions,
client slots) are released.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
dj-oyu 2026-02-24 05:38:21 +09:00
parent d77106da2e
commit efb6ec0d2e

View file

@ -210,6 +210,11 @@ type Handler struct {
const maxWSClients = 4
const (
wsPongWait = 60 * time.Second
wsPingPeriod = 54 * time.Second // must be less than wsPongWait
)
type wsClient struct {
conn *websocket.Conn
}
@ -936,6 +941,13 @@ func (h *Handler) wsLogs(w http.ResponseWriter, r *http.Request) {
sub := logger.Subscribe(filter)
defer logger.Unsubscribe(sub)
// Configure ping/pong to detect dead connections
conn.SetReadDeadline(time.Now().Add(wsPongWait))
conn.SetPongHandler(func(string) error {
conn.SetReadDeadline(time.Now().Add(wsPongWait))
return nil
})
// Send initial data
initial := logger.RecentLogs(minLevel, component, 50)
if err := conn.WriteJSON(map[string]any{"type": "init", "entries": initial}); err != nil {
@ -953,7 +965,10 @@ func (h *Handler) wsLogs(w http.ResponseWriter, r *http.Request) {
}
}()
// Stream loop
// Stream loop with periodic pings
ticker := time.NewTicker(wsPingPeriod)
defer ticker.Stop()
for {
select {
case entry, ok := <-sub.Ch:
@ -965,6 +980,10 @@ func (h *Handler) wsLogs(w http.ResponseWriter, r *http.Request) {
if err := conn.WriteJSON(map[string]any{"type": "entry", "entry": entry}); err != nil {
return
}
case <-ticker.C:
if err := conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
case <-done:
return
}