mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-07-05 01:50:26 +02:00
Compare commits
6 Commits
powerscrip
...
copilot/fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99a7fb15e7 | ||
|
|
21ba5d20e4 | ||
|
|
bf381b42c9 | ||
|
|
dddccef173 | ||
|
|
4c9e007fd9 | ||
|
|
11564d6fb0 |
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Shmuelie.WinRTServer;
|
||||
using Shmuelie.WinRTServer.CsWinRT;
|
||||
using System;
|
||||
@@ -21,14 +22,19 @@ public class Program
|
||||
global::Shmuelie.WinRTServer.ComServer server = new();
|
||||
|
||||
ManualResetEvent extensionDisposedEvent = new(false);
|
||||
|
||||
|
||||
// AppLifeMonitor creates a hidden window on a background STA thread to handle
|
||||
// WM_QUERYENDSESSION and WM_ENDSESSION. Without it, extensions hang on OS
|
||||
// shutdown because the MTA main thread has no message loop to receive those messages.
|
||||
using AppLifeMonitor monitor = new(extensionDisposedEvent);
|
||||
|
||||
// We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called.
|
||||
// This makes sure that only one instance of SampleExtension is alive, which is returned every time the host asks for the IExtension object.
|
||||
// If you want to instantiate a new instance each time the host asks, create the new instance inside the delegate.
|
||||
TemplateCmdPalExtension extensionInstance = new(extensionDisposedEvent);
|
||||
server.RegisterClass<TemplateCmdPalExtension, IExtension>(() => extensionInstance);
|
||||
server.Start();
|
||||
|
||||
|
||||
// This will make the main thread wait until the event is signalled by the extension class.
|
||||
// Since we have single instance of the extension object, we exit as soon as it is disposed.
|
||||
extensionDisposedEvent.WaitOne();
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Shmuelie.WinRTServer;
|
||||
using Shmuelie.WinRTServer.CsWinRT;
|
||||
|
||||
@@ -37,6 +38,11 @@ public class Program
|
||||
ManualResetEvent extensionDisposedEvent = new(false);
|
||||
try
|
||||
{
|
||||
// AppLifeMonitor creates a hidden window on a background STA thread to handle
|
||||
// WM_QUERYENDSESSION and WM_ENDSESSION. Without it, extensions hang on OS
|
||||
// shutdown because the MTA main thread has no message loop to receive those messages.
|
||||
using AppLifeMonitor monitor = new(extensionDisposedEvent);
|
||||
|
||||
PowerToysExtension extensionInstance = new(extensionDisposedEvent);
|
||||
Logger.LogInfo("Registering extension via Shmuelie.WinRTServer.");
|
||||
server.RegisterClass<PowerToysExtension, IExtension>(() => extensionInstance);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace ProcessMonitorExtension;
|
||||
|
||||
@@ -17,6 +18,12 @@ public class Program
|
||||
{
|
||||
using ExtensionServer server = new();
|
||||
var extensionDisposedEvent = new ManualResetEvent(false);
|
||||
|
||||
// AppLifeMonitor creates a hidden window on a background STA thread to handle
|
||||
// WM_QUERYENDSESSION and WM_ENDSESSION. Without it, extensions hang on OS
|
||||
// shutdown because the MTA main thread has no message loop to receive those messages.
|
||||
using AppLifeMonitor monitor = new(extensionDisposedEvent);
|
||||
|
||||
var extensionInstance = new SampleExtension(extensionDisposedEvent);
|
||||
|
||||
// We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
using System;
|
||||
using System.Threading;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Shmuelie.WinRTServer;
|
||||
using Shmuelie.WinRTServer.CsWinRT;
|
||||
|
||||
@@ -21,6 +22,11 @@ public class Program
|
||||
|
||||
ManualResetEvent extensionDisposedEvent = new(false);
|
||||
|
||||
// AppLifeMonitor creates a hidden window on a background STA thread to handle
|
||||
// WM_QUERYENDSESSION and WM_ENDSESSION. Without it, extensions hang on OS
|
||||
// shutdown because the MTA main thread has no message loop to receive those messages.
|
||||
using AppLifeMonitor monitor = new(extensionDisposedEvent);
|
||||
|
||||
// We are instantiating an extension instance once above, and returning it every time the callback in RegisterExtension below is called.
|
||||
// This makes sure that only one instance of SampleExtension is alive, which is returned every time the host asks for the IExtension object.
|
||||
// If you want to instantiate a new instance each time the host asks, create the new instance inside the delegate.
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
// Copyright (c) Microsoft Corporation
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
/// <summary>
|
||||
/// Monitors OS-initiated application lifecycle events (such as system shutdown or session end)
|
||||
/// and signals an event so the extension process can exit gracefully.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Extensions run as COM out-of-process servers with <c>[MTAThread]</c>, which means the main
|
||||
/// thread has no Win32 message loop. Without a message loop, the process cannot receive
|
||||
/// <c>WM_QUERYENDSESSION</c> or <c>WM_ENDSESSION</c> messages when the OS shuts down, causing
|
||||
/// hang reports and delayed Store updates.
|
||||
///
|
||||
/// This class creates a dedicated hidden window on a background STA thread whose message loop
|
||||
/// handles those messages and signals <paramref name="extensionDisposedEvent"/> so the
|
||||
/// extension exits promptly.
|
||||
///
|
||||
/// <b>Important:</b> The window must <em>not</em> use the <c>HWND_MESSAGE</c> parent because
|
||||
/// message-only windows are excluded from the OS broadcast of <c>WM_QUERYENDSESSION</c>.
|
||||
/// </remarks>
|
||||
public sealed class AppLifeMonitor : IDisposable
|
||||
{
|
||||
// Win32 window message constants
|
||||
private const uint WM_CLOSE = 0x0010;
|
||||
private const uint WM_DESTROY = 0x0002;
|
||||
private const uint WM_QUERYENDSESSION = 0x0011;
|
||||
private const uint WM_ENDSESSION = 0x0016;
|
||||
|
||||
// Invisible zero-size pop-up window style. Must NOT use HWND_MESSAGE parent:
|
||||
// message-only windows are excluded from OS shutdown broadcasts.
|
||||
private const uint WS_POPUP = 0x80000000;
|
||||
|
||||
private delegate nint WndProcDelegate(nint hWnd, uint msg, nint wParam, nint lParam);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct WNDCLASSW
|
||||
{
|
||||
public uint style;
|
||||
public nint lpfnWndProc;
|
||||
public int cbClsExtra;
|
||||
public int cbWndExtra;
|
||||
public nint hInstance;
|
||||
public nint hIcon;
|
||||
public nint hCursor;
|
||||
public nint hbrBackground;
|
||||
public nint lpszMenuName;
|
||||
public nint lpszClassName;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct MSG
|
||||
{
|
||||
public nint hwnd;
|
||||
public uint message;
|
||||
public nint wParam;
|
||||
public nint lParam;
|
||||
public uint time;
|
||||
public int ptX;
|
||||
public int ptY;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern ushort RegisterClassW(ref WNDCLASSW lpWndClass);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern nint CreateWindowExW(
|
||||
uint dwExStyle,
|
||||
nint lpClassName,
|
||||
nint lpWindowName,
|
||||
uint dwStyle,
|
||||
int x,
|
||||
int y,
|
||||
int nWidth,
|
||||
int nHeight,
|
||||
nint hWndParent,
|
||||
nint hMenu,
|
||||
nint hInstance,
|
||||
nint lpParam);
|
||||
|
||||
[DllImport("user32.dll", SetLastError = true)]
|
||||
private static extern int GetMessageW(out MSG lpMsg, nint hWnd, uint wMsgFilterMin, uint wMsgFilterMax);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern nint DispatchMessageW(ref MSG msg);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool TranslateMessage(ref MSG lpMsg);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern nint DefWindowProcW(nint hWnd, uint msg, nint wParam, nint lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool DestroyWindow(nint hWnd);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern void PostQuitMessage(int nExitCode);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool PostMessageW(nint hWnd, uint msg, nint wParam, nint lParam);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool UnregisterClassW(nint lpClassName, nint hInstance);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern nint GetModuleHandleW(nint lpModuleName);
|
||||
|
||||
private readonly ManualResetEvent _extensionDisposedEvent;
|
||||
private readonly ManualResetEvent _windowCreated = new(false);
|
||||
|
||||
// Keep the delegate alive for the lifetime of the window to prevent GC collection.
|
||||
// The GC may collect the delegate if it is only stored as a function pointer.
|
||||
private WndProcDelegate? _wndProcDelegate;
|
||||
|
||||
private nint _hwnd;
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="AppLifeMonitor"/> class and starts
|
||||
/// the background message loop immediately.
|
||||
/// </summary>
|
||||
/// <param name="extensionDisposedEvent">
|
||||
/// The event to signal when the OS requests the session to end (e.g. system shutdown,
|
||||
/// user logoff, or Store-initiated update). Signalling this event causes the
|
||||
/// <c>extensionDisposedEvent.WaitOne()</c> call in <c>Program.Main</c> to unblock so
|
||||
/// the COM server can stop gracefully before the OS deadline expires.
|
||||
/// </param>
|
||||
public AppLifeMonitor(ManualResetEvent extensionDisposedEvent)
|
||||
{
|
||||
_extensionDisposedEvent = extensionDisposedEvent ?? throw new ArgumentNullException(nameof(extensionDisposedEvent));
|
||||
|
||||
var thread = new Thread(RunMessageLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "AppLifeMonitor",
|
||||
};
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
|
||||
// Block until the window is ready (or has failed to initialize) before returning.
|
||||
_windowCreated.WaitOne();
|
||||
}
|
||||
|
||||
private void RunMessageLoop()
|
||||
{
|
||||
// Use a process-specific class name to avoid collisions if the same extension
|
||||
// is loaded multiple times in the same session.
|
||||
var className = $"AppLifeMonitor_{Environment.ProcessId}";
|
||||
var classNamePtr = Marshal.StringToHGlobalUni(className);
|
||||
var hInstance = GetModuleHandleW(0);
|
||||
var classRegistered = false;
|
||||
|
||||
try
|
||||
{
|
||||
// Store the delegate in a field so it is not collected by the GC.
|
||||
_wndProcDelegate = HandleMessage;
|
||||
var wndProcPtr = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate);
|
||||
|
||||
var wndClass = new WNDCLASSW
|
||||
{
|
||||
lpfnWndProc = wndProcPtr,
|
||||
hInstance = hInstance,
|
||||
lpszClassName = classNamePtr,
|
||||
};
|
||||
|
||||
var atom = RegisterClassW(ref wndClass);
|
||||
if (atom == 0)
|
||||
{
|
||||
Trace.TraceWarning("AppLifeMonitor: RegisterClassW failed with error {0}", Marshal.GetLastWin32Error());
|
||||
return;
|
||||
}
|
||||
|
||||
classRegistered = true;
|
||||
|
||||
// Create a 0×0 invisible pop-up window.
|
||||
// This must NOT be a message-only window (HWND_MESSAGE parent) because
|
||||
// message-only windows are excluded from OS shutdown broadcasts.
|
||||
_hwnd = CreateWindowExW(
|
||||
dwExStyle: 0,
|
||||
lpClassName: classNamePtr,
|
||||
lpWindowName: 0,
|
||||
dwStyle: WS_POPUP,
|
||||
x: 0,
|
||||
y: 0,
|
||||
nWidth: 0,
|
||||
nHeight: 0,
|
||||
hWndParent: 0,
|
||||
hMenu: 0,
|
||||
hInstance: hInstance,
|
||||
lpParam: 0);
|
||||
|
||||
if (_hwnd == 0)
|
||||
{
|
||||
Trace.TraceWarning("AppLifeMonitor: CreateWindowExW failed with error {0}", Marshal.GetLastWin32Error());
|
||||
return;
|
||||
}
|
||||
|
||||
// Signal that initialization succeeded before entering the message loop.
|
||||
// The constructor's WaitOne() unblocks here.
|
||||
_windowCreated.Set();
|
||||
|
||||
// Run the message loop until PostQuitMessage is called (WM_QUIT) or an error occurs.
|
||||
int ret;
|
||||
while ((ret = GetMessageW(out var msg, nint.Zero, 0, 0)) > 0)
|
||||
{
|
||||
TranslateMessage(ref msg);
|
||||
DispatchMessageW(ref msg);
|
||||
}
|
||||
|
||||
if (ret == -1)
|
||||
{
|
||||
Trace.TraceWarning("AppLifeMonitor: GetMessageW failed with error {0}", Marshal.GetLastWin32Error());
|
||||
}
|
||||
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (classRegistered)
|
||||
{
|
||||
UnregisterClassW(classNamePtr, hInstance);
|
||||
}
|
||||
|
||||
// Always signal so the constructor's WaitOne() unblocks even on failure paths.
|
||||
// Set() is idempotent, so calling it again after an early Set() in the try block is safe.
|
||||
// Note: _windowCreated is not disposed here because there is a theoretical window
|
||||
// between Set() and WaitOne() returning in the constructor where Dispose() could race.
|
||||
// ManualResetEvent wraps a kernel object that is released when the process exits
|
||||
// or the GC finalizes it; for a process-lifetime object this is acceptable.
|
||||
_windowCreated.Set();
|
||||
Marshal.FreeHGlobal(classNamePtr);
|
||||
_wndProcDelegate = null;
|
||||
}
|
||||
}
|
||||
|
||||
private nint HandleMessage(nint hWnd, uint msg, nint wParam, nint lParam)
|
||||
{
|
||||
switch (msg)
|
||||
{
|
||||
case WM_QUERYENDSESSION:
|
||||
// Return non-zero to permit the session to end.
|
||||
return 1;
|
||||
|
||||
case WM_ENDSESSION:
|
||||
// wParam is non-zero when the session is actually ending.
|
||||
if (wParam != 0)
|
||||
{
|
||||
_extensionDisposedEvent.Set();
|
||||
}
|
||||
|
||||
return 0;
|
||||
|
||||
case WM_CLOSE:
|
||||
DestroyWindow(hWnd);
|
||||
return 0;
|
||||
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
|
||||
default:
|
||||
return DefWindowProcW(hWnd, msg, wParam, lParam);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
var hwnd = Interlocked.Exchange(ref _hwnd, 0);
|
||||
if (hwnd != 0)
|
||||
{
|
||||
// Post WM_CLOSE to our window on the message thread.
|
||||
// The WndProc will call DestroyWindow → WM_DESTROY → PostQuitMessage,
|
||||
// which unblocks GetMessageW and lets the background thread exit cleanly.
|
||||
// The background thread's finally block will then signal and dispose _windowCreated.
|
||||
PostMessageW(hwnd, WM_CLOSE, 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user