返回 DeepSeek-Reasonix
startup.go
根目录 / internal / repair / startup.go
1 package repair
2
3 import (
4 "encoding/json"
5 "os"
6 "path/filepath"
7 "time"
8
9 "reasonix/internal/config"
10 )
11
12 // StartupState is the legacy startup-state.json shape written by v1.18-v1.19.
13 // v1.20 reads it only to attach bounded diagnostics to the next crash report;
14 // it never writes the record or uses it to select a launch mode.
15 type StartupState struct {
16 SchemaVersion int `json:"schemaVersion"`
17 Phase string `json:"phase"`
18 Version string `json:"version,omitempty"`
19 InstallProfile string `json:"installProfile,omitempty"`
20 UpdateFromVersion string `json:"updateFromVersion,omitempty"`
21 UpdateToVersion string `json:"updateToVersion,omitempty"`
22 PID int `json:"pid,omitempty"`
23 StartedAt string `json:"startedAt,omitempty"`
24 UpdatedAt string `json:"updatedAt,omitempty"`
25 }
26
27 // PreviousRunObservation is a privacy-safe description of a startup record
28 // whose owner is no longer alive. PID and filesystem paths remain local.
29 type PreviousRunObservation struct {
30 Abnormal bool
31 Phase string
32 Version string
33 InstallProfile string
34 UpdateFrom string
35 UpdateTo string
36 UptimeBucket string
37 }
38
39 // StartupTracker is a read-only adapter for legacy startup records.
40 type StartupTracker struct {
41 path string
42 processAlive func(int) bool
43 }
44
45 func NewStartupTracker(path string) *StartupTracker {
46 if path == "" {
47 if root := config.MemoryUserDir(); root != "" {
48 path = filepath.Join(root, "repair", "startup-state.json")
49 }
50 }
51 return &StartupTracker{path: path, processAlive: startupProcessAlive}
52 }
53
54 func (t *StartupTracker) Read() (StartupState, error) {
55 if t.path == "" {
56 return StartupState{}, nil
57 }
58 b, err := os.ReadFile(t.path)
59 if err != nil {
60 if os.IsNotExist(err) {
61 return StartupState{}, nil
62 }
63 return StartupState{}, err
64 }
65 var state StartupState
66 if err := json.Unmarshal(b, &state); err != nil {
67 return StartupState{}, err
68 }
69 return state, nil
70 }
71
72 // ObservePreviousRun reports an unclean prior process without mutating its
73 // record. No observation can alter startup behavior.
74 func (t *StartupTracker) ObservePreviousRun() PreviousRunObservation {
75 state, err := t.Read()
76 if err != nil || state.Phase == "" || state.Phase == "clean-exit" {
77 return PreviousRunObservation{}
78 }
79 if runningStartupPhase(state.Phase) && state.PID > 0 && t.processAlive(state.PID) {
80 return PreviousRunObservation{}
81 }
82 return PreviousRunObservation{
83 Abnormal: true,
84 Phase: state.Phase,
85 Version: state.Version,
86 InstallProfile: state.InstallProfile,
87 UpdateFrom: state.UpdateFromVersion,
88 UpdateTo: state.UpdateToVersion,
89 UptimeBucket: startupUptimeBucket(state),
90 }
91 }
92
93 func startupUptimeBucket(state StartupState) string {
94 started, startErr := time.Parse(time.RFC3339Nano, state.StartedAt)
95 updated, updateErr := time.Parse(time.RFC3339Nano, state.UpdatedAt)
96 if startErr != nil || updateErr != nil || updated.Before(started) {
97 return "unknown"
98 }
99 switch d := updated.Sub(started); {
100 case d < 30*time.Second:
101 return "s_0_30"
102 case d < 2*time.Minute:
103 return "m_0_2"
104 case d < 10*time.Minute:
105 return "m_2_10"
106 case d < time.Hour:
107 return "m_10_60"
108 case d < 6*time.Hour:
109 return "h_1_6"
110 default:
111 return "h_6_plus"
112 }
113 }
114
115 func runningStartupPhase(phase string) bool {
116 return phase == "starting" || phase == "ready" || phase == "healthy"
117 }
118
118 lines GO