feat_: detect goroutines with no recover
This commit is contained in:
parent
1618aab830
commit
610e904313
|
@ -0,0 +1,225 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) < 2 {
|
||||
fmt.Println("Usage: go run main.go <directory>")
|
||||
return
|
||||
}
|
||||
|
||||
// Initialize logger with colors
|
||||
handler := log.StreamHandler(os.Stdout, log.TerminalFormat(true))
|
||||
log.Root().SetHandler(log.LvlFilterHandler(log.LvlInfo, handler))
|
||||
|
||||
dir := os.Args[1]
|
||||
log.Info("Starting analysis...", "directory", dir)
|
||||
|
||||
// Step 1: Scan all files and look for `go` calls
|
||||
err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
log.Error("Error walking the path", "path", dir, "error", err)
|
||||
return err
|
||||
}
|
||||
if info.IsDir() || !strings.HasSuffix(info.Name(), ".go") {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Info("Scanning Go file", "file", path)
|
||||
checkFileForGoroutines(path)
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Error("Error during file walk", "error", err)
|
||||
}
|
||||
|
||||
log.Info("Analysis complete")
|
||||
}
|
||||
|
||||
// checkFileForGoroutines scans a Go file for any `go` statements (goroutines)
|
||||
func checkFileForGoroutines(filePath string) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
log.Error("Error opening file", "file", filePath, "error", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
var lineNumber int
|
||||
// Regex for non-anonymous function/method calls: `go functionName()`
|
||||
regex := regexp.MustCompile(`go\s+(\.|\w)+\(\)$`)
|
||||
|
||||
for scanner.Scan() {
|
||||
lineNumber++
|
||||
line := scanner.Text() // Do not trim spaces here
|
||||
|
||||
// Detect anonymous goroutines
|
||||
if strings.Contains(line, "go func") {
|
||||
log.Info("Found anonymous goroutine", "file", filePath, "line", lineNumber, "lineContent", line)
|
||||
checkAnonymousGoroutine(filePath, lineNumber)
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect non-anonymous goroutines using regex
|
||||
if regex.MatchString(line) {
|
||||
log.Info("Found non-anonymous goroutine", "file", filePath, "line", lineNumber, "lineContent", line)
|
||||
|
||||
// Find the position of the first occurrence of "()"
|
||||
cursorPos := strings.Index(line, "()")
|
||||
if cursorPos == -1 {
|
||||
log.Error("Failed to find function call", "file", filePath, "line", lineNumber)
|
||||
continue
|
||||
}
|
||||
|
||||
// Calculate the cursor position by adjusting for tabs (counting tabs as 4 characters)
|
||||
//tabs := strings.Count(line[:cursorPos], "\t")
|
||||
//adjustedCursorPos := cursorPos - (tabs * 4) // Subtract 3 for each tab since a tab counts as 4 chars
|
||||
|
||||
checkNamedFunction(filePath, lineNumber, cursorPos)
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Error("Error reading file", "file", filePath, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// checkAnonymousGoroutine checks if an anonymous goroutine has `defer utils.LogOnPanic()`
|
||||
func checkAnonymousGoroutine(filePath string, lineNumber int) {
|
||||
//log.Debug("Checking anonymous goroutine", "file", filePath, "line", lineNumber)
|
||||
|
||||
// Open the file again and scan from the `go func` line onwards
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
log.Error("Error opening file", "file", filePath, "error", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
var currentLine int
|
||||
for scanner.Scan() {
|
||||
currentLine++
|
||||
if currentLine <= lineNumber {
|
||||
continue
|
||||
}
|
||||
|
||||
line := scanner.Text()
|
||||
// First line of the function body
|
||||
if strings.Contains(line, "defer utils.LogOnPanic()") {
|
||||
log.Info("Found defer utils.LogOnPanic() in anonymous function", "file", filePath, "line", lineNumber)
|
||||
} else {
|
||||
log.Warn("Missing defer utils.LogOnPanic() in anonymous function", "file", filePath, "line", lineNumber)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Error("Error reading file", "file", filePath, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// extractFunctionNameAndCursorPosition extracts the function or method name and calculates the cursor position
|
||||
func extractFunctionNameAndCursorPosition(line string, matches []string) (string, int) {
|
||||
funcName := matches[1]
|
||||
|
||||
// Calculate the cursor position (count tabs as one character)
|
||||
regex := regexp.MustCompile(`\bgo\s+`)
|
||||
cursorPos := regex.FindStringIndex(line)[1] // Position after `go ` keyword
|
||||
|
||||
return funcName, cursorPos
|
||||
}
|
||||
|
||||
// checkNamedFunction uses `gopls` to find the definition of a named function/method and checks its first line
|
||||
func checkNamedFunction(filePath string, lineNumber, charPos int) {
|
||||
log.Debug("Checking named function", "file", filePath, "line", lineNumber, "char", charPos)
|
||||
|
||||
// Use `gopls` to find the definition of the function/method
|
||||
cmd := exec.Command("gopls", "definition", fmt.Sprintf("%s:%d:%d", filePath, lineNumber, charPos))
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
log.Error("Error running gopls definition", "file", filePath, "line", lineNumber, "error", err)
|
||||
return
|
||||
}
|
||||
|
||||
definitionOutput := string(output)
|
||||
// Parse the definition output to find the file and line number of the function definition
|
||||
parseAndCheckFunctionDefinition(definitionOutput)
|
||||
}
|
||||
|
||||
// parseAndCheckFunctionDefinition parses the output of `gopls definition` and checks the function body
|
||||
func parseAndCheckFunctionDefinition(definitionOutput string) {
|
||||
// The output of `gopls definition` will contain the file path and position of the function definition
|
||||
// Example output might be:
|
||||
// /path/to/file.go:23:5
|
||||
log.Debug("Parsed definition", "definition", definitionOutput)
|
||||
|
||||
// Extract file path and line number from the output
|
||||
parts := strings.Split(definitionOutput, ":")
|
||||
if len(parts) < 2 {
|
||||
log.Error("Failed to parse gopls definition output", "output", definitionOutput)
|
||||
return
|
||||
}
|
||||
defFilePath := parts[0]
|
||||
lineNumber := atoi(parts[1])
|
||||
|
||||
// Open the file and check the first statement inside the function body
|
||||
checkFirstLineInFunctionBody(defFilePath, lineNumber)
|
||||
}
|
||||
|
||||
// checkFirstLineInFunctionBody checks the first line inside a function body for `defer utils.LogOnPanic()`
|
||||
func checkFirstLineInFunctionBody(filePath string, startLine int) {
|
||||
log.Debug("Checking function body", "file", filePath, "startLine", startLine)
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
log.Error("Error opening file", "file", filePath, "error", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
var currentLine int
|
||||
for scanner.Scan() {
|
||||
currentLine++
|
||||
if currentLine <= startLine {
|
||||
continue
|
||||
}
|
||||
|
||||
line := scanner.Text()
|
||||
|
||||
if strings.Contains(line, "defer utils.LogOnPanic()") {
|
||||
log.Info("Found defer utils.LogOnPanic() in function", "file", filePath, "line", startLine)
|
||||
} else {
|
||||
log.Warn("Missing defer utils.LogOnPanic() in function", "file", filePath, "line", startLine)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Error("Error reading file", "file", filePath, "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
// atoi is a helper to safely convert a string to an int
|
||||
func atoi(s string) int {
|
||||
i, err := strconv.Atoi(s)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return i
|
||||
}
|
|
@ -19,6 +19,7 @@ import (
|
|||
v2protocol "github.com/waku-org/go-waku/waku/v2/protocol"
|
||||
|
||||
v1protocol "github.com/status-im/status-go/protocol/v1"
|
||||
"github.com/status-im/status-go/utils"
|
||||
)
|
||||
|
||||
type TelemetryType string
|
||||
|
@ -161,8 +162,19 @@ func (c *Client) SetDeviceType(deviceType string) {
|
|||
c.deviceType = deviceType
|
||||
}
|
||||
|
||||
func (c *Client) Foo() {
|
||||
|
||||
}
|
||||
|
||||
func Bar() {
|
||||
defer utils.LogOnPanic()
|
||||
}
|
||||
|
||||
func (c *Client) Start(ctx context.Context) {
|
||||
go c.Foo()
|
||||
go Bar()
|
||||
go func() {
|
||||
defer utils.LogOnPanic()
|
||||
for {
|
||||
select {
|
||||
case telemetryRequest := <-c.telemetryCh:
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package utils
|
||||
|
||||
import "github.com/ethereum/go-ethereum/log"
|
||||
|
||||
func LogOnPanic() {
|
||||
log.Info("<<< panic")
|
||||
}
|
Loading…
Reference in New Issue