package monitor
import (
"bytes"
"context"
"fmt"
"os"
"runtime"
"strings"
"sync"
"time"
"github.com/DataDog/gostackparse"
"github.com/projectdiscovery/gologger"
permissionutil "github.com/projectdiscovery/utils/permission"
unitutils "github.com/projectdiscovery/utils/unit"
"github.com/rs/xid"
)
type Agent struct {
lastStack []string
callbacks []Callback
goroutineCount int
currentIteration int
lock sync.Mutex
}
const defaultMonitorIteration = 6
func NewStackMonitor() *Agent {
return &Agent{}
}
type Callback func(dumpID string) error
func (s *Agent) RegisterCallback(callback Callback) {
s.lock.Lock()
defer s.lock.Unlock()
s.callbacks = append(s.callbacks, callback)
}
func (s *Agent) Start(interval time.Duration) context.CancelFunc {
ctx, cancel := context.WithCancel(context.Background())
ticker := time.NewTicker(interval)
go func() {
for {
select {
case <-ctx.Done():
ticker.Stop()
case <-ticker.C:
s.monitorWorker(cancel)
default:
continue
}
}
}()
return cancel
}
func (s *Agent) monitorWorker(cancel context.CancelFunc) {
current := runtime.NumGoroutine()
if current != s.goroutineCount {
s.goroutineCount = current
s.currentIteration = 0
return
}
s.currentIteration++
if s.currentIteration == defaultMonitorIteration-1 {
lastStackTrace := generateStackTraceSlice(getStack(true))
s.lastStack = lastStackTrace
return
}
if s.currentIteration == defaultMonitorIteration {
currentStack := getStack(true)
newStack := generateStackTraceSlice(currentStack)
if !compareStringSliceEqual(s.lastStack, newStack) {
s.currentIteration = 0
return
}
cancel()
dumpID := xid.New().String()
stackTraceFile := fmt.Sprintf("nuclei-stacktrace-%s.dump", dumpID)
gologger.Error().Msgf("Detected hanging goroutine (count=%d/%d) = %s\n", current, s.goroutineCount, stackTraceFile)
if err := os.WriteFile(stackTraceFile, currentStack, permissionutil.ConfigFilePermission); err != nil {
gologger.Error().Msgf("Could not write stack trace for goroutines: %s\n", err)
}
s.lock.Lock()
callbacks := s.callbacks
s.lock.Unlock()
for _, callback := range callbacks {
if err := callback(dumpID); err != nil {
gologger.Error().Msgf("Stack monitor callback error: %s\n", err)
}
}
os.Exit(1)
}
}
var getStack = func(all bool) []byte {
for i := unitutils.Mega; ; i *= 2 {
buf := make([]byte, i)
if n := runtime.Stack(buf, all); n < i {
return buf[:n-1]
}
}
}
func generateStackTraceSlice(stack []byte) []string {
goroutines, _ := gostackparse.Parse(bytes.NewReader(stack))
var builder strings.Builder
var stackList []string
for _, goroutine := range goroutines {
builder.WriteString(goroutine.State)
builder.WriteString("|")
for _, frame := range goroutine.Stack {
builder.WriteString(frame.Func)
builder.WriteString(";")
}
stackList = append(stackList, builder.String())
builder.Reset()
}
return stackList
}
func compareStringSliceEqual(first, second []string) bool {
if len(first) != len(second) {
return false
}
diff := make(map[string]int, len(first))
for _, x := range first {
diff[x]++
}
for _, y := range second {
if _, ok := diff[y]; !ok {
return false
}
diff[y] -= 1
if diff[y] == 0 {
delete(diff, y)
}
}
return len(diff) == 0
}