From e530360ccb4b57357b72a0e36e5e9e4e77ed4c76 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Tue, 24 Sep 2024 02:48:26 +0200 Subject: [PATCH 1/5] Initial version --- .../Features/ContextMenu.cs | 43 +++ .../RuntimeUnityEditor.Core.projitems | 4 + .../Windows/Breakpoints/BreakpointsWindow.cs | 295 ++++++++++++++++++ 3 files changed, 342 insertions(+) create mode 100644 RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs diff --git a/RuntimeUnityEditor.Core/Features/ContextMenu.cs b/RuntimeUnityEditor.Core/Features/ContextMenu.cs index 2d9522a..5e87f44 100644 --- a/RuntimeUnityEditor.Core/Features/ContextMenu.cs +++ b/RuntimeUnityEditor.Core/Features/ContextMenu.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Reflection; using HarmonyLib; +using RuntimeUnityEditor.Core.Breakpoints; using RuntimeUnityEditor.Core.ChangeHistory; using RuntimeUnityEditor.Core.Inspector.Entries; using RuntimeUnityEditor.Core.ObjectTree; @@ -51,6 +52,7 @@ public override bool Enabled /// protected override void Initialize(InitSettings initSettings) { + // TODO This mess needs a rewrite with a sane API MenuContents.AddRange(new[] { new MenuEntry("! Destroyed unity Object !", obj => obj is UnityEngine.Object uobj && !uobj, null), @@ -75,7 +77,12 @@ protected override void Initialize(InitSettings initSettings) new MenuEntry("Send to REPL", o => o != null && REPL.ReplWindow.Initialized, o => REPL.ReplWindow.Instance.IngestObject(o)), new MenuEntry(), + }); + + AddBreakpointControls(MenuContents); + MenuContents.AddRange(new[] + { new MenuEntry("Copy to clipboard", o => o != null && Clipboard.ClipboardWindow.Initialized, o => { if (Clipboard.ClipboardWindow.Contents.LastOrDefault() != o) @@ -167,6 +174,42 @@ o is Sprite || DisplayType = FeatureDisplayType.Hidden; } + private void AddBreakpointControls(List menuContents) + { + menuContents.AddRange(AddGroup("call", (o, info) => info as MethodBase)); + menuContents.AddRange(AddGroup("getter", (o, info) => info is PropertyInfo pi ? pi.GetGetMethod(true) : null)); + menuContents.AddRange(AddGroup("setter", (o, info) => info is PropertyInfo pi ? pi.GetSetMethod(true) : null)); + menuContents.Add(new MenuEntry()); + return; + + IEnumerable AddGroup(string name, Func getMethod) + { + yield return new MenuEntry("Attach " + name + " breakpoint (this instance)", o => + { + if (o == null) return false; + var target = getMethod(o, _objMemberInfo); + return target != null && !BreakpointsWindow.IsAttached(target, o); + }, o => BreakpointsWindow.AttachBreakpoint(getMethod(o, _objMemberInfo), o)); + yield return new MenuEntry("Detach " + name + " breakpoint (this instance)", o => + { + if (o == null) return false; + var target = getMethod(o, _objMemberInfo); + return target != null && BreakpointsWindow.IsAttached(target, o); + }, o => BreakpointsWindow.DetachBreakpoint(getMethod(o, _objMemberInfo), o)); + + yield return new MenuEntry("Attach " + name + " breakpoint (all instances)", o => + { + var target = getMethod(o, _objMemberInfo); + return target != null && !BreakpointsWindow.IsAttached(target, null); + }, o => BreakpointsWindow.AttachBreakpoint(getMethod(o, _objMemberInfo), null)); + yield return new MenuEntry("Detach " + name + " breakpoint (all instances)", o => + { + var target = getMethod(o, _objMemberInfo); + return target != null && BreakpointsWindow.IsAttached(target, null); + }, o => BreakpointsWindow.DetachBreakpoint(getMethod(o, _objMemberInfo), null)); + } + } + /// /// Show the context menu at current cursor position. /// diff --git a/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems b/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems index 879adfa..e1be4e3 100644 --- a/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems +++ b/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems @@ -57,6 +57,7 @@ + @@ -98,4 +99,7 @@ + + + \ No newline at end of file diff --git a/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs b/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs new file mode 100644 index 0000000..f3ea355 --- /dev/null +++ b/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs @@ -0,0 +1,295 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Reflection; +using HarmonyLib; +using RuntimeUnityEditor.Core.Inspector.Entries; +using RuntimeUnityEditor.Core.Utils; +using RuntimeUnityEditor.Core.Utils.Abstractions; +using UnityEngine; + +namespace RuntimeUnityEditor.Core.Breakpoints +{ + public class BreakpointsWindow : Window + { + #region Functionality + + private static readonly Harmony _harmony = new Harmony("RuntimeUnityEditor.Core.Breakpoints"); + private static readonly Dictionary _appliedPatches = new Dictionary(); + private static readonly HarmonyMethod _handlerMethodRet = new HarmonyMethod(typeof(BreakpointsWindow), nameof(BreakpointHandlerReturn)); + private static readonly HarmonyMethod _handlerMethodNoRet = new HarmonyMethod(typeof(BreakpointsWindow), nameof(BreakpointHandlerNoReturn)); + + private static readonly List _hits = new List(); + + private sealed class PatchInfo + { + public MethodBase Target { get; } + public MethodInfo Patch { get; } + public List InstanceFilters { get; } = new List(); + + public PatchInfo(MethodBase target, MethodInfo patch, object instanceFilter) + { + Target = target; + Patch = patch; + if (instanceFilter != null) + InstanceFilters.Add(instanceFilter); + } + } + + private sealed class BrekapointHit + { + public BrekapointHit(PatchInfo origin, object instance, object[] args, object result, StackTrace trace) + { + Origin = origin; + Instance = instance; + Args = args; + Result = result; + Trace = trace; + TraceString = trace.ToString(); + Time = DateTime.UtcNow; + } + + public readonly PatchInfo Origin; + public readonly object Instance; + public readonly object[] Args; + public readonly object Result; + public readonly StackTrace Trace; + internal readonly string TraceString; + public readonly DateTime Time; + } + + public static bool AttachBreakpoint(MethodBase target, object instance) + { + if (_appliedPatches.TryGetValue(target, out var pi)) + { + if (instance != null) + pi.InstanceFilters.Add(instance); + else + pi.InstanceFilters.Clear(); + return true; + } + + var hasReturn = target is MethodInfo mi && mi.ReturnType != typeof(void); + var patch = _harmony.Patch(target, postfix: hasReturn ? _handlerMethodRet : _handlerMethodNoRet); + if (patch != null) + { + _appliedPatches[target] = new PatchInfo(target, patch, instance); + return true; + } + + return false; + } + + public static bool DetachBreakpoint(MethodBase target, object instance) + { + if (_appliedPatches.TryGetValue(target, out var pi)) + { + if (instance == null) + pi.InstanceFilters.Clear(); + else + pi.InstanceFilters.Remove(instance); + + if (pi.InstanceFilters.Count == 0) + { + _harmony.Unpatch(target, pi.Patch); + _appliedPatches.Remove(target); + return true; + } + } + + return false; + } + + private static void BreakpointHandlerReturn(object __instance, MethodBase __originalMethod, object[] __args, object __result) + { + AddHit(__instance, __originalMethod, __args, __result); + } + private static void BreakpointHandlerNoReturn(object __instance, MethodBase __originalMethod, object[] __args) + { + AddHit(__instance, __originalMethod, __args, null); + } + private static void AddHit(object __instance, MethodBase __originalMethod, object[] __args, object __result) + { + if (_appliedPatches.TryGetValue(__originalMethod, out var pi)) + { + if (pi.InstanceFilters.Count == 0 || pi.InstanceFilters.Contains(__instance)) + _hits.Add(new BrekapointHit(pi, __instance, __args, __result, new StackTrace(1, true))); + } + } + + public static bool IsAttached(MethodBase target, object instance) + { + if (_appliedPatches.TryGetValue(target, out var pi)) + { + return instance == null && pi.InstanceFilters.Count == 0 || pi.InstanceFilters.Contains(instance); + } + return false; + } + + #endregion + + #region UI + + protected override void Initialize(InitSettings initSettings) + { + DisplayName = "Breakpoints"; + Title = "Breakpoint manager and breakpoint hit history"; + DefaultScreenPosition = ScreenPartition.CenterLower; + } + + protected override void LateUpdate() + { + if (_hits.Count > 100) + _hits.RemoveRange(0, _hits.Count - 100); + + base.LateUpdate(); + } + + private Vector2 _scrollPosHits, _scrollPosBreakpoints; + + private bool _showingHits = true; + + protected override void DrawContents() + { + GUILayout.BeginHorizontal(GUI.skin.box); + { + if (GUILayout.Toggle(!_showingHits, "Show active breakpoints")) + _showingHits = false; + if (GUILayout.Toggle(_showingHits, "Show breakpoint hits")) + _showingHits = true; + GUILayout.Space(10); + if (GUILayout.Button("Remove all")) + { + _harmony.UnpatchSelf(); + _appliedPatches.Clear(); + } + if (GUILayout.Button("Clear hits")) + { + _hits.Clear(); + } + GUILayout.FlexibleSpace(); + } + GUILayout.EndHorizontal(); + + if (_showingHits) + DrawHits(); + else + DrawBreakpoints(); + } + + private void DrawBreakpoints() + { + _scrollPosBreakpoints = GUILayout.BeginScrollView(_scrollPosBreakpoints, false, true); + { + if(_appliedPatches.Count > 0) + { + foreach (var appliedPatch in _appliedPatches) + { + GUILayout.BeginHorizontal(GUI.skin.box); + { + DrawHitOriginButton(appliedPatch.Value); + + if (GUILayout.Button("Remove breakpoint")) + DetachBreakpoint(appliedPatch.Key, null); + + if (appliedPatch.Value.InstanceFilters.Count > 0) + { + GUILayout.Label("or remove watched instances:"); + + var instanceFilters = appliedPatch.Value.InstanceFilters; + for (var i = 0; i < instanceFilters.Count; i++) + { + var obj = instanceFilters[i]; + if (GUILayout.Button(obj.ToString(), GUILayout.Width(80))) + DetachBreakpoint(appliedPatch.Key, obj); + } + } + } + GUILayout.EndHorizontal(); + } + } + else + { + GUILayout.Label("This window lists breakpoints set on methods and on property getters/setters.\nA breakpoint is tiggered whenever a method/property is called. The stack trace as well as any parameters and result value of that trigger are then stored in the list of hits.\n\nTo add breakpoints right click on methods and properties, and look for the 'Attach breakpoint' options. It's easiest to do in inspector by right clicking on the member names."); + } + } + GUILayout.EndScrollView(); + } + + private void DrawHits() + { + _scrollPosHits = GUILayout.BeginScrollView(_scrollPosHits, false, true); + { + if(_hits.Count > 0) + { + for (int i = _hits.Count - 1; i >= 0; i--) + { + var hit = _hits[i]; + + GUILayout.BeginHorizontal(GUI.skin.box); + { + GUILayout.Label($"{hit.Time:HH:mm:ss.fff}", GUILayout.Width(85)); + + DrawHitOriginButton(hit.Origin); + + if (GUILayout.Button(new GUIContent("Trace", null, hit.TraceString + "\n\nClick to copy to clipboard\nMiddle click to inspect\nRight click for more options"), GUI.skin.label, GUILayout.Width(60))) + { + if (IMGUIUtils.IsMouseRightClick()) + ContextMenu.Instance.Show(hit.Trace, null); + else if (IMGUIUtils.IsMouseWheelClick()) + Inspector.Inspector.Instance.Push(new InstanceStackEntry(hit.Trace.GetFrames(), "StackTrace"), true); + else + { + UnityFeatureHelper.systemCopyBuffer = hit.TraceString; + RuntimeUnityEditorCore.Logger.Log(LogLevel.Message, "Copied stack trace to clipboard"); + } + } + + ShowObjectButton(hit.Instance, "Call instance", GUILayout.Width(80)); + ShowObjectButton(hit.Result, "Method return value", GUILayout.Width(80)); + + //GUILayout.Label(hit.Args.Length.ToString() + ":", GUILayout.Width(40)); + + for (int j = 0; j < hit.Args.Length; j++) + { + ShowObjectButton(hit.Args[j], "Method argument #" + j, GUILayout.Width(80)); + } + } + GUILayout.EndHorizontal(); + } + } + else + { + GUILayout.Label("This window lists all breakpoint hits that were caught (in IL2CPP some purely native calls might be missed).\nA breakpoint is tiggered whenever a method/property is called. The stack trace as well as any parameters and result value of that trigger are then stored in the list of hits.\n\nTo add breakpoints right click on methods and properties, and look for the 'Attach breakpoint' options. It's easiest to do in inspector by right clicking on the member names."); + } + } + GUILayout.EndScrollView(); + } + + private static void DrawHitOriginButton(PatchInfo hitOrigin) + { + if (GUILayout.Button(new GUIContent(hitOrigin.Target.Name, null, $"Target: {hitOrigin.Target.FullDescription()}\n\nClick to open in dnSpy, right click for more options."), GUI.skin.label, GUILayout.Width(150))) + { + if (IMGUIUtils.IsMouseRightClick()) + ContextMenu.Instance.Show(null, hitOrigin.Target); + else + DnSpyHelper.OpenInDnSpy(hitOrigin.Target); + } + } + + private static void ShowObjectButton(object obj, string objName, params GUILayoutOption[] options) + { + var text = obj?.ToString() ?? "NULL"; + if (GUILayout.Button(new GUIContent(text, null, $"Name: {objName}\nType: {obj?.GetType().FullDescription() ?? "NULL"}\nToString: {text}\n\nClick to open in inspector, right click for more options."), GUI.skin.label, options) && obj != null) + { + if (IMGUIUtils.IsMouseRightClick()) + ContextMenu.Instance.Show(obj, null); + else + Inspector.Inspector.Instance.Push(new InstanceStackEntry(obj, objName), true); + } + } + + #endregion + } +} \ No newline at end of file From 9c6a028ff18879ed42733c7c855a749ff7fd7f5e Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Tue, 24 Sep 2024 02:52:28 +0200 Subject: [PATCH 2/5] Better hits desc --- .../Windows/Breakpoints/BreakpointsWindow.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs b/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs index f3ea355..1b3f854 100644 --- a/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs +++ b/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs @@ -140,8 +140,8 @@ protected override void Initialize(InitSettings initSettings) protected override void LateUpdate() { - if (_hits.Count > 100) - _hits.RemoveRange(0, _hits.Count - 100); + if (_hits.Count > _maxHitsToKeep) + _hits.RemoveRange(0, _hits.Count - _maxHitsToKeep); base.LateUpdate(); } @@ -149,6 +149,7 @@ protected override void LateUpdate() private Vector2 _scrollPosHits, _scrollPosBreakpoints; private bool _showingHits = true; + private int _maxHitsToKeep = 100; protected override void DrawContents() { @@ -182,7 +183,7 @@ private void DrawBreakpoints() { _scrollPosBreakpoints = GUILayout.BeginScrollView(_scrollPosBreakpoints, false, true); { - if(_appliedPatches.Count > 0) + if (_appliedPatches.Count > 0) { foreach (var appliedPatch in _appliedPatches) { @@ -221,7 +222,7 @@ private void DrawHits() { _scrollPosHits = GUILayout.BeginScrollView(_scrollPosHits, false, true); { - if(_hits.Count > 0) + if (_hits.Count > 0) { for (int i = _hits.Count - 1; i >= 0; i--) { @@ -261,7 +262,7 @@ private void DrawHits() } else { - GUILayout.Label("This window lists all breakpoint hits that were caught (in IL2CPP some purely native calls might be missed).\nA breakpoint is tiggered whenever a method/property is called. The stack trace as well as any parameters and result value of that trigger are then stored in the list of hits.\n\nTo add breakpoints right click on methods and properties, and look for the 'Attach breakpoint' options. It's easiest to do in inspector by right clicking on the member names."); + GUILayout.Label("This window lists the last " + _maxHitsToKeep + " breakpoint hits that were caught (in IL2CPP some purely native calls can be missed and stacktraces may be crap).\nA breakpoint is tiggered whenever a method/property is called. The stack trace as well as any parameters and result value of that trigger are then stored in the list of hits.\n\nTo add breakpoints right click on methods and properties, and look for the 'Attach breakpoint' options. It's easiest to do in inspector by right clicking on the member names."); } } GUILayout.EndScrollView(); From 435e61b3c3b2b2abcad965ae6f07e2c2ae202a06 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Tue, 24 Sep 2024 05:07:36 +0200 Subject: [PATCH 3/5] todo --- RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs b/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs index 1b3f854..2e525f6 100644 --- a/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs +++ b/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs @@ -11,6 +11,7 @@ namespace RuntimeUnityEditor.Core.Breakpoints { + // TODO aggregate results, etc. public class BreakpointsWindow : Window { #region Functionality From d002204c4ebecbdf4e0c7f98510999bf2ea45f03 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Tue, 24 Sep 2024 05:13:19 +0200 Subject: [PATCH 4/5] width --- .../Windows/Breakpoints/BreakpointsWindow.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs b/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs index 2e525f6..b8bbd97 100644 --- a/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs +++ b/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs @@ -192,12 +192,12 @@ private void DrawBreakpoints() { DrawHitOriginButton(appliedPatch.Value); - if (GUILayout.Button("Remove breakpoint")) + if (GUILayout.Button("Remove breakpoint", IMGUIUtils.LayoutOptionsExpandWidthFalse)) DetachBreakpoint(appliedPatch.Key, null); if (appliedPatch.Value.InstanceFilters.Count > 0) { - GUILayout.Label("or remove watched instances:"); + GUILayout.Label("or remove watched instances:", IMGUIUtils.LayoutOptionsExpandWidthFalse); var instanceFilters = appliedPatch.Value.InstanceFilters; for (var i = 0; i < instanceFilters.Count; i++) From e98ea72b118271c59bd9add6d5426ccebdc087e5 Mon Sep 17 00:00:00 2001 From: ManlyMarco <39247311+ManlyMarco@users.noreply.github.com> Date: Tue, 24 Sep 2024 20:16:51 +0200 Subject: [PATCH 5/5] Add debugger breaking and stuff --- .../Features/ContextMenu.cs | 16 +- .../RuntimeUnityEditor.Core.projitems | 8 +- .../Windows/Breakpoints/BreakpointHit.cs | 42 ++++ .../Breakpoints/BreakpointHitException.cs | 9 + .../Breakpoints/BreakpointPatchInfo.cs | 36 +++ .../Windows/Breakpoints/Breakpoints.cs | 114 +++++++++ .../Windows/Breakpoints/BreakpointsWindow.cs | 216 ++++++------------ .../Windows/Breakpoints/DebuggerBreakType.cs | 9 + 8 files changed, 296 insertions(+), 154 deletions(-) create mode 100644 RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointHit.cs create mode 100644 RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointHitException.cs create mode 100644 RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointPatchInfo.cs create mode 100644 RuntimeUnityEditor.Core/Windows/Breakpoints/Breakpoints.cs create mode 100644 RuntimeUnityEditor.Core/Windows/Breakpoints/DebuggerBreakType.cs diff --git a/RuntimeUnityEditor.Core/Features/ContextMenu.cs b/RuntimeUnityEditor.Core/Features/ContextMenu.cs index 5e87f44..95150ef 100644 --- a/RuntimeUnityEditor.Core/Features/ContextMenu.cs +++ b/RuntimeUnityEditor.Core/Features/ContextMenu.cs @@ -188,25 +188,25 @@ IEnumerable AddGroup(string name, Func BreakpointsWindow.AttachBreakpoint(getMethod(o, _objMemberInfo), o)); + return target != null && !Breakpoints.Breakpoints.IsAttached(target, o); + }, o => Breakpoints.Breakpoints.AttachBreakpoint(getMethod(o, _objMemberInfo), o)); yield return new MenuEntry("Detach " + name + " breakpoint (this instance)", o => { if (o == null) return false; var target = getMethod(o, _objMemberInfo); - return target != null && BreakpointsWindow.IsAttached(target, o); - }, o => BreakpointsWindow.DetachBreakpoint(getMethod(o, _objMemberInfo), o)); + return target != null && Breakpoints.Breakpoints.IsAttached(target, o); + }, o => Breakpoints.Breakpoints.DetachBreakpoint(getMethod(o, _objMemberInfo), o)); yield return new MenuEntry("Attach " + name + " breakpoint (all instances)", o => { var target = getMethod(o, _objMemberInfo); - return target != null && !BreakpointsWindow.IsAttached(target, null); - }, o => BreakpointsWindow.AttachBreakpoint(getMethod(o, _objMemberInfo), null)); + return target != null && !Breakpoints.Breakpoints.IsAttached(target, null); + }, o => Breakpoints.Breakpoints.AttachBreakpoint(getMethod(o, _objMemberInfo), null)); yield return new MenuEntry("Detach " + name + " breakpoint (all instances)", o => { var target = getMethod(o, _objMemberInfo); - return target != null && BreakpointsWindow.IsAttached(target, null); - }, o => BreakpointsWindow.DetachBreakpoint(getMethod(o, _objMemberInfo), null)); + return target != null && Breakpoints.Breakpoints.IsAttached(target, null); + }, o => Breakpoints.Breakpoints.DetachBreakpoint(getMethod(o, _objMemberInfo), null)); } } diff --git a/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems b/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems index e1be4e3..032606c 100644 --- a/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems +++ b/RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems @@ -57,7 +57,12 @@ + + + + + @@ -99,7 +104,4 @@ - - - \ No newline at end of file diff --git a/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointHit.cs b/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointHit.cs new file mode 100644 index 0000000..cfa74a1 --- /dev/null +++ b/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointHit.cs @@ -0,0 +1,42 @@ +using System; +using System.Diagnostics; +using System.Linq; + +namespace RuntimeUnityEditor.Core.Breakpoints +{ + public sealed class BreakpointHit + { + public BreakpointHit(BreakpointPatchInfo origin, object instance, object[] args, object result, StackTrace trace) + { + Origin = origin; + Instance = instance; + Args = args; + Result = result; + Trace = trace; + TraceString = trace.ToString(); + Time = DateTime.UtcNow; + } + + public readonly BreakpointPatchInfo Origin; + public readonly object Instance; + public readonly object[] Args; + public readonly object Result; + public readonly StackTrace Trace; + internal readonly string TraceString; + public readonly DateTime Time; + + private string _toStr, _searchStr; + public string GetSearchableString() + { + if (_searchStr == null) + _searchStr = $"{Origin.Target.DeclaringType?.FullName}.{Origin.Target.Name}\t{Result}\t{string.Join("\t", Args.Select(x => x?.ToString() ?? "").ToArray())}"; + return _searchStr; + } + public override string ToString() + { + if (_toStr == null) + _toStr = $"{Origin.Target.DeclaringType?.FullName ?? "???"}.{Origin.Target.Name} |Result> {Result?.ToString() ?? "NULL"} |Args> {string.Join(" | ", Args.Select(x => x?.ToString() ?? "NULL").ToArray())}"; + return _toStr; + } + } +} diff --git a/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointHitException.cs b/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointHitException.cs new file mode 100644 index 0000000..cc1c328 --- /dev/null +++ b/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointHitException.cs @@ -0,0 +1,9 @@ +using System; + +namespace RuntimeUnityEditor.Core.Breakpoints +{ + internal sealed class BreakpointHitException : Exception + { + public BreakpointHitException(string message) : base(message) { } + } +} diff --git a/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointPatchInfo.cs b/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointPatchInfo.cs new file mode 100644 index 0000000..068eddc --- /dev/null +++ b/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointPatchInfo.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace RuntimeUnityEditor.Core.Breakpoints +{ + public sealed class BreakpointPatchInfo + { + public MethodBase Target { get; } + public MethodInfo Patch { get; } + public List InstanceFilters { get; } = new List(); + + public BreakpointPatchInfo(MethodBase target, MethodInfo patch, object instanceFilter) + { + Target = target; + Patch = patch; + if (instanceFilter != null) + InstanceFilters.Add(instanceFilter); + } + + private string _toStr, _searchStr; + + internal string GetSearchableString() + { + if (_searchStr == null) + _searchStr = $"{Target.DeclaringType?.FullName}.{Target.Name}\t{string.Join("\t", InstanceFilters.Select(x => x?.ToString()).ToArray())}"; + return _searchStr; + } + public override string ToString() + { + if (_toStr == null) + _toStr = $"{Target.DeclaringType?.FullName ?? "???"}.{Target.Name} |Instances> {string.Join(" | ", InstanceFilters.Select(x => x?.ToString() ?? "NULL").ToArray())}"; + return _toStr; + } + } +} diff --git a/RuntimeUnityEditor.Core/Windows/Breakpoints/Breakpoints.cs b/RuntimeUnityEditor.Core/Windows/Breakpoints/Breakpoints.cs new file mode 100644 index 0000000..94463aa --- /dev/null +++ b/RuntimeUnityEditor.Core/Windows/Breakpoints/Breakpoints.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Reflection; +using HarmonyLib; + +namespace RuntimeUnityEditor.Core.Breakpoints +{ + public static class Breakpoints + { + private static readonly Harmony _harmony = new Harmony("RuntimeUnityEditor.Core.Breakpoints"); + private static readonly HarmonyMethod _handlerMethodRet = new HarmonyMethod(typeof(Hooks), nameof(Hooks.BreakpointHandlerReturn)); + private static readonly HarmonyMethod _handlerMethodNoRet = new HarmonyMethod(typeof(Hooks), nameof(Hooks.BreakpointHandlerNoReturn)); + private static readonly Dictionary _appliedPatches = new Dictionary(); + public static ICollection AppliedPatches => _appliedPatches.Values; + + public static bool Enabled { get; set; } = true; + public static DebuggerBreakType DebuggerBreaking { get; set; } + + public static event Action OnBreakpointHit; + + public static bool AttachBreakpoint(MethodBase target, object instance) + { + if (_appliedPatches.TryGetValue(target, out var pi)) + { + if (instance != null) + pi.InstanceFilters.Add(instance); + else + pi.InstanceFilters.Clear(); + return true; + } + + var hasReturn = target is MethodInfo mi && mi.ReturnType != typeof(void); + var patch = _harmony.Patch(target, postfix: hasReturn ? _handlerMethodRet : _handlerMethodNoRet); + if (patch != null) + { + _appliedPatches[target] = new BreakpointPatchInfo(target, patch, instance); + return true; + } + + return false; + } + + public static bool DetachBreakpoint(MethodBase target, object instance) + { + if (_appliedPatches.TryGetValue(target, out var pi)) + { + if (instance == null) + pi.InstanceFilters.Clear(); + else + pi.InstanceFilters.Remove(instance); + + if (pi.InstanceFilters.Count == 0) + { + _harmony.Unpatch(target, pi.Patch); + _appliedPatches.Remove(target); + return true; + } + } + + return false; + } + + public static bool IsAttached(MethodBase target, object instance) + { + if (_appliedPatches.TryGetValue(target, out var pi)) + { + return instance == null && pi.InstanceFilters.Count == 0 || pi.InstanceFilters.Contains(instance); + } + + return false; + } + + public static void DetachAll() + { + _harmony.UnpatchSelf(); + _appliedPatches.Clear(); + } + + private static void AddHit(object __instance, MethodBase __originalMethod, object[] __args, object __result) + { + if (!Enabled) return; + + if (!_appliedPatches.TryGetValue(__originalMethod, out var pi)) return; + + if (pi.InstanceFilters.Count > 0 && !pi.InstanceFilters.Contains(__instance)) return; + + if (DebuggerBreaking == DebuggerBreakType.ThrowCatch) + { + try { throw new BreakpointHitException(pi.Target.Name); } + catch (BreakpointHitException) { } + } + else if (DebuggerBreaking == DebuggerBreakType.DebuggerBreak) + { + Debugger.Break(); + } + + OnBreakpointHit?.Invoke(new BreakpointHit(pi, __instance, __args, __result, new StackTrace(2, true))); + } + + private static class Hooks + { + public static void BreakpointHandlerReturn(object __instance, MethodBase __originalMethod, object[] __args, object __result) + { + AddHit(__instance, __originalMethod, __args, __result); + } + + public static void BreakpointHandlerNoReturn(object __instance, MethodBase __originalMethod, object[] __args) + { + AddHit(__instance, __originalMethod, __args, null); + } + } + } +} diff --git a/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs b/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs index b8bbd97..ba3f7e8 100644 --- a/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs +++ b/RuntimeUnityEditor.Core/Windows/Breakpoints/BreakpointsWindow.cs @@ -1,8 +1,4 @@ -using System; using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Reflection; using HarmonyLib; using RuntimeUnityEditor.Core.Inspector.Entries; using RuntimeUnityEditor.Core.Utils; @@ -14,129 +10,15 @@ namespace RuntimeUnityEditor.Core.Breakpoints // TODO aggregate results, etc. public class BreakpointsWindow : Window { - #region Functionality - - private static readonly Harmony _harmony = new Harmony("RuntimeUnityEditor.Core.Breakpoints"); - private static readonly Dictionary _appliedPatches = new Dictionary(); - private static readonly HarmonyMethod _handlerMethodRet = new HarmonyMethod(typeof(BreakpointsWindow), nameof(BreakpointHandlerReturn)); - private static readonly HarmonyMethod _handlerMethodNoRet = new HarmonyMethod(typeof(BreakpointsWindow), nameof(BreakpointHandlerNoReturn)); - - private static readonly List _hits = new List(); - - private sealed class PatchInfo - { - public MethodBase Target { get; } - public MethodInfo Patch { get; } - public List InstanceFilters { get; } = new List(); - - public PatchInfo(MethodBase target, MethodInfo patch, object instanceFilter) - { - Target = target; - Patch = patch; - if (instanceFilter != null) - InstanceFilters.Add(instanceFilter); - } - } - - private sealed class BrekapointHit - { - public BrekapointHit(PatchInfo origin, object instance, object[] args, object result, StackTrace trace) - { - Origin = origin; - Instance = instance; - Args = args; - Result = result; - Trace = trace; - TraceString = trace.ToString(); - Time = DateTime.UtcNow; - } - - public readonly PatchInfo Origin; - public readonly object Instance; - public readonly object[] Args; - public readonly object Result; - public readonly StackTrace Trace; - internal readonly string TraceString; - public readonly DateTime Time; - } - - public static bool AttachBreakpoint(MethodBase target, object instance) - { - if (_appliedPatches.TryGetValue(target, out var pi)) - { - if (instance != null) - pi.InstanceFilters.Add(instance); - else - pi.InstanceFilters.Clear(); - return true; - } - - var hasReturn = target is MethodInfo mi && mi.ReturnType != typeof(void); - var patch = _harmony.Patch(target, postfix: hasReturn ? _handlerMethodRet : _handlerMethodNoRet); - if (patch != null) - { - _appliedPatches[target] = new PatchInfo(target, patch, instance); - return true; - } - - return false; - } - - public static bool DetachBreakpoint(MethodBase target, object instance) - { - if (_appliedPatches.TryGetValue(target, out var pi)) - { - if (instance == null) - pi.InstanceFilters.Clear(); - else - pi.InstanceFilters.Remove(instance); - - if (pi.InstanceFilters.Count == 0) - { - _harmony.Unpatch(target, pi.Patch); - _appliedPatches.Remove(target); - return true; - } - } - - return false; - } - - private static void BreakpointHandlerReturn(object __instance, MethodBase __originalMethod, object[] __args, object __result) - { - AddHit(__instance, __originalMethod, __args, __result); - } - private static void BreakpointHandlerNoReturn(object __instance, MethodBase __originalMethod, object[] __args) - { - AddHit(__instance, __originalMethod, __args, null); - } - private static void AddHit(object __instance, MethodBase __originalMethod, object[] __args, object __result) - { - if (_appliedPatches.TryGetValue(__originalMethod, out var pi)) - { - if (pi.InstanceFilters.Count == 0 || pi.InstanceFilters.Contains(__instance)) - _hits.Add(new BrekapointHit(pi, __instance, __args, __result, new StackTrace(1, true))); - } - } - - public static bool IsAttached(MethodBase target, object instance) - { - if (_appliedPatches.TryGetValue(target, out var pi)) - { - return instance == null && pi.InstanceFilters.Count == 0 || pi.InstanceFilters.Contains(instance); - } - return false; - } - - #endregion - - #region UI + private static readonly List _hits = new List(); protected override void Initialize(InitSettings initSettings) { DisplayName = "Breakpoints"; Title = "Breakpoint manager and breakpoint hit history"; DefaultScreenPosition = ScreenPartition.CenterLower; + + Breakpoints.OnBreakpointHit += hit => _hits.Add(hit); } protected override void LateUpdate() @@ -151,26 +33,62 @@ protected override void LateUpdate() private bool _showingHits = true; private int _maxHitsToKeep = 100; + private string _searchString = ""; protected override void DrawContents() { - GUILayout.BeginHorizontal(GUI.skin.box); + GUILayout.BeginHorizontal(); { - if (GUILayout.Toggle(!_showingHits, "Show active breakpoints")) - _showingHits = false; - if (GUILayout.Toggle(_showingHits, "Show breakpoint hits")) - _showingHits = true; - GUILayout.Space(10); - if (GUILayout.Button("Remove all")) + GUILayout.BeginHorizontal(GUI.skin.box); { - _harmony.UnpatchSelf(); - _appliedPatches.Clear(); + Breakpoints.Enabled = GUILayout.Toggle(Breakpoints.Enabled, "Enabled", IMGUIUtils.LayoutOptionsExpandWidthFalse); + + if (!Breakpoints.Enabled) + GUI.color = Color.gray; + + GUILayout.Space(10); + + GUILayout.Label("Show ", IMGUIUtils.LayoutOptionsExpandWidthFalse); + if (GUILayout.Toggle(!_showingHits, "active breakpoints", IMGUIUtils.LayoutOptionsExpandWidthFalse)) + _showingHits = false; + if (GUILayout.Toggle(_showingHits, "breakpoint hits", IMGUIUtils.LayoutOptionsExpandWidthFalse)) + _showingHits = true; + + GUILayout.Space(10); + + if (_showingHits) + { + if (GUILayout.Button("Clear hits", IMGUIUtils.LayoutOptionsExpandWidthFalse)) + _hits.Clear(); + } + else + { + if (GUILayout.Button("Remove all", IMGUIUtils.LayoutOptionsExpandWidthFalse)) + Breakpoints.DetachAll(); + } } - if (GUILayout.Button("Clear hits")) + GUILayout.EndHorizontal(); + } + GUILayout.EndHorizontal(); + + GUILayout.BeginHorizontal(); + { + GUILayout.BeginHorizontal(GUI.skin.box); { - _hits.Clear(); + GUILayout.Label("Search: ", IMGUIUtils.LayoutOptionsExpandWidthFalse); + _searchString = GUILayout.TextField(_searchString, IMGUIUtils.LayoutOptionsExpandWidthTrue); + + GUILayout.Space(10); + + GUILayout.Label("Break attached debugger: ", IMGUIUtils.LayoutOptionsExpandWidthFalse); + if (GUILayout.Toggle(Breakpoints.DebuggerBreaking == DebuggerBreakType.None, "No", IMGUIUtils.LayoutOptionsExpandWidthFalse)) + Breakpoints.DebuggerBreaking = DebuggerBreakType.None; + if (GUILayout.Toggle(Breakpoints.DebuggerBreaking == DebuggerBreakType.ThrowCatch, new GUIContent("Throw an exception", null, $"Throw and catch a {nameof(BreakpointHitException)}. The most reliable way."), IMGUIUtils.LayoutOptionsExpandWidthFalse)) + Breakpoints.DebuggerBreaking = DebuggerBreakType.ThrowCatch; + if (GUILayout.Toggle(Breakpoints.DebuggerBreaking == DebuggerBreakType.DebuggerBreak, new GUIContent("Debugger.Break", null, "This might not work with some debugging methods, and it might hard-crash some games."), IMGUIUtils.LayoutOptionsExpandWidthFalse)) + Breakpoints.DebuggerBreaking = DebuggerBreakType.DebuggerBreak; } - GUILayout.FlexibleSpace(); + GUILayout.EndHorizontal(); } GUILayout.EndHorizontal(); @@ -178,33 +96,41 @@ protected override void DrawContents() DrawHits(); else DrawBreakpoints(); + + GUI.color = Color.white; } private void DrawBreakpoints() { _scrollPosBreakpoints = GUILayout.BeginScrollView(_scrollPosBreakpoints, false, true); { - if (_appliedPatches.Count > 0) + if (Breakpoints.AppliedPatches.Count > 0) { - foreach (var appliedPatch in _appliedPatches) + foreach (var appliedPatch in Breakpoints.AppliedPatches) { + if (!string.IsNullOrEmpty(_searchString)) + { + if (!appliedPatch.GetSearchableString().Contains(_searchString)) + continue; + } + GUILayout.BeginHorizontal(GUI.skin.box); { - DrawHitOriginButton(appliedPatch.Value); + DrawHitOriginButton(appliedPatch); if (GUILayout.Button("Remove breakpoint", IMGUIUtils.LayoutOptionsExpandWidthFalse)) - DetachBreakpoint(appliedPatch.Key, null); + Breakpoints.DetachBreakpoint(appliedPatch.Target, null); - if (appliedPatch.Value.InstanceFilters.Count > 0) + if (appliedPatch.InstanceFilters.Count > 0) { GUILayout.Label("or remove watched instances:", IMGUIUtils.LayoutOptionsExpandWidthFalse); - var instanceFilters = appliedPatch.Value.InstanceFilters; + var instanceFilters = appliedPatch.InstanceFilters; for (var i = 0; i < instanceFilters.Count; i++) { var obj = instanceFilters[i]; if (GUILayout.Button(obj.ToString(), GUILayout.Width(80))) - DetachBreakpoint(appliedPatch.Key, obj); + Breakpoints.DetachBreakpoint(appliedPatch.Target, obj); } } } @@ -229,6 +155,12 @@ private void DrawHits() { var hit = _hits[i]; + if (!string.IsNullOrEmpty(_searchString)) + { + if (!hit.GetSearchableString().Contains(_searchString)) + continue; + } + GUILayout.BeginHorizontal(GUI.skin.box); { GUILayout.Label($"{hit.Time:HH:mm:ss.fff}", GUILayout.Width(85)); @@ -269,7 +201,7 @@ private void DrawHits() GUILayout.EndScrollView(); } - private static void DrawHitOriginButton(PatchInfo hitOrigin) + private static void DrawHitOriginButton(BreakpointPatchInfo hitOrigin) { if (GUILayout.Button(new GUIContent(hitOrigin.Target.Name, null, $"Target: {hitOrigin.Target.FullDescription()}\n\nClick to open in dnSpy, right click for more options."), GUI.skin.label, GUILayout.Width(150))) { @@ -291,7 +223,5 @@ private static void ShowObjectButton(object obj, string objName, params GUILayou Inspector.Inspector.Instance.Push(new InstanceStackEntry(obj, objName), true); } } - - #endregion } } \ No newline at end of file diff --git a/RuntimeUnityEditor.Core/Windows/Breakpoints/DebuggerBreakType.cs b/RuntimeUnityEditor.Core/Windows/Breakpoints/DebuggerBreakType.cs new file mode 100644 index 0000000..9b49d57 --- /dev/null +++ b/RuntimeUnityEditor.Core/Windows/Breakpoints/DebuggerBreakType.cs @@ -0,0 +1,9 @@ +namespace RuntimeUnityEditor.Core.Breakpoints +{ + public enum DebuggerBreakType + { + None = 0, + DebuggerBreak, + ThrowCatch + } +}