返回 DeepSeek-Reasonix
crash_fatal.go
根目录 / desktop / crash_fatal.go
1 package main
2
3 import (
4 "io"
5 "os"
6 "path/filepath"
7 "runtime/debug"
8 "strconv"
9 "strings"
10 "time"
11
12 "reasonix/internal/config"
13 )
14
15 const (
16 fatalCrashDirName = "crash-fatal"
17 fatalCrashLogSuffix = ".log"
18 fatalCrashCoveredSuffix = ".covered"
19 legacyFatalCrashFile = "crash-fatal.log"
20 legacyFatalCrashCoveredFile = "crash-fatal-covered"
21 )
22
23 var fatalCrashProcessAlive = desktopProcessAlive
24
25 func fatalCrashDir() string {
26 return filepath.Join(config.MemoryUserDir(), fatalCrashDirName)
27 }
28
29 func fatalCrashPath() string {
30 return fatalCrashPathForPID(os.Getpid())
31 }
32
33 func fatalCrashCoveredPath() string {
34 return fatalCrashCoveredPathForPID(os.Getpid())
35 }
36
37 func fatalCrashPathForPID(pid int) string {
38 return filepath.Join(fatalCrashDir(), strconv.Itoa(pid)+fatalCrashLogSuffix)
39 }
40
41 func fatalCrashCoveredPathForPID(pid int) string {
42 return filepath.Join(fatalCrashDir(), strconv.Itoa(pid)+fatalCrashCoveredSuffix)
43 }
44
45 func legacyFatalCrashPath() string {
46 return filepath.Join(config.MemoryUserDir(), legacyFatalCrashFile)
47 }
48
49 func legacyFatalCrashCoveredPath() string {
50 return filepath.Join(config.MemoryUserDir(), legacyFatalCrashCoveredFile)
51 }
52
53 func markFatalCrashCovered() {
54 markFatalCrashCoveredForPID(os.Getpid())
55 }
56
57 func markFatalCrashCoveredForPID(pid int) {
58 path := fatalCrashCoveredPathForPID(pid)
59 if os.MkdirAll(filepath.Dir(path), 0o700) == nil {
60 _ = os.WriteFile(path, []byte("structured\n"), 0o600)
61 }
62 }
63
64 // capturePreviousFatalCrash converts runtime.SetCrashOutput dumps from dead
65 // processes into the normal scrubbed queue. Per-PID files keep a routine second
66 // launch from truncating or unlinking the running primary process's dump.
67 func capturePreviousFatalCrash() {
68 // Preserve compatibility with the single-file format used by older builds.
69 // An empty legacy file may still be owned by a running older process, so it
70 // must be left untouched until it contains a completed crash dump.
71 captureFatalCrashFile(legacyFatalCrashPath(), legacyFatalCrashCoveredPath(), false)
72
73 entries, err := os.ReadDir(fatalCrashDir())
74 if err != nil {
75 return
76 }
77 for _, entry := range entries {
78 pid, ok := fatalCrashPID(entry.Name())
79 if !ok || pid == os.Getpid() || fatalCrashProcessAlive(pid) {
80 continue
81 }
82 captureFatalCrashFile(
83 filepath.Join(fatalCrashDir(), entry.Name()),
84 fatalCrashCoveredPathForPID(pid),
85 true,
86 )
87 }
88 for _, entry := range entries {
89 pid, ok := fatalCrashCoveredPID(entry.Name())
90 if !ok || pid == os.Getpid() || fatalCrashProcessAlive(pid) {
91 continue
92 }
93 if _, err := os.Stat(fatalCrashPathForPID(pid)); os.IsNotExist(err) {
94 _ = os.Remove(filepath.Join(fatalCrashDir(), entry.Name()))
95 }
96 }
97 // Best effort: succeeds only when no live/current process artifacts remain.
98 _ = os.Remove(fatalCrashDir())
99 }
100
101 func fatalCrashPID(name string) (int, bool) {
102 return fatalCrashPIDWithSuffix(name, fatalCrashLogSuffix)
103 }
104
105 func fatalCrashCoveredPID(name string) (int, bool) {
106 return fatalCrashPIDWithSuffix(name, fatalCrashCoveredSuffix)
107 }
108
109 func fatalCrashPIDWithSuffix(name, suffix string) (int, bool) {
110 if !strings.HasSuffix(name, suffix) {
111 return 0, false
112 }
113 pid, err := strconv.Atoi(strings.TrimSuffix(name, suffix))
114 return pid, err == nil && pid > 0
115 }
116
117 func captureFatalCrashFile(path, coveredPath string, removeEmpty bool) {
118 f, err := os.Open(path)
119 if err != nil {
120 return
121 }
122 occurredAt := time.Now().UTC()
123 if info, statErr := f.Stat(); statErr == nil {
124 occurredAt = info.ModTime().UTC()
125 }
126 raw, readErr := io.ReadAll(io.LimitReader(f, maxCrashStackBytes+1))
127 _ = f.Close()
128 if readErr != nil || len(strings.TrimSpace(string(raw))) == 0 {
129 if removeEmpty {
130 _ = os.Remove(coveredPath)
131 _ = os.Remove(path)
132 }
133 return
134 }
135 if _, err := os.Stat(coveredPath); err == nil {
136 _ = os.Remove(coveredPath)
137 _ = os.Remove(path)
138 return
139 }
140 stack := sanitizeFatalRuntimeDump(string(raw))
141 report := baseCrashReport("crash")
142 report.SchemaVersion = 2
143 report.Source = "go.runtime"
144 report.Label = "go.fatal"
145 report.ErrorType = "GoRuntimeFatal"
146 report.ErrorMessage = "Go runtime terminated the desktop process."
147 report.Stack = stack
148 report.TopFrame = topFrameFromStack(stack)
149 report.FingerprintHint = "go.runtime.fatal"
150 report.OccurredAt = occurredAt.Format(time.RFC3339)
151 report.Message = sanitizeCrashText("[go.runtime.fatal]\n\n"+stack, maxCrashDetailBytes)
152 if writePendingReport(report, true) {
153 _ = os.Remove(coveredPath)
154 _ = os.Remove(path)
155 }
156 }
157
158 // sanitizeFatalRuntimeDump removes panic values and preamble text that could
159 // originate in user-controlled errors, while retaining runtime classification
160 // and symbolized goroutine stacks for diagnosis.
161 func sanitizeFatalRuntimeDump(raw string) string {
162 lines := strings.Split(raw, "\n")
163 classification := "runtime crash output"
164 stackStart := -1
165 for i, line := range lines {
166 trimmed := strings.TrimSpace(line)
167 switch {
168 case strings.HasPrefix(trimmed, "fatal error:"):
169 classification = sanitizeCrashText(trimmed, 256)
170 case strings.HasPrefix(trimmed, "panic:"):
171 classification = "panic: [redacted panic value]"
172 }
173 if strings.HasPrefix(trimmed, "goroutine ") {
174 stackStart = i
175 break
176 }
177 }
178 stack := ""
179 if stackStart >= 0 {
180 stack = strings.Join(lines[stackStart:], "\n")
181 }
182 return sanitizeCrashText(classification+"\n\n"+stack, maxCrashStackBytes)
183 }
184
185 // installFatalCrashOutput asks the Go runtime to mirror unrecovered panics and
186 // fatal runtime errors to a durable file. The runtime duplicates the descriptor,
187 // so the file may be closed after SetCrashOutput returns.
188 func installFatalCrashOutput() {
189 path := fatalCrashPath()
190 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
191 return
192 }
193 f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
194 if err != nil {
195 return
196 }
197 if err := debug.SetCrashOutput(f, debug.CrashOptions{}); err != nil {
198 _ = f.Close()
199 _ = os.Remove(path)
200 return
201 }
202 _ = f.Close()
203 }
204
204 lines GO