Skip to content

Commit 87aca77

Browse files
committed
Refactor Deactivation to ensure WinEventHooks are Unhooked on the main thread.
* Add IntelliSenseServer Install / Uninstall() to be called from AutoOpen / AutoClose * Mark IntelliSenseServer.Register() as obsolete to highlight required changes Fixes #67
1 parent cd05a96 commit 87aca77

File tree

11 files changed

+129
-83
lines changed

11 files changed

+129
-83
lines changed
Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,4 @@
1-
using System.Collections.Generic;
2-
using System.ComponentModel;
3-
using System.Diagnostics;
4-
using System.Linq;
5-
using System.Runtime.InteropServices;
61
using ExcelDna.Integration;
7-
using ExcelDna.Integration.CustomUI;
82
using ExcelDna.IntelliSense;
93

104
namespace TestAddIn
@@ -13,14 +7,17 @@ public class AddIn : IExcelAddIn
137
{
148
public void AutoOpen()
159
{
16-
IntelliSenseServer.Register();
10+
IntelliSenseServer.Install();
1711
//ExcelDna.Logging.LogDisplay.Show();
1812
//ExcelDna.Logging.LogDisplay.DisplayOrder = ExcelDna.Logging.DisplayOrder.NewestFirst;
1913
}
2014

2115
public void AutoClose()
2216
{
23-
// CONSIDER: Do we implement an explicit call here, or is the AppDomain Unload event good enough?
17+
// The explicit Uninstall call was added in version 1.0.10,
18+
// to give us a chance to unhook the WinEvents (which needs to happen on the main thread)
19+
// No easy plan to do this from the AppDomain unload event, which runs on a ThreadPool thread.
20+
IntelliSenseServer.Uninstall();
2421
}
2522
}
2623
}

Source/ExcelDna.IntelliSense.Host/ExcelDna.IntelliSense.Host-AddIn.xll.config

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
<system.diagnostics>
44
<trace autoflush="true" indentsize="4"/>
55
<sources>
6-
<source name="ExcelDna.IntelliSense" switchValue="Off">
6+
<source name="ExcelDna.IntelliSense" switchValue="All">
77
<listeners>
88
<!--<remove name="Default" />-->
9-
<add name="LogDisplay" type="ExcelDna.Logging.LogDisplayTraceListener,ExcelDna.Integration">
9+
<!--<add name="LogDisplay" type="ExcelDna.Logging.LogDisplayTraceListener,ExcelDna.Integration">
1010
<filter type="System.Diagnostics.EventTypeFilter" initializeData="All"/>
11-
</add>
11+
</add>-->
1212
<!-- <add name="File" type="System.Diagnostics.TextWriterTraceListener" initializeData="ExcelDna.IntelliSense.log" >
1313
<filter type="System.Diagnostics.EventTypeFilter" initializeData="All"/>
1414
</add> -->

Source/ExcelDna.IntelliSense/IntelliSenseServer.cs

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,20 @@
66
using System.Text.RegularExpressions;
77
using Microsoft.Win32;
88
using ExcelDna.Integration;
9+
using System.Threading;
910

