Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
projectdiscovery
GitHub Repository: projectdiscovery/nuclei
Path: blob/dev/pkg/utils/monitor/monitor.go
2070 views
1
// Package monitor implements a goroutine based monitoring for
2
// detecting stuck scanner processes and dumping stack and other
3
// relevant information for investigation.
4
package monitor
5
6
import (
7
"bytes"
8
"context"
9
"fmt"
10
"os"
11
"runtime"
12
"strings"
13
"sync"
14
"time"
15
16
"github.com/DataDog/gostackparse"
17
"github.com/projectdiscovery/gologger"
18
permissionutil "github.com/projectdiscovery/utils/permission"
19
unitutils "github.com/projectdiscovery/utils/unit"
20
"github.com/rs/xid"
21
)
22
23
// Agent is an agent for monitoring hanging programs
24
type Agent struct {
25
lastStack []string
26
callbacks []Callback
27
28
goroutineCount int
29
currentIteration int // number of times we've checked hang
30
31
lock sync.Mutex
32
}
33
34
const defaultMonitorIteration = 6
35
36
// NewStackMonitor returns a new stack monitor instance
37
func NewStackMonitor() *Agent {
38
return &Agent{}
39
}
40
41
// Callback when crash is detected and stack trace is saved to disk
42
type Callback func(dumpID string) error
43
44
// RegisterCallback adds a callback to perform additional operations before bailing out.
45
func (s *Agent) RegisterCallback(callback Callback) {
46
s.lock.Lock()
47
defer s.lock.Unlock()
48
49
s.callbacks = append(s.callbacks, callback)
50
}
51
52
func (s *Agent) Start(interval time.Duration) context.CancelFunc {
53
ctx, cancel := context.WithCancel(context.Background())
54
ticker := time.NewTicker(interval)
55
56
go func() {
57
for {
58
select {
59
case <-ctx.Done():
60
ticker.Stop()
61
case <-ticker.C:
62
s.monitorWorker(cancel)
63
default:
64
continue
65
}
66
}
67
}()
68
return cancel
69
}
70
71
// monitorWorker is a worker for monitoring running goroutines
72
func (s *Agent) monitorWorker(cancel context.CancelFunc) {
73
current := runtime.NumGoroutine()
74
if current != s.goroutineCount {
75
s.goroutineCount = current
76
s.currentIteration = 0
77
return
78
}
79
s.currentIteration++
80
81
if s.currentIteration == defaultMonitorIteration-1 {
82
lastStackTrace := generateStackTraceSlice(getStack(true))
83
s.lastStack = lastStackTrace
84
return
85
}
86
87
// cancel the monitoring goroutine if we discover
88
// we've been stuck for some iterations.
89
if s.currentIteration == defaultMonitorIteration {
90
currentStack := getStack(true)
91
92
// Bail out if the stacks don't match from previous iteration
93
newStack := generateStackTraceSlice(currentStack)
94
if !compareStringSliceEqual(s.lastStack, newStack) {
95
s.currentIteration = 0
96
return
97
}
98
99
cancel()
100
dumpID := xid.New().String()
101
stackTraceFile := fmt.Sprintf("nuclei-stacktrace-%s.dump", dumpID)
102
gologger.Error().Msgf("Detected hanging goroutine (count=%d/%d) = %s\n", current, s.goroutineCount, stackTraceFile)
103
if err := os.WriteFile(stackTraceFile, currentStack, permissionutil.ConfigFilePermission); err != nil {
104
gologger.Error().Msgf("Could not write stack trace for goroutines: %s\n", err)
105
}
106
107
s.lock.Lock()
108
callbacks := s.callbacks
109
s.lock.Unlock()
110
for _, callback := range callbacks {
111
if err := callback(dumpID); err != nil {
112
gologger.Error().Msgf("Stack monitor callback error: %s\n", err)
113
}
114
}
115
116
os.Exit(1) // exit forcefully if we've been stuck
117
}
118
}
119
120
// getStack returns full stack trace of the program
121
var getStack = func(all bool) []byte {
122
for i := unitutils.Mega; ; i *= 2 {
123
buf := make([]byte, i)
124
if n := runtime.Stack(buf, all); n < i {
125
return buf[:n-1]
126
}
127
}
128
}
129
130
// generateStackTraceSlice returns a list of current stack in string slice format
131
func generateStackTraceSlice(stack []byte) []string {
132
goroutines, _ := gostackparse.Parse(bytes.NewReader(stack))
133
134
var builder strings.Builder
135
var stackList []string
136
for _, goroutine := range goroutines {
137
builder.WriteString(goroutine.State)
138
builder.WriteString("|")
139
140
for _, frame := range goroutine.Stack {
141
builder.WriteString(frame.Func)
142
builder.WriteString(";")
143
}
144
stackList = append(stackList, builder.String())
145
builder.Reset()
146
}
147
return stackList
148
}
149
150
// compareStringSliceEqual compares two string slices for equality without order
151
func compareStringSliceEqual(first, second []string) bool {
152
if len(first) != len(second) {
153
return false
154
}
155
diff := make(map[string]int, len(first))
156
for _, x := range first {
157
diff[x]++
158
}
159
for _, y := range second {
160
if _, ok := diff[y]; !ok {
161
return false
162
}
163
diff[y] -= 1
164
if diff[y] == 0 {
165
delete(diff, y)
166
}
167
}
168
return len(diff) == 0
169
}
170
171