package update import ( "context" "encoding/json" "fmt" "os" "path/filepath" ) const rollbackPendingFile = ".update-rollback-pending.json" type rollbackPendingState struct { State State `json:"state"` BackupDir string `json:"backup_dir"` Kept []string `json:"kept"` Deleted []string `json:"deleted"` RestoreBinary bool `json:"restore_binary"` LiveSHA256 string `json:"live_sha256"` PreviousSHA256 string `json:"previous_sha256"` } func readRollbackPending(root string) (*rollbackPendingState, error) { data, err := os.ReadFile(filepath.Join(root, rollbackPendingFile)) if os.IsNotExist(err) { return nil, nil } if err != nil { return nil, err } var pending rollbackPendingState if err := decodeRecoveryJSON(data, &pending); err != nil { return nil, err } var fields map[string]json.RawMessage _ = json.Unmarshal(data, &fields) if _, ok := fields["restore_binary"]; !ok { return nil, fmt.Errorf("回滚恢复记录缺少二进制恢复决策") } if err := validateRollbackEvidence(root, pending); err != nil { return nil, err } return &pending, nil } func writeRollbackPending(root string, pending rollbackPendingState) error { data, err := json.Marshal(pending) if err != nil { return err } path := filepath.Join(root, rollbackPendingFile) if err := os.WriteFile(path+".tmp", data, 0600); err != nil { return err } return os.Rename(path+".tmp", path) } // finishRollback can run again after a reset or binary rename was interrupted. // Hashes distinguish the old live binary from the already-restored one. func finishRollback(ctx context.Context, opts Options, pending rollbackPendingState) (*Result, error) { root, err := opts.installRoot() if err != nil { return nil, err } bin := filepath.Join(root, opts.binaryName()) if !pending.RestoreBinary && pending.LiveSHA256 == "" { if _, err := os.Lstat(bin); !os.IsNotExist(err) { return nil, &Error{Reason: "binary_changed", Message: "恢复记录要求二进制不存在,但现场不匹配;已保留记录"} } } if pending.RestoreBinary { current, currentErr := fileSHA256(bin) if currentErr != nil || current != pending.PreviousSHA256 { if currentErr == nil && current != pending.LiveSHA256 { return nil, &Error{Reason: "binary_changed", Message: "回滚中断后现有二进制发生变化,已保留现场"} } previous, previousErr := fileSHA256(bin + ".prev") if previousErr != nil || previous != pending.PreviousSHA256 { return nil, &Error{Reason: "binary_changed", Message: "回滚备份与恢复记录不一致,已保留现场"} } if err := restorePrevBinary(bin); err != nil { return nil, &Error{Reason: "swap_failed", Message: err.Error()} } } } else if pending.LiveSHA256 != "" { current, err := fileSHA256(bin) if err != nil || current != pending.LiveSHA256 { return nil, &Error{Reason: "binary_changed", Message: "回滚中断后现有二进制发生变化,已保留现场"} } } content := buildPendingState{BackupDir: pending.BackupDir, KeptContent: pending.Kept, DeletedContent: pending.Deleted} if err := restoreInterruptedProtected(ctx, root, content, true); err != nil { return nil, err } if err := clearBuildPending(root); err != nil { return nil, err } if err := restoreVersionAfterRollback(root, pending.State); err != nil { return nil, &Error{Reason: "version_restore_failed", Message: "源码和二进制已回滚,但版本恢复失败;修复配置写入后重试回滚:" + err.Error()} } if err := os.Remove(statePath(root)); err != nil && !os.IsNotExist(err) { return nil, err } if err := os.Remove(filepath.Join(root, rollbackPendingFile)); err != nil { return nil, err } return &Result{FromCommit: shortHash(pending.State.UpdatedCommit), ToCommit: shortHash(pending.State.PreviousCommit), KeptContent: pending.Kept, BinaryPath: bin, BinaryBuilt: pending.RestoreBinary, BackupDir: pending.BackupDir, NeedsRestart: true}, nil }