From cccb811293cc48c83514de3cb00c441c17e2843f Mon Sep 17 00:00:00 2001 From: Alex Barney Date: Fri, 25 Oct 2019 22:13:09 -0500 Subject: [PATCH] Add Result methods for debugging (#90) - Allow setting a callback function for when Result.Log is called. - Allow setting a function that returns a name for a Result value. - Print Result name in error messages --- src/LibHac/HorizonResultException.cs | 4 +- src/LibHac/Result.cs | 39 ++++++++++++- src/hactoolnet/CliParser.cs | 1 + src/hactoolnet/Options.cs | 1 + src/hactoolnet/Program.cs | 17 +++++- src/hactoolnet/ResultLog.cs | 87 ++++++++++++++++++++++++++++ 6 files changed, 145 insertions(+), 4 deletions(-) create mode 100644 src/hactoolnet/ResultLog.cs diff --git a/src/LibHac/HorizonResultException.cs b/src/LibHac/HorizonResultException.cs index 3b075898..209176bb 100644 --- a/src/LibHac/HorizonResultException.cs +++ b/src/LibHac/HorizonResultException.cs @@ -82,10 +82,10 @@ namespace LibHac { if (!string.IsNullOrWhiteSpace(InnerMessage)) { - return $"{ResultValue.ErrorCode}: {InnerMessage}"; + return $"{ResultValue.ToStringWithName()}: {InnerMessage}"; } - return ResultValue.ErrorCode; + return ResultValue.ToStringWithName(); } } } diff --git a/src/LibHac/Result.cs b/src/LibHac/Result.cs index d9fa6cf6..62c8e167 100644 --- a/src/LibHac/Result.cs +++ b/src/LibHac/Result.cs @@ -4,7 +4,7 @@ using System.Diagnostics; namespace LibHac { [Serializable] - [DebuggerDisplay("{ToString()}")] + [DebuggerDisplay("{ToStringWithName(),nq}")] public struct Result : IEquatable { public readonly int Value; @@ -42,6 +42,9 @@ namespace LibHac /// The called value. public Result Log() { +#if DEBUG + LogCallback?.Invoke(this); +#endif return this; } @@ -52,9 +55,43 @@ namespace LibHac /// The called value. public Result LogConverted(Result originalResult) { +#if DEBUG + ConvertedLogCallback?.Invoke(this, originalResult); +#endif return this; } + public delegate void ResultLogger(Result result); + public delegate void ConvertedResultLogger(Result result, Result originalResult); + public delegate bool ResultNameGetter(Result result, out string name); + + public static ResultLogger LogCallback { get; set; } + public static ConvertedResultLogger ConvertedLogCallback { get; set; } + public static ResultNameGetter GetResultNameHandler { get; set; } + + public bool TryGetResultName(out string name) + { + ResultNameGetter func = GetResultNameHandler; + + if (func == null) + { + name = default; + return false; + } + + return func(this, out name); + } + + public string ToStringWithName() + { + if (TryGetResultName(out string name)) + { + return $"{name} ({ErrorCode})"; + } + + return ErrorCode; + } + public override string ToString() { return IsSuccess() ? "Success" : ErrorCode; diff --git a/src/hactoolnet/CliParser.cs b/src/hactoolnet/CliParser.cs index 3f8ab7f1..c03d6785 100644 --- a/src/hactoolnet/CliParser.cs +++ b/src/hactoolnet/CliParser.cs @@ -19,6 +19,7 @@ namespace hactoolnet new CliOption("titlekeys", 1, (o, a) => o.TitleKeyFile = a[0]), new CliOption("consolekeys", 1, (o, a) => o.ConsoleKeyFile = a[0]), new CliOption("accesslog", 1, (o, a) => o.AccessLog = a[0]), + new CliOption("resultlog", 1, (o, a) => o.ResultLog = a[0]), new CliOption("section0", 1, (o, a) => o.SectionOut[0] = a[0]), new CliOption("section1", 1, (o, a) => o.SectionOut[1] = a[0]), new CliOption("section2", 1, (o, a) => o.SectionOut[2] = a[0]), diff --git a/src/hactoolnet/Options.cs b/src/hactoolnet/Options.cs index c99ac0a8..a9280de9 100644 --- a/src/hactoolnet/Options.cs +++ b/src/hactoolnet/Options.cs @@ -16,6 +16,7 @@ namespace hactoolnet public string TitleKeyFile; public string ConsoleKeyFile; public string AccessLog; + public string ResultLog; public string[] SectionOut = new string[4]; public string[] SectionOutDir = new string[4]; public string HeaderOut; diff --git a/src/hactoolnet/Program.cs b/src/hactoolnet/Program.cs index fdd01016..6058fdca 100644 --- a/src/hactoolnet/Program.cs +++ b/src/hactoolnet/Program.cs @@ -25,7 +25,7 @@ namespace hactoolnet if (ex.ResultValue != ex.InternalResultValue) { - Console.Error.WriteLine($"Internal Code: {ex.InternalResultValue.ErrorCode}"); + Console.Error.WriteLine($"Internal Code: {ex.InternalResultValue.ToStringWithName()}"); } Console.Error.WriteLine(); @@ -52,9 +52,12 @@ namespace hactoolnet if (ctx.Options == null) return false; StreamWriter logWriter = null; + StreamWriter resultWriter = null; try { + Result.GetResultNameHandler = ResultLogFunctions.TryGetResultName; + using (var logger = new ProgressBar()) { ctx.Logger = logger; @@ -71,6 +74,15 @@ namespace hactoolnet ctx.Horizon.Fs.SetAccessLogObject(accessLog); } + if (ctx.Options.ResultLog != null) + { + resultWriter = new StreamWriter(ctx.Options.ResultLog); + ResultLogFunctions.LogWriter = resultWriter; + + Result.LogCallback = ResultLogFunctions.LogResult; + Result.ConvertedLogCallback = ResultLogFunctions.LogConvertedResult; + } + OpenKeyset(ctx); if (ctx.Options.RunCustom) @@ -85,6 +97,9 @@ namespace hactoolnet finally { logWriter?.Dispose(); + resultWriter?.Dispose(); + + ResultLogFunctions.LogWriter = null; } return true; diff --git a/src/hactoolnet/ResultLog.cs b/src/hactoolnet/ResultLog.cs new file mode 100644 index 00000000..ad4da3a6 --- /dev/null +++ b/src/hactoolnet/ResultLog.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Reflection; +using LibHac; +using LibHac.Fs; + +namespace hactoolnet +{ + public static class ResultLogFunctions + { + private static Dictionary ResultNames { get; } = GetResultNames(); + + public static TextWriter LogWriter { get; set; } + + public static void LogResult(Result result) + { + if (LogWriter == null) return; + + var st = new StackTrace(2, true); + + if (st.FrameCount > 1) + { + MethodBase method = st.GetFrame(0).GetMethod(); + + // This result from these functions is usually noise because they + // are frequently used to detect if a file exists + if (result == ResultFs.PathNotFound && + typeof(IFileSystem).IsAssignableFrom(method.DeclaringType) && + method.Name.StartsWith(nameof(IFileSystem.GetEntryType)) || + method.Name.StartsWith(nameof(IAttributeFileSystem.GetFileAttributes))) + { + // return; + } + + string methodName = $"{method.DeclaringType?.FullName}.{method.Name}"; + + LogWriter.WriteLine($"{result.ToStringWithName()} returned by {methodName}"); + LogWriter.WriteLine(st); + } + } + + public static void LogConvertedResult(Result result, Result originalResult) + { + if (LogWriter == null) return; + + var st = new StackTrace(2, false); + + if (st.FrameCount > 1) + { + MethodBase method = st.GetFrame(0).GetMethod(); + + string methodName = $"{method.DeclaringType?.FullName}.{method.Name}"; + + LogWriter.WriteLine($"{originalResult.ToStringWithName()} was converted to {result.ToStringWithName()} by {methodName}"); + } + } + + public static Dictionary GetResultNames() + { + var dict = new Dictionary(); + + Assembly assembly = typeof(Result).Assembly; + + foreach (Type type in assembly.GetTypes().Where(x => x.Name.Contains("Result"))) + { + foreach (PropertyInfo property in type.GetProperties() + .Where(x => x.PropertyType == typeof(Result) && x.GetMethod.IsStatic && x.SetMethod == null)) + { + var value = (Result)property.GetValue(null, null); + string name = $"{type.Name}{property.Name}"; + + dict.Add(value, name); + } + } + + return dict; + } + + public static bool TryGetResultName(Result result, out string name) + { + return ResultNames.TryGetValue(result, out name); + } + } +}