Skip to content

Commit 8e5bf58

Browse files
committed
Merge #4492 Catch forbidden GUI calls via Util.Debounce
2 parents ce14c14 + 5940653 commit 8e5bf58

File tree

5 files changed

+60
-27
lines changed

5 files changed

+60
-27
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ All notable changes to this project will be documented in this file.
1212

1313
- [Core] Ensure encoding is UTF-8 when saving JSON (#4475, #4478 by: HebaruSan)
1414
- [GUI] Display release date in local time zone (#4488 by: HebaruSan)
15-
- [GUI] Fix freezing after startup with empty mod list (#4491 by: HebaruSan)
15+
- [GUI] Fix freezing after startup with empty mod list (#4491, #4492 by: HebaruSan)
1616

1717
### Internal
1818

GUI/Controls/EditModSearch.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ private void ImmediateHandler(object? sender, EventArgs? e)
140140
// Sync the search boxes immediately
141141
currentSearch = ModSearch.Parse(ModuleLabelList.ModuleLabels, inst, FilterCombinedTextBox.Text);
142142
}
143-
SearchToEditor();
143+
Util.Invoke(this, SearchToEditor);
144144
}
145145
catch (Kraken k)
146146
{

GUI/Controls/ManageMods.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,9 +1173,9 @@ private void InstallAllCheckbox_CheckedChanged(object? sender, EventArgs? e)
11731173
Properties.Resources.InstallAllCheckboxConfirmationNo)
11741174
is false)
11751175
{
1176-
InstallAllCheckbox.CheckedChanged -= this.InstallAllCheckbox_CheckedChanged;
1176+
InstallAllCheckbox.CheckedChanged -= InstallAllCheckbox_CheckedChanged;
11771177
InstallAllCheckbox.Checked = !InstallAllCheckbox.Checked;
1178-
InstallAllCheckbox.CheckedChanged += this.InstallAllCheckbox_CheckedChanged;
1178+
InstallAllCheckbox.CheckedChanged += InstallAllCheckbox_CheckedChanged;
11791179
}
11801180
else
11811181
{

Tests/GUI/ThreadSafetyTests.cs

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
using Mono.Cecil.Cil;
1111
using NUnit.Framework;
1212

13+
using CKAN.Extensions;
1314
using CKAN.GUI.Attributes;
1415

1516
namespace Tests.GUI
@@ -35,6 +36,7 @@ public void GUIAssemblyModule_MethodsWithForbidGUICalls_DontCallGUI()
3536
var calls = allMethods.Where(hasForbidGUIAttribute)
3637
.Select(meth => new MethodCall() { meth })
3738
.Concat(allMethods.SelectMany(FindStartedTasks))
39+
.Concat(allMethods.SelectMany(FindDebouncedTasks))
3840
.SelectMany(GetAllCallsWithoutForbidGUI);
3941

4042
// Assert
@@ -48,15 +50,6 @@ public void GUIAssemblyModule_MethodsWithForbidGUICalls_DontCallGUI()
4850
});
4951
}
5052

