diff --git a/src/LibHac/FsSrv/DefaultFsServerObjects.cs b/src/LibHac/FsSrv/DefaultFsServerObjects.cs index f759fea4..c97c046a 100644 --- a/src/LibHac/FsSrv/DefaultFsServerObjects.cs +++ b/src/LibHac/FsSrv/DefaultFsServerObjects.cs @@ -25,7 +25,7 @@ public class DefaultFsServerObjects var sdCard = new EmulatedSdCard(); var gameCardNew = new GameCardDummy(); - var sdmmcNew = new SdmmcApi(); + var sdmmcNew = new SdmmcApi(fsServer); var gcStorageCreator = new EmulatedGameCardStorageCreator(gameCard); diff --git a/src/LibHac/Os/Event.cs b/src/LibHac/Os/Event.cs new file mode 100644 index 00000000..078f081c --- /dev/null +++ b/src/LibHac/Os/Event.cs @@ -0,0 +1,73 @@ +using System; + +namespace LibHac.Os; + +public struct EventType +{ +} + +public class Event : IDisposable +{ + private EventType _event; + + private readonly OsState _os; + + public Event(OsState os, EventClearMode clearMode) + { + _os = os; + _os.InitializeEvent(ref _event, signaled: false, clearMode); + } + + public void Dispose() => _os.FinalizeEvent(ref _event); + public void Wait() => _os.WaitEvent(ref _event); + public bool TryWait() => _os.TryWaitEvent(ref _event); + public bool TimedWait(TimeSpan timeout) => _os.TimedWaitEvent(ref _event, timeout); + public void Signal() => _os.SignalEvent(ref _event); + public void Clear() => _os.ClearEvent(ref _event); + public ref EventType GetBase() => ref _event; +} + +public static class EventApi +{ + public static void InitializeEvent(this OsState os, ref EventType eventType, bool signaled, + EventClearMode clearMode) + { + throw new NotImplementedException(); + } + + public static void FinalizeEvent(this OsState os, ref EventType eventType) + { + throw new NotImplementedException(); + } + + public static void SignalEvent(this OsState os, ref EventType eventType) + { + throw new NotImplementedException(); + } + + public static void WaitEvent(this OsState os, ref EventType eventType) + { + throw new NotImplementedException(); + } + + public static bool TryWaitEvent(this OsState os, ref EventType eventType) + { + throw new NotImplementedException(); + } + + public static bool TimedWaitEvent(this OsState os, ref EventType eventType, TimeSpan timeout) + { + throw new NotImplementedException(); + } + + public static void ClearEvent(this OsState os, ref EventType eventType) + { + throw new NotImplementedException(); + } + + public static void InitializeMultiWaitHolder(this OsState os, ref MultiWaitHolderType multiWaitHolder, + ref EventType eventType) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/LibHac/Os/EventCommon.cs b/src/LibHac/Os/EventCommon.cs new file mode 100644 index 00000000..4b555130 --- /dev/null +++ b/src/LibHac/Os/EventCommon.cs @@ -0,0 +1,7 @@ +namespace LibHac.Os; + +public enum EventClearMode +{ + ManualClear = 0, + AutoClear = 1 +} \ No newline at end of file diff --git a/src/LibHac/Os/TimerEvent.cs b/src/LibHac/Os/TimerEvent.cs new file mode 100644 index 00000000..e0eae47a --- /dev/null +++ b/src/LibHac/Os/TimerEvent.cs @@ -0,0 +1,85 @@ +using System; + +namespace LibHac.Os; + +public struct TimerEventType +{ +} + +public class TimerEvent : IDisposable +{ + private TimerEventType _event; + + private readonly OsState _os; + + public TimerEvent(OsState os, EventClearMode clearMode) + { + _os = os; + _os.InitializeTimerEvent(ref _event, clearMode); + } + + public void Dispose() => _os.FinalizeTimerEvent(ref _event); + public void StartOneShot(TimeSpan firstTime) => _os.StartOneShotTimerEvent(ref _event, firstTime); + public void StartPeriodic(TimeSpan firstTime, TimeSpan interval) => _os.StartPeriodicTimerEvent(ref _event, firstTime, interval); + public void Stop() => _os.StopTimerEvent(ref _event); + public void Wait() => _os.WaitTimerEvent(ref _event); + public bool TryWait() => _os.TryWaitTimerEvent(ref _event); + public void Signal() => _os.SignalTimerEvent(ref _event); + public void Clear() => _os.ClearTimerEvent(ref _event); + public ref TimerEventType GetBase() => ref _event; +} + +public static class TimerEventApi +{ + public static void InitializeTimerEvent(this OsState os, ref TimerEventType eventType, EventClearMode clearMode) + { + throw new NotImplementedException(); + } + + public static void FinalizeTimerEvent(this OsState os, ref TimerEventType eventType) + { + throw new NotImplementedException(); + } + + public static void StartOneShotTimerEvent(this OsState os, ref TimerEventType eventType, TimeSpan firstTime) + { + throw new NotImplementedException(); + } + + public static void StartPeriodicTimerEvent(this OsState os, ref TimerEventType eventType, TimeSpan firstTime, + TimeSpan interval) + { + throw new NotImplementedException(); + } + + public static void StopTimerEvent(this OsState os, ref TimerEventType eventType) + { + throw new NotImplementedException(); + } + + public static void SignalTimerEvent(this OsState os, ref TimerEventType eventType) + { + throw new NotImplementedException(); + } + + public static void ClearTimerEvent(this OsState os, ref TimerEventType eventType) + { + throw new NotImplementedException(); + } + + public static void WaitTimerEvent(this OsState os, ref TimerEventType eventType) + { + throw new NotImplementedException(); + } + + public static bool TryWaitTimerEvent(this OsState os, ref TimerEventType eventType) + { + throw new NotImplementedException(); + } + + public static void InitializeMultiWaitHolder(this OsState os, ref MultiWaitHolderType multiWaitHolder, + ref TimerEventType eventType) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/LibHac/Sdmmc/Common.cs b/src/LibHac/Sdmmc/Common.cs index 4ee2b2e1..30b35194 100644 --- a/src/LibHac/Sdmmc/Common.cs +++ b/src/LibHac/Sdmmc/Common.cs @@ -1,4 +1,5 @@ using System; +using LibHac.FsSrv; namespace LibHac.Sdmmc; @@ -60,6 +61,14 @@ public partial class SdmmcApi public const int DeviceCidSize = 0x10; public const int DeviceCsdSize = 0x10; + private FileSystemServer _fsServer; + internal HorizonClient Hos => _fsServer.Hos; + + public SdmmcApi(FileSystemServer fsServer) + { + _fsServer = fsServer; + } + public void SwitchToPcvClockResetControl() { throw new NotImplementedException(); diff --git a/src/LibHac/SdmmcSrv/PatrolReader.cs b/src/LibHac/SdmmcSrv/PatrolReader.cs index 14a9744b..c30d4b13 100644 --- a/src/LibHac/SdmmcSrv/PatrolReader.cs +++ b/src/LibHac/SdmmcSrv/PatrolReader.cs @@ -1,73 +1,404 @@ using System; +using LibHac.Common; +using LibHac.Crypto; +using LibHac.Diag; +using LibHac.Fs; +using LibHac.FsSystem; using LibHac.Os; using LibHac.Sdmmc; +using static LibHac.SdmmcSrv.Common; +using MmcPartition = LibHac.Sdmmc.MmcPartition; namespace LibHac.SdmmcSrv; +/// +/// Performs regular patrol reads on the internal MMC. +/// +/// The patrol reader will read sequential half-megabyte blocks of data from the internal MMC, +/// starting at the beginning of the storage, progressing to the end, and then looping back to the beginning. +/// This helps with data integrity by ensuring the entire MMC is regularly read. +/// Every 6 seconds the patrol reader will read a half-megabyte block from the MMC. +/// Every 2 hours it will save the current state of the patrol read to Boot Partition 1 on the MMC. +/// This state contains the next sector index to be read and the number of times the MMC has been patrolled +/// from start to finish. +/// Based on nnSdk 14.3.0 (FS 14.1.0) internal class PatrolReader { + // Note: This class won't work until events and timer events are properly implemented. + + private const int MacSize = HmacSha256.HashSize; + + /// + /// The patrol reader state that's stored in Boot Partition 1. + /// + public struct State + { + public uint CurrentSector; + public uint PatrolCount; + } + + public enum ThreadState + { + Stop, + Active, + Sleep + } + + private readonly int _macSize; + private readonly MmcPartition _statePartition; + private readonly int _patrolStateOffset; + private readonly int _patrolStateSize; + private readonly int _saveStateIntervalSeconds; + private readonly int _patrolReadSize; + private readonly int _waitTimeAfterAllocationSuccess; + private readonly int _waitTimeAfterAllocationFailure; + private readonly int _firstRunDelaySeconds; + private uint _deviceCapacitySectors; + private State _patrolState; + private bool _isPatrolStateLoaded; + private SdkMutex _mutex; + private bool _areEventsInitialized; + private ThreadState _threadState; + // private Thread _patrolReaderThread; + private Event _stateChangeRequestedEvent; + private Event _stateChangeCompletedEvent; + private ulong _allocateSuccessCount; + private ulong _allocateFailureCount; + + // LibHac addition + private SdmmcApi _sdmmc; + + private static ReadOnlySpan PatrolStateKey => "U{W5>1Kq#Gt`f6r86o`9|*||hTy9U2C\0"u8; + public PatrolReader(SdkMutex mutex, SdmmcApi sdmmc) { - throw new NotImplementedException(); + _macSize = MacSize; + _statePartition = MmcPartition.BootPartition1; + _patrolStateOffset = 0x184000; + _patrolStateSize = 0x200; + _saveStateIntervalSeconds = 7200; + _patrolReadSize = 0x80000; + _waitTimeAfterAllocationSuccess = 6; + _waitTimeAfterAllocationFailure = 1; + _firstRunDelaySeconds = 18; + _isPatrolStateLoaded = false; + _mutex = mutex; + _areEventsInitialized = false; + _threadState = ThreadState.Stop; + _allocateSuccessCount = 0; + _allocateFailureCount = 0; + + _sdmmc = sdmmc; } public void Dispose() { - throw new NotImplementedException(); + FinalizeObject(); } public static void PatrolReaderThreadEntry(object args) { - throw new NotImplementedException(); + if (args is PatrolReader reader) + { + reader.PatrolReaderThread(); + } + else + { + Abort.DoAbort($"Expected an argument of type {nameof(PatrolReader)} in {nameof(PatrolReaderThreadEntry)}"); + } } - public void FinalizeObject() + private void FinalizeObject() { - throw new NotImplementedException(); + if (_areEventsInitialized) + { + if (_threadState == ThreadState.Sleep) + { + Resume(); + } + + if (_threadState == ThreadState.Active) + { + Stop(); + } + + _stateChangeRequestedEvent.Dispose(); + _stateChangeCompletedEvent.Dispose(); + + _areEventsInitialized = false; + } } public Result GetPatrolCount(out uint outCount) { - throw new NotImplementedException(); + UnsafeHelpers.SkipParamInit(out outCount); + + if (!_isPatrolStateLoaded) + return ResultFs.HasNotGottenPatrolCount.Log(); + + outCount = _patrolState.PatrolCount; + return Result.Success; } public void GetAndClearAllocateCount(out ulong outSuccessCount, out ulong outFailureCount) { - throw new NotImplementedException(); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); + + outSuccessCount = _allocateSuccessCount; + outFailureCount = _allocateFailureCount; + + _allocateSuccessCount = 0; + _allocateFailureCount = 0; } - public bool LoadState() + private bool LoadState() { - throw new NotImplementedException(); + using var pooledBuffer = new PooledBuffer(_patrolStateSize, 1); + if (pooledBuffer.GetSize() < _patrolStateSize) + return false; + + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); + + Abort.DoAbortUnlessSuccess(_sdmmc.SelectMmcPartition(Port.Mmc0, _statePartition)); + + try + { + Result res = _sdmmc.GetDeviceMemoryCapacity(out _deviceCapacitySectors, Port.Mmc0); + if (res.IsFailure()) + { + return false; + } + + Span patrolStateBuffer = pooledBuffer.GetBuffer().Slice(0, _patrolStateSize); + + res = _sdmmc.Read(patrolStateBuffer, Port.Mmc0, BytesToSectors(_patrolStateOffset), + BytesToSectors(_patrolStateSize)); + + if (res.IsFailure()) + { + return false; + } + + Span mac = stackalloc byte[MacSize]; + + // Load an empty state if the verification fails. + _patrolState = default; + + HmacSha256.GenerateHmacSha256(mac, patrolStateBuffer.Slice(_macSize), PatrolStateKey); + + if (CryptoUtil.IsSameBytes(mac, patrolStateBuffer, _macSize)) + { + ref State readState = ref SpanHelpers.AsStruct(patrolStateBuffer.Slice(_macSize)); + + if (readState.CurrentSector + BytesToSectors(_patrolReadSize) <= _deviceCapacitySectors) + { + _patrolState = readState; + } + } + + return true; + } + finally + { + Abort.DoAbortUnlessSuccess(_sdmmc.SelectMmcPartition(Port.Mmc0, MmcPartition.UserData)); + } } - public bool SaveState() + private void SaveState() { - throw new NotImplementedException(); + using var pooledBuffer = new PooledBuffer(_patrolStateSize, 1); + + if (pooledBuffer.GetSize() < _patrolStateSize) + return; + + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); + + Span patrolStateBuffer = pooledBuffer.GetBuffer().Slice(0, _patrolStateSize); + patrolStateBuffer.Clear(); + + SpanHelpers.AsStruct(patrolStateBuffer) = _patrolState; + + HmacSha256.GenerateHmacSha256(patrolStateBuffer.Slice(0, _macSize), patrolStateBuffer.Slice(_macSize), + PatrolStateKey); + + Abort.DoAbortUnlessSuccess(_sdmmc.SelectMmcPartition(Port.Mmc0, _statePartition)); + + try + { + _sdmmc.Write(Port.Mmc0, BytesToSectors(_patrolStateOffset), BytesToSectors(_patrolStateSize), + patrolStateBuffer); + } + finally + { + Abort.DoAbortUnlessSuccess(_sdmmc.SelectMmcPartition(Port.Mmc0, MmcPartition.UserData)); + } + } + + private bool DoPatrolRead() + { + using var pooledBuffer = new PooledBuffer(_patrolReadSize, 1); + using ScopedLock scopedLock = ScopedLock.Lock(ref _mutex); + + if (pooledBuffer.GetSize() < _patrolReadSize) + { + if (_allocateFailureCount != ulong.MaxValue) + _allocateFailureCount++; + + return false; + } + + if (_allocateSuccessCount != ulong.MaxValue) + _allocateSuccessCount++; + + _sdmmc.Read(pooledBuffer.GetBuffer().Slice(0, _patrolReadSize), Port.Mmc0, _patrolState.CurrentSector, + BytesToSectors(_patrolReadSize)).IgnoreResult(); + + _patrolState.CurrentSector += BytesToSectors(_patrolReadSize); + + if (_patrolState.CurrentSector + BytesToSectors(_patrolReadSize) > _deviceCapacitySectors) + { + _patrolState.CurrentSector = 0; + _patrolState.PatrolCount++; + } + + return true; } public void Start() { - throw new NotImplementedException(); + Assert.SdkAssert(_threadState is ThreadState.Active or ThreadState.Stop); + + if (_threadState == ThreadState.Stop) + { + if (!_areEventsInitialized) + { + _areEventsInitialized = true; + _threadState = ThreadState.Stop; + + _stateChangeRequestedEvent = new Event(_sdmmc.Hos.Os, EventClearMode.AutoClear); + _stateChangeCompletedEvent = new Event(_sdmmc.Hos.Os, EventClearMode.AutoClear); + } + + _threadState = ThreadState.Active; + + // CreateThread(_patrolReaderThread, PatrolReaderThreadEntry, this, pPatrolReaderStack, priority); + // SetThreadNamePointer(_patrolReaderThread, "nn.fs.PatrolReader"u8); + // StartThread(_patrolReaderThread); + } } public void Stop() { - throw new NotImplementedException(); + Assert.SdkAssert(_threadState is ThreadState.Active or ThreadState.Stop or ThreadState.Sleep); + + if (_threadState != ThreadState.Stop) + { + _threadState = ThreadState.Stop; + _stateChangeRequestedEvent.Signal(); + + // WaitThread(_patrolReaderThread); + // DestroyThread(_patrolReaderThread); + } } public void Sleep() { - throw new NotImplementedException(); + Assert.SdkAssert(_threadState is ThreadState.Active or ThreadState.Sleep); + + if (_threadState == ThreadState.Active) + { + _threadState = ThreadState.Sleep; + + _stateChangeRequestedEvent.Signal(); + _stateChangeCompletedEvent.Wait(); + } } public void Resume() { - throw new NotImplementedException(); + Assert.SdkAssert(_threadState is ThreadState.Active or ThreadState.Sleep); + + if (_threadState == ThreadState.Sleep) + { + _threadState = ThreadState.Active; + + _stateChangeRequestedEvent.Signal(); + _stateChangeCompletedEvent.Wait(); + } } - public void PatrolReaderThread() + private void PatrolReaderThread() { - throw new NotImplementedException(); + // Missing: SetServiceContext() + + using var timer = new TimerEvent(_sdmmc.Hos.Os, EventClearMode.AutoClear); + + timer.StartPeriodic(TimeSpan.FromSeconds(_firstRunDelaySeconds), TimeSpan.FromSeconds(_saveStateIntervalSeconds)); + + int currentWaitTime = _waitTimeAfterAllocationSuccess; + + while (true) + { + // Wait until the next thread state change or until the next patrol read should be done. + if (_stateChangeRequestedEvent.TimedWait(TimeSpan.FromSeconds(currentWaitTime))) + { + if (_threadState == ThreadState.Stop) + { + break; + } + + if (_threadState == ThreadState.Sleep) + { + // Acknowledge the request to sleep and wait for the next state change. + _stateChangeCompletedEvent.Signal(); + _stateChangeRequestedEvent.Wait(); + + if (_threadState == ThreadState.Stop) + { + break; + } + + if (_threadState == ThreadState.Active) + { + _stateChangeCompletedEvent.Signal(); + continue; + } + + Assert.SdkAssert(false); + } + } + + if (!_isPatrolStateLoaded) + { + // The patrol state will be loaded a single time when the console is booted. + // Don't load the patrol state or do patrol reads until the specified amount + // of time has past since creating the Patrol Reader. + if (!timer.TryWait() || !LoadState()) + { + continue; + } + + _isPatrolStateLoaded = true; + } + + if (DoPatrolRead()) + { + currentWaitTime = _waitTimeAfterAllocationSuccess; + } + else + { + currentWaitTime = _waitTimeAfterAllocationFailure; + } + + // Save the patrol state periodically once the specified amount of time has passed since the last save. + if (timer.TryWait()) + { + SaveState(); + } + } + + // Save the patrol state if necessary when shutting down. + if (_isPatrolStateLoaded) + SaveState(); } } \ No newline at end of file