返回 DeepSeek-Reasonix
fatal.go
根目录 / internal / crashreport / fatal.go
1 package crashreport
2
3 import (
4 "io"
5 "os"
6 "path/filepath"
7 "runtime"
8 "runtime/debug"
9 "strconv"
10 "strings"
11 "time"
12 )
13
14 const (
15 fatalDirName = "cli-crash-fatal"
16 fatalSuffix = ".log"
17 maxFatalDumpLen = 64 << 10
18 )
19
20 // InstallFatalOutput mirrors what the Go runtime prints when it kills the
21 // process — a panic on any goroutine, a fatal runtime error — into a per-PID
22 // file under home, so the next start can queue it. Call release on every path
23 // that does not end in such a crash; it detaches the file and removes it.
24 func InstallFatalOutput(home string) (release func()) {
25 release = func() {}
26 if strings.TrimSpace(home) == "" {
27 return release
28 }
29 path := filepath.Join(home, fatalDirName, strconv.Itoa(os.Getpid())+fatalSuffix)
30 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
31 return release
32 }
33 f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
34 if err != nil {
35 return release
36 }
37 // The runtime duplicates the descriptor, so ours can close immediately.
38 installErr := debug.SetCrashOutput(f, debug.CrashOptions{})
39 _ = f.Close()
40 if installErr != nil {
41 _ = os.Remove(path)
42 return release
43 }
44 return func() {
45 _ = debug.SetCrashOutput(nil, debug.CrashOptions{})
46 _ = os.Remove(path)
47 }
48 }
49
50 // CaptureFatalDumps queues the dumps left by processes that died after
51 // InstallFatalOutput, and discards the empty files of ones killed without one.
52 func CaptureFatalDumps(home, version string) {
53 if strings.TrimSpace(home) == "" {
54 return
55 }
56 dir := filepath.Join(home, fatalDirName)
57 entries, err := os.ReadDir(dir)
58 if err != nil {
59 return
60 }
61 for _, entry := range entries {
62 pid, err := strconv.Atoi(strings.TrimSuffix(entry.Name(), fatalSuffix))
63 if entry.IsDir() || !strings.HasSuffix(entry.Name(), fatalSuffix) || err != nil ||
64 pid <= 0 || pid == os.Getpid() || processAlive(pid) {
65 continue
66 }
67 path := filepath.Join(dir, entry.Name())
68 if captureFatalDump(home, version, path) == nil {
69 _ = os.Remove(path)
70 }
71 }
72 _ = os.Remove(dir)
73 }
74
75 func captureFatalDump(home, version, path string) error {
76 f, err := os.Open(path)
77 if err != nil {
78 return err
79 }
80 defer f.Close()
81 occurredAt := time.Now().UTC()
82 if info, statErr := f.Stat(); statErr == nil {
83 occurredAt = info.ModTime().UTC()
84 }
85 raw, err := io.ReadAll(io.LimitReader(f, maxFatalDumpLen))
86 if err != nil {
87 return err
88 }
89 if strings.TrimSpace(string(raw)) == "" {
90 return nil
91 }
92 classification, stack := splitFatalDump(string(raw))
93 cleanStack := sanitizeStack(stack)
94 return write(home, Report{
95 Kind: "crash",
96 Version: sanitizeField(defaultString(version, "unknown"), 64),
97 OS: runtime.GOOS,
98 Arch: runtime.GOARCH,
99 Message: "[cli fatal]\n\nGo runtime terminated the CLI process.",
100 SchemaVersion: currentSchemaVersion,
101 ErrorType: "GoRuntimeFatal",
102 ErrorMessage: classification,
103 Stack: cleanStack,
104 TopFrame: topFrame(cleanStack),
105 OccurredAt: occurredAt.Format(time.RFC3339Nano),
106 })
107 }
108
109 // splitFatalDump keeps the runtime's own classification and the goroutine
110 // stacks. A panic value is user-influenced text and never leaves the dump.
111 func splitFatalDump(raw string) (classification, stack string) {
112 const unclassified = "runtime crash output"
113 classification = unclassified
114 lines := strings.Split(raw, "\n")
115 for i, line := range lines {
116 trimmed := strings.TrimSpace(line)
117 switch {
118 case classification != unclassified && !strings.HasPrefix(trimmed, "goroutine "):
119 continue
120 case strings.HasPrefix(trimmed, "fatal error:"):
121 classification = sanitizeText(trimmed, 256)
122 case strings.HasPrefix(trimmed, "panic:"):
123 classification = "panic: [redacted panic value]"
124 case strings.HasPrefix(trimmed, "goroutine "):
125 return classification, strings.Join(lines[i:], "\n")
126 }
127 }
128 return classification, ""
129 }
130
130 lines GO