Merge pull request #3132 from SiYue-ZO/fix/goroutine-recover

fix: add panic recovery to core-path goroutines
This commit is contained in:
Mauro 2026-06-16 22:05:31 +02:00 committed by GitHub
commit e720be0dd7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 106 additions and 0 deletions

View file

@ -13,6 +13,7 @@ import (
"math" "math"
"net" "net"
"net/http" "net/http"
"runtime/debug"
"sort" "sort"
"strings" "strings"
"sync" "sync"
@ -1279,6 +1280,16 @@ func (m *Manager) StartAll(ctx context.Context) error {
for _, listener := range m.httpListeners { for _, listener := range m.httpListeners {
ln := listener ln := listener
go func() { go func() {
defer func() {
if r := recover(); r != nil {
logger.ErrorCF("channels", "HTTP server goroutine panic recovered",
map[string]any{
"addr": ln.Addr().String(),
"panic": fmt.Sprintf("%v", r),
"stack": string(debug.Stack()),
})
}
}()
logger.InfoCF("channels", "Shared HTTP server listening", map[string]any{ logger.InfoCF("channels", "Shared HTTP server listening", map[string]any{
"addr": ln.Addr().String(), "addr": ln.Addr().String(),
}) })
@ -1292,6 +1303,16 @@ func (m *Manager) StartAll(ctx context.Context) error {
} }
} else { } else {
go func() { go func() {
defer func() {
if r := recover(); r != nil {
logger.ErrorCF("channels", "HTTP server goroutine panic recovered",
map[string]any{
"addr": m.httpServer.Addr,
"panic": fmt.Sprintf("%v", r),
"stack": string(debug.Stack()),
})
}
}()
logger.InfoCF("channels", "Shared HTTP server listening", map[string]any{ logger.InfoCF("channels", "Shared HTTP server listening", map[string]any{
"addr": m.httpServer.Addr, "addr": m.httpServer.Addr,
}) })
@ -1943,6 +1964,15 @@ func (m *Manager) Reload(ctx context.Context, cfg *config.Config) error {
// Commit hashes only on full success. // Commit hashes only on full success.
m.channelHashes = list m.channelHashes = list
go func() { go func() {
defer func() {
if r := recover(); r != nil {
logger.ErrorCF("channels", "channel registration goroutine panic recovered",
map[string]any{
"panic": fmt.Sprintf("%v", r),
"stack": string(debug.Stack()),
})
}
}()
for _, f := range deferFuncs { for _, f := range deferFuncs {
f() f()
} }

View file

@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"log" "log"
"runtime/debug"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
@ -227,6 +228,11 @@ func (s *eventSubscription) dispatch(ctx context.Context, evt Event) {
s.wg.Add(1) s.wg.Add(1)
go func() { go func() {
defer s.wg.Done() defer s.wg.Done()
defer func() {
if r := recover(); r != nil {
log.Printf("events: subscriber %q goroutine panic recovered: %v\n%s", s.name, r, debug.Stack())
}
}()
s.handle(ctx, evt) s.handle(ctx, evt)
}() }()
case Keyed: case Keyed:
@ -253,6 +259,15 @@ func (s *eventSubscription) handle(ctx context.Context, evt Event) {
done := make(chan handlerResult, 1) done := make(chan handlerResult, 1)
go func() { go func() {
defer func() {
if r := recover(); r != nil {
log.Printf(
"events: subscriber %q timeout-handler goroutine panic recovered: %v\n%s",
s.name, r, debug.Stack(),
)
done <- handlerResult{panicked: true}
}
}()
done <- s.invokeHandler(ctx, evt) done <- s.invokeHandler(ctx, evt)
}() }()
@ -302,6 +317,14 @@ func (s *eventSubscription) watchContext(ctx context.Context) {
} }
go func() { go func() {
defer func() {
if r := recover(); r != nil {
log.Printf(
"events: subscriber %q watchContext goroutine panic recovered: %v\n%s",
s.name, r, debug.Stack(),
)
}
}()
select { select {
case <-ctx.Done(): case <-ctx.Done():
_ = s.Close() _ = s.Close()

View file

@ -12,6 +12,7 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime" "runtime"
"runtime/debug"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -417,6 +418,16 @@ func (t *ExecTool) runSync(ctx context.Context, command, cwd string) *ToolResult
done := make(chan error, 1) done := make(chan error, 1)
go func() { go func() {
defer func() {
if r := recover(); r != nil {
logger.ErrorCF("shell", "cmd.Wait goroutine panic recovered",
map[string]any{
"panic": fmt.Sprintf("%v", r),
"stack": string(debug.Stack()),
})
done <- fmt.Errorf("panic in cmd.Wait: %v", r)
}
}()
done <- cmd.Wait() done <- cmd.Wait()
}() }()
@ -573,6 +584,18 @@ func (t *ExecTool) runBackground(ctx context.Context, command, cwd string, ptyEn
// so we need cmd.Wait() in a separate goroutine to detect process exit. // so we need cmd.Wait() in a separate goroutine to detect process exit.
if session.PTY && session.ptyMaster != nil { if session.PTY && session.ptyMaster != nil {
go func() { go func() {
defer func() {
if r := recover(); r != nil {
logger.ErrorCF("shell", "PTY cmd.Wait goroutine panic recovered",
map[string]any{
"panic": fmt.Sprintf("%v", r),
"stack": string(debug.Stack()),
})
session.mu.Lock()
session.Status = "error"
session.mu.Unlock()
}
}()
cmd.Wait() // Wait for process to exit cmd.Wait() // Wait for process to exit
session.mu.Lock() session.mu.Lock()
if cmd.ProcessState != nil { if cmd.ProcessState != nil {
@ -583,6 +606,15 @@ func (t *ExecTool) runBackground(ctx context.Context, command, cwd string, ptyEn
}() }()
go func() { go func() {
defer func() {
if r := recover(); r != nil {
logger.ErrorCF("shell", "PTY read goroutine panic recovered",
map[string]any{
"panic": fmt.Sprintf("%v", r),
"stack": string(debug.Stack()),
})
}
}()
buf := make([]byte, 4096) buf := make([]byte, 4096)
for { for {
n, err := session.ptyMaster.Read(buf) n, err := session.ptyMaster.Read(buf)
@ -613,6 +645,15 @@ func (t *ExecTool) runBackground(ctx context.Context, command, cwd string, ptyEn
// When Read() returns EOF (pipe closed), we break. // When Read() returns EOF (pipe closed), we break.
// When process exits, OS closes pipe write end → Read() returns EOF → we exit. // When process exits, OS closes pipe write end → Read() returns EOF → we exit.
go func() { go func() {
defer func() {
if r := recover(); r != nil {
logger.ErrorCF("shell", "pipe read goroutine panic recovered",
map[string]any{
"panic": fmt.Sprintf("%v", r),
"stack": string(debug.Stack()),
})
}
}()
buf := make([]byte, 4096) buf := make([]byte, 4096)
// Read stdout // Read stdout

View file

@ -10,6 +10,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"runtime/debug"
"sync" "sync"
"github.com/sipeed/picoclaw/pkg/logger" "github.com/sipeed/picoclaw/pkg/logger"
@ -165,6 +166,17 @@ func RunToolLoop(
wg.Add(1) wg.Add(1)
go func(idx int, tc providers.ToolCall) { go func(idx int, tc providers.ToolCall) {
defer wg.Done() defer wg.Done()
defer func() {
if r := recover(); r != nil {
logger.ErrorCF("toolloop", "tool execution goroutine panic recovered",
map[string]any{
"tool": tc.Name,
"panic": fmt.Sprintf("%v", r),
"stack": string(debug.Stack()),
})
results[idx].result = ErrorResult(fmt.Sprintf("internal panic in tool %s", tc.Name))
}
}()
argsJSON, _ := json.Marshal(tc.Arguments) argsJSON, _ := json.Marshal(tc.Arguments)
argsPreview := utils.Truncate(string(argsJSON), 200) argsPreview := utils.Truncate(string(argsJSON), 200)