From ce54ae111cb0a64677ab458add72ef3923343df6 Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Sat, 21 Mar 2020 22:44:38 -0700 Subject: [PATCH] Add MultiCommitManager - Recovering from an interrupted commit isn't implemented yet --- build/CodeGen/results.csv | 7 + src/LibHac/Fs/FileSystemBase.cs | 36 +++ src/LibHac/Fs/IFileSystem.cs | 6 + src/LibHac/Fs/ResultFs.cs | 11 + src/LibHac/Fs/SaveDataStructs.cs | 1 + src/LibHac/FsService/FileSystemProxy.cs | 19 +- src/LibHac/FsService/IMultiCommitManager.cs | 10 + .../FsService/Impl/MultiCommitManager.cs | 262 ++++++++++++++++++ 8 files changed, 351 insertions(+), 1 deletion(-) create mode 100644 src/LibHac/FsService/IMultiCommitManager.cs create mode 100644 src/LibHac/FsService/Impl/MultiCommitManager.cs diff --git a/build/CodeGen/results.csv b/build/CodeGen/results.csv index 992e05a0..b788bb4b 100644 --- a/build/CodeGen/results.csv +++ b/build/CodeGen/results.csv @@ -127,6 +127,10 @@ Module,DescriptionStart,DescriptionEnd,Name,Summary 2,4771,4779,SignedSystemPartitionDataCorrupted, 2,4781,,GameCardLogoDataCorrupted, +2,4791,4799,MultiCommitContextCorrupted, +2,4791,,InvalidMultiCommitContextVersion, +2,4792,,InvalidMultiCommitContextState, + # The range name is a guess. 4812 is currently the only result in it 2,4811,4819,ZeroBitmapFileCorrupted, 2,4812,,IncompleteBlockInZeroBitmapHashStorageFile, @@ -197,14 +201,17 @@ Module,DescriptionStart,DescriptionEnd,Name,Summary 2,6454,,WriteStateUnflushed, 2,6457,,WriteModeFileNotClosed, 2,6461,,AllocatorAlignmentViolation, +2,6463,,MultiCommitFileSystemAlreadyAdded, 2,6465,,UserNotExist, 2,6600,6699,EntryNotFound, 2,6606,,TargetProgramIndexNotFound,Specified program index is not found + 2,6700,6799,OutOfResource, 2,6706,,MappingTableFull, 2,6707,,AllocationTableInsufficientFreeBlocks, 2,6709,,OpenCountLimit, +2,6710,,MultiCommitFileSystemLimit, 2,6800,6899,MappingFailed, 2,6811,,RemapStorageMapFull, diff --git a/src/LibHac/Fs/FileSystemBase.cs b/src/LibHac/Fs/FileSystemBase.cs index 5303b688..5e8fdc08 100644 --- a/src/LibHac/Fs/FileSystemBase.cs +++ b/src/LibHac/Fs/FileSystemBase.cs @@ -35,6 +35,21 @@ namespace LibHac.Fs return ResultFs.NotImplemented.Log(); } + protected virtual Result CommitProvisionallyImpl(long commitCount) + { + return ResultFs.NotImplemented.Log(); + } + + protected virtual Result RollbackImpl() + { + return ResultFs.NotImplemented.Log(); + } + + protected virtual Result FlushImpl() + { + return ResultFs.NotImplemented.Log(); + } + protected virtual Result GetFileTimeStampRawImpl(out FileTimeStampRaw timeStamp, U8Span path) { timeStamp = default; @@ -200,6 +215,27 @@ namespace LibHac.Fs return CommitImpl(); } + public Result CommitProvisionally(long commitCount) + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return CommitProvisionallyImpl(commitCount); + } + + public Result Rollback() + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return RollbackImpl(); + } + + public Result Flush() + { + if (IsDisposed) return ResultFs.PreconditionViolation.Log(); + + return FlushImpl(); + } + public Result QueryEntry(Span outBuffer, ReadOnlySpan inBuffer, QueryId queryId, U8Span path) { if (IsDisposed) return ResultFs.PreconditionViolation.Log(); diff --git a/src/LibHac/Fs/IFileSystem.cs b/src/LibHac/Fs/IFileSystem.cs index 7192378e..99415b6b 100644 --- a/src/LibHac/Fs/IFileSystem.cs +++ b/src/LibHac/Fs/IFileSystem.cs @@ -183,6 +183,12 @@ namespace LibHac.Fs /// The of the requested operation. Result Commit(); + Result CommitProvisionally(long commitCount); + + Result Rollback(); + + Result Flush(); + /// /// Gets the creation, last accessed, and last modified timestamps of a file or directory. /// diff --git a/src/LibHac/Fs/ResultFs.cs b/src/LibHac/Fs/ResultFs.cs index cb4f4bd0..11003473 100644 --- a/src/LibHac/Fs/ResultFs.cs +++ b/src/LibHac/Fs/ResultFs.cs @@ -256,6 +256,13 @@ namespace LibHac.Fs /// Error code: 2002-4781; Inner value: 0x255a02 public static Result.Base GameCardLogoDataCorrupted => new Result.Base(ModuleFs, 4781); + /// Error code: 2002-4791; Range: 4791-4799; Inner value: 0x256e02 + public static Result.Base MultiCommitContextCorrupted { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 4791, 4799); } + /// Error code: 2002-4791; Inner value: 0x256e02 + public static Result.Base InvalidMultiCommitContextVersion => new Result.Base(ModuleFs, 4791); + /// Error code: 2002-4792; Inner value: 0x257002 + public static Result.Base InvalidMultiCommitContextState => new Result.Base(ModuleFs, 4792); + /// Error code: 2002-4811; Range: 4811-4819; Inner value: 0x259602 public static Result.Base ZeroBitmapFileCorrupted { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 4811, 4819); } /// Error code: 2002-4812; Inner value: 0x259802 @@ -385,6 +392,8 @@ namespace LibHac.Fs public static Result.Base WriteModeFileNotClosed => new Result.Base(ModuleFs, 6457); /// Error code: 2002-6461; Inner value: 0x327a02 public static Result.Base AllocatorAlignmentViolation => new Result.Base(ModuleFs, 6461); + /// Error code: 2002-6463; Inner value: 0x327e02 + public static Result.Base MultiCommitFileSystemAlreadyAdded => new Result.Base(ModuleFs, 6463); /// Error code: 2002-6465; Inner value: 0x328202 public static Result.Base UserNotExist => new Result.Base(ModuleFs, 6465); @@ -401,6 +410,8 @@ namespace LibHac.Fs public static Result.Base AllocationTableInsufficientFreeBlocks => new Result.Base(ModuleFs, 6707); /// Error code: 2002-6709; Inner value: 0x346a02 public static Result.Base OpenCountLimit => new Result.Base(ModuleFs, 6709); + /// Error code: 2002-6710; Inner value: 0x346c02 + public static Result.Base MultiCommitFileSystemLimit => new Result.Base(ModuleFs, 6710); /// Error code: 2002-6800; Range: 6800-6899; Inner value: 0x352002 public static Result.Base MappingFailed { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => new Result.Base(ModuleFs, 6800, 6899); } diff --git a/src/LibHac/Fs/SaveDataStructs.cs b/src/LibHac/Fs/SaveDataStructs.cs index a9e07435..f2c9b9a3 100644 --- a/src/LibHac/Fs/SaveDataStructs.cs +++ b/src/LibHac/Fs/SaveDataStructs.cs @@ -8,6 +8,7 @@ namespace LibHac.Fs [StructLayout(LayoutKind.Explicit, Size = 0x40)] public struct SaveDataAttribute : IEquatable, IComparable { + // todo: rename to ProgramId [FieldOffset(0x00)] public TitleId TitleId; [FieldOffset(0x08)] public UserId UserId; [FieldOffset(0x18)] public ulong SaveDataId; diff --git a/src/LibHac/FsService/FileSystemProxy.cs b/src/LibHac/FsService/FileSystemProxy.cs index 77f1ae68..64a688fc 100644 --- a/src/LibHac/FsService/FileSystemProxy.cs +++ b/src/LibHac/FsService/FileSystemProxy.cs @@ -2,6 +2,7 @@ using System.Runtime.CompilerServices; using LibHac.Common; using LibHac.Fs; +using LibHac.FsService.Impl; using LibHac.FsSystem; using LibHac.Kvdb; using LibHac.Ncm; @@ -12,7 +13,7 @@ namespace LibHac.FsService public class FileSystemProxy : IFileSystemProxy { private FileSystemProxyCore FsProxyCore { get; } - private FileSystemServer FsServer { get; } + internal FileSystemServer FsServer { get; } public long CurrentProcess { get; private set; } @@ -1175,6 +1176,22 @@ namespace LibHac.FsService return Result.Success; } + internal Result OpenMultiCommitContextSaveData(out IFileSystem fileSystem) + { + fileSystem = default; + + SaveDataAttribute attribute = default; + attribute.TitleId = new TitleId(MultiCommitManager.ProgramId); + attribute.SaveDataId = MultiCommitManager.SaveDataId; + + Result rc = OpenSaveDataFileSystemImpl(out IFileSystem saveFs, out _, SaveDataSpaceId.System, ref attribute, + false, true); + if (rc.IsFailure()) return rc; + + fileSystem = saveFs; + return Result.Success; + } + private static bool IsSystemSaveDataId(ulong id) { return (long)id < 0; diff --git a/src/LibHac/FsService/IMultiCommitManager.cs b/src/LibHac/FsService/IMultiCommitManager.cs new file mode 100644 index 00000000..029946d9 --- /dev/null +++ b/src/LibHac/FsService/IMultiCommitManager.cs @@ -0,0 +1,10 @@ +using LibHac.Fs; + +namespace LibHac.FsService +{ + public interface IMultiCommitManager + { + Result Add(IFileSystem fileSystem); + Result Commit(); + } +} diff --git a/src/LibHac/FsService/Impl/MultiCommitManager.cs b/src/LibHac/FsService/Impl/MultiCommitManager.cs new file mode 100644 index 00000000..db7bbf9c --- /dev/null +++ b/src/LibHac/FsService/Impl/MultiCommitManager.cs @@ -0,0 +1,262 @@ +using System.Collections.Generic; +using System.Runtime.InteropServices; +using LibHac.Common; +using LibHac.Fs; +using LibHac.Fs.Shim; + +namespace LibHac.FsService.Impl +{ + internal class MultiCommitManager : IMultiCommitManager + { + private const int MaxFileSystemCount = 10; + private const int CurrentContextVersion = 0x10000; + + public const ulong ProgramId = 0x100000000000000; + public const ulong SaveDataId = 0x8000000000000001; + private const long SaveDataSize = 0xC000; + private const long SaveJournalSize = 0xC000; + + private const long ContextFileSize = 0x200; + + // /commitinfo + private static U8Span ContextFileName => + new U8Span(new[] { (byte)'/', (byte)'c', (byte)'o', (byte)'m', (byte)'m', (byte)'i', (byte)'t', (byte)'i', (byte)'n', (byte)'f', (byte)'o' }); + + private static readonly object Locker = new object(); + + private FileSystemProxy FsProxy { get; } + private List FileSystems { get; } = new List(MaxFileSystemCount); + private long CommitCount { get; set; } + + public MultiCommitManager(FileSystemProxy fsProxy) + { + FsProxy = fsProxy; + } + + public Result Add(IFileSystem fileSystem) + { + if (FileSystems.Count >= MaxFileSystemCount) + return ResultFs.MultiCommitFileSystemLimit.Log(); + + // Check that the file system hasn't already been added + for (int i = 0; i < FileSystems.Count; i++) + { + if (ReferenceEquals(FileSystems[i], fileSystem)) + return ResultFs.MultiCommitFileSystemAlreadyAdded.Log(); + } + + FileSystems.Add(fileSystem); + return Result.Success; + } + + public Result Commit() + { + lock (Locker) + { + Result rc = CreateSave(); + if (rc.IsFailure()) return rc; + + rc = FsProxy.OpenMultiCommitContextSaveData(out IFileSystem contextFs); + if (rc.IsFailure()) return rc; + + return CommitImpl(contextFs); + } + } + + private Result CommitImpl(IFileSystem contextFileSystem) + { + var context = new CommitContextManager(contextFileSystem); + + try + { + CommitCount = 1; + + Result rc = context.Initialize(CommitCount, FileSystems.Count); + if (rc.IsFailure()) return rc; + + rc = CommitProvisionally(); + if (rc.IsFailure()) return rc; + + rc = context.SetCommittedProvisionally(); + if (rc.IsFailure()) return rc; + + foreach (IFileSystem fs in FileSystems) + { + rc = fs.Commit(); + if (rc.IsFailure()) return rc; + } + + rc = context.Close(); + if (rc.IsFailure()) return rc; + } + finally + { + context.Dispose(); + } + + return Result.Success; + } + + private Result CreateSave() + { + Result rc = FsProxy.OpenMultiCommitContextSaveData(out IFileSystem contextFs); + + if (rc.IsFailure()) + { + if (!ResultFs.TargetNotFound.Includes(rc)) + { + return rc; + } + + rc = FsProxy.FsServer.FsClient.CreateSystemSaveData(SaveDataId, SaveDataSize, SaveJournalSize, + SaveDataFlags.None); + if (rc.IsFailure()) return rc; + } + + contextFs?.Dispose(); + return Result.Success; + } + + private Result CommitProvisionally() + { + Result rc = Result.Success; + int i; + + for (i = 0; i < FileSystems.Count; i++) + { + rc = FileSystems[i].CommitProvisionally(CommitCount); + + if (rc.IsFailure()) + break; + } + + if (rc.IsFailure()) + { + // Rollback all provisional commits including the failed commit + for (int j = 0; j <= i; j++) + { + FileSystems[j].Rollback().IgnoreResult(); + } + } + + return rc; + } + + [StructLayout(LayoutKind.Sequential, Size = 0x18)] + private struct CommitContext + { + public int Version; + public CommitState State; + public int FileSystemCount; + public long CommitCount; // I think? + } + + private enum CommitState + { + // ReSharper disable once UnusedMember.Local + None = 0, + NotCommitted = 1, + ProvisionallyCommitted = 2 + } + + private struct CommitContextManager + { + private IFileSystem _fileSystem; + private CommitContext _context; + + public CommitContextManager(IFileSystem contextFileSystem) + { + _fileSystem = contextFileSystem; + _context = default; + } + + public Result Initialize(long commitCount, int fileSystemCount) + { + IFile contextFile = null; + + try + { + // Open context file and create if it doesn't exist + Result rc = _fileSystem.OpenFile(out contextFile, ContextFileName, OpenMode.Read); + + if (rc.IsFailure()) + { + if (!ResultFs.PathNotFound.Includes(rc)) + return rc; + + rc = _fileSystem.CreateFile(ContextFileName, ContextFileSize, CreateFileOptions.None); + if (rc.IsFailure()) return rc; + + rc = _fileSystem.OpenFile(out contextFile, ContextFileName, OpenMode.Read); + if (rc.IsFailure()) return rc; + } + + _context.Version = CurrentContextVersion; + _context.State = CommitState.NotCommitted; + _context.FileSystemCount = fileSystemCount; + _context.CommitCount = commitCount; + + // Write the initial context to the file + rc = contextFile.Write(0, SpanHelpers.AsByteSpan(ref _context), WriteOption.None); + if (rc.IsFailure()) return rc; + + rc = contextFile.Flush(); + if (rc.IsFailure()) return rc; + } + finally + { + contextFile?.Dispose(); + } + + return _fileSystem.Commit(); + } + + public Result SetCommittedProvisionally() + { + IFile contextFile = null; + + try + { + Result rc = _fileSystem.OpenFile(out contextFile, ContextFileName, OpenMode.ReadWrite); + if (rc.IsFailure()) return rc; + + _context.State = CommitState.ProvisionallyCommitted; + + rc = contextFile.Write(0, SpanHelpers.AsByteSpan(ref _context), WriteOption.None); + if (rc.IsFailure()) return rc; + + rc = contextFile.Flush(); + if (rc.IsFailure()) return rc; + } + finally + { + contextFile?.Dispose(); + } + + return _fileSystem.Commit(); + } + + public Result Close() + { + Result rc = _fileSystem.DeleteFile(ContextFileName); + if (rc.IsFailure()) return rc; + + rc = _fileSystem.Commit(); + if (rc.IsFailure()) return rc; + + _fileSystem = null; + return Result.Success; + } + + public void Dispose() + { + if (_fileSystem is null) return; + + _fileSystem.DeleteFile(ContextFileName).IgnoreResult(); + _fileSystem.Commit().IgnoreResult(); + + _fileSystem = null; + } + } + } +}