using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Diagnostics.Eventing.Reader;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security;
using System.Security.Permissions;
using System.Text;
using System.Threading;
using System.Xml;
using CatLib.Diagnostics;
using Microsoft.CodeAnalysis;
[assembly: CompilationRelaxations(8)]
[assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)]
[assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)]
[assembly: TargetFramework(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]
[assembly: AssemblyCompany("CatLib contributors")]
[assembly: AssemblyConfiguration("Release")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("1.0.0+4162013e7c932bfa1cf0340558d83186f1fd5817")]
[assembly: AssemblyProduct("CatLib")]
[assembly: AssemblyTitle("CatLib.CrashWatcher")]
[assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)]
[assembly: AssemblyVersion("1.0.0.0")]
[module: UnverifiableCode]
[module: RefSafetyRules(11)]
namespace Microsoft.CodeAnalysis
{
[CompilerGenerated]
[Embedded]
internal sealed class EmbeddedAttribute : Attribute
{
}
}
namespace System.Runtime.CompilerServices
{
[CompilerGenerated]
[Embedded]
[AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)]
internal sealed class RefSafetyRulesAttribute : Attribute
{
public readonly int Version;
public RefSafetyRulesAttribute(int P_0)
{
Version = P_0;
}
}
}
namespace CatLib.Diagnostics
{
internal sealed class CrashSession
{
public const int CurrentFormat = 1;
public const string FormatKey = "format";
public const string CatLibKey = "catlib";
public const string GameKey = "game";
public const string GameBuildKey = "gameBuild";
public const string StartedKey = "started";
public const string ProcessKey = "pid";
public const string LanguageKey = "language";
public const string PlayerLogKey = "playerLog";
public const string BepInExLogKey = "bepinexLog";
public const string ReportsKey = "reports";
public const string ModKey = "mod";
public const string EventKey = "event";
public const string CleanKey = "clean";
public const string MainThreadKey = "mainThread";
public const string ExceptionKey = "exception";
public const string TextPrefix = "text.";
public int Format { get; set; }
public bool IsNewerFormat => Format > 1;
public string WatcherVersion { get; set; } = string.Empty;
public string CatLibVersion { get; set; } = string.Empty;
public string GameVersion { get; set; } = string.Empty;
public string GameBuild { get; set; } = string.Empty;
public string Started { get; set; } = string.Empty;
public int ProcessId { get; set; }
public string Language { get; set; } = "en";
public string PlayerLog { get; set; } = string.Empty;
public string BepInExLog { get; set; } = string.Empty;
public string ReportsDirectory { get; set; } = string.Empty;
public List<string> Mods { get; } = new List<string>();
public List<string> Events { get; } = new List<string>();
public string CleanExit { get; set; }
public int MainThreadId { get; set; }
public List<string> ExceptionLines { get; } = new List<string>();
public Dictionary<string, string> Texts { get; } = new Dictionary<string, string>(StringComparer.Ordinal);
public CrashStrings Strings => new CrashStrings(Texts);
public static string Line(string key, string value)
{
return key + "=" + Flatten(value);
}
public IEnumerable<string> HeaderLines()
{
yield return Line("format", Format.ToString(CultureInfo.InvariantCulture));
yield return Line("catlib", CatLibVersion);
yield return Line("game", GameVersion);
yield return Line("started", Started);
yield return Line("pid", ProcessId.ToString(CultureInfo.InvariantCulture));
yield return Line("language", Language);
yield return Line("playerLog", PlayerLog);
yield return Line("bepinexLog", BepInExLog);
yield return Line("reports", ReportsDirectory);
if (MainThreadId != 0)
{
yield return Line("mainThread", MainThreadId.ToString(CultureInfo.InvariantCulture));
}
foreach (string mod in Mods)
{
yield return Line("mod", mod);
}
}
public static CrashSession Parse(IEnumerable<string> lines)
{
CrashSession crashSession = new CrashSession();
foreach (string line in lines)
{
int num = line?.IndexOf('=') ?? (-1);
if (num <= 0)
{
continue;
}
string text = line.Substring(0, num);
string text2 = line.Substring(num + 1);
if (text.StartsWith("text.", StringComparison.Ordinal))
{
crashSession.Texts[text.Substring("text.".Length)] = text2;
continue;
}
switch (text)
{
case "format":
{
crashSession.Format = (int.TryParse(text2, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result3) ? result3 : 0);
break;
}
case "catlib":
crashSession.CatLibVersion = text2;
break;
case "game":
crashSession.GameVersion = text2;
break;
case "gameBuild":
crashSession.GameBuild = text2;
break;
case "started":
crashSession.Started = text2;
break;
case "pid":
{
crashSession.ProcessId = (int.TryParse(text2, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result2) ? result2 : 0);
break;
}
case "language":
crashSession.Language = text2;
break;
case "playerLog":
crashSession.PlayerLog = text2;
break;
case "bepinexLog":
crashSession.BepInExLog = text2;
break;
case "reports":
crashSession.ReportsDirectory = text2;
break;
case "mod":
crashSession.Mods.Add(text2);
break;
case "event":
crashSession.Events.Add(text2);
break;
case "clean":
crashSession.CleanExit = text2;
break;
case "exception":
crashSession.ExceptionLines.Add(text2);
break;
case "mainThread":
{
crashSession.MainThreadId = (int.TryParse(text2, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? result : 0);
break;
}
}
}
return crashSession;
}
private static string Flatten(string value)
{
return (value ?? string.Empty).Replace('\r', ' ').Replace('\n', ' ');
}
}
internal sealed class CrashEventInfo
{
public string Module { get; set; } = string.Empty;
public string Offset { get; set; } = string.Empty;
public string ExceptionCode { get; set; } = string.Empty;
public string ModulePath { get; set; } = string.Empty;
public string RuntimeMessage { get; set; } = string.Empty;
public int ThreadId { get; set; }
public bool FromDebugger { get; set; }
public bool Early { get; set; }
public string Access { get; set; } = string.Empty;
public int HangSeconds { get; set; }
public bool Hung => HangSeconds > 0;
public bool HasFault => Module.Length > 0;
}
internal sealed class CrashModule
{
public string Path { get; }
public string Name => Path.Substring(Path.LastIndexOfAny(new char[2] { '\\', '/' }) + 1);
public ulong Start { get; }
public ulong Size { get; }
public CrashModule(string path, ulong start, ulong size)
{
Path = path ?? string.Empty;
Start = start;
Size = size;
}
}
internal static class CrashText
{
public const int LogTailLines = 40;
public const int EventTailLines = 15;
public const int DetailEventLines = 6;
public const int HeadlineLength = 200;
public const string ApplicationErrorProvider = "Application Error";
public const string RuntimeProvider = ".NET Runtime";
public const string ExecuteAccess = "execute";
public static bool IsNormalExit(uint exitCode)
{
return exitCode == 0;
}
public static string AccessText(ulong kind, ulong address)
{
return kind switch
{
8uL => "execute",
1uL => "write",
0uL => "read",
_ => "access " + kind.ToString(CultureInfo.InvariantCulture),
} + " at " + Hex64(address);
}
public static string ExitKey(uint exitCode)
{
switch (exitCode)
{
case 1u:
if (exitCode != 1)
{
break;
}
return "exit.closedOutside";
case 0u:
return "exit.normal";
case 3221225477u:
return "exit.accessViolation";
case 3221226356u:
return "exit.heapCorruption";
case 3221226505u:
return "exit.failFast";
case 3221225725u:
return "exit.stackOverflow";
case 3221225501u:
return "exit.illegalInstruction";
case 3221225620u:
return "exit.divideByZero";
case 3221225786u:
return "exit.consoleClosed";
case 3762504530u:
return "exit.dotnetException";
case 2148734499u:
return "exit.dotnetFailFast";
}
return "exit.unknown";
}
public static string DescribeExitCode(uint exitCode, CrashStrings strings)
{
return (strings ?? CrashStrings.EnglishOnly).Get(ExitKey(exitCode));
}
public static string Hex(uint value)
{
return "0x" + value.ToString("X8", CultureInfo.InvariantCulture);
}
public static string Hex64(ulong value)
{
return "0x" + value.ToString("x16", CultureInfo.InvariantCulture);
}
public static CrashEventInfo Locate(IList<CrashModule> modules, ulong address, uint exceptionCode, int threadId)
{
CrashEventInfo crashEventInfo = new CrashEventInfo
{
ExceptionCode = "0x" + exceptionCode.ToString("x8", CultureInfo.InvariantCulture),
ThreadId = threadId,
FromDebugger = true
};
foreach (CrashModule item in modules ?? new List<CrashModule>())
{
if (address >= item.Start && address - item.Start < item.Size)
{
crashEventInfo.Module = item.Name;
crashEventInfo.ModulePath = item.Path;
crashEventInfo.Offset = Hex64(address - item.Start);
return crashEventInfo;
}
}
crashEventInfo.Module = "?";
crashEventInfo.Offset = Hex64(address);
return crashEventInfo;
}
public static bool IsManaged(CrashEventInfo info)
{
if (info.Module == "?")
{
return !info.Access.StartsWith("execute", StringComparison.Ordinal);
}
string text = info.ModulePath.Replace('/', '\\');
if (!string.Equals(info.Module, "coreclr.dll", StringComparison.OrdinalIgnoreCase) && !string.Equals(info.Module, "clrjit.dll", StringComparison.OrdinalIgnoreCase) && text.IndexOf("\\dotnet\\", StringComparison.OrdinalIgnoreCase) < 0)
{
return text.IndexOf("\\BepInEx\\", StringComparison.OrdinalIgnoreCase) >= 0;
}
return true;
}
public static CrashEventInfo WithManagedException(CrashEventInfo info, CrashSession session)
{
if (info != null && info.RuntimeMessage.Length == 0 && session != null && session.ExceptionLines.Count > 0)
{
info.RuntimeMessage = string.Join("\n", session.ExceptionLines);
}
return info;
}
public static CrashEventInfo Merge(CrashEventInfo fromDebugger, CrashEventInfo fromEvents)
{
if (fromDebugger == null)
{
return fromEvents ?? new CrashEventInfo();
}
if (fromEvents != null && fromDebugger.RuntimeMessage.Length == 0)
{
fromDebugger.RuntimeMessage = fromEvents.RuntimeMessage;
}
return fromDebugger;
}
public static CrashEventInfo ParseEvents(string xml, int processId, DateTime exitTimeUtc)
{
//IL_0010: Unknown result type (might be due to invalid IL or missing references)
//IL_0016: Expected O, but got Unknown
//IL_0066: Unknown result type (might be due to invalid IL or missing references)
//IL_006b: Unknown result type (might be due to invalid IL or missing references)
//IL_007b: Expected O, but got Unknown
//IL_0087: Unknown result type (might be due to invalid IL or missing references)
//IL_0097: Expected O, but got Unknown
//IL_00a8: Expected O, but got Unknown
CrashEventInfo crashEventInfo = new CrashEventInfo();
if (string.IsNullOrWhiteSpace(xml))
{
return crashEventInfo;
}
XmlDocument val = new XmlDocument();
try
{
val.LoadXml("<Events>" + xml + "</Events>");
}
catch (XmlException)
{
return crashEventInfo;
}
string text = processId.ToString("x", CultureInfo.InvariantCulture);
foreach (XmlNode childNode in ((XmlNode)val.DocumentElement).ChildNodes)
{
string text2 = Attribute(Find(childNode, "System", "Provider"), "Name");
string systemTime = Attribute(Find(childNode, "System", "TimeCreated"), "SystemTime");
List<string> list = DataValues(childNode);
if (text2 == "Application Error" && !crashEventInfo.HasFault && list.Count >= 9)
{
string text3 = list[8].Trim().ToLowerInvariant();
if (text3.StartsWith("0x", StringComparison.Ordinal))
{
text3 = text3.Substring(2);
}
if (!(text3.TrimStart(new char[1] { '0' }) != text.TrimStart(new char[1] { '0' })))
{
crashEventInfo.Module = list[3];
crashEventInfo.ExceptionCode = Prefixed(list[6]);
crashEventInfo.Offset = Prefixed(list[7]);
crashEventInfo.ModulePath = ((list.Count > 11) ? list[11] : string.Empty);
}
}
else if (text2 == ".NET Runtime" && crashEventInfo.RuntimeMessage.Length == 0 && list.Count > 0 && IsNear(systemTime, exitTimeUtc))
{
crashEventInfo.RuntimeMessage = list[0].Trim();
}
}
return crashEventInfo;
}
public static List<string> Tail(IList<string> lines, int count)
{
List<string> list = new List<string>();
if (lines == null)
{
return list;
}
for (int i = Math.Max(0, lines.Count - count); i < lines.Count; i++)
{
list.Add(lines[i]);
}
return list;
}
public static string Summary(CrashSession session, uint exitCode, CrashEventInfo info, TimeSpan played, CrashStrings strings, bool dumpSaved = false)
{
strings = strings ?? CrashStrings.EnglishOnly;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.Append(strings.Format("summary", DescribeExitCode(exitCode, strings), Hex(exitCode)));
if (info.HasFault)
{
stringBuilder.Append(' ').Append(strings.Format("where", info.Module + " + " + info.Offset));
}
if (played > TimeSpan.Zero)
{
stringBuilder.Append(' ').Append(strings.Format("ran", Duration(played, strings)));
}
if (info.Hung)
{
stringBuilder.Append(' ').Append(strings.Format("hang", Duration(TimeSpan.FromSeconds(info.HangSeconds), strings)));
}
else if (!string.IsNullOrEmpty(session.CleanExit))
{
stringBuilder.Append(' ').Append(strings.Get("quitting"));
}
if (dumpSaved)
{
stringBuilder.Append(' ').Append(strings.Get("dumpSaved"));
}
return stringBuilder.ToString();
}
public static string ThreadText(CrashSession session, CrashEventInfo info, CrashStrings strings)
{
if (info.ThreadId == 0)
{
return string.Empty;
}
string text = info.ThreadId.ToString(CultureInfo.InvariantCulture);
if (session.MainThreadId == 0)
{
return text;
}
return text + ", " + strings.Get((info.ThreadId == session.MainThreadId) ? "gameThread" : "otherThread");
}
public static string Report(CrashSession session, uint exitCode, CrashEventInfo info, DateTime exitTime, TimeSpan played, IList<string> logTail, string dumpPath = null)
{
CrashStrings englishOnly = CrashStrings.EnglishOnly;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendLine(Summary(session, exitCode, info, played, englishOnly));
stringBuilder.AppendLine();
stringBuilder.AppendLine("Time: " + exitTime.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture));
stringBuilder.AppendLine("Exit code: " + Hex(exitCode) + ", " + DescribeExitCode(exitCode, englishOnly));
if (info.HasFault)
{
string text = ((!info.FromDebugger) ? ", from the Windows event log" : (info.Early ? ", seen by the crash watcher when it was raised, the game closed right after" : ", seen by the crash watcher"));
stringBuilder.AppendLine("Module: " + info.Module + " + " + info.Offset + ", exception " + info.ExceptionCode + text);
if (info.Access.Length > 0)
{
stringBuilder.AppendLine("Access: " + info.Access);
}
if (info.ModulePath.Length > 0)
{
stringBuilder.AppendLine("Module path: " + info.ModulePath);
}
if (info.ThreadId != 0)
{
stringBuilder.AppendLine("Thread: " + ThreadText(session, info, englishOnly));
}
}
else if (!info.Hung)
{
stringBuilder.AppendLine("No crash record: the crash watcher saw no exception and the Windows event log has none for this process.");
}
if (info.Hung)
{
stringBuilder.AppendLine("Hang: the game began to quit at " + session.CleanExit + " and was still running " + info.HangSeconds.ToString(CultureInfo.InvariantCulture) + " s later" + (string.IsNullOrEmpty(dumpPath) ? string.Empty : "; the memory dump shows every thread at the moment it hung"));
}
stringBuilder.AppendLine("Game: " + session.GameVersion + ", CatLib " + session.CatLibVersion + (string.IsNullOrEmpty(session.WatcherVersion) ? string.Empty : (", crash watcher " + session.WatcherVersion)) + ", started " + session.Started + ", language " + session.Language);
if (session.IsNewerFormat)
{
stringBuilder.AppendLine("Session format " + session.Format.ToString(CultureInfo.InvariantCulture) + " is newer than this crash watcher reads (" + 1.ToString(CultureInfo.InvariantCulture) + "), some details may be missing: update CatLib Crash Watcher");
}
if (!string.IsNullOrEmpty(session.GameBuild))
{
stringBuilder.AppendLine("Game build check: " + session.GameBuild);
}
if (!string.IsNullOrEmpty(dumpPath))
{
stringBuilder.AppendLine("Memory dump: " + dumpPath);
}
if (info.RuntimeMessage.Length > 0)
{
stringBuilder.AppendLine();
stringBuilder.AppendLine(".NET message:");
foreach (string item in SplitLines(info.RuntimeMessage))
{
stringBuilder.AppendLine(" " + item);
}
}
stringBuilder.AppendLine();
stringBuilder.AppendLine("Mods:");
foreach (string mod in session.Mods)
{
stringBuilder.AppendLine(" " + mod);
}
stringBuilder.AppendLine();
stringBuilder.AppendLine("Last game events:");
foreach (string item2 in Tail(session.Events, 15))
{
stringBuilder.AppendLine(" " + item2);
}
stringBuilder.AppendLine();
stringBuilder.AppendLine("End of the BepInEx log:");
foreach (string item3 in logTail ?? new List<string>())
{
stringBuilder.AppendLine(" " + item3);
}
return stringBuilder.ToString();
}
public static string Details(CrashSession session, uint exitCode, CrashEventInfo info, DateTime exitTime, CrashStrings strings)
{
strings = strings ?? CrashStrings.EnglishOnly;
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.AppendLine(strings.Get("detailTime") + ": " + exitTime.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture));
stringBuilder.AppendLine(strings.Get("detailExitCode") + ": " + Hex(exitCode));
if (info.HasFault)
{
stringBuilder.AppendLine(strings.Get("detailModule") + ": " + info.Module + " + " + info.Offset + ", " + strings.Get("detailException") + " " + info.ExceptionCode);
string text = ThreadText(session, info, strings);
if (text.Length > 0)
{
stringBuilder.AppendLine(strings.Get("detailThread") + ": " + text);
}
}
string text2 = RuntimeHeadline(info.RuntimeMessage);
if (text2.Length > 0)
{
stringBuilder.AppendLine(".NET: " + text2);
}
stringBuilder.AppendLine(strings.Get("detailGame") + " " + session.GameVersion + ", CatLib " + session.CatLibVersion);
if (session.Mods.Count > 0)
{
stringBuilder.AppendLine(strings.Get("detailMods") + ": " + string.Join(", ", ModNames(session.Mods)));
}
List<string> list = Tail(session.Events, 6);
if (list.Count > 0)
{
stringBuilder.AppendLine(strings.Get("detailEvents") + ":");
foreach (string item in list)
{
stringBuilder.AppendLine(" " + item);
}
}
return stringBuilder.ToString().TrimEnd(Array.Empty<char>());
}
public static string RuntimeHeadline(string message)
{
if (string.IsNullOrWhiteSpace(message))
{
return string.Empty;
}
List<string> list = SplitLines(message);
foreach (string item in list)
{
if (item.StartsWith("Exception Info:", StringComparison.OrdinalIgnoreCase))
{
return Shorten(item.Substring("Exception Info:".Length).Trim(), 200);
}
}
foreach (string item2 in list)
{
if (item2.StartsWith("Description:", StringComparison.OrdinalIgnoreCase))
{
return Shorten(item2.Substring("Description:".Length).Trim(), 200);
}
}
return Shorten((list.Count == 0) ? string.Empty : list[0], 200);
}
public static List<string> ModNames(IEnumerable<string> mods)
{
List<string> list = new List<string>();
foreach (string mod in mods)
{
int num = mod.LastIndexOf(" (", StringComparison.Ordinal);
list.Add((num > 0) ? mod.Substring(0, num) : mod);
}
return list;
}
public static List<string> SplitLines(string text)
{
List<string> list = new List<string>();
string[] array = (text ?? string.Empty).Replace("\r\n", "\n").Replace('\r', '\n').Split(new char[1] { '\n' });
for (int i = 0; i < array.Length; i++)
{
string text2 = array[i].TrimEnd(Array.Empty<char>());
if (text2.Length > 0)
{
list.Add(text2);
}
}
return list;
}
public static string Shorten(string text, int length)
{
if (text != null && text.Length > length)
{
return text.Substring(0, length - 3) + "...";
}
return text ?? string.Empty;
}
public static string Duration(TimeSpan span, CrashStrings strings)
{
strings = strings ?? CrashStrings.EnglishOnly;
if (span.TotalHours >= 1.0)
{
return strings.Format("hoursMinutes", ((int)span.TotalHours).ToString(CultureInfo.InvariantCulture), span.Minutes.ToString(CultureInfo.InvariantCulture));
}
if (span.TotalMinutes >= 1.0)
{
return strings.Format("minutes", ((int)span.TotalMinutes).ToString(CultureInfo.InvariantCulture));
}
return strings.Format("seconds", ((int)span.TotalSeconds).ToString(CultureInfo.InvariantCulture));
}
private static string Prefixed(string value)
{
string text = (value ?? string.Empty).Trim();
if (!text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
return "0x" + text;
}
return text;
}
private static bool IsNear(string systemTime, DateTime exitTimeUtc)
{
if (!DateTime.TryParse(systemTime, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal | DateTimeStyles.AssumeUniversal, out var result))
{
return false;
}
return Math.Abs((result - exitTimeUtc).TotalSeconds) <= 120.0;
}
private static XmlNode Find(XmlNode node, string first, string second)
{
XmlNode val = Child(node, first);
if (val != null)
{
return Child(val, second);
}
return null;
}
private static XmlNode Child(XmlNode node, string name)
{
//IL_0014: Unknown result type (might be due to invalid IL or missing references)
//IL_001a: Expected O, but got Unknown
foreach (XmlNode childNode in node.ChildNodes)
{
XmlNode val = childNode;
if (val.LocalName == name)
{
return val;
}
}
return null;
}
private static string Attribute(XmlNode node, string name)
{
object obj;
if (node == null)
{
obj = null;
}
else
{
XmlAttributeCollection attributes = node.Attributes;
if (attributes == null)
{
obj = null;
}
else
{
XmlAttribute obj2 = attributes[name];
obj = ((obj2 != null) ? ((XmlNode)obj2).Value : null);
}
}
if (obj == null)
{
obj = string.Empty;
}
return (string)obj;
}
private static List<string> DataValues(XmlNode eventNode)
{
//IL_002b: Unknown result type (might be due to invalid IL or missing references)
//IL_0031: Expected O, but got Unknown
List<string> list = new List<string>();
XmlNode val = Child(eventNode, "EventData");
if (val == null)
{
return list;
}
foreach (XmlNode childNode in val.ChildNodes)
{
XmlNode val2 = childNode;
if (val2.LocalName == "Data")
{
list.Add(val2.InnerText ?? string.Empty);
}
}
return list;
}
}
internal sealed class CrashStrings
{
public const string CatalogPrefix = "crash.";
public const string PhrasePrefix = "phrase.";
public const int PhraseCount = 14;
public static readonly IReadOnlyDictionary<string, string> English = new Dictionary<string, string>(StringComparer.Ordinal)
{
["title"] = "Cat Mail Co: crash report",
["openFolder"] = "Open the report folder",
["copy"] = "Copy the report",
["showDetails"] = "Details",
["hideDetails"] = "Hide details",
["footer"] = "The report and logs are saved in {0}",
["help"] = "If this keeps happening, send the report folder to the mod authors.",
["copied"] = "The report is copied",
["summary"] = "The game closed unexpectedly: {0} ({1}).",
["where"] = "Where: {0}.",
["ran"] = "The game ran for {0}.",
["quitting"] = "It happened while the game was quitting.",
["hang"] = "The game stopped responding while quitting and was closed after {0}.",
["dumpSaved"] = "A memory dump is saved with the report.",
["hoursMinutes"] = "{0} h {1} min",
["minutes"] = "{0} min",
["seconds"] = "{0} s",
["detailTime"] = "Time",
["detailExitCode"] = "Exit code",
["detailModule"] = "Module",
["detailException"] = "exception",
["detailThread"] = "Thread",
["gameThread"] = "the game thread",
["otherThread"] = "another thread",
["detailGame"] = "Game",
["detailMods"] = "Mods",
["detailEvents"] = "Last events",
["exit.normal"] = "normal exit",
["exit.closedOutside"] = "the game was closed from outside, for example from the Task Manager",
["exit.accessViolation"] = "access to an invalid memory address (access violation)",
["exit.heapCorruption"] = "the memory heap is damaged (heap corruption)",
["exit.failFast"] = "fail fast or a stack buffer overrun",
["exit.stackOverflow"] = "stack overflow",
["exit.illegalInstruction"] = "illegal instruction",
["exit.divideByZero"] = "integer division by zero",
["exit.consoleClosed"] = "the game was closed through its console window",
["exit.dotnetException"] = "an unhandled .NET exception",
["exit.dotnetFailFast"] = ".NET fail fast",
["exit.unknown"] = "unknown reason",
["phrase.1"] = "Your game got meowed :(",
["phrase.2"] = "Cats don't like water, and the game didn't like this",
["phrase.3"] = "Someone knocked the game off the table",
["phrase.4"] = "A parcel fell off the top shelf",
["phrase.5"] = "A cat sat on the keyboard",
["phrase.6"] = "The game curled up and fell asleep",
["phrase.7"] = "The boat sailed off without the game",
["phrase.8"] = "Nine lives, and one of them is gone",
["phrase.9"] = "The yarn got all tangled up",
["phrase.10"] = "Hiss! Something went wrong",
["phrase.11"] = "The game chased a laser dot and got lost",
["phrase.12"] = "Oops, the mail got wet",
["phrase.13"] = "The game is hiding under the sofa",
["phrase.14"] = "Somebody stepped on a tail"
};
private readonly IDictionary<string, string> _texts;
public static CrashStrings EnglishOnly { get; } = new CrashStrings(null);
public static IEnumerable<string> Keys => English.Keys;
public CrashStrings(IDictionary<string, string> texts)
{
_texts = texts ?? new Dictionary<string, string>(StringComparer.Ordinal);
}
public string Get(string key)
{
if (_texts.TryGetValue(key, out var value) && !string.IsNullOrWhiteSpace(value))
{
return value;
}
if (!English.TryGetValue(key, out var value2))
{
return key;
}
return value2;
}
public string Format(string key, params object[] arguments)
{
try
{
return string.Format(CultureInfo.InvariantCulture, Get(key), arguments);
}
catch (FormatException)
{
string value;
return English.TryGetValue(key, out value) ? string.Format(CultureInfo.InvariantCulture, value, arguments) : key;
}
}
public string Phrase(Random random)
{
return Get("phrase." + (random.Next(14) + 1).ToString(CultureInfo.InvariantCulture));
}
}
}
namespace CatLib.CrashWatcher
{
internal static class ClipboardText
{
public const int Attempts = 10;
public static bool Set(IntPtr owner, string text)
{
for (int i = 0; i < 10; i++)
{
if (NativeMethods.OpenClipboard(owner))
{
try
{
return Put(text ?? string.Empty);
}
finally
{
NativeMethods.CloseClipboard();
}
}
Thread.Sleep(50);
}
return false;
}
private static bool Put(string text)
{
NativeMethods.EmptyClipboard();
char[] array = text.ToCharArray();
IntPtr intPtr = NativeMethods.GlobalAlloc(2u, new UIntPtr((uint)((array.Length + 1) * 2)));
if (intPtr == IntPtr.Zero)
{
return false;
}
IntPtr intPtr2 = NativeMethods.GlobalLock(intPtr);
if (intPtr2 == IntPtr.Zero)
{
NativeMethods.GlobalFree(intPtr);
return false;
}
Marshal.Copy(array, 0, intPtr2, array.Length);
Marshal.WriteInt16(intPtr2, array.Length * 2, 0);
NativeMethods.GlobalUnlock(intPtr);
if (NativeMethods.SetClipboardData(13u, intPtr) == IntPtr.Zero)
{
NativeMethods.GlobalFree(intPtr);
return false;
}
return true;
}
}
internal static class CrashEvents
{
public const string Query = "*[System[(EventID=1000 or EventID=1026) and TimeCreated[timediff(@SystemTime) <= 600000]]]";
public const int MaxEvents = 30;
public const int WaitMilliseconds = 6000;
public const int PollMilliseconds = 500;
public static CrashEventInfo Collect(int processId, DateTime exitTimeUtc, bool expectFault, WatcherLog log)
{
CrashEventInfo crashEventInfo = new CrashEventInfo();
int num = 0;
while (true)
{
try
{
crashEventInfo = CrashText.ParseEvents(Read(), processId, exitTimeUtc);
}
catch (Exception ex)
{
log.Write("Reading the Windows event log failed: " + ex.Message);
return crashEventInfo;
}
if (crashEventInfo.HasFault || !expectFault || num >= 6000)
{
break;
}
Thread.Sleep(500);
num += 500;
}
return crashEventInfo;
}
public static bool LooksLikeFault(uint exitCode)
{
if (exitCode < 3221225472u && exitCode != 3762504530u)
{
return exitCode == 2148734499u;
}
return true;
}
private static string Read()
{
//IL_0011: Unknown result type (might be due to invalid IL or missing references)
//IL_0016: Unknown result type (might be due to invalid IL or missing references)
//IL_0022: Expected O, but got Unknown
//IL_001d: Unknown result type (might be due to invalid IL or missing references)
//IL_0023: Expected O, but got Unknown
StringBuilder stringBuilder = new StringBuilder();
EventLogReader val = new EventLogReader(new EventLogQuery("Application", (PathType)1, "*[System[(EventID=1000 or EventID=1026) and TimeCreated[timediff(@SystemTime) <= 600000]]]")
{
ReverseDirection = true
});
try
{
for (int i = 0; i < 30; i++)
{
EventRecord val2 = val.ReadEvent();
try
{
if (val2 == null)
{
break;
}
stringBuilder.Append(val2.ToXml());
continue;
}
finally
{
((IDisposable)val2)?.Dispose();
}
}
}
finally
{
((IDisposable)val)?.Dispose();
}
return stringBuilder.ToString();
}
}
internal sealed class CrashWindow
{
public const int OpenFolderId = 100;
public const int CopyId = 101;
private readonly CrashReport _report;
private readonly CrashStrings _strings;
private readonly string _gamePath;
private readonly WatcherLog _log;
public CrashWindow(CrashReport report, CrashStrings strings, string gamePath, WatcherLog log)
{
_report = report;
_strings = strings ?? CrashStrings.EnglishOnly;
_gamePath = gamePath;
_log = log;
}
public void Show(string phrase)
{
string text = _report.Summary + "\n\n" + _strings.Get("help");
try
{
ShowTaskDialog(phrase, text);
}
catch (Exception ex)
{
_log.Write("The task dialog is unavailable, using a message box: " + ex.Message);
NativeMethods.MessageBox(IntPtr.Zero, phrase + "\n\n" + text + "\n\n" + _report.Details + "\n\n" + _strings.Format("footer", _report.Folder), _strings.Get("title"), 24u);
}
}
private void ShowTaskDialog(string phrase, string content)
{
(int, string)[] array = new(int, string)[2]
{
(100, _strings.Get("openFolder")),
(101, _strings.Get("copy"))
};
int num = 4 + IntPtr.Size;
IntPtr intPtr = Marshal.AllocHGlobal(num * array.Length);
IntPtr[] array2 = new IntPtr[array.Length];
IntPtr intPtr2 = GameIcon();
NativeMethods.TaskDialogCallback taskDialogCallback = OnNotification;
try
{
for (int i = 0; i < array.Length; i++)
{
array2[i] = Marshal.StringToHGlobalUni(array[i].Item2);
Marshal.WriteInt32(intPtr, i * num, array[i].Item1);
Marshal.WriteIntPtr(intPtr, i * num + 4, array2[i]);
}
NativeMethods.TaskDialogConfig config = new NativeMethods.TaskDialogConfig
{
Flags = (8 | ((!(intPtr2 == IntPtr.Zero)) ? 2 : 0)),
CommonButtons = 32,
WindowTitle = _strings.Get("title"),
MainIcon = ((intPtr2 == IntPtr.Zero) ? NativeMethods.WarningIcon : intPtr2),
MainInstruction = phrase,
Content = content,
ButtonCount = (uint)array.Length,
Buttons = intPtr,
DefaultButton = 8,
ExpandedInformation = _report.Details,
ExpandedControlText = _strings.Get("hideDetails"),
CollapsedControlText = _strings.Get("showDetails"),
Footer = _strings.Format("footer", _report.Folder),
Callback = taskDialogCallback
};
config.Size = (uint)Marshal.SizeOf(typeof(NativeMethods.TaskDialogConfig));
int button;
int radioButton;
bool verification;
int num2 = NativeMethods.TaskDialogIndirect(ref config, out button, out radioButton, out verification);
if (num2 < 0)
{
Marshal.ThrowExceptionForHR(num2);
}
}
finally
{
GC.KeepAlive(taskDialogCallback);
IntPtr[] array3 = array2;
foreach (IntPtr intPtr3 in array3)
{
if (intPtr3 != IntPtr.Zero)
{
Marshal.FreeHGlobal(intPtr3);
}
}
Marshal.FreeHGlobal(intPtr);
if (intPtr2 != IntPtr.Zero)
{
NativeMethods.DestroyIcon(intPtr2);
}
}
}
private int OnNotification(IntPtr window, int notification, IntPtr wParam, IntPtr lParam, IntPtr data)
{
try
{
switch (notification)
{
case 0:
NativeMethods.SetWindowPos(window, NativeMethods.TopMostWindow, 0, 0, 0, 0, 3u);
NativeMethods.SetForegroundWindow(window);
return 0;
default:
return 0;
case 2:
switch (wParam.ToInt32())
{
case 100:
OpenFolder();
return 1;
case 101:
if (ClipboardText.Set(window, _report.Text))
{
SetFooter(window, _strings.Get("copied") + ". " + _strings.Format("footer", _report.Folder));
}
return 1;
default:
return 0;
}
}
}
catch (Exception ex)
{
_log.Write("The crash window action failed: " + ex.Message);
return 1;
}
}
private void OpenFolder()
{
Process.Start(new ProcessStartInfo("explorer.exe", "\"" + _report.Folder + "\"")
{
UseShellExecute = false
});
}
private static void SetFooter(IntPtr window, string text)
{
IntPtr intPtr = Marshal.StringToHGlobalUni(text);
try
{
NativeMethods.SendMessage(window, 1132, new IntPtr(2), intPtr);
}
finally
{
Marshal.FreeHGlobal(intPtr);
}
}
private IntPtr GameIcon()
{
if (string.IsNullOrEmpty(_gamePath))
{
return IntPtr.Zero;
}
IntPtr[] array = new IntPtr[1];
IntPtr[] array2 = new IntPtr[1];
try
{
if (NativeMethods.ExtractIconEx(_gamePath, 0, array, array2, 1u) == 0)
{
return IntPtr.Zero;
}
}
catch (Exception)
{
return IntPtr.Zero;
}
if (array2[0] != IntPtr.Zero)
{
NativeMethods.DestroyIcon(array2[0]);
}
return array[0];
}
}
internal sealed class DebugOutcome
{
public GameExit Exit { get; set; }
public CrashEventInfo Crash { get; set; }
public string DumpPath { get; set; }
public int PassedExceptions { get; set; }
public int EarlyDumps { get; set; }
}
internal static class GameDebugger
{
public const uint DumpType = 6500u;
public const uint BreakpointCode = 2147483651u;
public const uint Wow64BreakpointCode = 1073741855u;
public const int MaxEarlyDumps = 10;
public static readonly TimeSpan EarlyDumpPause = TimeSpan.FromSeconds(5.0);
public static readonly uint[] FatalCodes = new uint[7] { 3221225477u, 3221225501u, 3221225622u, 3221225725u, 3221226356u, 3221226505u, 3221226528u };
public static DebugOutcome Watch(int processId, string dumpPath, WatcherLog log)
{
IntPtr intPtr = NativeMethods.OpenProcess(1053776u, inherit: false, processId);
if (intPtr == IntPtr.Zero)
{
log.Write("Could not open the game for dumps (" + new Win32Exception().Message + "), waiting without them");
return null;
}
if (!NativeMethods.DebugActiveProcess(processId))
{
log.Write("Could not follow the game as a debugger (" + new Win32Exception().Message + "), waiting without dumps");
NativeMethods.CloseHandle(intPtr);
return null;
}
NativeMethods.DebugSetProcessKillOnExit(killOnExit: false);
DebugOutcome debugOutcome = new DebugOutcome
{
Exit = new GameExit
{
ImagePath = GameProcess.ImagePath(intPtr)
}
};
IntPtr intPtr2 = Marshal.AllocHGlobal(176);
bool flag = false;
bool flag2 = false;
string text = (string.IsNullOrEmpty(dumpPath) ? null : Path.ChangeExtension(dumpPath, ".early.dmp"));
CrashEventInfo crashEventInfo = null;
DateTime dateTime = DateTime.MinValue;
try
{
while (!flag2)
{
if (!NativeMethods.WaitForDebugEvent(intPtr2, uint.MaxValue))
{
log.Write("Following the game stopped (" + new Win32Exception().Message + "), waiting without dumps");
NativeMethods.DebugActiveProcessStop(processId);
break;
}
int num = Marshal.ReadInt32(intPtr2, 0);
int processId2 = Marshal.ReadInt32(intPtr2, 4);
int num2 = Marshal.ReadInt32(intPtr2, 8);
uint continueStatus = 65538u;
switch (num)
{
case 1:
{
uint num3 = (uint)Marshal.ReadInt32(intPtr2, 16);
bool flag3 = Marshal.ReadInt32(intPtr2, 168) != 0;
if (flag3 && !flag && (num3 == 2147483651u || num3 == 1073741855))
{
flag = true;
break;
}
if (flag3)
{
continueStatus = 2147549185u;
debugOutcome.PassedExceptions++;
if (text == null || debugOutcome.Crash != null || Array.IndexOf(FatalCodes, num3) < 0 || debugOutcome.EarlyDumps >= 10 || !(DateTime.UtcNow - dateTime >= EarlyDumpPause))
{
break;
}
CrashEventInfo crashEventInfo2 = Describe(intPtr, num2, num3, intPtr2, log);
if (!CrashText.IsManaged(crashEventInfo2))
{
dateTime = DateTime.UtcNow;
debugOutcome.EarlyDumps++;
crashEventInfo = crashEventInfo2;
crashEventInfo.Early = true;
log.Write($"The game raised {CrashText.Hex(num3)} in {crashEventInfo.Module} + {crashEventInfo.Offset} on thread {num2}, keeping a dump in case it does not recover");
if (!TryWriteDump(intPtr, processId, num2, intPtr2, text, log))
{
crashEventInfo = null;
}
}
break;
}
continueStatus = 2147549185u;
if (debugOutcome.Crash == null)
{
debugOutcome.Crash = Describe(intPtr, num2, num3, intPtr2, log);
log.Write($"The game crashed with {CrashText.Hex(num3)} in {debugOutcome.Crash.Module} + {debugOutcome.Crash.Offset} on thread {num2}");
if (!string.IsNullOrEmpty(dumpPath) && TryWriteDump(intPtr, processId, num2, intPtr2, dumpPath, log))
{
debugOutcome.DumpPath = dumpPath;
}
}
break;
}
case 3:
case 6:
{
IntPtr intPtr3 = Marshal.ReadIntPtr(intPtr2, 16);
if (intPtr3 != IntPtr.Zero)
{
NativeMethods.CloseHandle(intPtr3);
}
break;
}
case 5:
debugOutcome.Exit.ExitCode = (uint)Marshal.ReadInt32(intPtr2, 16);
flag2 = true;
break;
}
NativeMethods.ContinueDebugEvent(processId2, num2, continueStatus);
}
if (!flag2)
{
NativeMethods.WaitForSingleObject(intPtr, uint.MaxValue);
if (NativeMethods.GetExitCodeProcess(intPtr, out var exitCode))
{
debugOutcome.Exit.ExitCode = exitCode;
}
}
NativeMethods.WaitForSingleObject(intPtr, 5000u);
GameProcess.FillTimes(intPtr, debugOutcome.Exit);
if (debugOutcome.Crash == null && crashEventInfo != null && debugOutcome.Exit.ExitCode == ParseCode(crashEventInfo.ExceptionCode))
{
debugOutcome.Crash = crashEventInfo;
if (Move(text, dumpPath, log))
{
debugOutcome.DumpPath = dumpPath;
}
log.Write("The game closed with the exception it raised in " + crashEventInfo.Module + " + " + crashEventInfo.Offset + ", the dump of that moment is the crash dump");
}
return debugOutcome;
}
finally
{
Marshal.FreeHGlobal(intPtr2);
NativeMethods.CloseHandle(intPtr);
if (text != null && File.Exists(text))
{
try
{
File.Delete(text);
}
catch (Exception)
{
}
}
}
}
private static uint ParseCode(string code)
{
string text = (code ?? string.Empty).Trim();
if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
text = text.Substring(2);
}
if (!uint.TryParse(text, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out var result))
{
return 0u;
}
return result;
}
private static bool Move(string source, string target, WatcherLog log)
{
try
{
if (File.Exists(target))
{
File.Delete(target);
}
File.Move(source, target);
return true;
}
catch (Exception ex)
{
log.Write("Could not keep the dump of the raised exception: " + ex.Message);
return false;
}
}
public static List<CrashModule> Modules(IntPtr process)
{
List<CrashModule> list = new List<CrashModule>();
IntPtr[] array = new IntPtr[1024];
if (!NativeMethods.EnumProcessModulesEx(process, array, (uint)(array.Length * IntPtr.Size), out var needed, 3u))
{
return list;
}
int num = Math.Min(array.Length, (int)(needed / (uint)IntPtr.Size));
StringBuilder stringBuilder = new StringBuilder(1024);
for (int i = 0; i < num; i++)
{
if (NativeMethods.GetModuleInformation(process, array[i], out var info, (uint)Marshal.SizeOf(typeof(NativeMethods.ModuleInfo))))
{
stringBuilder.Clear();
NativeMethods.GetModuleFileNameEx(process, array[i], stringBuilder, (uint)stringBuilder.Capacity);
list.Add(new CrashModule(stringBuilder.ToString(), (ulong)info.BaseOfDll.ToInt64(), info.SizeOfImage));
}
}
return list;
}
private static CrashEventInfo Describe(IntPtr process, int thread, uint exceptionCode, IntPtr debugEvent, WatcherLog log)
{
ulong address = (ulong)Marshal.ReadInt64(debugEvent, 32);
CrashEventInfo crashEventInfo;
try
{
crashEventInfo = CrashText.Locate(Modules(process), address, exceptionCode, thread);
}
catch (Exception ex)
{
crashEventInfo = CrashText.Locate(null, address, exceptionCode, thread);
log.Write("Could not list the game modules: " + ex.Message);
}
int num = Marshal.ReadInt32(debugEvent, 40);
if (exceptionCode == 3221225477u && num >= 2)
{
crashEventInfo.Access = CrashText.AccessText((ulong)Marshal.ReadInt64(debugEvent, 48), (ulong)Marshal.ReadInt64(debugEvent, 56));
}
return crashEventInfo;
}
public static bool TryWriteSnapshot(int processId, string path, WatcherLog log)
{
IntPtr intPtr = NativeMethods.OpenProcess(1104u, inherit: false, processId);
if (intPtr == IntPtr.Zero)
{
log.Write("Could not open the game for a dump of the hang (" + new Win32Exception().Message + ")");
return false;
}
try
{
using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.ReadWrite, FileShare.None))
{
if (!NativeMethods.MiniDumpWriteDump(intPtr, processId, fileStream.SafeFileHandle.DangerousGetHandle(), 6500u, IntPtr.Zero, IntPtr.Zero, IntPtr.Zero))
{
throw new Win32Exception();
}
}
log.Write($"Memory dump of the hang written to {path}, {new FileInfo(path).Length / 1024} KiB");
return true;
}
catch (Exception ex)
{
log.Write("The memory dump of the hang could not be written: " + ex.Message);
try
{
File.Delete(path);
}
catch (Exception)
{
}
return false;
}
finally
{
NativeMethods.CloseHandle(intPtr);
}
}
private static bool TryWriteDump(IntPtr process, int processId, int thread, IntPtr debugEvent, string path, WatcherLog log)
{
try
{
WriteDump(process, processId, thread, debugEvent, path);
log.Write($"Memory dump written to {path}, {new FileInfo(path).Length / 1024} KiB");
return true;
}
catch (Exception ex)
{
log.Write("The memory dump could not be written: " + ex.Message);
try
{
File.Delete(path);
}
catch (Exception)
{
}
return false;
}
}
private static void WriteDump(IntPtr process, int processId, int thread, IntPtr debugEvent, string dumpPath)
{
IntPtr intPtr = Marshal.AllocHGlobal(152);
IntPtr hglobal = Marshal.AllocHGlobal(1248);
IntPtr intPtr2 = Marshal.AllocHGlobal(2 * IntPtr.Size);
IntPtr intPtr3 = Marshal.AllocHGlobal(16);
IntPtr intPtr4 = NativeMethods.OpenThread(72u, inherit: false, thread);
try
{
byte[] array = new byte[152];
Marshal.Copy(debugEvent + 16, array, 0, array.Length);
Array.Clear(array, 8, 8);
Marshal.Copy(array, 0, intPtr, array.Length);
IntPtr intPtr5 = new IntPtr((hglobal.ToInt64() + 15) & -16);
Marshal.Copy(new byte[1232], 0, intPtr5, 1232);
Marshal.WriteInt32(intPtr5, 48, 1048607);
bool flag = intPtr4 != IntPtr.Zero && NativeMethods.GetThreadContext(intPtr4, intPtr5);
Marshal.WriteIntPtr(intPtr2, 0, intPtr);
Marshal.WriteIntPtr(intPtr2, IntPtr.Size, flag ? intPtr5 : IntPtr.Zero);
Marshal.WriteInt32(intPtr3, 0, thread);
Marshal.WriteInt64(intPtr3, 4, intPtr2.ToInt64());
Marshal.WriteInt32(intPtr3, 12, 0);
using FileStream fileStream = new FileStream(dumpPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None);
if (!NativeMethods.MiniDumpWriteDump(process, processId, fileStream.SafeFileHandle.DangerousGetHandle(), 6500u, flag ? intPtr3 : IntPtr.Zero, IntPtr.Zero, IntPtr.Zero))
{
throw new Win32Exception();
}
}
finally
{
if (intPtr4 != IntPtr.Zero)
{
NativeMethods.CloseHandle(intPtr4);
}
Marshal.FreeHGlobal(intPtr3);
Marshal.FreeHGlobal(intPtr2);
Marshal.FreeHGlobal(hglobal);
Marshal.FreeHGlobal(intPtr);
}
}
}
internal sealed class GameExit
{
public uint ExitCode { get; set; }
public DateTime Started { get; set; }
public DateTime Exited { get; set; }
public string ImagePath { get; set; } = string.Empty;
public TimeSpan Played
{
get
{
if (!(Exited > Started))
{
return TimeSpan.Zero;
}
return Exited - Started;
}
}
}
internal static class GameProcess
{
public static GameExit WaitForExit(int processId)
{
IntPtr intPtr = NativeMethods.OpenProcess(1052672u, inherit: false, processId);
if (intPtr == IntPtr.Zero)
{
throw new Win32Exception();
}
try
{
GameExit gameExit = new GameExit
{
ImagePath = ImagePath(intPtr)
};
NativeMethods.WaitForSingleObject(intPtr, uint.MaxValue);
if (!NativeMethods.GetExitCodeProcess(intPtr, out var exitCode))
{
throw new Win32Exception();
}
gameExit.ExitCode = exitCode;
FillTimes(intPtr, gameExit);
return gameExit;
}
finally
{
NativeMethods.CloseHandle(intPtr);
}
}
public static void FillTimes(IntPtr handle, GameExit exit)
{
if (NativeMethods.GetProcessTimes(handle, out var creation, out var exit2, out var _, out var _) && exit2 > 0)
{
exit.Started = DateTime.FromFileTimeUtc(creation).ToLocalTime();
exit.Exited = DateTime.FromFileTimeUtc(exit2).ToLocalTime();
}
else
{
exit.Exited = DateTime.Now;
}
}
public static string ImagePath(IntPtr handle)
{
uint size = 1024u;
StringBuilder stringBuilder = new StringBuilder((int)size);
if (!NativeMethods.QueryFullProcessImageName(handle, 0u, stringBuilder, ref size))
{
return string.Empty;
}
return stringBuilder.ToString();
}
}
internal sealed class HangWatcher
{
public static readonly TimeSpan Limit = TimeSpan.FromSeconds(8.0);
public static readonly TimeSpan Poll = TimeSpan.FromSeconds(1.0);
private readonly int _processId;
private readonly string _sessionFile;
private readonly string _dumpPath;
private readonly WatcherLog _log;
private readonly ManualResetEvent _stop = new ManualResetEvent(initialState: false);
private Thread _thread;
private long _seenLength = -1L;
public DateTime? QuitStarted { get; private set; }
public bool Hung { get; private set; }
public string DumpPath { get; private set; }
public HangWatcher(int processId, string sessionFile, string dumpPath, WatcherLog log)
{
_processId = processId;
_sessionFile = sessionFile;
_dumpPath = dumpPath;
_log = log;
}
public void Start()
{
_thread = new Thread(Run)
{
IsBackground = true,
Name = "CatLib hang watch"
};
_thread.Start();
}
public void Stop()
{
_stop.Set();
_thread?.Join(TimeSpan.FromMinutes(2.0));
}
public int HungSeconds(DateTime exited)
{
if (!Hung || !QuitStarted.HasValue)
{
return 0;
}
TimeSpan limit = Limit;
return Math.Max((int)limit.TotalSeconds, (int)Math.Round((exited - QuitStarted.Value).TotalSeconds));
}
private void Run()
{
try
{
while (!_stop.WaitOne(Poll))
{
if (!QuitStarted.HasValue)
{
if (IsQuitting())
{
QuitStarted = DateTime.Now;
WatcherLog log = _log;
object arg = _processId;
TimeSpan limit = Limit;
log.Write($"The game (process {arg}) began to quit, it is reported as hung if it still runs in {limit.TotalSeconds} s");
}
}
else if (!(DateTime.Now - QuitStarted.Value < Limit))
{
Hung = true;
WatcherLog log2 = _log;
object arg2 = _processId;
TimeSpan limit = Limit;
log2.Write($"The game (process {arg2}) still runs {limit.TotalSeconds} s after it began to quit, it looks hung");
if (_dumpPath != null && GameDebugger.TryWriteSnapshot(_processId, _dumpPath, _log))
{
DumpPath = _dumpPath;
}
break;
}
}
}
catch (Exception ex)
{
_log.Write("Watching the game for a hang failed: " + ex.Message);
}
}
private bool IsQuitting()
{
try
{
FileInfo fileInfo = new FileInfo(_sessionFile);
if (!fileInfo.Exists || fileInfo.Length == _seenLength)
{
return false;
}
_seenLength = fileInfo.Length;
string value = "clean=";
foreach (string item in ReportWriter.ReadLines(_sessionFile))
{
if (item.StartsWith(value, StringComparison.Ordinal))
{
return true;
}
}
}
catch (Exception)
{
}
return false;
}
}
internal static class NativeMethods
{
public struct ModuleInfo
{
public IntPtr BaseOfDll;
public uint SizeOfImage;
public IntPtr EntryPoint;
}
public delegate int TaskDialogCallback(IntPtr window, int notification, IntPtr wParam, IntPtr lParam, IntPtr data);
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode, Pack = 1)]
public struct TaskDialogConfig
{
public uint Size;
public IntPtr Parent;
public IntPtr Instance;
public int Flags;
public int CommonButtons;
[MarshalAs(UnmanagedType.LPWStr)]
public string WindowTitle;
public IntPtr MainIcon;
[MarshalAs(UnmanagedType.LPWStr)]
public string MainInstruction;
[MarshalAs(UnmanagedType.LPWStr)]
public string Content;
public uint ButtonCount;
public IntPtr Buttons;
public int DefaultButton;
public uint RadioButtonCount;
public IntPtr RadioButtons;
public int DefaultRadioButton;
[MarshalAs(UnmanagedType.LPWStr)]
public string VerificationText;
[MarshalAs(UnmanagedType.LPWStr)]
public string ExpandedInformation;
[MarshalAs(UnmanagedType.LPWStr)]
public string ExpandedControlText;
[MarshalAs(UnmanagedType.LPWStr)]
public string CollapsedControlText;
public IntPtr FooterIcon;
[MarshalAs(UnmanagedType.LPWStr)]
public string Footer;
[MarshalAs(UnmanagedType.FunctionPtr)]
public TaskDialogCallback Callback;
public IntPtr CallbackData;
public uint Width;
}
public const uint Synchronize = 1048576u;
public const uint ProcessQueryLimitedInformation = 4096u;
public const uint Infinite = uint.MaxValue;
public const uint UnicodeText = 13u;
public const uint MovableMemory = 2u;
public const uint TopMost = 8u;
public const uint NoSize = 1u;
public const uint NoMove = 2u;
public const uint IconError = 16u;
public const int TaskDialogCreated = 0;
public const int TaskDialogButtonClicked = 2;
public const int SetElementText = 1132;
public const int FooterElement = 2;
public const int AllowCancellation = 8;
public const int UseMainIconHandle = 2;
public const int CloseButton = 32;
public const int CloseResult = 8;
public const int KeepOpen = 1;
public const int Ok = 0;
public const uint ProcessQueryInformation = 1024u;
public const uint ProcessVmRead = 16u;
public const uint ProcessDuplicateHandle = 64u;
public const uint ThreadGetContext = 8u;
public const uint ThreadQueryInformation = 64u;
public const uint DebugContinue = 65538u;
public const uint DebugExceptionNotHandled = 2147549185u;
public const int ExceptionDebugEvent = 1;
public const int CreateProcessDebugEvent = 3;
public const int ExitProcessDebugEvent = 5;
public const int LoadDllDebugEvent = 6;
public const int DebugEventSize = 176;
public const int ExceptionRecordSize = 152;
public const int ContextSize = 1232;
public const int ContextFlagsOffset = 48;
public const int ContextAll = 1048607;
public const uint ListModulesAll = 3u;
public static readonly IntPtr WarningIcon = new IntPtr(65535);
public static readonly IntPtr TopMostWindow = new IntPtr(-1);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenProcess(uint access, bool inherit, int processId);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint WaitForSingleObject(IntPtr handle, uint milliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool GetExitCodeProcess(IntPtr process, out uint exitCode);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool GetProcessTimes(IntPtr process, out long creation, out long exit, out long kernel, out long user);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern bool QueryFullProcessImageName(IntPtr process, uint flags, StringBuilder name, ref uint size);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool CloseHandle(IntPtr handle);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool DebugActiveProcess(int processId);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool DebugActiveProcessStop(int processId);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool DebugSetProcessKillOnExit(bool killOnExit);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool WaitForDebugEvent(IntPtr debugEvent, uint milliseconds);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool ContinueDebugEvent(int processId, int threadId, uint continueStatus);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr OpenThread(uint access, bool inherit, int threadId);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool GetThreadContext(IntPtr thread, IntPtr context);
[DllImport("kernel32.dll", EntryPoint = "K32EnumProcessModulesEx", SetLastError = true)]
public static extern bool EnumProcessModulesEx(IntPtr process, [Out] IntPtr[] modules, uint size, out uint needed, uint filter);
[DllImport("kernel32.dll", EntryPoint = "K32GetModuleInformation", SetLastError = true)]
public static extern bool GetModuleInformation(IntPtr process, IntPtr module, out ModuleInfo info, uint size);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, EntryPoint = "K32GetModuleFileNameExW", SetLastError = true)]
public static extern uint GetModuleFileNameEx(IntPtr process, IntPtr module, StringBuilder name, uint size);
[DllImport("dbghelp.dll", SetLastError = true)]
public static extern bool MiniDumpWriteDump(IntPtr process, int processId, IntPtr file, uint type, IntPtr exceptionParam, IntPtr userStreamParam, IntPtr callbackParam);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GlobalAlloc(uint flags, UIntPtr bytes);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GlobalLock(IntPtr memory);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool GlobalUnlock(IntPtr memory);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern IntPtr GlobalFree(IntPtr memory);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool OpenClipboard(IntPtr owner);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool EmptyClipboard();
[DllImport("user32.dll", SetLastError = true)]
public static extern IntPtr SetClipboardData(uint format, IntPtr memory);
[DllImport("user32.dll", SetLastError = true)]
public static extern bool CloseClipboard();
[DllImport("user32.dll", SetLastError = true)]
public static extern bool SetWindowPos(IntPtr window, IntPtr after, int x, int y, int width, int height, uint flags);
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr window);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern IntPtr SendMessage(IntPtr window, int message, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int MessageBox(IntPtr owner, string text, string caption, uint type);
[DllImport("user32.dll")]
public static extern bool DestroyIcon(IntPtr icon);
[DllImport("user32.dll")]
public static extern bool SetProcessDPIAware();
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
public static extern uint ExtractIconEx(string file, int index, IntPtr[] large, IntPtr[] small, uint count);
[DllImport("comctl32.dll", CharSet = CharSet.Unicode)]
public static extern int TaskDialogIndirect(ref TaskDialogConfig config, out int button, out int radioButton, out bool verification);
}
internal static class Program
{
public const string ProcessArgument = "--pid";
public const string SessionArgument = "--session";
public const string PreviewArgument = "--preview";
public const string DumpArgument = "--dump";
public const string LogFileName = "watcher.log";
public static string WatcherVersion
{
get
{
Version version = typeof(Program).Assembly.GetName().Version;
if (!(version == null))
{
return version.Major + "." + version.Minor + "." + version.Build;
}
return "unknown";
}
}
[STAThread]
private static int Main(string[] args)
{
int result = 0;
string text = null;
bool flag = false;
bool dump = false;
for (int i = 0; i < args.Length; i++)
{
if (args[i] == "--pid" && i + 1 < args.Length)
{
int.TryParse(args[++i], NumberStyles.Integer, CultureInfo.InvariantCulture, out result);
}
else if (args[i] == "--session" && i + 1 < args.Length)
{
text = args[++i];
}
else if (args[i] == "--preview")
{
flag = true;
}
else if (args[i] == "--dump")
{
dump = true;
}
}
if (string.IsNullOrEmpty(text) || (result <= 0 && !flag))
{
return 2;
}
string text2 = Path.GetDirectoryName(Path.GetFullPath(text)) ?? ".";
WatcherLog watcherLog = new WatcherLog(Path.Combine(text2, "watcher.log"));
try
{
return flag ? Preview(text, text2, watcherLog) : Watch(result, text, text2, dump, watcherLog);
}
catch (Exception arg)
{
watcherLog.Write($"The crash watcher failed: {arg}");
return 1;
}
}
private static int Watch(int processId, string sessionFile, string reportsDirectory, bool dump, WatcherLog log)
{
string text = Path.Combine(reportsDirectory, "crash_" + processId.ToString(CultureInfo.InvariantCulture) + ".dmp");
string text2 = Path.ChangeExtension(text, ".hang.dmp");
HangWatcher hangWatcher = new HangWatcher(processId, sessionFile, dump ? text2 : null, log);
hangWatcher.Start();
DebugOutcome debugOutcome = (dump ? GameDebugger.Watch(processId, text, log) : null);
GameExit gameExit = debugOutcome?.Exit ?? GameProcess.WaitForExit(processId);
hangWatcher.Stop();
CrashSession crashSession = CrashSession.Parse(ReportWriter.ReadLines(sessionFile));
crashSession.WatcherVersion = WatcherVersion;
if (crashSession.ProcessId == 0)
{
crashSession.ProcessId = processId;
}
if (crashSession.IsNewerFormat)
{
log.Write($"The session file has format {crashSession.Format}, this crash watcher {WatcherVersion} reads format {1}: update CatLib Crash Watcher, the report may miss details");
}
int num = QuitSeconds(crashSession, gameExit);
if (num >= 0)
{
log.Write($"The game (process {processId}) closed with {CrashText.Hex(gameExit.ExitCode)} {num} s after it began to quit");
}
if (CrashText.IsNormalExit(gameExit.ExitCode))
{
if (hangWatcher.Hung)
{
log.Write($"The game (process {processId}) closed normally after all, the dump of the hang is not kept");
}
TryDelete(sessionFile, log);
TryDelete(text, log);
TryDelete(text2, log);
return 0;
}
log.Write(string.Format("The game (process {0}) exited with {1}{2}{3}, writing a report", processId, CrashText.Hex(gameExit.ExitCode), (debugOutcome == null) ? string.Empty : $" after {debugOutcome.PassedExceptions} handled exception(s)", hangWatcher.Hung ? ", it was hung while quitting" : string.Empty));
bool expectFault = debugOutcome?.Crash == null && CrashEvents.LooksLikeFault(gameExit.ExitCode);
CrashEventInfo fromEvents = CrashEvents.Collect(processId, gameExit.Exited.ToUniversalTime(), expectFault, log);
CrashEventInfo crashEventInfo = CrashText.WithManagedException(CrashText.Merge(debugOutcome?.Crash, fromEvents), crashSession);
string text3 = debugOutcome?.DumpPath;
if (hangWatcher.Hung && !crashEventInfo.HasFault)
{
crashEventInfo.HangSeconds = ((num > 0) ? num : hangWatcher.HungSeconds(gameExit.Exited));
text3 = text3 ?? hangWatcher.DumpPath;
}
CrashReport crashReport = ReportWriter.Write(reportsDirectory, sessionFile, crashSession, gameExit, crashEventInfo, text3, log);
TryDelete(sessionFile, log);
if (!string.Equals(crashReport.DumpPath, text2, StringComparison.OrdinalIgnoreCase))
{
TryDelete(text2, log);
}
log.Write("Report written to " + crashReport.Folder);
CrashStrings strings = crashSession.Strings;
new CrashWindow(crashReport, strings, gameExit.ImagePath, log).Show(strings.Phrase(new Random()));
return 0;
}
private static int Preview(string sessionFile, string reportsDirectory, WatcherLog log)
{
CrashSession crashSession = CrashSession.Parse(ReportWriter.ReadLines(sessionFile));
crashSession.WatcherVersion = WatcherVersion;
GameExit gameExit = new GameExit
{
ExitCode = 3221225477u,
Started = DateTime.Now.AddMinutes(-42.0),
Exited = DateTime.Now
};
CrashEventInfo crashEventInfo = new CrashEventInfo
{
Module = "example.dll",
Offset = "0x0000000000001234",
ExceptionCode = "0xc0000005"
};
CrashStrings strings = crashSession.Strings;
crashEventInfo.ThreadId = crashSession.MainThreadId;
CrashReport crashReport = new CrashReport();
crashReport.Folder = reportsDirectory;
crashReport.Summary = CrashText.Summary(crashSession, gameExit.ExitCode, crashEventInfo, gameExit.Played, strings, dumpSaved: true);
crashReport.Details = CrashText.Details(crashSession, gameExit.ExitCode, crashEventInfo, gameExit.Exited, strings);
crashReport.Text = CrashText.Report(crashSession, gameExit.ExitCode, crashEventInfo, gameExit.Exited, gameExit.Played, new string[0]);
new CrashWindow(crashReport, strings, string.Empty, log).Show(strings.Phrase(new Random()));
return 0;
}
private static int QuitSeconds(CrashSession session, GameExit exit)
{
if (string.IsNullOrEmpty(session.CleanExit) || !DateTime.TryParseExact(session.CleanExit, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture, DateTimeStyles.AssumeLocal, out var result))
{
return -1;
}
return Math.Max(0, (int)Math.Round((exit.Exited - result).TotalSeconds));
}
private static void TryDelete(string path, WatcherLog log)
{
try
{
if (File.Exists(path))
{
File.Delete(path);
}
}
catch (Exception ex)
{
log.Write("Could not remove " + path + ": " + ex.Message);
}
}
}
internal sealed class CrashReport
{
public string Folder { get; set; } = string.Empty;
public string Text { get; set; } = string.Empty;
public string Details { get; set; } = string.Empty;
public string Summary { get; set; } = string.Empty;
public string DumpPath { get; set; }
}
internal static class ReportWriter
{
public const int KeptReports = 10;
public const int KeptDumps = 3;
public const string DumpFileName = "crash.dmp";
public const string ReportFileName = "report.txt";
public const string SessionFileName = "session.txt";
public const string FolderTimeFormat = "yyyy-MM-dd_HH-mm-ss";
public static CrashReport Write(string reportsDirectory, string sessionFile, CrashSession session, GameExit exit, CrashEventInfo info, string ownDump, WatcherLog log)
{
string text = Path.Combine(reportsDirectory, exit.Exited.ToString("yyyy-MM-dd_HH-mm-ss", CultureInfo.InvariantCulture) + "_" + session.ProcessId.ToString(CultureInfo.InvariantCulture));
Directory.CreateDirectory(text);
string text2 = CopyInto(session.BepInExLog, text, log);
CopyInto(session.PlayerLog, text, log);
CopyInto(sessionFile, text, log, "session.txt");
if (text2 == null)
{
text2 = session.BepInExLog;
}
List<string> logTail = CrashText.Tail(ReadLines(text2), 40);
string text3 = MoveDump(ownDump, text, log);
bool dumpSaved = text3 != null;
text3 = text3 ?? FindDump(session.ProcessId);
string text4 = CrashText.Report(session, exit.ExitCode, info, exit.Exited, exit.Played, logTail, text3);
File.WriteAllText(Path.Combine(text, "report.txt"), text4, Encoding.UTF8);
Prune(reportsDirectory, text, log);
CrashStrings strings = session.Strings;
return new CrashReport
{
Folder = text,
Text = text4,
DumpPath = text3,
Summary = CrashText.Summary(session, exit.ExitCode, info, exit.Played, strings, dumpSaved),
Details = CrashText.Details(session, exit.ExitCode, info, exit.Exited, strings)
};
}
public static List<string> ReadLines(string path)
{
List<string> list = new List<string>();
if (string.IsNullOrEmpty(path) || !File.Exists(path))
{
return list;
}
try
{
using FileStream stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
using StreamReader streamReader = new StreamReader(stream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true);
string item;
while ((item = streamReader.ReadLine()) != null)
{
list.Add(item);
}
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
return list;
}
private static string CopyInto(string source, string folder, WatcherLog log, string name = null)
{
if (string.IsNullOrEmpty(source) || !File.Exists(source))
{
return null;
}
string text = Path.Combine(folder, name ?? Path.GetFileName(source));
try
{
using (FileStream fileStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete))
{
using FileStream destination = new FileStream(text, FileMode.Create, FileAccess.Write);
fileStream.CopyTo(destination);
}
return text;
}
catch (Exception ex)
{
log.Write("Could not copy " + source + ": " + ex.Message);
return null;
}
}
private static string MoveDump(string source, string folder, WatcherLog log)
{
if (string.IsNullOrEmpty(source) || !File.Exists(source))
{
return null;
}
string text = Path.Combine(folder, "crash.dmp");
try
{
File.Move(source, text);
return text;
}
catch (Exception ex)
{
log.Write("Could not move the memory dump into the report folder: " + ex.Message);
return source;
}
}
private static string FindDump(int processId)
{
try
{
string path = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "CrashDumps");
if (!Directory.Exists(path))
{
return null;
}
return Directory.GetFiles(path, "*." + processId.ToString(CultureInfo.InvariantCulture) + ".dmp").FirstOrDefault();
}
catch (Exception)
{
return null;
}
}
private static void Prune(string reportsDirectory, string current, WatcherLog log)
{
List<string> source = (from path in Directory.GetDirectories(reportsDirectory)
where File.Exists(Path.Combine(path, "report.txt"))
select path).OrderByDescending<string, string>((string path) => Path.GetFileName(path), StringComparer.Ordinal).ToList();
foreach (string item in source.Skip(3).Take(Math.Max(0, 7)))
{
string text = Path.Combine(item, "crash.dmp");
try
{
if (File.Exists(text))
{
File.Delete(text);
}
}
catch (Exception ex)
{
log.Write("Could not remove the old memory dump " + text + ": " + ex.Message);
}
}
foreach (string item2 in source.Skip(10))
{
if (!string.Equals(item2, current, StringComparison.OrdinalIgnoreCase))
{
try
{
Directory.Delete(item2, recursive: true);
}
catch (Exception ex2)
{
log.Write("Could not remove the old report " + item2 + ": " + ex2.Message);
}
}
}
}
}
internal sealed class WatcherLog
{
public const long MaxBytes = 262144L;
private readonly string _path;
public WatcherLog(string path)
{
_path = path;
}
public void Write(string message)
{
try
{
if (File.Exists(_path) && new FileInfo(_path).Length > 262144)
{
File.Delete(_path);
}
File.AppendAllText(_path, DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture) + " " + message + Environment.NewLine, Encoding.UTF8);
}
catch (IOException)
{
}
catch (UnauthorizedAccessException)
{
}
}
}
}