Skip to content

Commit 3922e0f

Browse files
committed
Fix WinEventHook to be out of process, installed on the main thread.
1 parent 3acf950 commit 3922e0f

File tree

8 files changed

+86
-68
lines changed

8 files changed

+86
-68
lines changed

Source/ExcelDna.IntelliSense/IntelliSenseServer.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,11 @@ private static void CurrentDomain_ProcessExit(object sender, EventArgs e)
125125
{
126126
// Parachute in asap a call to prevent further XLPenHelper calls
127127
XlCall.ShutdownStarted();
128-
Deactivate();
128+
129+
// Don't try to clean up clean up on process exit - all our resources are in-process anyway
130+
// Leads to an error with the main thread SynchronizationContext, which might be shut down already.
131+
// In particular we don't have to call UnhookWinEvent: "If the client's thread ends, the system automatically calls this function."
132+
// Deactivate();
129133
}
130134
Logger.Initialization.Verbose("IntelliSenseServer ProcessExit End");
131135

Source/ExcelDna.IntelliSense/UIMonitor/FormulaEditWatcher.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,17 @@ public IntPtr FormulaEditWindow
6868
}
6969

7070
SynchronizationContext _syncContextAuto;
71+
SynchronizationContext _syncContextMain;
7172
WindowWatcher _windowWatcher;
7273

7374
IntPtr _hwndFormulaBar;
7475
IntPtr _hwndInCellEdit;
7576
FormulaEditFocus _formulaEditFocus;
7677

77-
public FormulaEditWatcher(WindowWatcher windowWatcher, SynchronizationContext syncContextAuto)
78+
public FormulaEditWatcher(WindowWatcher windowWatcher, SynchronizationContext syncContextAuto, SynchronizationContext syncContextMain)
7879
{
7980
_syncContextAuto = syncContextAuto;
81+
_syncContextMain = syncContextMain;
8082
_windowWatcher = windowWatcher;
8183
_windowWatcher.FormulaBarWindowChanged += _windowWatcher_FormulaBarWindowChanged;
8284
_windowWatcher.InCellEditWindowChanged += _windowWatcher_InCellEditWindowChanged;
@@ -283,7 +285,7 @@ void InstallLocationMonitor(IntPtr hWnd)
283285
{
284286
_windowLocationWatcher.Dispose();
285287
}
286-
_windowLocationWatcher = new WindowLocationWatcher(hWnd, _syncContextAuto);
288+
_windowLocationWatcher = new WindowLocationWatcher(hWnd, _syncContextAuto, _syncContextMain);
287289
_windowLocationWatcher.LocationChanged += _windowLocationWatcher_LocationChanged;
288290
}
289291

Source/ExcelDna.IntelliSense/UIMonitor/PopupListWatcher.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,14 @@ class PopupListWatcher : IDisposable
2121
public IntPtr PopupListHandle => _hwndPopupList;
2222

2323
SynchronizationContext _syncContextAuto;
24+
SynchronizationContext _syncContextMain;
2425
WindowWatcher _windowWatcher;
2526
WinEventHook _selectionChangeHook = null;
2627

27-
public PopupListWatcher(WindowWatcher windowWatcher, SynchronizationContext syncContextAuto)
28+
public PopupListWatcher(WindowWatcher windowWatcher, SynchronizationContext syncContextAuto, SynchronizationContext syncContextMain)
2829
{
2930
_syncContextAuto = syncContextAuto;
31+
_syncContextMain = syncContextMain;
3032
_windowWatcher = windowWatcher;
3133
_windowWatcher.PopupListWindowChanged += _windowWatcher_PopupListWindowChanged;
3234
_windowWatcher.FormulaEditLocationChanged += _windowWatcher_FormulaEditLocationChanged;
@@ -104,7 +106,7 @@ void InstallEventHandlers()
104106
// TODO: Clean up
105107
var hwndListView = Win32Helper.GetFirstChildWindow(_hwndPopupList);
106108

107-
_selectionChangeHook = new WinEventHook(WinEventHook.WinEvent.EVENT_OBJECT_SELECTION, WinEventHook.WinEvent.EVENT_OBJECT_SELECTION, _syncContextAuto, hwndListView);
109+
_selectionChangeHook = new WinEventHook(WinEventHook.WinEvent.EVENT_OBJECT_SELECTION, WinEventHook.WinEvent.EVENT_OBJECT_SELECTION, _syncContextAuto, _syncContextMain, hwndListView);
108110
_selectionChangeHook.WinEventReceived += _selectionChangeHook_WinEventReceived;
109111
Logger.WindowWatcher.Verbose($"PopupList selection event handler added");
110112
}

