diff --git a/src/LibHac/FsSrv/FileSystemServer.cs b/src/LibHac/FsSrv/FileSystemServer.cs
index 8a6ed7cb..8a4efc27 100644
--- a/src/LibHac/FsSrv/FileSystemServer.cs
+++ b/src/LibHac/FsSrv/FileSystemServer.cs
@@ -31,6 +31,7 @@ namespace LibHac.FsSrv
public AccessControlGlobals AccessControl;
public StorageDeviceManagerFactoryGlobals StorageDeviceManagerFactory;
public SaveDataSharedFileStorageGlobals SaveDataSharedFileStorage;
+ public MultiCommitManagerGlobals MultiCommitManager;
public void Initialize(HorizonClient horizonClient, FileSystemServer fsServer)
{
@@ -38,6 +39,7 @@ namespace LibHac.FsSrv
InitMutex = new object();
SaveDataSharedFileStorage.Initialize(fsServer);
+ MultiCommitManager.Initialize();
}
}
diff --git a/src/LibHac/FsSrv/Impl/MultiCommitManager.cs b/src/LibHac/FsSrv/Impl/MultiCommitManager.cs
index 32320b21..955d7d3a 100644
--- a/src/LibHac/FsSrv/Impl/MultiCommitManager.cs
+++ b/src/LibHac/FsSrv/Impl/MultiCommitManager.cs
@@ -1,19 +1,49 @@
using System;
-using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using LibHac.Common;
+using LibHac.Diag;
using LibHac.Fs;
-using LibHac.Fs.Fsa;
using LibHac.Fs.Shim;
using LibHac.FsSrv.Sf;
+using LibHac.Os;
using LibHac.Sf;
-using IFileSystem = LibHac.Fs.Fsa.IFileSystem;
+
using IFile = LibHac.Fs.Fsa.IFile;
+using IFileSystem = LibHac.Fs.Fsa.IFileSystem;
using IFileSystemSf = LibHac.FsSrv.Sf.IFileSystem;
namespace LibHac.FsSrv.Impl
{
+ internal struct MultiCommitManagerGlobals
+ {
+ public SdkMutexType MultiCommitMutex;
+
+ public void Initialize()
+ {
+ MultiCommitMutex.Initialize();
+ }
+ }
+
+ ///
+ /// Manages atomically committing a group of file systems.
+ ///
+ ///
+ /// The commit process is as follows:
+ /// 1. Create a commit context file that tracks the progress of the commit in case it is interrupted.
+ /// 2. Provisionally commit each file system individually. If any fail, rollback the file systems that were provisionally committed.
+ /// 3. Update the commit context file to note that the file systems have been provisionally committed.
+ /// If the multi-commit is interrupted past this point, the file systems will be fully committed during recovery.
+ /// 4. Fully commit each file system individually.
+ /// 5. Delete the commit context file.
+ ///
+ /// Even though multi-commits are supposed to be atomic, issues can arise from errors during the process of fully committing the save data.
+ /// Save data image files are designed so that minimal changes are made when fully committing a provisionally committed save.
+ /// However if any commit fails for any reason, all other saves in the multi-commit will still be committed.
+ /// This can especially cause issues with directory save data where finishing a commit is much more involved.
+ ///
+ /// Based on FS 12.0.3 (nnSdk 12.3.1)
+ ///
internal class MultiCommitManager : IMultiCommitManager
{
private const int MaxFileSystemCount = 10;
@@ -27,42 +57,43 @@ namespace LibHac.FsSrv.Impl
private const long CommitContextFileSize = 0x200;
// /commitinfo
- private static U8Span CommitContextFileName =>
- 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 ReadOnlySpan CommitContextFileName =>
+ new[] { (byte)'/', (byte)'c', (byte)'o', (byte)'m', (byte)'m', (byte)'i', (byte)'t', (byte)'i', (byte)'n', (byte)'f', (byte)'o' };
- // Todo: Don't use global lock object
- private static readonly object Locker = new object();
+ private ReferenceCountedDisposable _multiCommitInterface;
+ private readonly ReferenceCountedDisposable[] _fileSystems;
+ private int _fileSystemCount;
+ private long _counter;
- private ReferenceCountedDisposable MultiCommitInterface { get; }
+ // Extra field used in LibHac
+ private readonly FileSystemServer _fsServer;
+ private ref MultiCommitManagerGlobals Globals => ref _fsServer.Globals.MultiCommitManager;
- private List> FileSystems { get; } =
- new List>(MaxFileSystemCount);
-
- private long Counter { get; set; }
- private HorizonClient Hos { get; }
-
- public MultiCommitManager(
- ref ReferenceCountedDisposable multiCommitInterface,
- HorizonClient client)
+ public MultiCommitManager(FileSystemServer fsServer, ref ReferenceCountedDisposable multiCommitInterface)
{
- Hos = client;
- MultiCommitInterface = Shared.Move(ref multiCommitInterface);
+ _fsServer = fsServer;
+
+ _multiCommitInterface = Shared.Move(ref multiCommitInterface);
+ _fileSystems = new ReferenceCountedDisposable[MaxFileSystemCount];
+ _fileSystemCount = 0;
+ _counter = 0;
}
- public static ReferenceCountedDisposable CreateShared(
- ref ReferenceCountedDisposable multiCommitInterface,
- HorizonClient client)
+ public static ReferenceCountedDisposable CreateShared(FileSystemServer fsServer,
+ ref ReferenceCountedDisposable multiCommitInterface)
{
- var manager = new MultiCommitManager(ref multiCommitInterface, client);
+ var manager = new MultiCommitManager(fsServer, ref multiCommitInterface);
return new ReferenceCountedDisposable(manager);
}
public void Dispose()
{
- foreach (ReferenceCountedDisposable fs in FileSystems)
+ foreach (ReferenceCountedDisposable fs in _fileSystems)
{
- fs.Dispose();
+ fs?.Dispose();
}
+
+ _multiCommitInterface?.Dispose();
}
///
@@ -71,20 +102,26 @@ namespace LibHac.FsSrv.Impl
/// The of the operation.
private Result EnsureSaveDataForContext()
{
- Result rc = MultiCommitInterface.Target.OpenMultiCommitContext(
- out ReferenceCountedDisposable contextFs);
-
- if (rc.IsFailure())
+ ReferenceCountedDisposable contextFs = null;
+ try
{
- if (!ResultFs.TargetNotFound.Includes(rc))
- return rc;
+ Result rc = _multiCommitInterface.Target.OpenMultiCommitContext(out contextFs);
- rc = Hos.Fs.CreateSystemSaveData(SaveDataId, SaveDataSize, SaveJournalSize, SaveDataFlags.None);
- if (rc.IsFailure()) return rc;
+ if (rc.IsFailure())
+ {
+ if (!ResultFs.TargetNotFound.Includes(rc))
+ return rc;
+
+ rc = _fsServer.Hos.Fs.CreateSystemSaveData(SaveDataId, SaveDataSize, SaveJournalSize, SaveDataFlags.None);
+ if (rc.IsFailure()) return rc;
+ }
+
+ return Result.Success;
+ }
+ finally
+ {
+ contextFs?.Dispose();
}
-
- contextFs?.Dispose();
- return Result.Success;
}
///
@@ -97,7 +134,7 @@ namespace LibHac.FsSrv.Impl
/// : The provided file system has already been added.
public Result Add(ReferenceCountedDisposable fileSystem)
{
- if (FileSystems.Count >= MaxFileSystemCount)
+ if (_fileSystemCount >= MaxFileSystemCount)
return ResultFs.MultiCommitFileSystemLimit.Log();
ReferenceCountedDisposable fsaFileSystem = null;
@@ -107,14 +144,14 @@ namespace LibHac.FsSrv.Impl
if (rc.IsFailure()) return rc;
// Check that the file system hasn't already been added
- foreach (ReferenceCountedDisposable fs in FileSystems)
+ for (int i = 0; i < _fileSystemCount; i++)
{
- if (ReferenceEquals(fs.Target, fsaFileSystem.Target))
+ if (ReferenceEquals(fsaFileSystem.Target, _fileSystems[i].Target))
return ResultFs.MultiCommitFileSystemAlreadyAdded.Log();
}
- FileSystems.Add(fsaFileSystem);
- fsaFileSystem = null;
+ _fileSystems[_fileSystemCount] = Shared.Move(ref fsaFileSystem);
+ _fileSystemCount++;
return Result.Success;
}
@@ -132,32 +169,23 @@ namespace LibHac.FsSrv.Impl
/// The of the operation.
private Result Commit(IFileSystem contextFileSystem)
{
- ContextUpdater context = default;
+ _counter = 1;
- try
- {
- Counter = 1;
+ using var contextUpdater = new ContextUpdater(contextFileSystem);
+ Result rc = contextUpdater.Create(_counter, _fileSystemCount);
+ if (rc.IsFailure()) return rc;
- context = new ContextUpdater(contextFileSystem);
- Result rc = context.Create(Counter, FileSystems.Count);
- if (rc.IsFailure()) return rc;
+ rc = CommitProvisionallyFileSystem(_counter);
+ if (rc.IsFailure()) return rc;
- rc = CommitProvisionallyFileSystem(Counter);
- if (rc.IsFailure()) return rc;
+ rc = contextUpdater.CommitProvisionallyDone();
+ if (rc.IsFailure()) return rc;
- rc = context.CommitProvisionallyDone();
- if (rc.IsFailure()) return rc;
+ rc = CommitFileSystem();
+ if (rc.IsFailure()) return rc;
- rc = CommitFileSystem();
- if (rc.IsFailure()) return rc;
-
- rc = context.CommitDone();
- if (rc.IsFailure()) return rc;
- }
- finally
- {
- context.Dispose();
- }
+ rc = contextUpdater.CommitDone();
+ if (rc.IsFailure()) return rc;
return Result.Success;
}
@@ -168,23 +196,22 @@ namespace LibHac.FsSrv.Impl
/// The of the operation.
public Result Commit()
{
- lock (Locker)
+ using ScopedLock scopedLock = ScopedLock.Lock(ref Globals.MultiCommitMutex);
+
+ ReferenceCountedDisposable contextFs = null;
+ try
{
Result rc = EnsureSaveDataForContext();
if (rc.IsFailure()) return rc;
- ReferenceCountedDisposable contextFs = null;
- try
- {
- rc = MultiCommitInterface.Target.OpenMultiCommitContext(out contextFs);
- if (rc.IsFailure()) return rc;
+ rc = _multiCommitInterface.Target.OpenMultiCommitContext(out contextFs);
+ if (rc.IsFailure()) return rc;
- return Commit(contextFs.Target);
- }
- finally
- {
- contextFs?.Dispose();
- }
+ return Commit(contextFs.Target);
+ }
+ finally
+ {
+ contextFs?.Dispose();
}
}
@@ -198,9 +225,11 @@ namespace LibHac.FsSrv.Impl
Result rc = Result.Success;
int i;
- for (i = 0; i < FileSystems.Count; i++)
+ for (i = 0; i < _fileSystemCount; i++)
{
- rc = FileSystems[i].Target.CommitProvisionally(counter);
+ Assert.SdkNotNull(_fileSystems[i]);
+
+ rc = _fileSystems[i].Target.CommitProvisionally(counter);
if (rc.IsFailure())
break;
@@ -211,7 +240,9 @@ namespace LibHac.FsSrv.Impl
// Rollback all provisional commits including the failed commit
for (int j = 0; j <= i; j++)
{
- FileSystems[j].Target.Rollback().IgnoreResult();
+ Assert.SdkNotNull(_fileSystems[j]);
+
+ _fileSystems[j].Target.Rollback().IgnoreResult();
}
}
@@ -224,14 +255,17 @@ namespace LibHac.FsSrv.Impl
/// The of the operation.
private Result CommitFileSystem()
{
- // All file systems will try to be recovered committed, even if one fails.
+ // Try to commit all file systems even if one fails.
// If any commits fail, the result from the first failed recovery will be returned.
Result result = Result.Success;
- foreach (ReferenceCountedDisposable fs in FileSystems)
+ for (int i = 0; i < _fileSystemCount; i++)
{
- Result rc = fs.Target.Commit();
+ Assert.SdkNotNull(_fileSystems[i]);
+ Result rc = _fileSystems[i].Target.Commit();
+
+ // If the commit failed, set the overall result if it hasn't been set yet.
if (result.IsSuccess() && rc.IsFailure())
{
result = rc;
@@ -256,11 +290,15 @@ namespace LibHac.FsSrv.Impl
private static Result RecoverCommit(ISaveDataMultiCommitCoreInterface multiCommitInterface,
IFileSystem contextFs, SaveDataFileSystemServiceImpl saveService)
{
+ var contextFilePath = new Fs.Path();
+ Result rc = PathFunctions.SetUpFixedPath(ref contextFilePath, CommitContextFileName);
+ if (rc.IsFailure()) return rc;
+
IFile contextFile = null;
try
{
// Read the multi-commit context
- Result rc = contextFs.OpenFile(out contextFile, CommitContextFileName, OpenMode.ReadWrite);
+ rc = contextFs.OpenFile(out contextFile, in contextFilePath, OpenMode.ReadWrite);
if (rc.IsFailure()) return rc;
Unsafe.SkipInit(out Context context);
@@ -330,12 +368,13 @@ namespace LibHac.FsSrv.Impl
{
rc = multiCommitInterface.RecoverProvisionallyCommittedSaveData(in savesToRecover[i], false);
- if (recoveryResult.IsSuccess() && rc.IsFailure())
+ if (rc.IsFailure() && !recoveryResult.IsFailure())
{
recoveryResult = rc;
}
}
+ contextFilePath.Dispose();
return recoveryResult;
}
finally
@@ -426,13 +465,18 @@ namespace LibHac.FsSrv.Impl
}
}
+ var contextFilePath = new Fs.Path();
+ rc = PathFunctions.SetUpFixedPath(ref contextFilePath, CommitContextFileName);
+ if (rc.IsFailure()) return rc;
+
// Delete the commit context file
- rc = contextFs.DeleteFile(CommitContextFileName);
+ rc = contextFs.DeleteFile(in contextFilePath);
if (rc.IsFailure()) return rc;
rc = contextFs.Commit();
if (rc.IsFailure()) return rc;
+ contextFilePath.Dispose();
return recoveryResult;
}
@@ -440,55 +484,62 @@ namespace LibHac.FsSrv.Impl
/// 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.
///
+ /// The that contains the save data to recover.
/// The core interface used for multi-commits.
/// The save data service.
/// The of the operation.
/// : The recovery was successful or there was no multi-commit to recover.
- public static Result Recover(ISaveDataMultiCommitCoreInterface multiCommitInterface,
+ public static Result Recover(FileSystemServer fsServer, ISaveDataMultiCommitCoreInterface multiCommitInterface,
SaveDataFileSystemServiceImpl saveService)
{
- lock (Locker)
- {
- bool needsRecover = true;
- ReferenceCountedDisposable fileSystem = null;
+ ref MultiCommitManagerGlobals globals = ref fsServer.Globals.MultiCommitManager;
+ using ScopedLock scopedLock = ScopedLock.Lock(ref globals.MultiCommitMutex);
- try
+ bool needsRecovery = true;
+ ReferenceCountedDisposable 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())
{
- // Check if a multi-commit was interrupted by checking if there's a commit context file.
- Result rc = multiCommitInterface.OpenMultiCommitContext(out fileSystem);
+ 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
+ needsRecovery = false;
+ }
+
+ if (needsRecovery)
+ {
+ var contextFilePath = new Fs.Path();
+ rc = PathFunctions.SetUpFixedPath(ref contextFilePath, CommitContextFileName);
+ if (rc.IsFailure()) return rc;
+
+ rc = fileSystem.Target.OpenFile(out IFile file, in contextFilePath, OpenMode.Read);
+ file?.Dispose();
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;
+ // Unable to open the context file. No multi-commit to recover.
+ if (ResultFs.PathNotFound.Includes(rc))
+ needsRecovery = 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();
+ contextFilePath.Dispose();
}
+
+ if (!needsRecovery)
+ return Result.Success;
+
+ // There was a context file. Recover the unfinished commit.
+ return Recover(multiCommitInterface, fileSystem.Target, saveService);
+ }
+ finally
+ {
+ fileSystem?.Dispose();
}
}
@@ -509,41 +560,58 @@ namespace LibHac.FsSrv.Impl
ProvisionallyCommitted = 2
}
- private struct ContextUpdater
+ private struct ContextUpdater : IDisposable
{
- private IFileSystem _fileSystem;
private Context _context;
+ private IFileSystem _fileSystem;
public ContextUpdater(IFileSystem contextFileSystem)
{
- _fileSystem = contextFileSystem;
_context = default;
+ _fileSystem = contextFileSystem;
+ }
+
+ public void Dispose()
+ {
+ if (_fileSystem is null) return;
+
+ var contextFilePath = new Fs.Path();
+ PathFunctions.SetUpFixedPath(ref contextFilePath, CommitContextFileName).IgnoreResult();
+ _fileSystem.DeleteFile(in contextFilePath).IgnoreResult();
+ _fileSystem.Commit().IgnoreResult();
+
+ _fileSystem = null;
+ contextFilePath.Dispose();
}
///
/// Creates and writes the initial commit context to a file.
///
- /// The counter.
+ /// The counter.
/// The number of file systems being committed.
/// The of the operation.
- public Result Create(long commitCount, int fileSystemCount)
+ public Result Create(long counter, int fileSystemCount)
{
+ var contextFilePath = new Fs.Path();
+ Result rc = PathFunctions.SetUpFixedPath(ref contextFilePath, CommitContextFileName);
+ if (rc.IsFailure()) return rc;
+
IFile contextFile = null;
try
{
// Open context file and create if it doesn't exist
- Result rc = _fileSystem.OpenFile(out contextFile, CommitContextFileName, OpenMode.Read);
+ rc = _fileSystem.OpenFile(out contextFile, in contextFilePath, OpenMode.Read);
if (rc.IsFailure())
{
if (!ResultFs.PathNotFound.Includes(rc))
return rc;
- rc = _fileSystem.CreateFile(CommitContextFileName, CommitContextFileSize, CreateFileOptions.None);
+ rc = _fileSystem.CreateFile(in contextFilePath, CommitContextFileSize);
if (rc.IsFailure()) return rc;
- rc = _fileSystem.OpenFile(out contextFile, CommitContextFileName, OpenMode.Read);
+ rc = _fileSystem.OpenFile(out contextFile, in contextFilePath, OpenMode.Read);
if (rc.IsFailure()) return rc;
}
}
@@ -554,13 +622,13 @@ namespace LibHac.FsSrv.Impl
try
{
- Result rc = _fileSystem.OpenFile(out contextFile, CommitContextFileName, OpenMode.ReadWrite);
+ rc = _fileSystem.OpenFile(out contextFile, in contextFilePath, OpenMode.ReadWrite);
if (rc.IsFailure()) return rc;
_context.Version = CurrentCommitContextVersion;
_context.State = CommitState.NotCommitted;
_context.FileSystemCount = fileSystemCount;
- _context.Counter = commitCount;
+ _context.Counter = counter;
// Write the initial context to the file
rc = contextFile.Write(0, SpanHelpers.AsByteSpan(ref _context), WriteOption.None);
@@ -574,7 +642,11 @@ namespace LibHac.FsSrv.Impl
contextFile?.Dispose();
}
- return _fileSystem.Commit();
+ rc = _fileSystem.Commit();
+ if (rc.IsFailure()) return rc;
+
+ contextFilePath.Dispose();
+ return Result.Success;
}
///
@@ -584,11 +656,15 @@ namespace LibHac.FsSrv.Impl
/// The of the operation.
public Result CommitProvisionallyDone()
{
+ var contextFilePath = new Fs.Path();
+ Result rc = PathFunctions.SetUpFixedPath(ref contextFilePath, CommitContextFileName);
+ if (rc.IsFailure()) return rc;
+
IFile contextFile = null;
try
{
- Result rc = _fileSystem.OpenFile(out contextFile, CommitContextFileName, OpenMode.ReadWrite);
+ rc = _fileSystem.OpenFile(out contextFile, in contextFilePath, OpenMode.ReadWrite);
if (rc.IsFailure()) return rc;
_context.State = CommitState.ProvisionallyCommitted;
@@ -604,6 +680,7 @@ namespace LibHac.FsSrv.Impl
contextFile?.Dispose();
}
+ contextFilePath.Dispose();
return _fileSystem.Commit();
}
@@ -613,25 +690,20 @@ namespace LibHac.FsSrv.Impl
/// The of the operation.
public Result CommitDone()
{
- Result rc = _fileSystem.DeleteFile(CommitContextFileName);
+ var contextFilePath = new Fs.Path();
+ Result rc = PathFunctions.SetUpFixedPath(ref contextFilePath, CommitContextFileName);
+ if (rc.IsFailure()) return rc;
+
+ rc = _fileSystem.DeleteFile(in contextFilePath);
if (rc.IsFailure()) return rc;
rc = _fileSystem.Commit();
if (rc.IsFailure()) return rc;
_fileSystem = null;
+ contextFilePath.Dispose();
return Result.Success;
}
-
- public void Dispose()
- {
- if (_fileSystem is null) return;
-
- _fileSystem.DeleteFile(CommitContextFileName).IgnoreResult();
- _fileSystem.Commit().IgnoreResult();
-
- _fileSystem = null;
- }
}
}
}
diff --git a/src/LibHac/FsSrv/SaveDataFileSystemService.cs b/src/LibHac/FsSrv/SaveDataFileSystemService.cs
index 354d02b7..c791495b 100644
--- a/src/LibHac/FsSrv/SaveDataFileSystemService.cs
+++ b/src/LibHac/FsSrv/SaveDataFileSystemService.cs
@@ -2119,7 +2119,7 @@ namespace LibHac.FsSrv
saveService = SelfReference.AddReference();
commitInterface = saveService.AddReference();
- commitManager = MultiCommitManager.CreateShared(ref commitInterface, Hos);
+ commitManager = MultiCommitManager.CreateShared(ServiceImpl.FsServer, ref commitInterface);
return Result.Success;
}
finally
@@ -2146,7 +2146,7 @@ namespace LibHac.FsSrv
public Result RecoverMultiCommit()
{
- return MultiCommitManager.Recover(this, ServiceImpl);
+ return MultiCommitManager.Recover(ServiceImpl.FsServer, this, ServiceImpl);
}
public Result IsProvisionallyCommittedSaveData(out bool isProvisionallyCommitted, in SaveDataInfo saveInfo)
diff --git a/src/LibHac/FsSrv/SaveDataFileSystemServiceImpl.cs b/src/LibHac/FsSrv/SaveDataFileSystemServiceImpl.cs
index 0378549f..2e439a57 100644
--- a/src/LibHac/FsSrv/SaveDataFileSystemServiceImpl.cs
+++ b/src/LibHac/FsSrv/SaveDataFileSystemServiceImpl.cs
@@ -26,6 +26,7 @@ namespace LibHac.FsSrv
private TimeStampGetter _timeStampGetter;
internal HorizonClient Hos => _config.FsServer.Hos;
+ internal FileSystemServer FsServer => _config.FsServer;
private class TimeStampGetter : ISaveDataCommitTimeStampGetter
{