返回 DeepSeek-Reasonix
config.go
根目录 / internal / repair / config.go
1 package repair
2
3 import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8 "time"
9
10 "reasonix/internal/config"
11 "reasonix/internal/fileutil"
12 )
13
14 type ConfigCheck struct {
15 Scope string `json:"scope"`
16 Path string `json:"path"`
17 Exists bool `json:"exists"`
18 Valid bool `json:"valid"`
19 Error string `json:"error,omitempty"`
20 SnapshotPath string `json:"snapshotPath,omitempty"`
21 }
22
23 type ConfigReport struct {
24 Checks []ConfigCheck `json:"checks"`
25 Applied []string `json:"applied"`
26 }
27
28 type ConfigOptions struct {
29 Root string
30 Apply bool
31 IncludeProject bool
32 OnlyScope string
33 Now func() time.Time
34
35 expectedStates map[string]string
36 confirmedGlobalRestore []byte
37 hasConfirmedRestore bool
38 repairTransaction *RepairTransaction
39 }
40
41 func InspectAndRepairConfig(opts ConfigOptions) (ConfigReport, error) {
42 if !opts.Apply {
43 return inspectAndRepairConfigUnlocked(opts)
44 }
45 paths, err := configRepairTargetPaths(opts)
46 if err != nil {
47 return ConfigReport{}, err
48 }
49 // Direct callers do not carry an external preview ID. Bind their invocation
50 // before waiting on either lock so a newer config or snapshot cannot become
51 // the implicit object of an older call.
52 if opts.expectedStates == nil {
53 opts.expectedStates = make(map[string]string, len(paths))
54 for _, path := range paths {
55 opts.expectedStates[path] = repairPlanFileState(path)
56 }
57 if snapshot := lastKnownGoodConfigPath(); snapshot != "" {
58 bound := repairPlanFileSnapshotAt(snapshot)
59 opts.confirmedGlobalRestore = append([]byte(nil), bound.Content...)
60 opts.hasConfirmedRestore = bound.Readable
61 }
62 }
63 unlockTransaction, err := lockRepairTransaction()
64 if err != nil {
65 return ConfigReport{}, err
66 }
67 defer unlockTransaction()
68 if err := reconcilePreparedRepairTransaction(); err != nil {
69 return ConfigReport{}, fmt.Errorf("repair config: reconcile pending mutation: %w", err)
70 }
71 unlock, err := lockRepairMutations(paths...)
72 if err != nil {
73 return ConfigReport{}, err
74 }
75 defer unlock()
76 if err := verifyRepairPlanFileStates(opts.expectedStates); err != nil {
77 return ConfigReport{}, err
78 }
79 return inspectAndRepairConfigUnlocked(opts)
80 }
81
82 func inspectAndRepairConfigUnlocked(opts ConfigOptions) (ConfigReport, error) {
83 if opts.OnlyScope != "" && opts.OnlyScope != "global" && opts.OnlyScope != "project" {
84 return ConfigReport{}, fmt.Errorf("unknown config repair scope %q", opts.OnlyScope)
85 }
86 if opts.Now == nil {
87 opts.Now = time.Now
88 }
89 global := config.UserConfigPath()
90 project := filepath.Join(opts.Root, "reasonix.toml")
91 if opts.Root == "" || opts.Root == "." {
92 project = "reasonix.toml"
93 }
94 paths := []struct{ scope, path string }{{"global", global}, {"project", project}}
95 report := ConfigReport{Checks: make([]ConfigCheck, 0, len(paths)), Applied: []string{}}
96 tx := opts.repairTransaction
97 if tx == nil {
98 tx = newRepairTransaction(opts.Now())
99 }
100 for _, item := range paths {
101 check := inspectConfig(item.scope, item.path)
102 if item.scope == "global" {
103 check.SnapshotPath = lastKnownGoodConfigPath()
104 }
105 report.Checks = append(report.Checks, check)
106 if !opts.Apply || !check.Exists || check.Valid || (opts.OnlyScope != "" && item.scope != opts.OnlyScope) || (item.scope == "project" && !opts.IncludeProject) {
107 continue
108 }
109 if err := verifyRepairPlanFileState(item.path, opts.expectedStates); err != nil {
110 return report, err
111 }
112 if item.scope == "global" {
113 if err := verifyRepairPlanFileState(lastKnownGoodConfigPath(), opts.expectedStates); err != nil {
114 return report, err
115 }
116 }
117 repairMutationBeforeRename(item.path)
118 if err := verifyRepairPlanFileState(item.path, opts.expectedStates); err != nil {
119 return report, err
120 }
121 if item.scope == "global" {
122 if err := verifyRepairPlanFileState(lastKnownGoodConfigPath(), opts.expectedStates); err != nil {
123 return report, err
124 }
125 }
126 quarantine := item.path + ".reasonix-quarantine-" + opts.Now().UTC().Format("20060102T150405Z")
127 changeIndex := len(tx.Changes)
128 tx.Changes = append(tx.Changes, preparedRepairChangeForPrevious(item.scope, item.path, quarantine))
129 if err := persistPreparedRepairTransaction(tx); err != nil {
130 return report, fmt.Errorf("prepare quarantine %s config: %w", item.scope, err)
131 }
132 repairMutationAfterPrepare(item.path)
133 if err := renameRepairNodeNoReplace(item.path, quarantine); err != nil {
134 return report, fmt.Errorf("quarantine %s config: %w", item.scope, err)
135 }
136 repairMutationAfterRename(item.path)
137 if expected := opts.expectedStates[item.path]; expected != "" {
138 if err := verifyRepairPlanStateIDFor(quarantine, item.path, expected); err != nil {
139 if restoreErr := restoreRepairNodeIfAbsent(quarantine, item.path); restoreErr != nil {
140 return report, fmt.Errorf("quarantine %s config changed after confirmation and restore failed: %v: %w", item.scope, restoreErr, err)
141 }
142 return report, err
143 }
144 }
145 if durable, err := commitPreparedRepairTransaction(tx, changeIndex); err != nil {
146 if durable {
147 return report, fmt.Errorf("commit quarantine %s config undo state: cleanup pending journal: %w", item.scope, err)
148 }
149 restoreErr := restoreRepairNodeIfAbsent(quarantine, item.path)
150 if restoreErr != nil {
151 return report, fmt.Errorf("commit quarantine %s config undo state: %w; confirmed config retained at %s: %v", item.scope, err, quarantine, restoreErr)
152 }
153 return report, fmt.Errorf("commit quarantine %s config undo state: %w", item.scope, err)
154 }
155 if _, err := os.Lstat(item.path); err == nil {
156 appendRepairLogBestEffort(tx)
157 return report, fmt.Errorf("repair plan preview changed since confirmation; target was recreated during quarantine; confirmed state remains at %s", quarantine)
158 } else if !os.IsNotExist(err) {
159 return report, err
160 }
161 report.Applied = append(report.Applied, "quarantined "+item.scope+" config at "+quarantine)
162 if item.scope == "global" {
163 restoreErr := os.ErrNotExist
164 if opts.expectedStates != nil {
165 if opts.hasConfirmedRestore {
166 if restoreErr = config.ValidateBytes(opts.confirmedGlobalRestore); restoreErr == nil {
167 restoreErr = fileutil.AtomicCreateFile(item.path, opts.confirmedGlobalRestore, 0o600)
168 }
169 }
170 } else {
171 restoreErr = restoreLastKnownGoodConfig(item.path)
172 }
173 if restoreErr == nil {
174 report.Applied = append(report.Applied, "restored global config from last-known-good snapshot")
175 } else if opts.expectedStates != nil && opts.hasConfirmedRestore {
176 return report, fmt.Errorf("restore confirmed last-known-good config: %w", restoreErr)
177 }
178 }
179 report.Checks[len(report.Checks)-1] = inspectConfig(item.scope, item.path)
180 if item.scope == "global" {
181 report.Checks[len(report.Checks)-1].SnapshotPath = lastKnownGoodConfigPath()
182 }
183 }
184 if len(tx.Changes) > 0 {
185 appendRepairLogBestEffort(tx)
186 }
187 return report, nil
188 }
189
190 func configRepairTargetPaths(opts ConfigOptions) ([]string, error) {
191 if opts.OnlyScope != "" && opts.OnlyScope != "global" && opts.OnlyScope != "project" {
192 return nil, fmt.Errorf("unknown config repair scope %q", opts.OnlyScope)
193 }
194 globalPaths := func() []string {
195 paths := []string{config.UserConfigPath()}
196 if snapshot := lastKnownGoodConfigPath(); snapshot != "" {
197 paths = append(paths, snapshot)
198 }
199 return paths
200 }
201 project := filepath.Join(opts.Root, "reasonix.toml")
202 if opts.Root == "" || opts.Root == "." {
203 project = "reasonix.toml"
204 }
205 switch opts.OnlyScope {
206 case "global":
207 return globalPaths(), nil
208 case "project":
209 return []string{project}, nil
210 default:
211 paths := globalPaths()
212 if opts.IncludeProject {
213 paths = append(paths, project)
214 }
215 return paths, nil
216 }
217 }
218
219 func inspectConfig(scope, path string) ConfigCheck {
220 check := ConfigCheck{Scope: scope, Path: path, Valid: true}
221 if path == "" {
222 return check
223 }
224 if _, err := os.Lstat(path); err != nil {
225 if !os.IsNotExist(err) {
226 check.Valid = false
227 check.Error = err.Error()
228 }
229 return check
230 }
231 check.Exists = true
232 b, err := os.ReadFile(path)
233 if err == nil {
234 err = config.ValidateBytes(b)
235 }
236 if err != nil {
237 check.Valid = false
238 check.Error = err.Error()
239 }
240 return check
241 }
242
243 type snapshotMeta struct {
244 SchemaVersion int `json:"schemaVersion"`
245 SourcePath string `json:"sourcePath"`
246 RecordedAt string `json:"recordedAt"`
247 Version string `json:"version,omitempty"`
248 }
249
250 func RecordHealthyConfig(version string) error {
251 path := config.UserConfigPath()
252 if path == "" {
253 return nil
254 }
255 snapshot := lastKnownGoodConfigPath()
256 if snapshot == "" {
257 return nil
258 }
259 unlock, err := lockRepairMutations(path, snapshot, snapshot+".json", snapshotDir())
260 if err != nil {
261 return err
262 }
263 defer unlock()
264
265 b, err := os.ReadFile(path)
266 if err != nil {
267 if os.IsNotExist(err) {
268 return nil
269 }
270 return err
271 }
272 if err := config.ValidateBytes(b); err != nil {
273 return err
274 }
275 now := time.Now().UTC()
276 meta := snapshotMeta{SchemaVersion: 1, SourcePath: path, RecordedAt: now.Format(time.RFC3339Nano), Version: version}
277 encoded, err := json.MarshalIndent(meta, "", " ")
278 if err != nil {
279 return err
280 }
281 // Publish the immutable, versioned recovery point first. If a later fixed
282 // last-known-good write fails, readers retain the previous fixed snapshot
283 // while the newly recorded version remains independently recoverable.
284 if err := recordConfigSnapshot(path, b, version, now); err != nil {
285 return err
286 }
287 if err := fileutil.AtomicWriteFile(snapshot, b, 0o600); err != nil {
288 return err
289 }
290 // The metadata is informational; restore consumes and validates only the
291 // content file. Both writers are serialized by the same mutation locks, so
292 // a failed metadata replacement cannot expose unverified recovery bytes.
293 if err := fileutil.AtomicWriteFile(snapshot+".json", append(encoded, '\n'), 0o600); err != nil {
294 return err
295 }
296 return nil
297 }
298
299 func lastKnownGoodConfigPath() string {
300 root := config.MemoryUserDir()
301 if root == "" {
302 return ""
303 }
304 return filepath.Join(root, "repair", "config.toml.last-known-good")
305 }
306
307 func restoreLastKnownGoodConfig(dest string) error {
308 snapshot := lastKnownGoodConfigPath()
309 b, err := os.ReadFile(snapshot)
310 if err != nil {
311 return err
312 }
313 if err := config.ValidateBytes(b); err != nil {
314 return err
315 }
316 return fileutil.AtomicCreateFile(dest, b, 0o600)
317 }
318
318 lines GO