| 1 | package main |
| 2 | |
| 3 | import ( |
| 4 | "fmt" |
| 5 | "runtime" |
| 6 | "time" |
| 7 | |
| 8 | "reasonix/internal/repair" |
| 9 | ) |
| 10 | |
| 11 | func (a *App) recordPreviousRunDiagnostics() { |
| 12 | previous := a.lifecycle.previousRun |
| 13 | if previous.Abnormal { |
| 14 | report := previousRunReport(previous) |
| 15 | _ = writePendingReport(report, true) |
| 16 | if m := a.metrics.Load(); m != nil { |
| 17 | m.inc("desktop_legacy_exit", "abnormal") |
| 18 | m.inc("desktop_legacy_exit_phase", metricBucket(previous.Phase)) |
| 19 | m.persist() |
| 20 | } |
| 21 | } |
| 22 | for _, lifecycle := range a.lifecycle.previousRuns { |
| 23 | persisted := writePendingReport(desktopLifecycleReport(lifecycle), true) |
| 24 | a.lifecycle.tracker.finalizeObservation(lifecycle, persisted) |
| 25 | if !persisted { |
| 26 | continue |
| 27 | } |
| 28 | if m := a.metrics.Load(); m != nil { |
| 29 | m.inc("desktop_exit", "abnormal") |
| 30 | m.inc("desktop_exit_phase", metricBucket(lifecycle.Phase)) |
| 31 | m.persist() |
| 32 | } |
| 33 | } |
| 34 | } |
| 35 | |
| 36 | func previousRunReport(previous repair.PreviousRunObservation) crashReport { |
| 37 | phase := metricBucket(previous.Phase) |
| 38 | uptime := metricBucket(previous.UptimeBucket) |
| 39 | profile := metricBucket(previous.InstallProfile) |
| 40 | update := "none" |
| 41 | if previous.UpdateTo != "" { |
| 42 | update = sanitizeCrashField(previous.UpdateFrom+" -> "+previous.UpdateTo, 128) |
| 43 | } |
| 44 | message := fmt.Sprintf(`[desktop.legacy_abnormal_exit] |
| 45 | |
| 46 | Reasonix consumed a legacy v1.18-v1.19 startup record whose owner was no longer running. |
| 47 | |
| 48 | --- lifecycle context --- |
| 49 | phase: %s |
| 50 | uptime bucket: %s |
| 51 | install profile: %s |
| 52 | previous version: %s |
| 53 | update transition: %s`, |
| 54 | phase, |
| 55 | uptime, |
| 56 | profile, |
| 57 | sanitizeCrashField(previous.Version, 64), |
| 58 | update, |
| 59 | ) |
| 60 | report := baseCrashReport("crash") |
| 61 | report.SchemaVersion = 4 |
| 62 | report.Source = "native.lifecycle.legacy" |
| 63 | report.Label = "desktop.legacy_abnormal_exit" |
| 64 | report.ErrorType = "LegacyAbnormalDesktopExit" |
| 65 | report.ErrorMessage = "A legacy startup record was consumed once after its owner stopped." |
| 66 | report.TopFrame = "desktop.lifecycle.legacy." + phase |
| 67 | report.FingerprintHint = "desktop.legacy_abnormal_exit." + runtime.GOOS + "." + phase |
| 68 | report.Diagnostics = &crashDiagnostics{ |
| 69 | SubjectVersion: previous.Version, ObserverVersion: version, ObserverBuildCommit: buildCommit(), |
| 70 | ProcessRole: "service", LastPhase: phase, ObservedAt: time.Now().UTC().Format(time.RFC3339Nano), |
| 71 | TerminationReason: "unknown", CleanupOutcome: "unknown", Evidence: "observed", |
| 72 | Category: "historical_record", LegacyParsed: true, |
| 73 | } |
| 74 | report.Message = sanitizeCrashText(message, maxCrashDetailBytes) |
| 75 | return report |
| 76 | } |
| 77 | |
| 78 | func desktopLifecycleReport(previous desktopLifecycleObservation) crashReport { |
| 79 | phase := metricBucket(previous.Phase) |
| 80 | category := "unclean_exit" |
| 81 | processRole := previous.ProcessRole |
| 82 | if processRole == "" { |
| 83 | processRole = "service" |
| 84 | } |
| 85 | if previous.TerminationReason == shutdownReasonStartupFailure { |
| 86 | category = "startup_failure" |
| 87 | } |
| 88 | message := fmt.Sprintf(`[desktop.abnormal_exit.v2] |
| 89 | |
| 90 | Reasonix found a per-process lifecycle record whose desktop process was no longer running. |
| 91 | |
| 92 | --- lifecycle context --- |
| 93 | phase: %s |
| 94 | previous version: %s |
| 95 | previous channel: %s |
| 96 | started at: %s |
| 97 | last phase update: %s`, |
| 98 | phase, |
| 99 | sanitizeCrashField(previous.Version, 64), |
| 100 | sanitizeCrashField(previous.Channel, 32), |
| 101 | sanitizeCrashField(previous.StartedAt, 64), |
| 102 | sanitizeCrashField(previous.UpdatedAt, 64), |
| 103 | ) |
| 104 | report := baseCrashReport("crash") |
| 105 | report.SchemaVersion = 4 |
| 106 | report.Source = "native.lifecycle" |
| 107 | report.Label = "desktop.abnormal_exit.v2" |
| 108 | report.ErrorType = "AbnormalDesktopExit" |
| 109 | report.ErrorMessage = "A per-process lifecycle record remained after its desktop process stopped." |
| 110 | report.TopFrame = "desktop.lifecycle.v2." + phase |
| 111 | report.FingerprintHint = "desktop.abnormal_exit.v2." + runtime.GOOS + "." + phase |
| 112 | report.Diagnostics = &crashDiagnostics{ |
| 113 | SubjectVersion: previous.Version, SubjectBuildCommit: previous.BuildCommit, SubjectChannel: previous.Channel, |
| 114 | ObserverVersion: version, ObserverBuildCommit: buildCommit(), RunID: previous.RunID, IncidentID: previous.IncidentID, |
| 115 | ProcessRole: processRole, LastPhase: phase, LastPhaseAt: previous.UpdatedAt, |
| 116 | ObservedAt: time.Now().UTC().Format(time.RFC3339Nano), TerminationReason: previous.TerminationReason, |
| 117 | CleanupOutcome: previous.CleanupOutcome, Evidence: "observed", Category: category, |
| 118 | } |
| 119 | report.Message = sanitizeCrashText(message, maxCrashDetailBytes) |
| 120 | return report |
| 121 | } |
| 122 |