Skip to content
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

Avoid Blocking Finalizer Thread During Shutdown in SystemEvents #108489

Merged
merged 21 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/project/list-of-diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ The PR that reveals the implementation of the `<IncludeInternalObsoleteAttribute
| __`SYSLIB0056`__ | LoadFrom with a custom AssemblyHashAlgorithm is obsolete. Use overloads without an AssemblyHashAlgorithm. |
| __`SYSLIB0057`__ | Loading certificate data through the constructor or Import is obsolete. Use X509CertificateLoader instead to load certificates. |
| __`SYSLIB0058`__ | KeyExchangeAlgorithm, KeyExchangeStrength, CipherAlgorithm, CipherAlgorithmStrength, HashAlgorithm and HashStrength properties of SslStream are obsolete. Use NegotiatedCipherSuite instead. |
| __`SYSLIB0059`__ | SystemEvents.EventsThreadShutdown callbacks are not run before process exits. Use AppDomain.ProcessExit instead. |
lonitra marked this conversation as resolved.
Show resolved Hide resolved

## Analyzer Warnings

Expand Down
3 changes: 3 additions & 0 deletions src/libraries/Common/src/System/Obsoletions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ internal static class Obsoletions
internal const string TlsCipherAlgorithmEnumsMessage = "KeyExchangeAlgorithm, KeyExchangeStrength, CipherAlgorithm, CipherAlgorithmStrength, HashAlgorithm and HashStrength properties of SslStream are obsolete. Use NegotiatedCipherSuite instead.";
internal const string TlsCipherAlgorithmEnumsDiagId = "SYSLIB0058";

internal const string SystemEventsEventsThreadShutdownMessage = "SystemEvents.EventsThreadShutdown callbacks are not run before process exits. Use AppDomain.ProcessExit instead.";
lonitra marked this conversation as resolved.
Show resolved Hide resolved
internal const string SystemEventsEventsThreadShutdownDiagId = "SYSLIB0059";

// When adding a new diagnostic ID, add it to the table in docs\project\list-of-diagnostics.md as well.
// Keep new const identifiers above this comment.
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ public sealed partial class SystemEvents
internal SystemEvents() { }
public static event System.EventHandler? DisplaySettingsChanged { add { } remove { } }
public static event System.EventHandler? DisplaySettingsChanging { add { } remove { } }
[System.Obsolete("SystemEvents.EventsThreadShutdown callbacks are not run before process exits. Use AppDomain.ProcessExit instead.")]
lonitra marked this conversation as resolved.
Show resolved Hide resolved
public static event System.EventHandler? EventsThreadShutdown { add { } remove { } }
public static event System.EventHandler? InstalledFontsChanged { add { } remove { } }
[System.ComponentModel.BrowsableAttribute(false)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
<ItemGroup>
<Compile Include="Microsoft.Win32.SystemEvents.cs" Condition="'$(TargetFrameworkIdentifier)' != '.NETFramework'" />
<Compile Include="Microsoft.Win32.SystemEvents.netframework.cs" Condition="'$(TargetFrameworkIdentifier)' == '.NETFramework'" />
<Compile Include="$(CommonPath)System\Obsoletions.cs" Link="Common\System\Obsoletions.cs" />
lonitra marked this conversation as resolved.
Show resolved Hide resolved
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,22 @@
<Left>lib/netstandard2.0/Microsoft.Win32.SystemEvents.dll</Left>
<Right>lib/net462/Microsoft.Win32.SystemEvents.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0015</DiagnosticId>
<Target>E:Microsoft.Win32.SystemEvents.EventsThreadShutdown:[T:System.ObsoleteAttribute]</Target>
<Left>ref/net10.0/Microsoft.Win32.SystemEvents.dll</Left>
<Right>runtimes/win/lib/net10.0/Microsoft.Win32.SystemEvents.dll</Right>
</Suppression>
lonitra marked this conversation as resolved.
Show resolved Hide resolved
<Suppression>
<DiagnosticId>CP0015</DiagnosticId>
<Target>E:Microsoft.Win32.SystemEvents.EventsThreadShutdown:[T:System.ObsoleteAttribute]</Target>
<Left>ref/net8.0/Microsoft.Win32.SystemEvents.dll</Left>
<Right>runtimes/win/lib/net8.0/Microsoft.Win32.SystemEvents.dll</Right>
</Suppression>
<Suppression>
<DiagnosticId>CP0015</DiagnosticId>
<Target>E:Microsoft.Win32.SystemEvents.EventsThreadShutdown:[T:System.ObsoleteAttribute]</Target>
<Left>ref/net9.0/Microsoft.Win32.SystemEvents.dll</Left>
<Right>runtimes/win/lib/net9.0/Microsoft.Win32.SystemEvents.dll</Right>
</Suppression>
</Suppressions>
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ Microsoft.Win32.SystemEvents</PackageDescription>
Link="Common\Interop\Windows\WtsApi32\Interop.WTSUnRegisterSessionNotification.cs" />
<Compile Include="$(CommonPath)Microsoft\Win32\SafeHandles\SafeLibraryHandle.cs"
Link="Common\Microsoft\Win32\SafeHandles\SafeLibraryHandle.cs" />
<Compile Include="$(CommonPath)System\Obsoletions.cs"
Link="Common\System\Obsoletions.cs" />
ViktorHofer marked this conversation as resolved.
Show resolved Hide resolved
<Compile Include="Microsoft\Win32\PowerModeChangedEventArgs.cs" />
<Compile Include="Microsoft\Win32\PowerModeChangedEventHandler.cs" />
<Compile Include="Microsoft\Win32\PowerModes.cs" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -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;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these just stylistic changes?

Copy link
Member Author

@lonitra lonitra Nov 8, 2024

Choose a reason for hiding this comment

The 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 ==/!= to use is/is not


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);
}
}
}
Expand Down Expand Up @@ -694,8 +693,6 @@ private unsafe void Initialize()
hInstance, IntPtr.Zero);
}
}

AppDomain.CurrentDomain.ProcessExit += new EventHandler(Shutdown);
}

/// <summary>
Expand Down Expand Up @@ -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>();
Expand All @@ -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>
Expand Down Expand Up @@ -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>
Expand Down Expand Up @@ -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>
Expand All @@ -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)
{
Expand All @@ -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());
}
Expand Down
41 changes: 23 additions & 18 deletions src/libraries/Microsoft.Win32.SystemEvents/tests/ShutdownTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Member Author

@lonitra lonitra Oct 2, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure why this was originally marked as abstract but it seems that was causing the tests not to run for this class

[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) { } };
Copy link
Member Author

Choose a reason for hiding this comment

The 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();
}
}
Loading