1011
namespace ExcelDna.IntelliSense
1112
{
1213
// This class implements the registration and activation of the calling add-in as an IntelliSense Server.
1314
//
1415
// Among different add-ins that are loaded into an Excel process, at most one IntelliSenseServer can be Active.
15-
// This should always be the IntelliSenseServer with the greatest version number among those registered.
16+
// This should always be an IntelliSenseServer with the greatest version number among those registered.
17+
//
1618
// At the moment the bookkeeping for registration and activation in the process is done with environment variables.
1719
// (An attractive alternative is the hidden Excel name space: http://www.cpearson.com/excel/hidden.htm )
18-
// This prevents cross-AppDomain calls, which are problematic because assemblies are then loaded into multiple AppDomains, and
19-
// since the mechanism is intended to cater for different assembly versions, this would be a problem. Also, we don't control
20-
// the CLR hosting configuration, so can't always set the MultiDomain flag on setup. COM mechanisms could work, but are complicated.
20+
// This allows us not to make cross-AppDomain calls, which are problematic because assemblies are then loaded into multiple AppDomains, and
21+
// since the mechanism is intended to cater for different assembly versions, this would be a problem. Also, as we don't control
22+
// the CLR hosting configuration, we can't always set the MultiDomain flag on setup. COM mechanisms could work, but are complicated.
2123
// Another approach would be to use a hidden Excel function that the Active server provides, and have all server register with the active server.
2224
// When a new server should become active, it then tells the active server, and somehow gets all the other registrations...
2325

@@ -28,7 +30,7 @@ namespace ExcelDna.IntelliSense
2830
// REMEMBER: COM events are not necessarily safe macro contexts.
2931
public static class IntelliSenseServer
3032
{
31-
const string ServerVersion = "1.0.9"; // TODO: Define and manage this somewhere else
33+
const string ServerVersion = "1.0.10"; // TODO: Define and manage this somewhere else
3234

3335
// NOTE: Do not change these constants in custom versions.
3436
// They are part of the co-operative safety mechanism allowing different add-ins providing IntelliSense to work together safely.
@@ -55,12 +57,19 @@ public static class IntelliSenseServer
5557
static bool _isActive = false;
5658
static IntelliSenseHelper _helper = null;
5759

58-
// Called directly from AutoOpen.
60+
// The name change from Register to Install is just to force attention on this message for anyone upgrading
61+
// - omitting the Uninstall call causes an Excel crash when the add-in is unloaded for any reason
62+
[Obsolete("IntelliSenseServer now requires matching calls to Install (inside AutoOpen) and Uninstall (inside AutoClose)", true)]
5963
public static void Register()
64+
{
65+
}
66+
67+
// Called directly from AutoOpen()
68+
public static void Install()
6069
{
6170
TraceLogger.Initialize();
6271

63-
Logger.Initialization.Info($"IntelliSenseServer.Register Begin: Version {ServerVersion} in {AppDomain.CurrentDomain.FriendlyName}");
72+
Logger.Initialization.Info($"IntelliSenseServer.Install Begin: Version {ServerVersion} in {AppDomain.CurrentDomain.FriendlyName}");
6473
if (IsDisabled())
6574
return;
6675

@@ -93,7 +102,30 @@ public static void Register()
93102

94103
AppDomain.CurrentDomain.DomainUnload += CurrentDomain_DomainUnload;
95104
AppDomain.CurrentDomain.ProcessExit += CurrentDomain_ProcessExit;
96-
Logger.Initialization.Info("IntelliSenseServer.Register End");
105+
Logger.Initialization.Info("IntelliSenseServer.Install End");
106+
}
107+
108+
// Should be called in a macro context, e.g. from AutoClose()
109+
// Was added again in 1.0.10 to clean up WinEvents
110+
// Maybe we won't need it if we can move to our own thread handling WinEvents
111+
public static void Uninstall()
112+
{
113+
Logger.Initialization.Info($"IntelliSenseServer.Uninstall Begin: Version {ServerVersion} in {AppDomain.CurrentDomain.FriendlyName}");
114+
115+
UnpublishRegistration();
116+
if (_isActive)
117+
{
118+
Deactivate();
119+
120+
// See if there is another server to active
121+
var highestRegistration = GetHighestPublishedRegistration();
122+
if (highestRegistration != null)
123+
{
124+
ActivateServer(highestRegistration);
125+
}
126+
}
127+
128+
Logger.Initialization.Info($"IntelliSenseServer.Uninstall End");
97129
}
98130

99131
// Invokes a Refresh on the Active server (if there is on)
@@ -115,9 +147,8 @@ public static void Refresh()
115147
Logger.Initialization.Info($"IntelliSenseServer.Refresh End");
116148
}
117149

118-
private static void CurrentDomain_ProcessExit(object sender, EventArgs e)
150+
static void CurrentDomain_ProcessExit(object sender, EventArgs e)
119151
{
120-
121152
// CONSIDER: We get this quite late in the shutdown
122153
// We should try to find a way to identify Excel shutdown a lot earlier
123154
Logger.Initialization.Verbose("IntelliSenseServer ProcessExit Begin");
@@ -129,10 +160,8 @@ private static void CurrentDomain_ProcessExit(object sender, EventArgs e)
129160
// Don't try to clean up clean up on process exit - all our resources are in-process anyway
130161
// Leads to an error with the main thread SynchronizationContext, which might be shut down already.
131162
// In particular we don't have to call UnhookWinEvent: "If the client's thread ends, the system automatically calls this function."
132-
// Deactivate();
133163
}
134164
Logger.Initialization.Verbose("IntelliSenseServer ProcessExit End");
135-
136165
}
137166

