返回 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 = currentCrashSchema
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.Diagnostics = &crashDiagnostics{
152 ObserverVersion: version, ObserverBuildCommit: buildCommit(), ProcessRole: "service",
153 ObservedAt: time.Now().UTC().Format(time.RFC3339Nano), TerminationReason: "unknown",
154 CleanupOutcome: "interrupted", Evidence: "confirmed", Category: "crash",
155 }
156 report.Message = sanitizeCrashText("[go.runtime.fatal]\n\n"+stack, maxCrashDetailBytes)
157 if writePendingReport(report, true) {
158 _ = os.Remove(coveredPath)
159 _ = os.Remove(path)
160 }
161 }
162
163 // sanitizeFatalRuntimeDump removes panic values and preamble text that could
164 // originate in user-controlled errors, while retaining runtime classification
165 // and symbolized goroutine stacks for diagnosis.
166 func sanitizeFatalRuntimeDump(raw string) string {
167 lines := strings.Split(raw, "\n")
168 classification := "runtime crash output"
169 stackStart := -1
170 for i, line := range lines {
171 trimmed := strings.TrimSpace(line)
172 switch {
173 case strings.HasPrefix(trimmed, "fatal error:"):
174 classification = sanitizeCrashText(trimmed, 256)
175 case strings.HasPrefix(trimmed, "panic:"):
176 classification = "panic: [redacted panic value]"
177 }
178 if strings.HasPrefix(trimmed, "goroutine ") {
179 stackStart = i
180 break
181 }
182 }
183 stack := ""
184 if stackStart >= 0 {
185 stack = strings.Join(lines[stackStart:], "\n")
186 }
187 return sanitizeCrashText(classification+"\n\n"+stack, maxCrashStackBytes)
188 }
189
190 // installFatalCrashOutput asks the Go runtime to mirror unrecovered panics and
191 // fatal runtime errors to a durable file. The runtime duplicates the descriptor,
192 // so the file may be closed after SetCrashOutput returns.
193 func installFatalCrashOutput() {
194 path := fatalCrashPath()
195 if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
196 return
197 }
198 f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600)
199 if err != nil {
200 return
201 }
202 if err := debug.SetCrashOutput(f, debug.CrashOptions{}); err != nil {
203 _ = f.Close()
204 _ = os.Remove(path)
205 return
206 }
207 _ = f.Close()
208 }
209
209 lines GO