diff --git a/src/LibHac/Diag/DiagClient.cs b/src/LibHac/Diag/DiagClient.cs new file mode 100644 index 00000000..b4990101 --- /dev/null +++ b/src/LibHac/Diag/DiagClient.cs @@ -0,0 +1,38 @@ +namespace LibHac.Diag +{ + public class DiagClient + { + internal DiagClientGlobals Globals; + + public DiagClientImpl Impl => new DiagClientImpl(this); + internal HorizonClient Hos => Globals.Hos; + + public DiagClient(HorizonClient horizonClient) + { + Globals.Initialize(this, horizonClient); + } + } + + internal struct DiagClientGlobals + { + public HorizonClient Hos; + public object InitMutex; + public LogObserverGlobals LogObserver; + + public void Initialize(DiagClient diagClient, HorizonClient horizonClient) + { + Hos = horizonClient; + InitMutex = new object(); + } + } + + // Functions in the nn::diag::detail namespace use this struct. + public readonly struct DiagClientImpl + { + internal readonly DiagClient Diag; + internal HorizonClient Hos => Diag.Hos; + internal ref DiagClientGlobals Globals => ref Diag.Globals; + + internal DiagClientImpl(DiagClient parentClient) => Diag = parentClient; + } +} diff --git a/src/LibHac/Diag/Impl/ObserverManager.cs b/src/LibHac/Diag/Impl/ObserverManager.cs new file mode 100644 index 00000000..3ec47b53 --- /dev/null +++ b/src/LibHac/Diag/Impl/ObserverManager.cs @@ -0,0 +1,145 @@ +using System.Collections.Generic; +using LibHac.Os; + +namespace LibHac.Diag.Impl +{ + internal interface IObserverHolder + { + bool IsRegistered { get; set; } + } + + internal class ObserverManager where TObserver : IObserverHolder + { + private LinkedList _observers; + private ReaderWriterLock _rwLock; + + public delegate void Function(ref TObserver observer, in TItem item); + + public ObserverManager(HorizonClient hos) + { + _observers = new LinkedList(); + _rwLock = new ReaderWriterLock(hos.Os); + } + + public void RegisterObserver(TObserver observerHolder) + { + Assert.False(observerHolder.IsRegistered); + + using ScopedLock lk = ScopedLock.Lock(ref _rwLock); + + _observers.AddFirst(observerHolder); + observerHolder.IsRegistered = true; + } + + public void UnregisterObserver(TObserver observerHolder) + { + Assert.True(observerHolder.IsRegistered); + + using ScopedLock lk = ScopedLock.Lock(ref _rwLock); + + LinkedListNode foundObserver = _observers.Find(observerHolder); + if (foundObserver is not null) + { + _observers.Remove(foundObserver); + } + + observerHolder.IsRegistered = false; + } + + public void UnregisterAllObservers() + { + using ScopedLock lk = ScopedLock.Lock(ref _rwLock); + + LinkedListNode curNode = _observers.First; + + while (curNode is not null) + { + curNode.ValueRef.IsRegistered = false; + curNode = curNode.Next; + } + + _observers.Clear(); + } + + public void InvokeAllObserver(in TItem item, Function function) + { + using ScopedLock lk = ScopedLock.Lock(ref _rwLock); + + LinkedListNode curNode = _observers.First; + + while (curNode is not null) + { + function(ref curNode.ValueRef, in item); + curNode = curNode.Next; + } + } + } + + // Todo: Use generic class when ref structs can be used as generics + internal class LogObserverManager + { + private readonly LinkedList _observers; + private ReaderWriterLock _rwLock; + + public delegate void Function(ref LogObserverHolder observer, in LogObserverContext item); + + public LogObserverManager(HorizonClient hos) + { + _observers = new LinkedList(); + _rwLock = new ReaderWriterLock(hos.Os); + } + + public void RegisterObserver(LogObserverHolder observerHolder) + { + Assert.False(observerHolder.IsRegistered); + + using ScopedLock lk = ScopedLock.Lock(ref _rwLock); + + _observers.AddFirst(observerHolder); + observerHolder.IsRegistered = true; + } + + public void UnregisterObserver(LogObserverHolder observerHolder) + { + Assert.True(observerHolder.IsRegistered); + + using ScopedLock lk = ScopedLock.Lock(ref _rwLock); + + LinkedListNode foundObserver = _observers.Find(observerHolder); + if (foundObserver is not null) + { + _observers.Remove(foundObserver); + } + + observerHolder.IsRegistered = false; + } + + public void UnregisterAllObservers() + { + using ScopedLock lk = ScopedLock.Lock(ref _rwLock); + + LinkedListNode curNode = _observers.First; + + while (curNode is not null) + { + curNode.ValueRef.IsRegistered = false; + curNode = curNode.Next; + } + + _observers.Clear(); + } + + public void InvokeAllObserver(in LogObserverContext item, Function function) + { + using ScopedLock lk = ScopedLock.Lock(ref _rwLock); + + LinkedListNode curNode = _observers.First; + + while (curNode is not null) + { + function(ref curNode.ValueRef, in item); + curNode = curNode.Next; + } + } + } +} diff --git a/src/LibHac/Diag/Log.cs b/src/LibHac/Diag/Log.cs new file mode 100644 index 00000000..eb2e4504 --- /dev/null +++ b/src/LibHac/Diag/Log.cs @@ -0,0 +1,47 @@ +using System; +using System.Runtime.CompilerServices; +using LibHac.Common; + +namespace LibHac.Diag +{ + public static class Log + { + // Todo: Should we split large logs into smaller chunks like Horizon does? + public static void LogImpl(this DiagClientImpl diag, in LogMetaData metaData, ReadOnlySpan message) + { + diag.PutImpl(in metaData, message); + } + + public static void PutImpl(this DiagClientImpl diag, in LogMetaData metaData, ReadOnlySpan message) + { + var logBody = new LogBody + { + Message = new U8Span(message), + IsHead = true, + IsTail = true + }; + + diag.CallAllLogObserver(in metaData, in logBody); + } + + public static void LogImpl(this DiagClientImpl diag, ReadOnlySpan moduleName, LogSeverity severity, + ReadOnlySpan message, [CallerLineNumber] int lineNumber = 0, [CallerFilePath] string fileName = "", + [CallerMemberName] string functionName = "") + { + var metaData = new LogMetaData + { + SourceInfo = new SourceInfo + { + LineNumber = lineNumber, + FileName = fileName, + FunctionName = functionName + }, + ModuleName = new U8Span(moduleName), + Severity = severity, + Verbosity = 0 + }; + + diag.LogImpl(in metaData, message); + } + } +} diff --git a/src/LibHac/Diag/LogObserver.cs b/src/LibHac/Diag/LogObserver.cs new file mode 100644 index 00000000..6464bcaa --- /dev/null +++ b/src/LibHac/Diag/LogObserver.cs @@ -0,0 +1,110 @@ +using LibHac.Common; +using LibHac.Diag.Impl; + +namespace LibHac.Diag +{ + public delegate void LogObserver(in LogMetaData metaData, in LogBody body, object arguments); + + internal struct LogObserverGlobals + { + public LogObserverHolder DefaultLogObserverHolder; + + public nint ManagerGuard; + public LogObserverManager Manager; + } + + public class LogObserverHolder + { + internal LogObserver Observer; + internal bool IsRegistered; + internal object Arguments; + } + + public static class LogObserverFuncs + { + public static void InitializeLogObserverHolder(this DiagClient diag, ref LogObserverHolder holder, + LogObserver observer, object arguments) + { + holder.Observer = observer; + holder.IsRegistered = false; + holder.Arguments = arguments; + } + + public static void RegisterLogObserver(this DiagClient diag, LogObserverHolder observerHolder) + { + diag.Impl.GetLogObserverManager().RegisterObserver(observerHolder); + } + + public static void UnregisterLogObserver(this DiagClient diag, LogObserverHolder observerHolder) + { + diag.Impl.GetLogObserverManager().UnregisterObserver(observerHolder); + } + + private static void TentativeDefaultLogObserver(in LogMetaData metaData, in LogBody body, object arguments) + { + + } + + private static LogObserverManager GetLogObserverManager(this DiagClientImpl diag) + { + ref LogObserverGlobals g = ref diag.Globals.LogObserver; + using var guard = new InitializationGuard(ref g.ManagerGuard, diag.Globals.InitMutex); + + if (!guard.IsInitialized) + { + g.Manager = new LogObserverManager(diag.Hos); + g.DefaultLogObserverHolder = new LogObserverHolder(); + diag.Diag.InitializeLogObserverHolder(ref g.DefaultLogObserverHolder, TentativeDefaultLogObserver, null); + g.Manager.RegisterObserver(g.DefaultLogObserverHolder); + } + + return g.Manager; + } + + internal static void CallAllLogObserver(this DiagClientImpl diag, in LogMetaData metaData, in LogBody body) + { + var context = new LogObserverContext + { + MetaData = metaData, + Body = body + }; + + LogObserverManager manager = diag.GetLogObserverManager(); + + manager.InvokeAllObserver(in context, InvokeFunction); + + static void InvokeFunction(ref LogObserverHolder holder, in LogObserverContext item) + { + holder.Observer(in item.MetaData, in item.Body, holder.Arguments); + } + } + + internal static void ReplaceDefaultLogObserver(this DiagClientImpl diag, LogObserver observer) + { + ref LogObserverGlobals g = ref diag.Globals.LogObserver; + + diag.Diag.UnregisterLogObserver(g.DefaultLogObserverHolder); + diag.Diag.InitializeLogObserverHolder(ref g.DefaultLogObserverHolder, observer, null); + diag.Diag.RegisterLogObserver(g.DefaultLogObserverHolder); + } + + internal static void ResetDefaultLogObserver(this DiagClientImpl diag) + { + ref LogObserverGlobals g = ref diag.Globals.LogObserver; + + diag.Diag.UnregisterLogObserver(g.DefaultLogObserverHolder); + diag.Diag.InitializeLogObserverHolder(ref g.DefaultLogObserverHolder, TentativeDefaultLogObserver, null); + diag.Diag.RegisterLogObserver(g.DefaultLogObserverHolder); + } + } +} + +namespace LibHac.Diag.Impl +{ + // Todo: Make fields references once C# 10 is released + internal ref struct LogObserverContext + { + public LogMetaData MetaData; + public LogBody Body; + } +} diff --git a/src/LibHac/Diag/LogTypes.cs b/src/LibHac/Diag/LogTypes.cs new file mode 100644 index 00000000..eb673f15 --- /dev/null +++ b/src/LibHac/Diag/LogTypes.cs @@ -0,0 +1,38 @@ +using System; +using LibHac.Common; + +namespace LibHac.Diag +{ + public enum LogSeverity + { + Trace, + Info, + Warn, + Error, + Fatal + } + + public ref struct LogMetaData + { + public SourceInfo SourceInfo; + public U8Span ModuleName; + public LogSeverity Severity; + public int Verbosity; + public bool UseDefaultLocaleCharset; + public Span AdditionalData; + } + + public struct SourceInfo + { + public int LineNumber; + public string FileName; + public string FunctionName; + } + + public ref struct LogBody + { + public U8Span Message; + public bool IsHead; + public bool IsTail; + } +} diff --git a/src/LibHac/Horizon.cs b/src/LibHac/Horizon.cs index b27742b6..f9c30aae 100644 --- a/src/LibHac/Horizon.cs +++ b/src/LibHac/Horizon.cs @@ -24,7 +24,7 @@ namespace LibHac { _currentProcessId = InitialProcessCountMax; - TickGenerator = config.TickGenerator; + TickGenerator = config.TickGenerator ?? new DefaultTickGenerator(); ServiceManager = new ServiceManager(); LoaderClient = CreatePrivilegedHorizonClient(); diff --git a/src/LibHac/HorizonClient.cs b/src/LibHac/HorizonClient.cs index 6b0f6d3a..f03b5ac9 100644 --- a/src/LibHac/HorizonClient.cs +++ b/src/LibHac/HorizonClient.cs @@ -1,5 +1,6 @@ using System; using LibHac.Arp; +using LibHac.Diag; using LibHac.Fs; using LibHac.Lr; using LibHac.Os; @@ -18,6 +19,7 @@ namespace LibHac public FileSystemClient Fs { get; } public ServiceManagerClient Sm { get; } public OsState Os { get; } + public DiagClient Diag { get; } public LrClient Lr { get; } public ArpClient Arp => ArpLazy.Value; @@ -29,6 +31,7 @@ namespace LibHac Fs = new FileSystemClient(this); Sm = new ServiceManagerClient(Horizon.ServiceManager); Os = new OsState(this, horizon.TickGenerator); + Diag = new DiagClient(this); Lr = new LrClient(this); ArpLazy = new Lazy(InitArpClient, true); diff --git a/src/LibHac/HorizonConfiguration.cs b/src/LibHac/HorizonConfiguration.cs index 6e665ea3..71a7535d 100644 --- a/src/LibHac/HorizonConfiguration.cs +++ b/src/LibHac/HorizonConfiguration.cs @@ -14,9 +14,4 @@ namespace LibHac /// public ITickGenerator? TickGenerator { get; set; } } - - public interface ITickGenerator - { - Tick GetCurrentTick(); - } } diff --git a/src/LibHac/Os/ITickGenerator.cs b/src/LibHac/Os/ITickGenerator.cs new file mode 100644 index 00000000..050d7c67 --- /dev/null +++ b/src/LibHac/Os/ITickGenerator.cs @@ -0,0 +1,24 @@ +using System.Diagnostics; + +namespace LibHac.Os +{ + public interface ITickGenerator + { + Tick GetCurrentTick(); + } + + internal class DefaultTickGenerator : ITickGenerator + { + private readonly long _initialTick; + + public DefaultTickGenerator() + { + _initialTick = Stopwatch.GetTimestamp(); + } + + public Tick GetCurrentTick() + { + return new Tick(Stopwatch.GetTimestamp() - _initialTick); + } + } +} diff --git a/src/hactoolnet/Options.cs b/src/hactoolnet/Options.cs index bd73f28a..33b723eb 100644 --- a/src/hactoolnet/Options.cs +++ b/src/hactoolnet/Options.cs @@ -1,6 +1,5 @@ using LibHac; using LibHac.Common.Keys; -using LibHac.Fs; using LibHac.FsSystem; namespace hactoolnet @@ -100,6 +99,6 @@ namespace hactoolnet public Options Options; public KeySet KeySet; public ProgressBar Logger; - public FileSystemClient FsClient; + public HorizonClient Horizon; } } diff --git a/src/hactoolnet/ProcessNca.cs b/src/hactoolnet/ProcessNca.cs index e3ba8ffb..5b3e82a2 100644 --- a/src/hactoolnet/ProcessNca.cs +++ b/src/hactoolnet/ProcessNca.cs @@ -4,6 +4,7 @@ using LibHac; using LibHac.Common; using LibHac.Fs; using LibHac.Fs.Fsa; +using LibHac.Fs.Impl; using LibHac.FsSystem; using LibHac.FsSystem.NcaUtils; using LibHac.Npdm; @@ -45,7 +46,7 @@ namespace hactoolnet if (ctx.Options.SectionOutDir[i] != null) { - FileSystemClient fs = ctx.FsClient; + FileSystemClient fs = ctx.Horizon.Fs; string mountName = $"section{i}"; @@ -96,11 +97,14 @@ namespace hactoolnet if (ctx.Options.RomfsOutDir != null) { - FileSystemClient fs = ctx.FsClient; + FileSystemClient fs = ctx.Horizon.Fs; fs.Register("rom".ToU8Span(), OpenFileSystemByType(NcaSectionType.Data)); fs.Register("output".ToU8Span(), new LocalFileSystem(ctx.Options.RomfsOutDir)); + fs.Impl.EnableFileSystemAccessorAccessLog("rom".ToU8Span()); + fs.Impl.EnableFileSystemAccessorAccessLog("output".ToU8Span()); + FsUtils.CopyDirectoryWithProgress(fs, "rom:/".ToU8Span(), "output:/".ToU8Span(), logger: ctx.Logger).ThrowIfFailure(); fs.Unmount("rom".ToU8Span()); @@ -153,7 +157,7 @@ namespace hactoolnet if (ctx.Options.ExefsOutDir != null) { - FileSystemClient fs = ctx.FsClient; + FileSystemClient fs = ctx.Horizon.Fs; fs.Register("code".ToU8Span(), OpenFileSystemByType(NcaSectionType.Code)); fs.Register("output".ToU8Span(), new LocalFileSystem(ctx.Options.ExefsOutDir)); diff --git a/src/hactoolnet/ProcessSave.cs b/src/hactoolnet/ProcessSave.cs index f717d56a..6ef580ac 100644 --- a/src/hactoolnet/ProcessSave.cs +++ b/src/hactoolnet/ProcessSave.cs @@ -31,7 +31,7 @@ namespace hactoolnet bool signNeeded = ctx.Options.SignSave; var save = new SaveDataFileSystem(ctx.KeySet, file, ctx.Options.IntegrityLevel, true); - FileSystemClient fs = ctx.FsClient; + FileSystemClient fs = ctx.Horizon.Fs; fs.Register("save".ToU8Span(), save); diff --git a/src/hactoolnet/Program.cs b/src/hactoolnet/Program.cs index fa174dac..4679700d 100644 --- a/src/hactoolnet/Program.cs +++ b/src/hactoolnet/Program.cs @@ -3,6 +3,7 @@ using System.IO; using System.Text; using LibHac; using LibHac.Common.Keys; +using LibHac.Diag; using LibHac.Fs; using LibHac.Util; @@ -61,6 +62,7 @@ namespace hactoolnet StreamWriter logWriter = null; ResultLogger resultLogger = null; + LogObserverHolder logObserver = null; try { @@ -71,14 +73,23 @@ namespace hactoolnet Horizon horizon = HorizonFactory.CreateWithDefaultFsConfig(new HorizonConfiguration(), new InMemoryFileSystem(), ctx.KeySet); - ctx.FsClient = horizon.CreatePrivilegedHorizonClient().Fs; + ctx.Horizon = horizon.CreatePrivilegedHorizonClient(); if (ctx.Options.AccessLog != null) { logWriter = new StreamWriter(ctx.Options.AccessLog); + logObserver = new LogObserverHolder(); - ctx.FsClient.SetLocalSystemAccessLogForDebug(true); - ctx.FsClient.SetGlobalAccessLogMode(GlobalAccessLogMode.Log); + // ReSharper disable once AccessToDisposedClosure + // References to logWriter should be gone by the time it's disposed + ctx.Horizon.Diag.InitializeLogObserverHolder(ref logObserver, + (in LogMetaData data, in LogBody body, object arguments) => + logWriter.Write(body.Message.ToString()), null); + + ctx.Horizon.Diag.RegisterLogObserver(logObserver); + + ctx.Horizon.Fs.SetLocalSystemAccessLogForDebug(true); + ctx.Horizon.Fs.SetGlobalAccessLogMode(GlobalAccessLogMode.Log).ThrowIfFailure(); } if (ctx.Options.ResultLog != null) @@ -100,6 +111,11 @@ namespace hactoolnet } finally { + if (logObserver != null) + { + ctx.Horizon.Diag.UnregisterLogObserver(logObserver); + } + logWriter?.Dispose(); if (resultLogger != null)