/// <summary> /// Recovers an interrupted multi-commit. The commit will either be completed or rolled back depending on /// where in the commit process it was interrupted. Does nothing if there is no commit to recover. /// </summary> /// <param name="multiCommitInterface">The core interface used for multi-commits.</param> /// <param name="saveService">The save data service.</param> /// <returns>The <see cref="Result"/> of the operation.<br/> /// <see cref="Result.Success"/>: The recovery was successful or there was no multi-commit to recover.</returns> public static Result Recover(ISaveDataMultiCommitCoreInterface multiCommitInterface, SaveDataFileSystemServiceImpl saveService) { lock (Locker) { bool needsRecover = true; ReferenceCountedDisposable <IFileSystem> fileSystem = null; try { // Check if a multi-commit was interrupted by checking if there's a commit context file. Result rc = multiCommitInterface.OpenMultiCommitContext(out fileSystem); if (rc.IsFailure()) { if (!ResultFs.PathNotFound.Includes(rc) && !ResultFs.TargetNotFound.Includes(rc)) { return(rc); } // Unable to open the multi-commit context file system, so there's nothing to recover needsRecover = false; } if (needsRecover) { rc = fileSystem.Target.OpenFile(out IFile file, CommitContextFileName, OpenMode.Read); file?.Dispose(); if (rc.IsFailure()) { // Unable to open the context file. No multi-commit to recover. if (ResultFs.PathNotFound.Includes(rc)) { needsRecover = false; } } } if (!needsRecover) { return(Result.Success); } // There was a context file. Recover the unfinished commit. return(Recover(multiCommitInterface, fileSystem.Target, saveService)); } finally { fileSystem?.Dispose(); } } }
/// <summary> /// Tries to recover a multi-commit using the context in the provided file system. /// </summary> /// <param name="multiCommitInterface">The core interface used for multi-commits.</param> /// <param name="contextFs">The file system containing the multi-commit context file.</param> /// <param name="saveService">The save data service.</param> /// <returns></returns> private static Result Recover(ISaveDataMultiCommitCoreInterface multiCommitInterface, IFileSystem contextFs, SaveDataFileSystemServiceImpl saveService) { if (multiCommitInterface is null) { return(ResultFs.InvalidArgument.Log()); } if (contextFs is null) { return(ResultFs.InvalidArgument.Log()); } // Keep track of the first error that occurs during the recovery Result recoveryResult = Result.Success; Result rc = RecoverCommit(multiCommitInterface, contextFs, saveService); if (rc.IsFailure()) { // Note: Yes, the next ~50 lines are exactly the same as the code in RecoverCommit except // for a single bool value. No, Nintendo doesn't split it out into its own function. int saveCount = 0; Span <SaveDataInfo> savesToRecover = stackalloc SaveDataInfo[MaxFileSystemCount]; SaveDataIndexerAccessor accessor = null; ReferenceCountedDisposable <SaveDataInfoReaderImpl> infoReader = null; try { rc = saveService.OpenSaveDataIndexerAccessor(out accessor, out _, SaveDataSpaceId.User); if (rc.IsFailure()) { return(rc); } rc = accessor.Indexer.OpenSaveDataInfoReader(out infoReader); if (rc.IsFailure()) { return(rc); } // Iterate through all the saves to find any provisionally committed save data while (true) { Unsafe.SkipInit(out SaveDataInfo info); rc = infoReader.Target.Read(out long readCount, OutBuffer.FromStruct(ref info)); if (rc.IsFailure()) { return(rc); } // Break once we're done iterating all save data if (readCount == 0) { break; } rc = multiCommitInterface.IsProvisionallyCommittedSaveData(out bool isProvisionallyCommitted, in info); // Note: Some saves could be missed if there are more than MaxFileSystemCount // provisionally committed saves. Not sure why Nintendo doesn't catch this. if (rc.IsSuccess() && isProvisionallyCommitted && saveCount < MaxFileSystemCount) { savesToRecover[saveCount] = info; saveCount++; } } } finally { accessor?.Dispose(); infoReader?.Dispose(); } // Recover the saves by rolling them back to the previous commit. // All file systems will try to be recovered, even if one fails. // If any commits fail, the result from the first failed recovery will be returned. for (int i = 0; i < saveCount; i++) { rc = multiCommitInterface.RecoverProvisionallyCommittedSaveData(in savesToRecover[i], true); if (recoveryResult.IsSuccess() && rc.IsFailure()) { recoveryResult = rc; } } } // Delete the commit context file rc = contextFs.DeleteFile(CommitContextFileName); if (rc.IsFailure()) { return(rc); } rc = contextFs.Commit(); if (rc.IsFailure()) { return(rc); } return(recoveryResult); }
/// <summary> /// Recovers a multi-commit that was interrupted after all file systems had been provisionally committed. /// The recovery will finish committing any file systems that are still provisionally committed. /// </summary> /// <param name="multiCommitInterface">The core interface used for multi-commits.</param> /// <param name="contextFs">The file system containing the multi-commit context file.</param> /// <param name="saveService">The save data service.</param> /// <returns><see cref="Result.Success"/>: The operation was successful.<br/> /// <see cref="ResultFs.InvalidMultiCommitContextVersion"/>: The version of the commit context /// file isn't supported.<br/> /// <see cref="ResultFs.InvalidMultiCommitContextState"/>: The multi-commit hadn't finished /// provisionally committing all the file systems.</returns> private static Result RecoverCommit(ISaveDataMultiCommitCoreInterface multiCommitInterface, IFileSystem contextFs, SaveDataFileSystemServiceImpl saveService) { IFile contextFile = null; try { // Read the multi-commit context Result rc = contextFs.OpenFile(out contextFile, CommitContextFileName, OpenMode.ReadWrite); if (rc.IsFailure()) { return(rc); } Unsafe.SkipInit(out Context context); rc = contextFile.Read(out _, 0, SpanHelpers.AsByteSpan(ref context), ReadOption.None); if (rc.IsFailure()) { return(rc); } // Note: Nintendo doesn't check if the proper amount of bytes were read, but it // doesn't really matter since the context is validated. if (context.Version > CurrentCommitContextVersion) { return(ResultFs.InvalidMultiCommitContextVersion.Log()); } // All the file systems in the multi-commit must have been at least provisionally committed // before we can try to recover the commit. if (context.State != CommitState.ProvisionallyCommitted) { return(ResultFs.InvalidMultiCommitContextState.Log()); } // Keep track of the first error that occurs during the recovery Result recoveryResult = Result.Success; int saveCount = 0; Span <SaveDataInfo> savesToRecover = stackalloc SaveDataInfo[MaxFileSystemCount]; SaveDataIndexerAccessor accessor = null; ReferenceCountedDisposable <SaveDataInfoReaderImpl> infoReader = null; try { rc = saveService.OpenSaveDataIndexerAccessor(out accessor, out _, SaveDataSpaceId.User); if (rc.IsFailure()) { return(rc); } rc = accessor.Indexer.OpenSaveDataInfoReader(out infoReader); if (rc.IsFailure()) { return(rc); } // Iterate through all the saves to find any provisionally committed save data while (true) { Unsafe.SkipInit(out SaveDataInfo info); rc = infoReader.Target.Read(out long readCount, OutBuffer.FromStruct(ref info)); if (rc.IsFailure()) { return(rc); } // Break once we're done iterating all save data if (readCount == 0) { break; } rc = multiCommitInterface.IsProvisionallyCommittedSaveData(out bool isProvisionallyCommitted, in info); // Note: Some saves could be missed if there are more than MaxFileSystemCount // provisionally committed saves. Not sure why Nintendo doesn't catch this. if (rc.IsSuccess() && isProvisionallyCommitted && saveCount < MaxFileSystemCount) { savesToRecover[saveCount] = info; saveCount++; } } } finally { accessor?.Dispose(); infoReader?.Dispose(); } // Recover the saves by finishing their commits. // All file systems will try to be recovered, even if one fails. // If any commits fail, the result from the first failed recovery will be returned. for (int i = 0; i < saveCount; i++) { rc = multiCommitInterface.RecoverProvisionallyCommittedSaveData(in savesToRecover[i], false); if (recoveryResult.IsSuccess() && rc.IsFailure()) { recoveryResult = rc; } } return(recoveryResult); } finally { contextFile?.Dispose(); } }