返回 DeepSeek-Reasonix
apply_failure.go
根目录 / internal / repair / apply_failure.go
1 package repair
2
3 import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "path/filepath"
8 "strings"
9 "time"
10
11 "reasonix/internal/config"
12 "reasonix/internal/fileutil"
13 )
14
15 // UpdateApplyFailure records that an update installer failed after the desktop
16 // handed off and exited. The Windows update helper cannot roll back itself —
17 // it runs from the cache directory, outside the validated Guard installation —
18 // so it records this marker and relaunches Guard, which performs the rollback
19 // from inside the install directory on its next start.
20 type UpdateApplyFailure struct {
21 SchemaVersion int `json:"schemaVersion"`
22 ToVersion string `json:"toVersion,omitempty"`
23 UpdateCreatedAt string `json:"updateCreatedAt,omitempty"`
24 UpdateTransactionID string `json:"updateTransactionId,omitempty"`
25 Reason string `json:"reason,omitempty"`
26 RecordedAt string `json:"recordedAt"`
27 }
28
29 func updateApplyFailurePath() string {
30 root := config.MemoryUserDir()
31 if root == "" {
32 return ""
33 }
34 return filepath.Join(root, "repair", "update-apply-failed.json")
35 }
36
37 // MarkUpdateApplyFailed persists the installer-failure marker. It is written
38 // by the update helper after the NSIS installer exits non-zero.
39 func MarkUpdateApplyFailed(toVersion, reason string) error {
40 tx, err := ReadPendingUpdate()
41 if err == nil {
42 if strings.TrimSpace(tx.ToVersion) != strings.TrimSpace(toVersion) {
43 return fmt.Errorf("update apply failure: pending transaction does not match")
44 }
45 return markUpdateApplyFailedInvocation(tx, reason)
46 }
47 if !os.IsNotExist(err) {
48 return fmt.Errorf("update apply failure: read pending transaction: %w", err)
49 }
50 // Keep accepting a diagnostic marker when no matching transaction exists.
51 // Recovery treats markers without a complete transaction ID as stale and
52 // never lets them authorize rollback.
53 unlock, lockErr := acquirePendingUpdateLock()
54 if lockErr != nil {
55 return fmt.Errorf("update apply failure: lock pending transaction: %w", lockErr)
56 }
57 defer unlock()
58 if _, currentErr := ReadPendingUpdate(); currentErr == nil {
59 return fmt.Errorf("update apply failure: pending transaction appeared while waiting")
60 } else if !os.IsNotExist(currentErr) {
61 return fmt.Errorf("update apply failure: read pending transaction: %w", currentErr)
62 }
63 return markUpdateApplyFailed(toVersion, "", "", reason)
64 }
65
66 // MarkUpdateApplyFailedMatching binds the marker to the exact transaction held
67 // by an updater claim. The additive creation identity keeps a same-version
68 // marker from authorizing rollback of a later retry.
69 func MarkUpdateApplyFailedMatching(toVersion, updateCreatedAt, reason string) error {
70 updateCreatedAt = strings.TrimSpace(updateCreatedAt)
71 if updateCreatedAt == "" {
72 return fmt.Errorf("update apply failure: transaction identity is incomplete")
73 }
74 tx, err := ReadPendingUpdate()
75 if err != nil {
76 return fmt.Errorf("update apply failure: read pending transaction: %w", err)
77 }
78 if strings.TrimSpace(tx.ToVersion) != strings.TrimSpace(toVersion) ||
79 strings.TrimSpace(tx.CreatedAt) != updateCreatedAt {
80 return fmt.Errorf("update apply failure: pending transaction does not match")
81 }
82 return markUpdateApplyFailedInvocation(tx, reason)
83 }
84
85 func markUpdateApplyFailedInvocation(invocation *UpdateTransaction, reason string) error {
86 if invocation == nil {
87 return fmt.Errorf("update apply failure: transaction identity is incomplete")
88 }
89 invocationID := UpdateTransactionID(invocation)
90 if invocationID == "" {
91 return fmt.Errorf("update apply failure: transaction identity is incomplete")
92 }
93 unlock, err := acquirePendingUpdateLock()
94 if err != nil {
95 return fmt.Errorf("update apply failure: lock pending transaction: %w", err)
96 }
97 defer unlock()
98 current, err := ReadPendingUpdate()
99 if err != nil {
100 return fmt.Errorf("update apply failure: read pending transaction: %w", err)
101 }
102 if UpdateTransactionID(current) != invocationID {
103 return fmt.Errorf("update apply failure: pending transaction changed while waiting")
104 }
105 return markUpdateApplyFailed(
106 current.ToVersion,
107 current.CreatedAt,
108 invocationID,
109 reason,
110 )
111 }
112
113 // MarkUpdateApplyFailedExact records a failure for the complete transaction
114 // held by an updater claim. The caller must keep that claim's pending lock
115 // until this write returns; taking it again here would deadlock the updater.
116 func MarkUpdateApplyFailedExact(tx *UpdateTransaction, reason string) error {
117 if tx == nil || strings.TrimSpace(tx.CreatedAt) == "" {
118 return fmt.Errorf("update apply failure: transaction identity is incomplete")
119 }
120 transactionID := UpdateTransactionID(tx)
121 if transactionID == "" {
122 return fmt.Errorf("update apply failure: transaction identity is incomplete")
123 }
124 return markUpdateApplyFailed(tx.ToVersion, tx.CreatedAt, transactionID, reason)
125 }
126
127 func markUpdateApplyFailed(toVersion, updateCreatedAt, updateTransactionID, reason string) error {
128 path := updateApplyFailurePath()
129 if path == "" {
130 return fmt.Errorf("update apply failure: Reasonix state directory is unavailable")
131 }
132 failure := UpdateApplyFailure{
133 SchemaVersion: 1,
134 ToVersion: toVersion,
135 UpdateCreatedAt: updateCreatedAt,
136 UpdateTransactionID: updateTransactionID,
137 Reason: reason,
138 RecordedAt: time.Now().UTC().Format(time.RFC3339Nano),
139 }
140 b, err := json.MarshalIndent(failure, "", " ")
141 if err != nil {
142 return err
143 }
144 return fileutil.AtomicWriteFile(path, append(b, '\n'), 0o600)
145 }
146
147 // ReadUpdateApplyFailure reports the recorded installer failure, if any.
148 func ReadUpdateApplyFailure() (*UpdateApplyFailure, bool) {
149 path := updateApplyFailurePath()
150 if path == "" {
151 return nil, false
152 }
153 b, err := os.ReadFile(path)
154 if err != nil {
155 return nil, false
156 }
157 var failure UpdateApplyFailure
158 if json.Unmarshal(b, &failure) != nil || failure.SchemaVersion != 1 {
159 return nil, false
160 }
161 return &failure, true
162 }
163
164 // ClearUpdateApplyFailure removes the marker; a missing marker is not an error.
165 func ClearUpdateApplyFailure() error {
166 failure, ok := ReadUpdateApplyFailure()
167 if !ok {
168 return nil
169 }
170 return clearUpdateApplyFailureExact(failure)
171 }
172
173 // ClearUpdateApplyFailureExact removes only the marker created for tx. Platform
174 // updaters call this after the installed release-unit state is durable; a marker
175 // concurrently replaced by another transaction is retained.
176 func ClearUpdateApplyFailureExact(tx *UpdateTransaction) error {
177 if tx == nil {
178 return fmt.Errorf("clear update apply failure: transaction identity is incomplete")
179 }
180 expectedID := UpdateTransactionID(tx)
181 if expectedID == "" {
182 return fmt.Errorf("clear update apply failure: transaction identity is incomplete")
183 }
184 failure, ok := ReadUpdateApplyFailure()
185 if !ok {
186 return nil
187 }
188 if strings.TrimSpace(failure.UpdateTransactionID) != expectedID {
189 return fmt.Errorf("clear update apply failure: marker does not match transaction")
190 }
191 return clearUpdateApplyFailureExact(failure)
192 }
193
194 func clearUpdateApplyFailureExact(expected *UpdateApplyFailure) error {
195 if expected == nil {
196 return fmt.Errorf("clear update apply failure: marker identity is incomplete")
197 }
198 path := updateApplyFailurePath()
199 if path == "" {
200 return nil
201 }
202 cleanup, err := moveRepairNodeToUniqueCleanup(path)
203 if err != nil || cleanup == "" {
204 return err
205 }
206 updateCleanupAfterRename(path, cleanup)
207 restore := func(cause error) error {
208 if restoreErr := renameRepairNodeNoReplace(cleanup, path); restoreErr != nil {
209 return fmt.Errorf("%w; update failure marker retained at %s: %v", cause, cleanup, restoreErr)
210 }
211 return cause
212 }
213 b, err := os.ReadFile(cleanup)
214 if err != nil {
215 return restore(err)
216 }
217 var actual UpdateApplyFailure
218 if err := json.Unmarshal(b, &actual); err != nil {
219 return restore(err)
220 }
221 if repairPlanStateID(&actual) != repairPlanStateID(expected) {
222 return restore(fmt.Errorf("clear update apply failure: marker changed"))
223 }
224 if err := os.Remove(cleanup); err != nil {
225 return restore(err)
226 }
227 return nil
228 }
229
230 // RecoverFailedInstall rolls back the pending update when an update helper
231 // recorded an installer failure, restoring the previous release unit without
232 // waiting for a crash loop. The marker is cleared once the rollback succeeded
233 // (or when nothing was left to roll back); on rollback errors both the marker
234 // and the pending transaction are kept so the next launch retries.
235 func RecoverFailedInstall() (UpdateRollbackResult, *UpdateApplyFailure, error) {
236 invocationFailure, ok := ReadUpdateApplyFailure()
237 if !ok {
238 return UpdateRollbackResult{}, nil, nil
239 }
240 invocationFailureID := repairPlanStateID(invocationFailure)
241 invocationTx, invocationTxErr := ReadPendingUpdate()
242 if invocationTxErr != nil && !os.IsNotExist(invocationTxErr) {
243 return UpdateRollbackResult{}, invocationFailure, invocationTxErr
244 }
245 unlock, lockErr := acquirePendingUpdateLock()
246 if lockErr != nil {
247 return UpdateRollbackResult{}, invocationFailure, fmt.Errorf("recover failed install: lock pending transaction: %w", lockErr)
248 }
249 defer unlock()
250 failure, ok := ReadUpdateApplyFailure()
251 if !ok {
252 return UpdateRollbackResult{}, nil, nil
253 }
254 if repairPlanStateID(failure) != invocationFailureID {
255 return UpdateRollbackResult{}, failure, fmt.Errorf("recover failed install: failure marker changed while waiting")
256 }
257 tx, txErr := ReadPendingUpdate()
258 if txErr != nil {
259 if !os.IsNotExist(txErr) {
260 return UpdateRollbackResult{}, failure, txErr
261 }
262 if clearErr := clearUpdateApplyFailureExact(failure); clearErr != nil {
263 return UpdateRollbackResult{}, failure, clearErr
264 }
265 return UpdateRollbackResult{}, failure, nil
266 }
267 if invocationTxErr == nil && UpdateTransactionID(tx) != UpdateTransactionID(invocationTx) {
268 return UpdateRollbackResult{}, failure, fmt.Errorf("recover failed install: pending transaction changed while waiting")
269 }
270 if os.IsNotExist(invocationTxErr) {
271 if clearErr := clearUpdateApplyFailureExact(failure); clearErr != nil {
272 return UpdateRollbackResult{}, failure, clearErr
273 }
274 return UpdateRollbackResult{}, failure, nil
275 }
276 if !applyFailureMatchesUpdate(failure, tx) {
277 // A marker can survive when the helper cannot relaunch Guard. Never let
278 // that stale marker roll back a later, unrelated update transaction.
279 if clearErr := clearUpdateApplyFailureExact(failure); clearErr != nil {
280 return UpdateRollbackResult{}, failure, clearErr
281 }
282 return UpdateRollbackResult{}, failure, nil
283 }
284 // Keep the exact identity check in the rollback transition as a second
285 // fail-closed guard even though correlation and recovery share this lock.
286 stateID, states := pendingUpdateBoundPreview(tx)
287 result, err := rollbackPendingUpdateMatchingLocked(
288 tx.ToVersion,
289 tx.CreatedAt,
290 stateID,
291 states,
292 UpdateTransactionID(tx),
293 false,
294 )
295 if err != nil {
296 return result, failure, err
297 }
298 if clearErr := clearUpdateApplyFailureExact(failure); clearErr != nil {
299 return result, failure, clearErr
300 }
301 return result, failure, nil
302 }
303
304 func applyFailureMatchesUpdate(failure *UpdateApplyFailure, tx *UpdateTransaction) bool {
305 if failure == nil || tx == nil {
306 return false
307 }
308 toVersion := strings.TrimSpace(failure.ToVersion)
309 if toVersion == "" || toVersion != strings.TrimSpace(tx.ToVersion) {
310 return false
311 }
312 transactionID := strings.TrimSpace(failure.UpdateTransactionID)
313 return transactionID != "" && transactionID == UpdateTransactionID(tx)
314 }
315
315 lines GO