Source/ExcelDna.IntelliSense/UIMonitor/UIMonitor.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,9 @@ void RunUIAutomation()
4747
Logger.Monitor.Verbose("UIMonitor.RunUIAutomation installing watchers");
4848

4949
// Create and hook together the various watchers
50-
_windowWatcher = new WindowWatcher(_syncContextAuto);
51-
_formulaEditWatcher = new FormulaEditWatcher(_windowWatcher, _syncContextAuto);
52-
_popupListWatcher = new PopupListWatcher(_windowWatcher, _syncContextAuto);
50+
_windowWatcher = new WindowWatcher(_syncContextAuto, _syncContextMain);
51+
_formulaEditWatcher = new FormulaEditWatcher(_windowWatcher, _syncContextAuto, _syncContextMain);
52+
_popupListWatcher = new PopupListWatcher(_windowWatcher, _syncContextAuto, _syncContextMain);
5353
_excelToolTipWatcher = new ExcelToolTipWatcher(_windowWatcher, _syncContextAuto);
5454

5555
// These are the events we're interested in for showing, hiding and updating the IntelliSense forms
@@ -279,7 +279,6 @@ public void Dispose()
279279
_syncContextAuto.Send(delegate
280280
{
281281
// Remove all event handlers ASAP
282-
// Automation.RemoveAllEventHandlers();
283282
if (_windowWatcher != null)
284283
{
285284
_windowWatcher.Dispose();

Source/ExcelDna.IntelliSense/UIMonitor/WinEvents.cs

Lines changed: 63 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
using System;
22
using System.ComponentModel;
3+
using System.Diagnostics;
34
using System.Runtime.InteropServices;
45
using System.Threading;
56

67
namespace ExcelDna.IntelliSense
78
{
89
// This class sets up a WinEventHook for the main Excel process - watching for a range or event types specified.
9-
// Events received (on the main Excel thread) are posted onto the Automation thread (via syncContextAuto)
10+
// Events received (on the main Excel thread) are posted by the handler onto the Automation thread (via syncContextAuto)
11+
// NOTE: Currently we make the SetWinEventHook call on the main Excel thread, which is pumping Windows messages.
12+
// In an alternative implementation we could either create our own thread that pumps Windows messages and use the for WinEvents,
13+
// or we could change our automation thread to also pump windows messages.
1014
class WinEventHook : IDisposable
1115
{
1216
public class WinEventArgs : EventArgs
@@ -106,39 +110,82 @@ public enum WinEventObjectId : int
106110

107111
public event EventHandler<WinEventArgs> WinEventReceived;
108112

109-
readonly IntPtr _hWinEventHook;
113+
/* readonly */ IntPtr _hWinEventHook;
110114
readonly SynchronizationContext _syncContextAuto;
115+
readonly SynchronizationContext _syncContextMain;
111116
readonly IntPtr _hWndFilterOrZero; // If non-zero, only these window events are processed
112117
readonly WinEventDelegate _handleWinEventDelegate; // Ensures delegate that we pass to SetWinEventHook is not GC'd
118+
readonly WinEvent _eventMin;
119+
readonly WinEvent _eventMax;
113120

114-
public WinEventHook(WinEvent eventMin, WinEvent eventMax, SynchronizationContext syncContextAuto, IntPtr hWndFilterOrZero)
121+
// Can be called on any thread, but installed by calling in to the main thread, and will only start receiving events then
122+
public WinEventHook(WinEvent eventMin, WinEvent eventMax, SynchronizationContext syncContextAuto, SynchronizationContext syncContextMain, IntPtr hWndFilterOrZero)
115123
{
116-
if (syncContextAuto == null)
117-
throw new ArgumentNullException(nameof(syncContextAuto));
118-
_syncContextAuto = syncContextAuto;
124+
_syncContextAuto = syncContextAuto ?? throw new ArgumentNullException(nameof(syncContextAuto));
125+
_syncContextMain = syncContextMain ?? throw new ArgumentNullException(nameof(syncContextMain));
119126
_hWndFilterOrZero = hWndFilterOrZero;
120-
var xllModuleHandle = Win32Helper.GetXllModuleHandle();
121-
var excelProcessId = Win32Helper.GetExcelProcessId();
122127
_handleWinEventDelegate = HandleWinEvent;
123-
_hWinEventHook = SetWinEventHook(eventMin, eventMax, xllModuleHandle, _handleWinEventDelegate, excelProcessId, 0, SetWinEventHookFlags.WINEVENT_INCONTEXT);
128+
_eventMin = eventMin;
129+
_eventMax = eventMax;
130+
syncContextMain.Post(InstallWinEventHook, null);
131+
}
132+
133+
// Must run on the main Excel thread (or another thread where Windows messages are pumped)
134+
void InstallWinEventHook(object _)
135+
{
136+
var excelProcessId = Win32Helper.GetExcelProcessId();
137+
_hWinEventHook = SetWinEventHook(_eventMin, _eventMax, IntPtr.Zero, _handleWinEventDelegate, excelProcessId, 0, SetWinEventHookFlags.WINEVENT_OUTOFCONTEXT);
124138
if (_hWinEventHook == IntPtr.Zero)
125139
{
140+
Debug.Print("++++++++++++++ SetWinEventHook failed +++++++++++++++++++++++++++");
126141
Logger.WinEvents.Error("SetWinEventHook failed");
127142
// Is SetLastError used? - SetWinEventHook documentation does not indicate so
128143
throw new Win32Exception("SetWinEventHook failed");
129144
}
145+
// Debug.Print($"++++++++++++++ SetWinEventHook Success on thread {Thread.CurrentThread.ManagedThreadId} \r\n {Environment.StackTrace} \r\n +++++++++++++++++++++++++++");
130146
Logger.WinEvents.Info($"SetWinEventHook success on thread {Thread.CurrentThread.ManagedThreadId}");
131147
}
132148

133-
// This runs on the Excel main thread - get off quickly
149+
// Must run on the same thread that InstallWinEventHook ran on
150+
void UninstallWinEventHook(object _)
151+
{
152+
if (_hWinEventHook == IntPtr.Zero)
153+
{
154+
Logger.WinEvents.Warn($"UnhookWinEvent unexpectedly called with no hook installed - thread {Thread.CurrentThread.ManagedThreadId}");
155+
return;
156+
}
157+
158+
try
159+
{
160+
Logger.WinEvents.Info($"UnhookWinEvent called on thread {Thread.CurrentThread.ManagedThreadId}");
161+
bool result = UnhookWinEvent(_hWinEventHook);
162+
if (!result)
163+
{
164+
// GetLastError?
165+
Logger.WinEvents.Info($"UnhookWinEvent failed");
166+
}
167+
else
168+
{
169+
Logger.WinEvents.Info("UnhookWinEvent success");
170+
}
171+
}
172+
catch (Exception ex)
173+
{
174+
Logger.WinEvents.Warn($"UnhookWinEvent Exception {ex}");
175+
}
176+
finally
177+
{
178+
_hWinEventHook = IntPtr.Zero;
179+
}
180+
}
181+
182+
// This runs on the Excel main thread (usually, not always) - get off quickly
134183
void HandleWinEvent(IntPtr hWinEventHook, WinEvent eventType, IntPtr hWnd,
135184
WinEventObjectId idObject, int idChild, uint dwEventThread, uint dwmsEventTime)
136185
{
186+
// Debug.Print($"++++++++++++++ WinEvent Received: {eventType} on thread {Thread.CurrentThread.ManagedThreadId} from thread {dwEventThread} +++++++++++++++++++++++++++");
137187
try
138188
{
139-
if (disposedValue)
140-
return;
141-
142189
if (_hWndFilterOrZero != IntPtr.Zero && hWnd != _hWndFilterOrZero)
143190
return;
144191

@@ -154,6 +201,7 @@ void HandleWinEvent(IntPtr hWinEventHook, WinEvent eventType, IntPtr hWnd,
154201
}
155202
}
156203

204+
// A quick filter that runs on the Excel main thread (or other thread handling the WinEvent)
157205
bool IsSupportedWinEvent(WinEvent winEvent)
158206
{
159207
return winEvent == WinEvent.EVENT_OBJECT_CREATE ||
@@ -179,49 +227,10 @@ void OnWinEventReceived(object winEventArgsObj)
179227
}
180228

181229
#region IDisposable Support
182-
bool disposedValue = false; // To detect redundant calls
183-
184-
protected virtual void Dispose(bool disposing)
185-
{
186-
if (!disposedValue)
187-
{
188-
Logger.WinEvents.Info($"WinEventHook Dispose on thread {Thread.CurrentThread.ManagedThreadId}");
189-
if (disposing)
190-
{
191-
// TODO: dispose managed state (managed objects).
192-
}
193-
_syncContextAuto.Send(winEventHook =>
194-
{
195-
try
196-
{
197-
Logger.WinEvents.Info($"UnhookWinEvent called on thread {Thread.CurrentThread.ManagedThreadId}");
198-
bool result = UnhookWinEvent((IntPtr)winEventHook);
199-
Logger.WinEvents.Info($"UnhookWinEvent success? {result}");
200-
}
201-
catch (Exception ex)
202-
{
203-
Logger.WinEvents.Warn($"UnhookWinEvent Exception {ex}");
204-
}
205-
}, _hWinEventHook);
206-
207-
disposedValue = true;
208-
}
209-
}
210-
211-
// TODO: Does this make any sense?
212-
// We _have_to_ Unhook from the automation thread...
213-
// ~WinEventHook()
214-
//{
215-
// // Do not change this code. Put cleanup code in Dispose(bool disposing) above.
216-
// Dispose(false);
217-
// }
218-
219-
// This code added to correctly implement the disposable pattern.
220230
public void Dispose()
221231
{
222-
// Do not change this code. Put cleanup code in Dispose(bool disposing) above.
223-
Dispose(true);
224-
// GC.SuppressFinalize(this);
232+
Logger.WinEvents.Info($"WinEventHook Dispose on thread {Thread.CurrentThread.ManagedThreadId}");
233+
_syncContextMain.Send(UninstallWinEventHook, null);
225234
}
226235
#endregion
227236
}

Source/ExcelDna.IntelliSense/UIMonitor/WindowLocationWatcher.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,11 @@ public class WindowLocationWatcher : IDisposable
1515
// This nearly worked, and meant we were watching many fewer events ...
1616
// ...but we missed some of the resizing events for the window, leaving our tooltip stranded.
1717
// So until we can find a workaround for that (perhaps a timer would work fine for this), we watch all the LOCATIONCHANGE events.
18-
public WindowLocationWatcher(IntPtr hWnd, SynchronizationContext syncContextAuto)
18+
public WindowLocationWatcher(IntPtr hWnd, SynchronizationContext syncContextAuto, SynchronizationContext syncContextMain)
1919
{
2020
_hWnd = hWnd;
2121
_syncContextAuto = syncContextAuto;
22-
_windowLocationChangeHook = new WinEventHook(WinEventHook.WinEvent.EVENT_OBJECT_LOCATIONCHANGE, WinEventHook.WinEvent.EVENT_OBJECT_LOCATIONCHANGE, _syncContextAuto, _hWnd);
22+
_windowLocationChangeHook = new WinEventHook(WinEventHook.WinEvent.EVENT_OBJECT_LOCATIONCHANGE, WinEventHook.WinEvent.EVENT_OBJECT_LOCATIONCHANGE, _syncContextAuto, syncContextMain, _hWnd);
2323
_windowLocationChangeHook.WinEventReceived += _windowLocationChangeHook_WinEventReceived;
2424
}
2525

Source/ExcelDna.IntelliSense/UIMonitor/WindowWatcher.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -124,15 +124,15 @@ internal WindowChangedEventArgs(IntPtr windowHandle, WinEventHook.WinEvent winEv
124124
public event EventHandler FormulaEditLocationChanged;
125125
// public event EventHandler<WindowChangedEventArgs> SelectDataSourceWindowChanged;
126126

127-
public WindowWatcher(SynchronizationContext syncContextAuto)
127+
public WindowWatcher(SynchronizationContext syncContextAuto, SynchronizationContext syncContextMain)
128128
{
129129
#pragma warning disable CS0618 // Type or member is obsolete (GetCurrentThreadId) - But for debugging we want to monitor this anyway
130130
// Debug.Print($"### WindowWatcher created on thread: Managed {Thread.CurrentThread.ManagedThreadId}, Native {AppDomain.GetCurrentThreadId()}");
131131
#pragma warning restore CS0618 // Type or member is obsolete
132132

133133
// Using WinEvents instead of Automation so that we can watch top-level window changes, but only from the right (current Excel) process.
134134
// TODO: We need to dramatically reduce the number of events we grab here...
135-
_windowStateChangeHook = new WinEventHook(WinEventHook.WinEvent.EVENT_OBJECT_CREATE, WinEventHook.WinEvent.EVENT_OBJECT_LOCATIONCHANGE, syncContextAuto, IntPtr.Zero);
135+
_windowStateChangeHook = new WinEventHook(WinEventHook.WinEvent.EVENT_OBJECT_CREATE, WinEventHook.WinEvent.EVENT_OBJECT_LOCATIONCHANGE, syncContextAuto, syncContextMain, IntPtr.Zero);
136136
// _windowStateChangeHook = new WinEventHook(WinEventHook.WinEvent.EVENT_OBJECT_CREATE, WinEventHook.WinEvent.EVENT_OBJECT_END, syncContextAuto, IntPtr.Zero);
137137
// _windowStateChangeHook = new WinEventHook(WinEventHook.WinEvent.EVENT_MIN, WinEventHook.WinEvent.EVENT_AIA_END, syncContextAuto, IntPtr.Zero);
138138

@@ -146,7 +146,7 @@ public void TryInitialize()
146146
var focusedWindowHandle = Win32Helper.GetFocusedWindowHandle();
147147
string className = null;
148148
if (focusedWindowHandle != IntPtr.Zero)
149-
className = Win32Helper.GetClassName(_focusedWindowHandle);
149+
className = Win32Helper.GetClassName(focusedWindowHandle);
150150

151151
UpdateFocus(focusedWindowHandle, className);
152152
}

Source/ExcelDna.IntelliSense/UIMonitor/XlCall.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ public static string GetFormulaEditPrefix()
6868
return null;
6969
}
7070

71+
// We seem to mis-track in the click case when wPointMode changed from xlModeEnter to xlModeEdit
72+
7173
Logger.WindowWatcher.Verbose("LPenHelper Status: PointMode: {0}, Formula: {1}, First: {2}, Last: {3}, Caret: {4}",
7274
fmlaInfo.wPointMode, Marshal.PtrToStringUni(fmlaInfo.lpch, fmlaInfo.cch), fmlaInfo.ichFirst, fmlaInfo.ichLast, fmlaInfo.ichCaret);
7375

0 commit comments

Comments
 (0)