Skip to content

Commit 4dc653b

Browse files
authored
New feature - Breakpoints (#85)
1 parent b29d870 commit 4dc653b

File tree

8 files changed

+486
-0
lines changed

8 files changed

+486
-0
lines changed

RuntimeUnityEditor.Core/Features/ContextMenu.cs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
using System.Linq;
44
using System.Reflection;
55
using HarmonyLib;
6+
using RuntimeUnityEditor.Core.Breakpoints;
67
using RuntimeUnityEditor.Core.ChangeHistory;
78
using RuntimeUnityEditor.Core.Inspector.Entries;
89
using RuntimeUnityEditor.Core.ObjectTree;
@@ -51,6 +52,7 @@ public override bool Enabled
5152
/// <inheritdoc />
5253
protected override void Initialize(InitSettings initSettings)
5354
{
55+
// TODO This mess needs a rewrite with a sane API
5456
MenuContents.AddRange(new[]
5557
{
5658
new MenuEntry("! Destroyed unity Object !", obj => obj is UnityEngine.Object uobj && !uobj, null),
@@ -75,7 +77,12 @@ protected override void Initialize(InitSettings initSettings)
7577
new MenuEntry("Send to REPL", o => o != null && REPL.ReplWindow.Initialized, o => REPL.ReplWindow.Instance.IngestObject(o)),
7678

7779
new MenuEntry(),
80+
});
81+
82+
AddBreakpointControls(MenuContents);
7883

84+
MenuContents.AddRange(new[]
85+
{
7986
new MenuEntry("Copy to clipboard", o => o != null && Clipboard.ClipboardWindow.Initialized, o =>
8087
{
8188
if (Clipboard.ClipboardWindow.Contents.LastOrDefault() != o)
@@ -167,6 +174,42 @@ o is Sprite ||
167174
DisplayType = FeatureDisplayType.Hidden;
168175
}
169176

177+
private void AddBreakpointControls(List<MenuEntry> menuContents)
178+
{
179+
menuContents.AddRange(AddGroup("call", (o, info) => info as MethodBase));
180+
menuContents.AddRange(AddGroup("getter", (o, info) => info is PropertyInfo pi ? pi.GetGetMethod(true) : null));
181+
menuContents.AddRange(AddGroup("setter", (o, info) => info is PropertyInfo pi ? pi.GetSetMethod(true) : null));
182+
menuContents.Add(new MenuEntry());
183+
return;
184+
185+
IEnumerable<MenuEntry> AddGroup(string name, Func<object, MemberInfo, MethodBase> getMethod)
186+
{
187+
yield return new MenuEntry("Attach " + name + " breakpoint (this instance)", o =>
188+
{
189+
if (o == null) return false;
190+
var target = getMethod(o, _objMemberInfo);
191+
return target != null && !Breakpoints.Breakpoints.IsAttached(target, o);
192+
}, o => Breakpoints.Breakpoints.AttachBreakpoint(getMethod(o, _objMemberInfo), o));
193+
yield return new MenuEntry("Detach " + name + " breakpoint (this instance)", o =>
194+
{
195+
if (o == null) return false;
196+
var target = getMethod(o, _objMemberInfo);
197+
return target != null && Breakpoints.Breakpoints.IsAttached(target, o);
198+
}, o => Breakpoints.Breakpoints.DetachBreakpoint(getMethod(o, _objMemberInfo), o));
199+
200+
yield return new MenuEntry("Attach " + name + " breakpoint (all instances)", o =>
201+
{
202+
var target = getMethod(o, _objMemberInfo);
203+
return target != null && !Breakpoints.Breakpoints.IsAttached(target, null);
204+
}, o => Breakpoints.Breakpoints.AttachBreakpoint(getMethod(o, _objMemberInfo), null));
205+
yield return new MenuEntry("Detach " + name + " breakpoint (all instances)", o =>
206+
{
207+
var target = getMethod(o, _objMemberInfo);
208+
return target != null && Breakpoints.Breakpoints.IsAttached(target, null);
209+
}, o => Breakpoints.Breakpoints.DetachBreakpoint(getMethod(o, _objMemberInfo), null));
210+
}
211+
}
212+
170213
/// <summary>
171214
/// Show the context menu at current cursor position.
172215
/// </summary>

RuntimeUnityEditor.Core/RuntimeUnityEditor.Core.projitems

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,12 @@
5757
<Compile Include="$(MSBuildThisFileDirectory)Utils\UI\InterfaceMaker.cs" />
5858
<Compile Include="$(MSBuildThisFileDirectory)WindowBase.cs" />
5959
<Compile Include="$(MSBuildThisFileDirectory)WindowManager.cs" />
60+
<Compile Include="$(MSBuildThisFileDirectory)Windows\Breakpoints\BreakpointHit.cs" />
61+
<Compile Include="$(MSBuildThisFileDirectory)Windows\Breakpoints\BreakpointHitException.cs" />
62+
<Compile Include="$(MSBuildThisFileDirectory)Windows\Breakpoints\BreakpointPatchInfo.cs" />
63+
<Compile Include="$(MSBuildThisFileDirectory)Windows\Breakpoints\Breakpoints.cs" />
64+
<Compile Include="$(MSBuildThisFileDirectory)windows\breakpoints\BreakpointsWindow.cs" />
65+
<Compile Include="$(MSBuildThisFileDirectory)Windows\Breakpoints\DebuggerBreakType.cs" />
6066
<Compile Include="$(MSBuildThisFileDirectory)Windows\ChangeHistory\Change.cs" />
6167
<Compile Include="$(MSBuildThisFileDirectory)Windows\ChangeHistory\ChangeAction.cs" />
6268
<Compile Include="$(MSBuildThisFileDirectory)Windows\ChangeHistory\ChangeAssignment.cs" />
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.Linq;
4+
5+
namespace RuntimeUnityEditor.Core.Breakpoints
6+
{
7+
public sealed class BreakpointHit
8+
{
9+
public BreakpointHit(BreakpointPatchInfo origin, object instance, object[] args, object result, StackTrace trace)
10+
{
11+
Origin = origin;
12+
Instance = instance;
13+
Args = args;
14+
Result = result;
15+
Trace = trace;
16+
TraceString = trace.ToString();
17+
Time = DateTime.UtcNow;
18+
}
19+
20+
public readonly BreakpointPatchInfo Origin;
21+
public readonly object Instance;
22+
public readonly object[] Args;
23+
public readonly object Result;
24+
public readonly StackTrace Trace;
25+
internal readonly string TraceString;
26+
public readonly DateTime Time;
27+
28+
private string _toStr, _searchStr;
29+
public string GetSearchableString()
30+
{
31+
if (_searchStr == null)
32+
_searchStr = $"{Origin.Target.DeclaringType?.FullName}.{Origin.Target.Name}\t{Result}\t{string.Join("\t", Args.Select(x => x?.ToString() ?? "").ToArray())}";
33+
return _searchStr;
34+
}
35+
public override string ToString()
36+
{
37+
if (_toStr == null)
38+
_toStr = $"{Origin.Target.DeclaringType?.FullName ?? "???"}.{Origin.Target.Name} |Result> {Result?.ToString() ?? "NULL"} |Args> {string.Join(" | ", Args.Select(x => x?.ToString() ?? "NULL").ToArray())}";
39+
return _toStr;
40+
}
41+
}
42+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
using System;
2+
3+
namespace RuntimeUnityEditor.Core.Breakpoints
4+
{
5+
internal sealed class BreakpointHitException : Exception
6+
{
7+
public BreakpointHitException(string message) : base(message) { }
8+
}
9+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using System.Reflection;
4+
5+
namespace RuntimeUnityEditor.Core.Breakpoints
6+
{
7+
public sealed class BreakpointPatchInfo
8+
{
9+
public MethodBase Target { get; }
10+
public MethodInfo Patch { get; }
11+
public List<object> InstanceFilters { get; } = new List<object>();
12+
13+
public BreakpointPatchInfo(MethodBase target, MethodInfo patch, object instanceFilter)
14+
{
15+
Target = target;
16+
Patch = patch;
17+
if (instanceFilter != null)
18+
InstanceFilters.Add(instanceFilter);
19+
}
20+
21+
private string _toStr, _searchStr;
22+
23+
internal string GetSearchableString()
24+
{
25+
if (_searchStr == null)
26+
_searchStr = $"{Target.DeclaringType?.FullName}.{Target.Name}\t{string.Join("\t", InstanceFilters.Select(x => x?.ToString()).ToArray())}";
27+
return _searchStr;
28+
}
29+
public override string ToString()
30+
{
31+
if (_toStr == null)
32+
_toStr = $"{Target.DeclaringType?.FullName ?? "???"}.{Target.Name} |Instances> {string.Join(" | ", InstanceFilters.Select(x => x?.ToString() ?? "NULL").ToArray())}";
33+
return _toStr;
34+
}
35+
}
36+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Diagnostics;
4+
using System.Reflection;
5+
using HarmonyLib;
6+
7+
namespace RuntimeUnityEditor.Core.Breakpoints
8+
{
9+
public static class Breakpoints
10+
{
11+
private static readonly Harmony _harmony = new Harmony("RuntimeUnityEditor.Core.Breakpoints");
12+
private static readonly HarmonyMethod _handlerMethodRet = new HarmonyMethod(typeof(Hooks), nameof(Hooks.BreakpointHandlerReturn));
13+
private static readonly HarmonyMethod _handlerMethodNoRet = new HarmonyMethod(typeof(Hooks), nameof(Hooks.BreakpointHandlerNoReturn));
14+
private static readonly Dictionary<MethodBase, BreakpointPatchInfo> _appliedPatches = new Dictionary<MethodBase, BreakpointPatchInfo>();
15+
public static ICollection<BreakpointPatchInfo> AppliedPatches => _appliedPatches.Values;
16+
17+
public static bool Enabled { get; set; } = true;
18+
public static DebuggerBreakType DebuggerBreaking { get; set; }
19+
20+
public static event Action<BreakpointHit> OnBreakpointHit;
21+
22+
public static bool AttachBreakpoint(MethodBase target, object instance)
23+
{
24+
if (_appliedPatches.TryGetValue(target, out var pi))
25+
{
26+
if (instance != null)
27+
pi.InstanceFilters.Add(instance);
28+
else
29+
pi.InstanceFilters.Clear();
30+
return true;
31+
}
32+
33+
var hasReturn = target is MethodInfo mi && mi.ReturnType != typeof(void);
34+
var patch = _harmony.Patch(target, postfix: hasReturn ? _handlerMethodRet : _handlerMethodNoRet);
35+
if (patch != null)
36+
{
37+
_appliedPatches[target] = new BreakpointPatchInfo(target, patch, instance);
38+
return true;
39+
}
40+
41+
return false;
42+
}
43+
44+
public static bool DetachBreakpoint(MethodBase target, object instance)
45+
{
46+
if (_appliedPatches.TryGetValue(target, out var pi))
47+
{
48+
if (instance == null)
49+
pi.InstanceFilters.Clear();
50+
else
51+
pi.InstanceFilters.Remove(instance);
52+
53+
if (pi.InstanceFilters.Count == 0)
54+
{
55+
_harmony.Unpatch(target, pi.Patch);
56+
_appliedPatches.Remove(target);
57+
return true;
58+
}
59+
}
60+
61+
return false;
62+
}
63+
64+
public static bool IsAttached(MethodBase target, object instance)
65+
{
66+
if (_appliedPatches.TryGetValue(target, out var pi))
67+
{
68+
return instance == null && pi.InstanceFilters.Count == 0 || pi.InstanceFilters.Contains(instance);
69+
}
70+
71+
return false;
72+
}
73+
74+
public static void DetachAll()
75+
{
76+
_harmony.UnpatchSelf();
77+
_appliedPatches.Clear();
78+
}
79+
80+
private static void AddHit(object __instance, MethodBase __originalMethod, object[] __args, object __result)
81+
{
82+
if (!Enabled) return;
83+
84+
if (!_appliedPatches.TryGetValue(__originalMethod, out var pi)) return;
85+
86+
if (pi.InstanceFilters.Count > 0 && !pi.InstanceFilters.Contains(__instance)) return;
87+
88+
if (DebuggerBreaking == DebuggerBreakType.ThrowCatch)
89+
{
90+
try { throw new BreakpointHitException(pi.Target.Name); }
91+
catch (BreakpointHitException) { }
92+
}
93+
else if (DebuggerBreaking == DebuggerBreakType.DebuggerBreak)
94+
{
95+
Debugger.Break();
96+
}
97+
98+
OnBreakpointHit?.Invoke(new BreakpointHit(pi, __instance, __args, __result, new StackTrace(2, true)));
99+
}
100+
101+
private static class Hooks
102+
{
103+
public static void BreakpointHandlerReturn(object __instance, MethodBase __originalMethod, object[] __args, object __result)
104+
{
105+
AddHit(__instance, __originalMethod, __args, __result);
106+
}
107+
108+
public static void BreakpointHandlerNoReturn(object __instance, MethodBase __originalMethod, object[] __args)
109+
{
110+
AddHit(__instance, __originalMethod, __args, null);
111+
}
112+
}
113+
}
114+
}

0 commit comments

Comments
 (0)