51-
// The sequence to start a task seems to be:
52-
// 1. ldftn the function to start
53-
// 2. newobj a System.Action to hold it
54-
// 3. callvirt StartNew
55-
// ... so find the operand of the ldftn most immediately preceding the call
56-
private static MethodDefinition? FindStartNewArgument(Instruction instr)
57-
=> instr.OpCode.Name == "ldftn" ? instr.Operand as MethodDefinition
58-
: FindStartNewArgument(instr.Previous);
59-
6053
private static IEnumerable<MethodCall> GetAllCallsWithoutForbidGUI(MethodCall initialStack)
6154
=> VisitMethodDefinition(initialStack,
6255
initialStack.Last(),
@@ -79,9 +72,14 @@ private static bool unsafeCall(MethodDefinition md)
7972
&& unsafeType(md.DeclaringType);
8073

8174
// Any method on a type in WinForms or inheriting from anything in WinForms is presumed unsafe
82-
private static bool unsafeType(TypeDefinition t)
83-
=> t.Namespace == winformsNamespace
84-
|| (t.BaseType != null && unsafeType(t.BaseType.Resolve()));
75+
private static bool unsafeType(TypeDefinition td)
76+
=> td.TraverseNodes(t => t.BaseType?.Resolve())
77+
.Any(t => t.Namespace == winformsNamespace)
78+
&& !isEventArgs(td);
79+
80+
private static bool isEventArgs(TypeDefinition td)
81+
=> td.TraverseNodes(t => t.BaseType?.Resolve())
82+
.Any(t => t is { Namespace: "System", Name: "EventArgs" });
8583

8684
private static readonly Type forbidAttrib = typeof(ForbidGUICallsAttribute);
8785
private static readonly string winformsNamespace = typeof(Control).Namespace!;

Tests/MonoCecilAnalysisTestsBase.cs

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
using Mono.Cecil;
66
using Mono.Cecil.Cil;
7+
78
using CKAN.Extensions;
89

910
namespace Tests
@@ -74,9 +75,11 @@ protected static IEnumerable<TypeDefinition> GetAllNestedTypes(TypeDefinition td
7475
.Concat(td.NestedTypes.SelectMany(GetAllNestedTypes));
7576

7677
protected static IEnumerable<MethodCall> FindStartedTasks(MethodDefinition md)
77-
=> StartNewCalls(md).Select(FindStartNewArgument)
78-
.OfType<MethodDefinition>()
79-
.Select(taskArg => new MethodCall() { md, taskArg });
78+
=> StartNewCalls(md).SelectMany(sn =>
79+
sn.Operand is MethodReference snMethod
80+
&& FindStartNewArgument(sn) is MethodDefinition taskArg
81+
? Enumerable.Repeat(new MethodCall() { md, snMethod.Resolve(), taskArg, }, 1)
82+
: Enumerable.Empty<MethodCall>());
8083

8184
private static IEnumerable<Instruction> StartNewCalls(MethodDefinition md)
8285
=> md.Body?.Instructions.Where(instr => callOpCodes.Contains(instr.OpCode.Name)
@@ -85,16 +88,48 @@ private static IEnumerable<Instruction> StartNewCalls(MethodDefinition md)
8588
?? Enumerable.Empty<Instruction>();
8689

8790
private static bool isStartNew(MethodReference mr)
88-
=> (mr.DeclaringType.Namespace == "System.Threading.Tasks"
89-
&& mr.DeclaringType.Name == "TaskFactory"
90-
&& mr.Name == "StartNew")
91-
|| (mr.DeclaringType.Namespace == "System.Threading.Tasks"
92-
&& mr.DeclaringType.Name == "Task"
93-
&& mr.Name == "Run");
91+
=> mr is
92+
{
93+
DeclaringType: { Namespace: "System.Threading.Tasks", Name: "TaskFactory" },
94+
Name: "StartNew",
95+
}
96+
or
97+
{
98+
DeclaringType: { Namespace: "System.Threading.Tasks", Name: "Task" },
99+
Name: "Run",
100+
};
94101

95102
private static MethodDefinition? FindStartNewArgument(Instruction instr)
96-
=> instr.OpCode.Name == "ldftn" ? instr.Operand as MethodDefinition
97-
: FindStartNewArgument(instr.Previous);
103+
=> FindFuncArguments(instr).FirstOrDefault();
104+
105+
protected static IEnumerable<MethodCall> FindDebouncedTasks(MethodDefinition md)
106+
=> DebounceCalls(md).SelectMany(db =>
107+
db.Operand is MethodReference dbMethod
108+
? FindDebounceArguments(db)
109+
.Select(dbArg => new MethodCall() { md, dbMethod.Resolve(), dbArg, })
110+
: Enumerable.Empty<MethodCall>());
111+
112+
private static IEnumerable<Instruction> DebounceCalls(MethodDefinition md)
113+
=> md.Body?.Instructions.Where(instr => callOpCodes.Contains(instr.OpCode.Name)
114+
&& instr.Operand is MethodReference mr
115+
&& isDebounce(mr))
116+
?? Enumerable.Empty<Instruction>();
117+
118+
private static bool isDebounce(MethodReference mr)
119+
=> mr is
120+
{
121+
DeclaringType: { Namespace: "CKAN.GUI", Name: "Util" },
122+
Name: "Debounce",
123+
};
124+
125+
private static IEnumerable<MethodDefinition> FindDebounceArguments(Instruction instr)
126+
=> FindFuncArguments(instr).Take(4);
127+
128+
private static IEnumerable<MethodDefinition> FindFuncArguments(Instruction instr)
129+
=> instr.TraverseNodes(i => i.Previous)
130+
.Where(i => i.OpCode.Name == "ldftn")
131+
.Select(i => i.Operand)
132+
.OfType<MethodDefinition>();
98133

99134
private static readonly HashSet<string> callOpCodes = new HashSet<string>
100135
{

0 commit comments

Comments
 (0)