138167
// DomainUnload runs when AutoClose() would run on the add-in.
@@ -142,20 +171,12 @@ private static void CurrentDomain_ProcessExit(object sender, EventArgs e)
142171
static void CurrentDomain_DomainUnload(object sender, EventArgs e)
143172
{
144173
Logger.Initialization.Verbose("IntelliSenseServer DomainUnload Begin");
145-
//// Early shutdown notification
146-
//XlCall.ShutdownStarted();
147174

148-
UnpublishRegistration();
149175
if (_isActive)
150176
{
151-
Deactivate();
152-
153-
var highestRegistration = GetHighestPublishedRegistration();
154-
if (highestRegistration != null)
155-
{
156-
ActivateServer(highestRegistration);
157-
}
177+
Logger.Initialization.Error("IntelliSenseServer DomainUnload while active - Add and IntelliSenseServer.Unregister() call in AutoClose() !");
158178
}
179+
159180
Logger.Initialization.Verbose("IntelliSenseServer DomainUnload End");
160181
}
161182

@@ -167,7 +188,6 @@ internal static bool Activate()
167188
SetActiveRegistrationInfo();
168189
_isActive = true;
169190

170-
// Now initialize (TODO: perhaps lazily...?)
171191
_helper = new IntelliSenseHelper();
172192
// TODO: Perhaps also register macro to trigger updates
173193
return true;
@@ -179,9 +199,15 @@ internal static bool Activate()
179199
}
180200
}
181201

182-
// Called internally from the AppDomain_DomainUnload or AppDomain_ProcessExit event handler, and via the control function from another server when that server figures out that it must become the active server.
202+
// Should be called only on the Excel main thread, either from Unregister (AutoClose)
203+
// or via the control function from another server when that server figures out that it must become the active server.
183204
internal static bool Deactivate()
184205
{
206+
if (Thread.CurrentThread.ManagedThreadId != 1)
207+
{
208+
// At least try to log
209+
Logger.Initialization.Error($"IntelliSenseServer.Deactivate not called on the main Excel thread!");
210+
}
185211
try
186212
{
187213
if (_helper != null)
@@ -193,7 +219,6 @@ internal static bool Deactivate()
193219
}
194220
catch (Exception ex)
195221
{
196-
// TODO: Log
197222
Logger.Initialization.Error($"IntelliSenseServer.Deactivate error: {ex}");
198223
return false;
199224
}
@@ -509,7 +534,6 @@ static object IntelliSenseServerControl(object control)
509534
}
510535
return false;
511536
}
512-
513537
#endregion
514538
}
515539
}

Source/ExcelDna.IntelliSense/IntellisenseHelper.cs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66

77
namespace ExcelDna.IntelliSense
88
{
9+
// This is really the running IntelliSenseServer
10+
// It brings together:
11+
// * the UIMonitor which monitors the state of Excel, including the current function prefix,
12+
// * the IntelliSenseDisplay which presents the pop-ups, and
13+
// * the IntelliSenseProviders which figure out what information is available.
914
class IntelliSenseHelper : IDisposable
1015
{
1116
readonly SynchronizationContext _syncContextMain; // Main thread, not macro context
@@ -78,16 +83,18 @@ internal void RefreshProviders()
7883
}
7984
}
8085

86+
// Must run on the main thread
8187
public void Dispose()
8288
{
89+
Debug.Assert(Thread.CurrentThread.ManagedThreadId == 1);
8390
Logger.Initialization.Verbose("IntelliSenseHelper Dispose Start");
8491

8592
foreach (var provider in _providers)
8693
{
8794
provider.Dispose();
8895
}
89-
_uiMonitor.Dispose();
9096
_display.Dispose();
97+
_uiMonitor.Dispose();
9198

9299
Logger.Initialization.Verbose("IntelliSenseHelper Dispose End");
93100
}

Source/ExcelDna.IntelliSense/Providers/XmlIntelliSenseProvider.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ public IList<FunctionInfo> GetFunctionInfos()
160160

161161
void OnInvalidate(object _unused_)
162162
{
163-
Logger.Provider.Verbose($"XmlIntelliSenseProvider.ROnInvalidate - Invoking Invalidate event");
163+
Logger.Provider.Verbose($"XmlIntelliSenseProvider.OnInvalidate - Invoking Invalidate event");
164164
Invalidate?.Invoke(this, EventArgs.Empty);
165165
}
166166

Source/ExcelDna.IntelliSense/UIMonitor/FormulaEditWatcher.cs

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,11 @@ public IntPtr FormulaEditWindow
6767
}
6868
}
6969

