feat_: detect goroutines with no recover

This commit is contained in:
Igor Sirotin 2024-09-20 13:24:34 +01:00
parent 1618aab830
commit 610e904313
No known key found for this signature in database
GPG Key ID: 425E227CAAB81F95
3 changed files with 244 additions and 0 deletions

View File

@ -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
}

View File

@ -19,6 +19,7 @@ import (
v2protocol "github.com/waku-org/go-waku/waku/v2/protocol" v2protocol "github.com/waku-org/go-waku/waku/v2/protocol"
v1protocol "github.com/status-im/status-go/protocol/v1" v1protocol "github.com/status-im/status-go/protocol/v1"
"github.com/status-im/status-go/utils"
) )
type TelemetryType string type TelemetryType string
@ -161,8 +162,19 @@ func (c *Client) SetDeviceType(deviceType string) {
c.deviceType = deviceType c.deviceType = deviceType
} }
func (c *Client) Foo() {
}
func Bar() {
defer utils.LogOnPanic()
}
func (c *Client) Start(ctx context.Context) { func (c *Client) Start(ctx context.Context) {
go c.Foo()
go Bar()
go func() { go func() {
defer utils.LogOnPanic()
for { for {
select { select {
case telemetryRequest := <-c.telemetryCh: case telemetryRequest := <-c.telemetryCh:

7
utils/utils.go Normal file
View File

@ -0,0 +1,7 @@
package utils
import "github.com/ethereum/go-ethereum/log"
func LogOnPanic() {
log.Info("<<< panic")
}