-
Notifications
You must be signed in to change notification settings - Fork 5.3k
Avoid Blocking Finalizer Thread During Shutdown in SystemEvents #108489
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
f15fdf2
2434963
52fc6ed
13d2c4f
dc8813c
7694fa7
13a6d37
25a247b
428aa3b
c4817f2
b11670e
7ee2524
4635420
134aca8
aeb1008
f70c8b6
452146d
787aa6a
ab4f074
5a28279
92f78ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -130,17 +130,12 @@ public static event EventHandler? DisplaySettingsChanged | |
| /// Occurs before the thread that listens for system events is terminated. | ||
| /// Delegates will be invoked on the events thread. | ||
| /// </summary> | ||
| [Obsolete(Obsoletions.SystemEventsEventsThreadShutdownMessage, DiagnosticId = Obsoletions.SystemEventsEventsThreadShutdownDiagId, UrlFormat = Obsoletions.SharedUrlFormat)] | ||
lonitra marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| public static event EventHandler? EventsThreadShutdown | ||
| { | ||
| // Really only here for GDI+ initialization and shut down | ||
| add | ||
| { | ||
| AddEventHandler(s_onEventsThreadShutdownEvent, value); | ||
| } | ||
| remove | ||
| { | ||
| RemoveEventHandler(s_onEventsThreadShutdownEvent, value); | ||
| } | ||
| add => AddEventHandler(s_onEventsThreadShutdownEvent, value); | ||
| remove => RemoveEventHandler(s_onEventsThreadShutdownEvent, value); | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -464,39 +459,43 @@ private void Dispose() | |
| /// </summary> | ||
| private static void EnsureSystemEvents(bool requireHandle) | ||
| { | ||
| if (s_systemEvents == null) | ||
| if (s_systemEvents is not null) | ||
| { | ||
| lock (s_procLockObject) | ||
| return; | ||
| } | ||
|
|
||
| lock (s_procLockObject) | ||
| { | ||
| if (s_systemEvents is not null) | ||
| { | ||
| if (s_systemEvents == null) | ||
| { | ||
| // Create a new pumping thread. We always create one even if the current thread | ||
| // is STA, as there are no guarantees this thread will pump nor still be alive | ||
| // for the desired duration. | ||
| return; | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are these just stylistic changes?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, this is just a style change. I inverted a couple if statement in this method and changed |
||
|
|
||
| s_eventWindowReady = new ManualResetEvent(false); | ||
| SystemEvents systemEvents = new SystemEvents(); | ||
| s_windowThread = new Thread(new ThreadStart(systemEvents.WindowThreadProc)) | ||
| { | ||
| IsBackground = true, | ||
| Name = ".NET System Events" | ||
| }; | ||
| s_windowThread.Start(); | ||
| s_eventWindowReady.WaitOne(); | ||
| // Create a new pumping thread. We always create one even if the current thread | ||
| // is STA, as there are no guarantees this thread will pump nor still be alive | ||
| // for the desired duration. | ||
|
|
||
| // ensure this is initialized last as that will force concurrent threads calling | ||
| // this method to block until after we've initialized. | ||
| s_systemEvents = systemEvents; | ||
| s_eventWindowReady = new ManualResetEvent(false); | ||
| SystemEvents systemEvents = new SystemEvents(); | ||
| s_windowThread = new Thread(new ThreadStart(systemEvents.WindowThreadProc)) | ||
| { | ||
| IsBackground = true, | ||
| Name = ".NET System Events" | ||
| }; | ||
| s_windowThread.Start(); | ||
| s_eventWindowReady.WaitOne(); | ||
|
|
||
| if (requireHandle && s_systemEvents._windowHandle == IntPtr.Zero) | ||
| { | ||
| // In theory, it's not the end of the world that | ||
| // we don't get system events. Unfortunately, the main reason windowHandle == 0 | ||
| // is CreateWindowEx failed for mysterious reasons, and when that happens, | ||
| // subsequent (and more important) CreateWindowEx calls also fail. | ||
| throw new ExternalException(SR.ErrorCreateSystemEvents); | ||
| } | ||
| } | ||
| // Ensure this is initialized last as that will force concurrent threads calling | ||
| // this method to block until after we've initialized. | ||
| s_systemEvents = systemEvents; | ||
|
|
||
| if (requireHandle && s_systemEvents._windowHandle == IntPtr.Zero) | ||
| { | ||
| // In theory, it's not the end of the world that | ||
| // we don't get system events. Unfortunately, the main reason windowHandle == 0 | ||
| // is CreateWindowEx failed for mysterious reasons, and when that happens, | ||
| // subsequent (and more important) CreateWindowEx calls also fail. | ||
| throw new ExternalException(SR.ErrorCreateSystemEvents); | ||
| } | ||
| } | ||
| } | ||
|
|
@@ -694,8 +693,6 @@ private unsafe void Initialize() | |
| hInstance, IntPtr.Zero); | ||
| } | ||
| } | ||
|
|
||
| AppDomain.CurrentDomain.ProcessExit += new EventHandler(Shutdown); | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -759,16 +756,15 @@ public static void InvokeOnEventsThread(Delegate method) | |
| { | ||
| int pid; | ||
| int thread = Interop.User32.GetWindowThreadProcessId(s_systemEvents!._windowHandle, &pid); | ||
| GC.KeepAlive(s_systemEvents); | ||
| Debug.Assert(s_windowThread == null || thread != Interop.Kernel32.GetCurrentThreadId(), "Don't call MarshaledInvoke on the system events thread"); | ||
| } | ||
| #endif | ||
|
|
||
| if (s_threadCallbackList == null) | ||
| if (s_threadCallbackList is null) | ||
| { | ||
| lock (s_eventLockObject) | ||
| { | ||
| if (s_threadCallbackList == null) | ||
| if (s_threadCallbackList is null) | ||
| { | ||
| s_threadCallbackMessage = Interop.User32.RegisterWindowMessageW("SystemEventsThreadCallbackMessage"); | ||
| s_threadCallbackList = new Queue<Delegate>(); | ||
|
|
@@ -784,7 +780,6 @@ public static void InvokeOnEventsThread(Delegate method) | |
| } | ||
|
|
||
| Interop.User32.PostMessageW(s_systemEvents!._windowHandle, s_threadCallbackMessage, IntPtr.Zero, IntPtr.Zero); | ||
| GC.KeepAlive(s_systemEvents); | ||
| } | ||
|
|
||
| /// <summary> | ||
|
|
@@ -1073,56 +1068,6 @@ private static void RemoveEventHandler(object key, Delegate? value) | |
| } | ||
| } | ||
|
|
||
| private static void Shutdown() | ||
| { | ||
| if (s_systemEvents != null) | ||
| { | ||
| lock (s_procLockObject) | ||
| { | ||
| if (s_systemEvents != null) | ||
| { | ||
| // If we are using system events from another thread, request that it terminate | ||
| if (s_windowThread != null) | ||
| { | ||
| #if DEBUG | ||
| unsafe | ||
| { | ||
| int pid; | ||
| int thread = Interop.User32.GetWindowThreadProcessId(s_systemEvents._windowHandle, &pid); | ||
| Debug.Assert(thread != Interop.Kernel32.GetCurrentThreadId(), "Don't call Shutdown on the system events thread"); | ||
|
|
||
| } | ||
| #endif | ||
| // The handle could be valid, Zero or invalid depending on the state of the thread | ||
| // that is processing the messages. We optimistically expect it to be valid to | ||
| // notify the thread to shutdown. The Zero or invalid values should be present | ||
| // only when the thread is already shutting down due to external factors. | ||
| if (s_systemEvents._windowHandle != IntPtr.Zero) | ||
| { | ||
| Interop.User32.PostMessageW(s_systemEvents._windowHandle, Interop.User32.WM_QUIT, IntPtr.Zero, IntPtr.Zero); | ||
| GC.KeepAlive(s_systemEvents); | ||
| } | ||
|
|
||
| s_windowThread.Join(); | ||
| } | ||
| else | ||
| { | ||
| s_systemEvents.Dispose(); | ||
| s_systemEvents = null; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| #if FEATURE_CER | ||
| [PrePrepareMethod] | ||
| #endif | ||
| private static void Shutdown(object? sender, EventArgs e) | ||
| { | ||
| Shutdown(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// A standard Win32 window proc for our broadcast window. | ||
| /// </summary> | ||
|
|
@@ -1250,8 +1195,8 @@ private IntPtr WindowProc(IntPtr hWnd, int msg, nint wParam, nint lParam) | |
| } | ||
|
|
||
| /// <summary> | ||
| /// This is the method that runs our window thread. This method | ||
| /// creates a window and spins up a message loop. The window | ||
| /// This is the method that runs our window thread. This method | ||
| /// creates a window and spins up a message loop. The window | ||
| /// is made visible with a size of 0, 0, so that it will trap | ||
| /// global broadcast messages. | ||
| /// </summary> | ||
|
|
@@ -1264,7 +1209,7 @@ private void WindowThreadProc() | |
|
|
||
| if (_windowHandle != IntPtr.Zero) | ||
| { | ||
| Interop.User32.MSG msg = default(Interop.User32.MSG); | ||
| Interop.User32.MSG msg = default; | ||
|
|
||
| while (Interop.User32.GetMessageW(ref msg, _windowHandle, 0, 0) > 0) | ||
| { | ||
|
|
@@ -1277,11 +1222,11 @@ private void WindowThreadProc() | |
| } | ||
| catch (Exception e) | ||
| { | ||
| // In case something very very wrong happend during the creation action. | ||
| // In case something very very wrong happened during the creation action. | ||
| // This will unblock the calling thread. | ||
| s_eventWindowReady!.Set(); | ||
|
|
||
| if (!((e is ThreadInterruptedException) || (e is ThreadAbortException))) | ||
| if (e is not (ThreadInterruptedException or ThreadAbortException)) | ||
| { | ||
| Debug.Fail("Unexpected thread exception in system events window thread proc", e.ToString()); | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,32 +2,37 @@ | |
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System; | ||
| using System.Threading; | ||
| using Microsoft.DotNet.RemoteExecutor; | ||
| using Xunit; | ||
| using static Interop; | ||
|
|
||
| namespace Microsoft.Win32.SystemEventsTests | ||
| namespace Microsoft.Win32.SystemEventsTests; | ||
|
|
||
| public class ShutdownTest : SystemEventsTest | ||
| { | ||
| public abstract class ShutdownTest : SystemEventsTest | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure why this was originally marked as |
||
| [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoNorServerCore))] | ||
| [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] | ||
| public void ShutdownThroughRestartManager() | ||
| { | ||
| [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoNorServerCore))] | ||
| [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] | ||
| public void ShutdownThroughRestartManager() | ||
| RemoteExecutor.Invoke(() => | ||
| { | ||
| RemoteExecutor.Invoke(() => | ||
| { | ||
| // Register any event to ensure that SystemEvents get initialized | ||
| SystemEvents.TimeChanged += (o, e) => { }; | ||
| // Register any event to ensure that SystemEvents get initialized | ||
| SystemEvents.TimeChanged += (o, e) => { }; | ||
|
|
||
| // Fake Restart Manager behavior by sending external WM_CLOSE message | ||
| SendMessage(Interop.User32.WM_CLOSE, IntPtr.Zero, IntPtr.Zero); | ||
| // Fake Restart Manager behavior by sending external WM_CLOSE message | ||
| SendMessage(Interop.User32.WM_CLOSE, IntPtr.Zero, IntPtr.Zero); | ||
| }).Dispose(); | ||
| } | ||
|
|
||
| // Emulate calling the Shutdown event | ||
| var shutdownMethod = typeof(SystemEvents).GetMethod("Shutdown", System.Reflection.BindingFlags.Static | System.Reflection.BindingFlags.NonPublic, null, new Type[0], null); | ||
| Assert.NotNull(shutdownMethod); | ||
| shutdownMethod.Invoke(null, null); | ||
| }).Dispose(); | ||
| } | ||
| [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoNorServerCore))] | ||
| [SkipOnTargetFramework(TargetFrameworkMonikers.NetFramework)] | ||
| public void ShutdownSuccessDespiteThreadBlock() | ||
| { | ||
| RemoteExecutor.Invoke(() => | ||
| { | ||
| // Block the SystemEvents thread. Regression test for https://github.com/dotnet/winforms/issues/11944 | ||
| SystemEvents.UserPreferenceChanged += (o, e) => { while (true) { } }; | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an artificial repro to make sure shutdown can happen despite a hang occuring. We will include a regression test of the issue scenario in the winforms repo. |
||
| SendMessage(User32.WM_SETTINGCHANGE, IntPtr.Zero, IntPtr.Zero); | ||
| }).Dispose(); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.