70-
SynchronizationContext _syncContextAuto;
71-
SynchronizationContext _syncContextMain;
72-
WindowWatcher _windowWatcher;
70+
readonly SynchronizationContext _syncContextAuto;
71+
readonly SynchronizationContext _syncContextMain;
72+
73+
readonly WindowWatcher _windowWatcher; // Passed in
74+
WindowLocationWatcher _windowLocationWatcher; // Managed here
7375

7476
IntPtr _hwndFormulaBar;
7577
IntPtr _hwndInCellEdit;
@@ -240,6 +242,7 @@ void _windowWatcher_InCellEditWindowChanged(object sender, WindowWatcher.WindowC
240242
}
241243
}
242244

245+
// Runs on the automation thread
243246
void SetEditWindow(IntPtr newWindowHandle, ref IntPtr hwnd)
244247
{
245248
if (hwnd != newWindowHandle)
@@ -276,26 +279,21 @@ void SetEditWindow(IntPtr newWindowHandle, ref IntPtr hwnd)
276279
hwnd = newWindowHandle;
277280
}
278281

279-
WindowLocationWatcher _windowLocationWatcher;
280-
281282
// Runs on our Automation thread
282283
void InstallLocationMonitor(IntPtr hWnd)
283284
{
284-
if (_windowLocationWatcher != null)
285-
{
286-
_windowLocationWatcher.Dispose();
287-
}
285+
UninstallLocationMonitor();
288286
_windowLocationWatcher = new WindowLocationWatcher(hWnd, _syncContextAuto, _syncContextMain);
289287
_windowLocationWatcher.LocationChanged += _windowLocationWatcher_LocationChanged;
290288
}
291289

292290
// Runs on our Automation thread
293291
void UninstallLocationMonitor()
294292
{
295-
if (_windowLocationWatcher != null)
293+
WindowLocationWatcher tempWatcher = Interlocked.Exchange(ref _windowLocationWatcher, null);
294+
if (tempWatcher != null)
296295
{
297-
_windowLocationWatcher.Dispose();
298-
_windowLocationWatcher = null;
296+
_syncContextMain.Post(disp => ((IDisposable)disp).Dispose(), tempWatcher);
299297
}
300298
}
301299

@@ -315,6 +313,7 @@ void _windowLocationWatcher_LocationChanged(object sender, EventArgs e)
315313
// UpdateFormula(textChangedOnly: true);
316314
//}
317315

316+
// TODO: Get rid of this somehow - added to make the mouse clicks in the in-cell editing work, by delaying the call to the PenHelper
318317
void UpdateEditStateDelayed()
319318
{
320319
_syncContextAuto.Post(_ =>
@@ -406,11 +405,21 @@ void OnStateChanged(StateChangeType stateChangeType)
406405
StateChanged?.Invoke(this, stateChangedArgs);
407406
}
408407

408+
// Runs on the main thread
409409
public void Dispose()
410410
{
411+
Debug.Assert(Thread.CurrentThread.ManagedThreadId == 1);
412+
411413
Logger.WindowWatcher.Verbose("FormulaEdit Dispose Begin");
412414
_windowWatcher.FormulaBarWindowChanged -= _windowWatcher_FormulaBarWindowChanged;
413415
_windowWatcher.InCellEditWindowChanged -= _windowWatcher_InCellEditWindowChanged;
416+
417+
// Can't call UninstallLocationMonitor - we might be shutting down on the main thread, and don't want to post
418+
WindowLocationWatcher tempWatcher = Interlocked.Exchange(ref _windowLocationWatcher, null);
419+
if (tempWatcher != null)
420+
{
421+
tempWatcher.Dispose();
422+
}
414423
Logger.WindowWatcher.Verbose("FormulaEdit Dispose End");
415424
}
416425
}

Source/ExcelDna.IntelliSense/UIMonitor/PopupListWatcher.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Diagnostics;
23
using System.Threading;
34
using System.Windows;
45

@@ -186,6 +187,7 @@ void OnSelectedItemChanged()
186187

187188
public void Dispose()
188189
{
190+
Debug.Assert(Thread.CurrentThread.ManagedThreadId == 1);
189191
Logger.WindowWatcher.Info($"PopupList Dispose Begin");
190192
_windowWatcher.PopupListWindowChanged -= _windowWatcher_PopupListWindowChanged;
191193
_windowWatcher = null;

0 commit comments

Comments
 (0)