1use std::collections::{HashMap, HashSet};
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use crate::domain::{AbsentPolicy, CommitOwnership, MemberId, Ownership};
7use crate::error::{Error, Result};
8use crate::integrity::hash_file;
9use crate::lock::MutationLock;
10use crate::stage::PreparedTransaction;
11
12static NEXT_BACKUP_NONCE: AtomicU64 = AtomicU64::new(0);
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum TransactionDisposition {
20 Committed,
22 RolledBack,
24 RecoveryRequired,
26}
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
36pub enum CleanupDisposition {
37 Cleaned,
39 RetainedForRecovery,
41}
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq)]
47#[non_exhaustive]
48pub enum FailurePhase {
49 Lock,
51 Ownership,
53 StageRevalidation,
55 Backup,
57 Commit,
59 Rollback,
61 Finalize,
63}
64
65impl FailurePhase {
66 pub fn as_str(&self) -> &'static str {
68 match self {
69 Self::Lock => "lock",
70 Self::Ownership => "ownership",
71 Self::StageRevalidation => "stage-revalidation",
72 Self::Backup => "backup",
73 Self::Commit => "commit",
74 Self::Rollback => "rollback",
75 Self::Finalize => "finalize",
76 }
77 }
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82#[non_exhaustive]
83pub enum FailureCategory {
84 LockContention,
86 OwnershipConflict,
88 Verification,
90 Filesystem,
92 InvalidInput,
94 Injected,
96}
97
98impl FailureCategory {
99 pub fn as_str(&self) -> &'static str {
101 match self {
102 Self::LockContention => "lock-contention",
103 Self::OwnershipConflict => "ownership-conflict",
104 Self::Verification => "verification",
105 Self::Filesystem => "filesystem",
106 Self::InvalidInput => "invalid-input",
107 Self::Injected => "injected",
108 }
109 }
110}
111
112#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct FailureReport {
115 phase: FailurePhase,
116 member: Option<MemberId>,
117 category: FailureCategory,
118 detail: String,
119}
120
121impl FailureReport {
122 pub(crate) fn new(
123 phase: FailurePhase,
124 member: Option<MemberId>,
125 category: FailureCategory,
126 detail: impl Into<String>,
127 ) -> Self {
128 let mut detail = detail.into();
129 if detail.len() > 512 {
131 detail.truncate(512);
132 }
133 Self {
134 phase,
135 member,
136 category,
137 detail,
138 }
139 }
140
141 pub fn phase(&self) -> FailurePhase {
143 self.phase
144 }
145
146 pub fn member(&self) -> Option<&MemberId> {
148 self.member.as_ref()
149 }
150
151 pub fn category(&self) -> FailureCategory {
153 self.category
154 }
155
156 pub fn detail(&self) -> &str {
158 &self.detail
159 }
160}
161
162pub(crate) fn report_for_error(
163 phase: FailurePhase,
164 member: Option<MemberId>,
165 error: &Error,
166) -> FailureReport {
167 let (category, detail) = match error {
168 Error::UpdateInProgress { lock } => (
169 FailureCategory::LockContention,
170 format!("lock contention at {}", lock.display()),
171 ),
172 Error::DestinationConflict { destination } => (
173 FailureCategory::OwnershipConflict,
174 format!("destination conflict at {}", destination.display()),
175 ),
176 Error::VerificationFailed(m) => (FailureCategory::Verification, m.clone()),
177 Error::CandidateExecution(m) => (FailureCategory::Verification, m.clone()),
178 Error::InvalidInput(m) => {
179 let category = if m.contains("injected") {
180 FailureCategory::Injected
181 } else {
182 FailureCategory::InvalidInput
183 };
184 (category, m.clone())
185 }
186 Error::UnknownMember(m) => (FailureCategory::InvalidInput, format!("unknown member {m}")),
187 Error::Io { operation, source } => (
188 FailureCategory::Filesystem,
189 format!("{operation}: {source}"),
190 ),
191 };
192 FailureReport::new(phase, member, category, detail)
193}
194
195#[derive(Debug, Clone, PartialEq, Eq)]
197pub struct TransactionReceipt {
198 product: crate::domain::ProductId,
199 release: crate::domain::ReleaseId,
200 disposition: TransactionDisposition,
201 rollback_performed: bool,
202 rollback_verified: bool,
203 cleanup: CleanupDisposition,
204 recovery_path: Option<PathBuf>,
205 failure: Option<FailureReport>,
206 rollback_failure: Option<FailureReport>,
207}
208
209impl TransactionReceipt {
210 pub fn product(&self) -> &crate::domain::ProductId {
212 &self.product
213 }
214
215 pub fn release(&self) -> &crate::domain::ReleaseId {
217 &self.release
218 }
219
220 pub fn disposition(&self) -> TransactionDisposition {
222 self.disposition
223 }
224
225 pub fn rollback_performed(&self) -> bool {
227 self.rollback_performed
228 }
229
230 pub fn rollback_verified(&self) -> bool {
232 self.rollback_verified
233 }
234
235 pub fn cleanup(&self) -> CleanupDisposition {
237 self.cleanup
238 }
239
240 pub fn recovery_path(&self) -> Option<&Path> {
247 self.recovery_path.as_deref()
248 }
249
250 pub fn failure(&self) -> Option<&FailureReport> {
257 self.failure.as_ref()
258 }
259
260 pub fn rollback_failure(&self) -> Option<&FailureReport> {
265 self.rollback_failure.as_ref()
266 }
267}
268
269#[derive(Debug, Clone, PartialEq, Eq)]
270#[cfg_attr(not(test), allow(dead_code))]
271pub(crate) enum CommitFault {
272 LockCreation,
273 Backup(MemberId),
274 BeforeFirstCommit,
275 Commit(MemberId),
276 CommitThenRollback(MemberId, MemberId),
277 Finalize,
278}
279
280#[derive(Debug)]
281struct BackupEntry {
282 member: MemberId,
283 destination: PathBuf,
284 backup: Option<PathBuf>,
285}
286
287impl PreparedTransaction {
288 pub(crate) fn commit_inner(
289 self,
290 ownership: CommitOwnership<'_>,
291 verified_digests: &HashMap<MemberId, [u8; 32]>,
292 fault: Option<CommitFault>,
293 ) -> Result<TransactionReceipt> {
294 if fault == Some(CommitFault::LockCreation) {
295 return Err(Error::invalid("injected lock creation failure"));
296 }
297 let preflight = classify_all(&self, &ownership)?;
300 let mut lock = MutationLock::acquire(
301 self.plan.installation_root(),
302 self.plan.product(),
303 self.plan.release(),
304 )?;
305 if let Err(error) = revalidate_ownership_locked(&self, &ownership, &preflight) {
308 let report = report_for_error(FailurePhase::Ownership, None, &error);
309 let verified = true;
310 return finish_failure_no_mutation(self, lock, verified, report);
311 }
312 if let Err((member, error)) = revalidate_staged_under_lock(&self, verified_digests) {
315 let report = report_for_error(FailurePhase::StageRevalidation, member, &error);
316 return finish_failure_no_mutation(self, lock, true, report);
317 }
318 let backup_root = match create_backup_directory(self.plan.installation_root()) {
319 Ok(p) => p,
320 Err(error) => {
321 let report = report_for_error(FailurePhase::Backup, None, &error);
322 return finish_failure_no_mutation(self, lock, true, report);
323 }
324 };
325 let mut entries = Vec::with_capacity(self.plan.artifacts().len());
326 if let Err((member, error)) = self.backup_members(&mut entries, &backup_root, fault.clone())
327 {
328 let phase = FailurePhase::Backup;
329 let report = report_for_error(phase, member, &error);
330 let (verified, rollback_failure) =
331 restore_entries(&entries, &HashSet::new(), fault.clone());
332 return finish_failure(self, lock, backup_root, verified, rollback_failure, report);
333 }
334 if fault == Some(CommitFault::BeforeFirstCommit) {
335 let error = Error::invalid("injected pre-commit failure");
336 let report = report_for_error(FailurePhase::Commit, None, &error);
337 let (verified, rollback_failure) =
338 restore_entries(&entries, &HashSet::new(), fault.clone());
339 return finish_failure(self, lock, backup_root, verified, rollback_failure, report);
340 }
341
342 let mut committed = HashSet::new();
343 let members = self.plan.artifacts().iter().cloned().collect::<Vec<_>>();
344 for member in members {
345 let commit_should_fail = fault.as_ref().is_some_and(|candidate| match candidate {
346 CommitFault::Commit(id) | CommitFault::CommitThenRollback(id, _) => {
347 id == member.id()
348 }
349 _ => false,
350 });
351 if commit_should_fail {
352 let error = Error::invalid(format!("injected commit failure at {}", member.id()));
353 let report =
354 report_for_error(FailurePhase::Commit, Some(member.id().clone()), &error);
355 let (verified, rollback_failure) =
356 restore_entries(&entries, &committed, fault.clone());
357 return finish_failure(self, lock, backup_root, verified, rollback_failure, report);
358 }
359 let destination = match self.plan.destination(member.id()) {
360 Ok(destination) => destination,
361 Err(error) => {
362 let report =
363 report_for_error(FailurePhase::Commit, Some(member.id().clone()), &error);
364 let (verified, rollback_failure) =
365 restore_entries(&entries, &committed, fault.clone());
366 return finish_failure(
367 self,
368 lock,
369 backup_root,
370 verified,
371 rollback_failure,
372 report,
373 );
374 }
375 };
376 if let Err(error) = require_ready_parent(&destination, self.plan.installation_root()) {
377 let report =
378 report_for_error(FailurePhase::Commit, Some(member.id().clone()), &error);
379 let (verified, rollback_failure) =
380 restore_entries(&entries, &committed, fault.clone());
381 return finish_failure(self, lock, backup_root, verified, rollback_failure, report);
382 }
383 let staged = match self.staged_path(member.id()) {
384 Ok(staged) => staged,
385 Err(error) => {
386 let report =
387 report_for_error(FailurePhase::Commit, Some(member.id().clone()), &error);
388 let (verified, rollback_failure) =
389 restore_entries(&entries, &committed, fault.clone());
390 return finish_failure(
391 self,
392 lock,
393 backup_root,
394 verified,
395 rollback_failure,
396 report,
397 );
398 }
399 };
400 if let Err(source) = fs::rename(&staged, &destination) {
401 let error = Error::io("replacing destination from prepared stage", source);
402 let report =
403 report_for_error(FailurePhase::Commit, Some(member.id().clone()), &error);
404 let (verified, rollback_failure) =
405 restore_entries(&entries, &committed, fault.clone());
406 return finish_failure(self, lock, backup_root, verified, rollback_failure, report);
407 }
408 committed.insert(member.id().clone());
409 }
410
411 if fault == Some(CommitFault::Finalize) {
412 lock.preserve();
413 let report = FailureReport::new(
414 FailurePhase::Finalize,
415 None,
416 FailureCategory::Injected,
417 "injected finalize failure",
418 );
419 return Ok(TransactionReceipt {
420 product: self.plan.product().clone(),
421 release: self.plan.release().clone(),
422 disposition: TransactionDisposition::Committed,
423 rollback_performed: false,
424 rollback_verified: false,
425 cleanup: CleanupDisposition::RetainedForRecovery,
426 recovery_path: Some(backup_root),
427 failure: Some(report),
428 rollback_failure: None,
429 });
430 }
431 if let Err(source) = fs::remove_dir_all(&backup_root) {
432 lock.preserve();
433 let error = Error::io("removing backup set after commit", source);
434 let report = report_for_error(FailurePhase::Finalize, None, &error);
435 return Ok(TransactionReceipt {
436 product: self.plan.product().clone(),
437 release: self.plan.release().clone(),
438 disposition: TransactionDisposition::Committed,
439 rollback_performed: false,
440 rollback_verified: false,
441 cleanup: CleanupDisposition::RetainedForRecovery,
442 recovery_path: Some(backup_root),
443 failure: Some(report),
444 rollback_failure: None,
445 });
446 }
447 Ok(TransactionReceipt {
448 product: self.plan.product().clone(),
449 release: self.plan.release().clone(),
450 disposition: TransactionDisposition::Committed,
451 rollback_performed: false,
452 rollback_verified: false,
453 cleanup: CleanupDisposition::Cleaned,
454 recovery_path: None,
455 failure: None,
456 rollback_failure: None,
457 })
458 }
459
460 fn backup_members(
461 &self,
462 entries: &mut Vec<BackupEntry>,
463 backup_root: &Path,
464 fault: Option<CommitFault>,
465 ) -> std::result::Result<(), (Option<MemberId>, Error)> {
466 let members = self.plan.artifacts().iter().cloned().collect::<Vec<_>>();
467 for member in members {
468 if fault == Some(CommitFault::Backup(member.id().clone())) {
469 return Err((
470 Some(member.id().clone()),
471 Error::invalid(format!("injected backup failure at {}", member.id())),
472 ));
473 }
474 let destination = self
475 .plan
476 .destination(member.id())
477 .map_err(|e| (Some(member.id().clone()), e))?;
478 revalidate_destination(&destination, self.plan.installation_root())
479 .map_err(|e| (Some(member.id().clone()), e))?;
480 let backup = match fs::symlink_metadata(&destination) {
481 Ok(_) => {
482 let path = backup_root.join(member.destination());
483 if let Some(parent) = path.parent() {
484 fs::create_dir_all(parent).map_err(|source| {
485 (
486 Some(member.id().clone()),
487 Error::io("creating backup directory", source),
488 )
489 })?;
490 }
491 fs::rename(&destination, &path).map_err(|source| {
492 (
493 Some(member.id().clone()),
494 Error::io("backing up existing destination", source),
495 )
496 })?;
497 Some(path)
498 }
499 Err(source) if source.kind() == std::io::ErrorKind::NotFound => None,
500 Err(source) => {
501 return Err((
502 Some(member.id().clone()),
503 Error::io("reading destination before backup", source),
504 ));
505 }
506 };
507 entries.push(BackupEntry {
508 member: member.id().clone(),
509 destination,
510 backup,
511 });
512 }
513 Ok(())
514 }
515}
516
517fn classify_all(
518 txn: &PreparedTransaction,
519 ownership: &CommitOwnership<'_>,
520) -> Result<HashMap<MemberId, Ownership>> {
521 let mut out = HashMap::new();
522 for member in txn.artifacts().iter() {
523 let destination = txn.destination(member.id())?;
524 out.insert(
525 member.id().clone(),
526 ownership.verifier.verify(member.id(), &destination),
527 );
528 }
529 Ok(out)
530}
531
532fn revalidate_ownership_locked(
533 txn: &PreparedTransaction,
534 ownership: &CommitOwnership<'_>,
535 preflight: &HashMap<MemberId, Ownership>,
536) -> Result<()> {
537 for member in txn.artifacts().iter() {
538 let destination = txn.destination(member.id())?;
539 let locked = ownership.verifier.verify(member.id(), &destination);
540 let first = preflight
541 .get(member.id())
542 .copied()
543 .unwrap_or(Ownership::Unknown);
544 if locked != first {
547 return Err(Error::DestinationConflict { destination });
548 }
549 match locked {
550 Ownership::Owned => {}
551 Ownership::Absent if ownership.absent == AbsentPolicy::AllowCreate => {}
552 Ownership::Absent | Ownership::Foreign | Ownership::Unknown => {
553 return Err(Error::DestinationConflict { destination });
554 }
555 }
556 require_ready_parent(&destination, txn.plan.installation_root())?;
559 revalidate_destination(&destination, txn.plan.installation_root())?;
560 }
561 Ok(())
562}
563
564fn revalidate_staged_under_lock(
565 txn: &PreparedTransaction,
566 verified_digests: &HashMap<MemberId, [u8; 32]>,
567) -> std::result::Result<(), (Option<MemberId>, Error)> {
568 for member in txn.artifacts().iter() {
569 let expected = verified_digests.get(member.id()).ok_or_else(|| {
570 (
571 Some(member.id().clone()),
572 Error::VerificationFailed(format!(
573 "member {} has no verified digest; commit requires verified integrity",
574 member.id()
575 )),
576 )
577 })?;
578 let staged = txn
579 .staged_path(member.id())
580 .map_err(|e| (Some(member.id().clone()), e))?;
581 let metadata = fs::symlink_metadata(&staged).map_err(|source| {
582 (
583 Some(member.id().clone()),
584 Error::io("reading staged candidate", source),
585 )
586 })?;
587 if !metadata.is_file() {
588 return Err((
589 Some(member.id().clone()),
590 Error::VerificationFailed(format!(
591 "staged member {} is not a regular file",
592 member.id()
593 )),
594 ));
595 }
596 #[cfg(unix)]
597 {
598 use std::os::unix::fs::MetadataExt;
599 if metadata.nlink() != 1 {
600 return Err((
601 Some(member.id().clone()),
602 Error::VerificationFailed(format!(
603 "staged member {} is hard-linked",
604 member.id()
605 )),
606 ));
607 }
608 }
609 let actual = hash_file(&staged).map_err(|e| (Some(member.id().clone()), e))?;
610 if &actual != expected {
611 return Err((
612 Some(member.id().clone()),
613 Error::VerificationFailed(format!(
614 "staged member {} changed after verification",
615 member.id()
616 )),
617 ));
618 }
619 }
620 Ok(())
621}
622
623fn create_backup_directory(root: &Path) -> Result<PathBuf> {
624 use std::time::{SystemTime, UNIX_EPOCH};
625 for _ in 0..32 {
626 let nonce = NEXT_BACKUP_NONCE.fetch_add(1, Ordering::Relaxed);
627 let nanos = SystemTime::now()
628 .duration_since(UNIX_EPOCH)
629 .map(|d| d.as_nanos())
630 .unwrap_or(0);
631 let path = root.join(format!(
632 ".eggup-backup-{}-{nonce}-{nanos}",
633 std::process::id()
634 ));
635 match fs::create_dir(&path) {
636 Ok(()) => {
637 #[cfg(unix)]
638 {
639 use std::os::unix::fs::PermissionsExt;
640 let _ = fs::set_permissions(&path, fs::Permissions::from_mode(0o700));
641 }
642 return Ok(path);
643 }
644 Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
645 Err(source) => return Err(Error::io("creating backup set", source)),
646 }
647 }
648 Err(Error::invalid("could not create a unique backup directory"))
649}
650
651fn revalidate_destination(destination: &Path, root: &Path) -> Result<()> {
652 let root = fs::canonicalize(root)
653 .map_err(|source| Error::io("canonicalizing install root", source))?;
654 let mut ancestor = destination;
655 while !ancestor.exists() {
656 ancestor = ancestor
657 .parent()
658 .ok_or_else(|| Error::invalid("destination has no existing ancestor"))?;
659 }
660 let canonical_ancestor = fs::canonicalize(ancestor)
661 .map_err(|source| Error::io("canonicalizing destination ancestor", source))?;
662 if !canonical_ancestor.starts_with(&root) {
663 return Err(Error::DestinationConflict {
664 destination: destination.to_path_buf(),
665 });
666 }
667 match fs::symlink_metadata(destination) {
668 Ok(metadata) => {
669 if !metadata.is_file() {
670 return Err(Error::DestinationConflict {
671 destination: destination.to_path_buf(),
672 });
673 }
674 #[cfg(unix)]
675 {
676 use std::os::unix::fs::MetadataExt;
677 if metadata.nlink() != 1 {
678 return Err(Error::DestinationConflict {
679 destination: destination.to_path_buf(),
680 });
681 }
682 }
683 }
684 Err(source) if source.kind() == std::io::ErrorKind::NotFound => {}
685 Err(source) => return Err(Error::io("reading destination during revalidation", source)),
686 }
687 Ok(())
688}
689
690fn require_ready_parent(destination: &Path, root: &Path) -> Result<()> {
697 let parent = destination
698 .parent()
699 .ok_or_else(|| Error::invalid("destination has no parent"))?;
700 let parent_meta = fs::symlink_metadata(parent).map_err(|source| {
701 if source.kind() == std::io::ErrorKind::NotFound {
702 Error::invalid(format!(
703 "destination parent does not exist: {}",
704 parent.display()
705 ))
706 } else {
707 Error::io("reading destination parent", source)
708 }
709 })?;
710 if parent_meta.file_type().is_symlink() {
711 return Err(Error::DestinationConflict {
712 destination: destination.to_path_buf(),
713 });
714 }
715 if !parent_meta.is_dir() {
716 return Err(Error::DestinationConflict {
717 destination: destination.to_path_buf(),
718 });
719 }
720 let canonical_root = fs::canonicalize(root)
721 .map_err(|source| Error::io("canonicalizing install root", source))?;
722 let canonical_parent = fs::canonicalize(parent)
723 .map_err(|source| Error::io("canonicalizing destination parent", source))?;
724 if !canonical_parent.starts_with(&canonical_root) {
725 return Err(Error::DestinationConflict {
726 destination: destination.to_path_buf(),
727 });
728 }
729 let mut cursor = Some(parent);
732 while let Some(p) = cursor {
733 if p == root {
734 break;
735 }
736 let meta = fs::symlink_metadata(p)
737 .map_err(|source| Error::io("reading destination ancestor", source))?;
738 if meta.file_type().is_symlink() {
739 return Err(Error::DestinationConflict {
740 destination: destination.to_path_buf(),
741 });
742 }
743 cursor = p.parent();
744 if let Some(next) = cursor {
745 if next.as_os_str().is_empty() {
746 break;
747 }
748 } else {
749 break;
750 }
751 }
752 Ok(())
753}
754
755fn restore_entries(
756 entries: &[BackupEntry],
757 committed: &HashSet<MemberId>,
758 fault: Option<CommitFault>,
759) -> (bool, Option<FailureReport>) {
760 let mut verified = true;
761 let mut rollback_failure: Option<FailureReport> = None;
762 for entry in entries.iter().rev() {
763 let rollback_target = match fault.as_ref() {
764 Some(CommitFault::CommitThenRollback(_, id)) => Some(id),
765 _ => None,
766 };
767 if rollback_target == Some(&entry.member) {
768 verified = false;
769 rollback_failure = Some(FailureReport::new(
770 FailurePhase::Rollback,
771 Some(entry.member.clone()),
772 FailureCategory::Injected,
773 format!("injected rollback failure at {}", entry.member),
774 ));
775 continue;
776 }
777 if entry.backup.is_some() || committed.contains(&entry.member) {
778 if let Ok(metadata) = fs::symlink_metadata(&entry.destination) {
779 if !metadata.is_file() || fs::remove_file(&entry.destination).is_err() {
780 verified = false;
781 if rollback_failure.is_none() {
782 rollback_failure = Some(FailureReport::new(
783 FailurePhase::Rollback,
784 Some(entry.member.clone()),
785 FailureCategory::Filesystem,
786 format!("could not remove partial member {}", entry.member),
787 ));
788 }
789 continue;
790 }
791 }
792 }
793 if let Some(backup) = &entry.backup {
794 if fs::rename(backup, &entry.destination).is_err() {
795 verified = false;
796 if rollback_failure.is_none() {
797 rollback_failure = Some(FailureReport::new(
798 FailurePhase::Rollback,
799 Some(entry.member.clone()),
800 FailureCategory::Filesystem,
801 format!("could not restore backup for {}", entry.member),
802 ));
803 }
804 }
805 }
806 }
807 for entry in entries {
808 let restored = match &entry.backup {
809 Some(_) => fs::symlink_metadata(&entry.destination).is_ok(),
810 None => {
811 !committed.contains(&entry.member)
812 || fs::symlink_metadata(&entry.destination).is_err()
813 }
814 };
815 if !restored {
816 verified = false;
817 }
818 }
819 (verified, rollback_failure)
820}
821
822fn finish_failure_no_mutation(
823 transaction: PreparedTransaction,
824 lock: MutationLock,
825 nothing_mutated: bool,
826 report: FailureReport,
827) -> Result<TransactionReceipt> {
828 debug_assert!(nothing_mutated);
829 drop(lock);
830 Ok(TransactionReceipt {
831 product: transaction.plan.product().clone(),
832 release: transaction.plan.release().clone(),
833 disposition: TransactionDisposition::RolledBack,
834 rollback_performed: false,
835 rollback_verified: true,
836 cleanup: CleanupDisposition::Cleaned,
837 recovery_path: None,
838 failure: Some(report),
839 rollback_failure: None,
840 })
841}
842
843fn finish_failure(
844 transaction: PreparedTransaction,
845 mut lock: MutationLock,
846 backup_root: PathBuf,
847 rollback_verified: bool,
848 rollback_failure: Option<FailureReport>,
849 report: FailureReport,
850) -> Result<TransactionReceipt> {
851 if rollback_verified && rollback_failure.is_none() && fs::remove_dir_all(&backup_root).is_ok() {
852 return Ok(TransactionReceipt {
853 product: transaction.plan.product().clone(),
854 release: transaction.plan.release().clone(),
855 disposition: TransactionDisposition::RolledBack,
856 rollback_performed: true,
857 rollback_verified: true,
858 cleanup: CleanupDisposition::Cleaned,
859 recovery_path: None,
860 failure: Some(report),
861 rollback_failure: None,
862 });
863 }
864 lock.preserve();
865 Ok(TransactionReceipt {
866 product: transaction.plan.product().clone(),
867 release: transaction.plan.release().clone(),
868 disposition: TransactionDisposition::RecoveryRequired,
869 rollback_performed: true,
870 rollback_verified: false,
871 cleanup: CleanupDisposition::RetainedForRecovery,
872 recovery_path: Some(backup_root),
873 failure: Some(report),
874 rollback_failure,
875 })
876}