Refactor PowerDisplay to use Windows Named Events

Replaced IPC-based communication with Windows Named Events for
simpler and more reliable process interaction. Introduced the
`NativeEventWaiter` helper class to handle event signaling and
callbacks. Removed the `PowerDisplayProcessManager` class and
refactored process lifecycle management to use direct process
launching and event signaling.

Simplified `App.xaml.cs` by removing IPC logic and adding event-
based handling for window visibility, monitor refresh, settings
updates, and termination. Enhanced `MainWindow` initialization
and show logic with detailed logging and error handling.

Updated `dllmain.cpp` to manage persistent event handles and
refactored the `enable` and `disable` methods to use event-based
communication. Improved process termination logic with additional
checks and logging.

Performed general cleanup, including removing unused code,
improving readability, and enhancing error handling throughout
the codebase.
This commit is contained in:
Yu Leng
2025-11-13 12:40:56 +08:00
parent 753fecbe9f
commit ac9fd27095
11 changed files with 447 additions and 283 deletions

View File

@@ -203,4 +203,12 @@ namespace winrt::PowerToys::Interop::implementation
{ {
return CommonSharedConstants::TERMINATE_POWER_DISPLAY_EVENT; return CommonSharedConstants::TERMINATE_POWER_DISPLAY_EVENT;
} }
hstring Constants::RefreshPowerDisplayMonitorsEvent()
{
return CommonSharedConstants::REFRESH_POWER_DISPLAY_MONITORS_EVENT;
}
hstring Constants::SettingsUpdatedPowerDisplayEvent()
{
return CommonSharedConstants::SETTINGS_UPDATED_POWER_DISPLAY_EVENT;
}
} }

View File

@@ -54,6 +54,8 @@ namespace winrt::PowerToys::Interop::implementation
static hstring ShowCmdPalEvent(); static hstring ShowCmdPalEvent();
static hstring ShowPowerDisplayEvent(); static hstring ShowPowerDisplayEvent();
static hstring TerminatePowerDisplayEvent(); static hstring TerminatePowerDisplayEvent();
static hstring RefreshPowerDisplayMonitorsEvent();
static hstring SettingsUpdatedPowerDisplayEvent();
}; };
} }

View File

@@ -134,6 +134,8 @@ namespace CommonSharedConstants
// Path to the events used by PowerDisplay // Path to the events used by PowerDisplay
const wchar_t SHOW_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ShowEvent-d8a4e0e3-2c5b-4a1c-9e7f-8b3d6c1a2f4e"; const wchar_t SHOW_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-ShowEvent-d8a4e0e3-2c5b-4a1c-9e7f-8b3d6c1a2f4e";
const wchar_t TERMINATE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a"; const wchar_t TERMINATE_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a";
const wchar_t REFRESH_POWER_DISPLAY_MONITORS_EVENT[] = L"Local\\PowerToysPowerDisplay-RefreshMonitorsEvent-a3f5c8e7-9d1b-4e2f-8c6a-3b5d7e9f1a2c";
const wchar_t SETTINGS_UPDATED_POWER_DISPLAY_EVENT[] = L"Local\\PowerToysPowerDisplay-SettingsUpdatedEvent-2e4d6f8a-1c3b-5e7f-9a1d-4c6e8f0b2d3e";
// used from quick access window // used from quick access window
const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a"; const wchar_t CMDPAL_SHOW_EVENT[] = L"Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a";

View File

@@ -0,0 +1,41 @@
// 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.Threading;
using Microsoft.UI.Dispatching;
namespace PowerDisplay.Helpers
{
/// <summary>
/// Helper class for waiting on Windows Named Events (Awake pattern)
/// Based on Peek.UI implementation
/// </summary>
public static class NativeEventWaiter
{
/// <summary>
/// Wait for a Windows Event in a background thread and invoke callback on UI thread when signaled
/// </summary>
/// <param name="eventName">Name of the Windows Event to wait for</param>
/// <param name="callback">Callback to invoke when event is signaled</param>
public static void WaitForEventLoop(string eventName, Action callback)
{
var dispatcherQueue = DispatcherQueue.GetForCurrentThread();
var t = new Thread(() =>
{
var eventHandle = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
while (true)
{
if (eventHandle.WaitOne())
{
dispatcherQueue.TryEnqueue(() => callback());
}
}
});
t.IsBackground = true;
t.Start();
}
}
}

View File

@@ -67,6 +67,7 @@
<!-- Removed Common.UI dependency - SettingsDeepLink is now implemented locally --> <!-- Removed Common.UI dependency - SettingsDeepLink is now implemented locally -->
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" /> <ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" /> <ProjectReference Include="..\..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" /> <ProjectReference Include="..\..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
</ItemGroup> </ItemGroup>
<!-- Copy Assets folder to output directory --> <!-- Copy Assets folder to output directory -->

View File

@@ -14,6 +14,7 @@ using Microsoft.UI.Xaml.Media;
using Microsoft.Windows.AppLifecycle; using Microsoft.Windows.AppLifecycle;
using PowerDisplay.Helpers; using PowerDisplay.Helpers;
using PowerDisplay.Serialization; using PowerDisplay.Serialization;
using PowerToys.Interop;
namespace PowerDisplay namespace PowerDisplay
{ {
@@ -24,15 +25,18 @@ namespace PowerDisplay
public partial class App : Application public partial class App : Application
#pragma warning restore CA1001 #pragma warning restore CA1001
{ {
// Windows Event names (from shared_constants.h)
private const string ShowPowerDisplayEvent = "Local\\PowerToysPowerDisplay-ShowEvent-d8a4e0e3-2c5b-4a1c-9e7f-8b3d6c1a2f4e";
private const string TerminatePowerDisplayEvent = "Local\\PowerToysPowerDisplay-TerminateEvent-7b9c2e1f-8a5d-4c3e-9f6b-2a1d8c5e3b7a";
private const string RefreshMonitorsEvent = "Local\\PowerToysPowerDisplay-RefreshMonitorsEvent-a3f5c8e7-9d1b-4e2f-8c6a-3b5d7e9f1a2c";
private const string SettingsUpdatedEvent = "Local\\PowerToysPowerDisplay-SettingsUpdatedEvent-2e4d6f8a-1c3b-5e7f-9a1d-4c6e8f0b2d3e";
private Window? _mainWindow; private Window? _mainWindow;
private int _powerToysRunnerPid; private int _powerToysRunnerPid;
private string _pipeUuid = string.Empty;
private CancellationTokenSource? _ipcCancellationTokenSource;
public App(int runnerPid, string pipeUuid) public App(int runnerPid)
{ {
_powerToysRunnerPid = runnerPid; _powerToysRunnerPid = runnerPid;
_pipeUuid = pipeUuid;
this.InitializeComponent(); this.InitializeComponent();
@@ -84,84 +88,100 @@ namespace PowerDisplay
try try
{ {
// Single instance is already ensured by AppInstance.FindOrRegisterForKey() in Program.cs // Single instance is already ensured by AppInstance.FindOrRegisterForKey() in Program.cs
// No need for additional Mutex check here // PID is already parsed in Program.cs and passed to constructor
// Parse command line arguments // Set up Windows Events monitoring (Awake pattern)
var cmdArgs = Environment.GetCommandLineArgs(); NativeEventWaiter.WaitForEventLoop(
if (cmdArgs?.Length > 1) ShowPowerDisplayEvent,
{ () =>
// Support two formats: direct PID or --pid PID
int pidValue = -1;
// Check if using --pid format
for (int i = 1; i < cmdArgs.Length - 1; i++)
{ {
if (cmdArgs[i] == "--pid" && int.TryParse(cmdArgs[i + 1], out pidValue)) Logger.LogInfo("[EVENT] Show event received");
Logger.LogInfo($"[EVENT] _mainWindow is null: {_mainWindow == null}");
Logger.LogInfo($"[EVENT] _mainWindow type: {_mainWindow?.GetType().Name}");
Logger.LogInfo($"[EVENT] Current thread ID: {Environment.CurrentManagedThreadId}");
// Direct call - NativeEventWaiter already marshalled to UI thread
// No need for double DispatcherQueue.TryEnqueue
if (_mainWindow is MainWindow mainWindow)
{ {
break; Logger.LogInfo("[EVENT] Calling ShowWindow directly");
mainWindow.ShowWindow();
Logger.LogInfo("[EVENT] ShowWindow returned");
} }
} else
// If not --pid format, try parsing last argument (compatible with old format)
if (pidValue == -1 && cmdArgs.Length > 1)
{
_ = int.TryParse(cmdArgs[cmdArgs.Length - 1], out pidValue);
}
if (pidValue > 0)
{
_powerToysRunnerPid = pidValue;
// Started from PowerToys Runner
Logger.LogInfo($"PowerDisplay started from PowerToys Runner. Runner pid={_powerToysRunnerPid}");
// Monitor parent process
RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () =>
{ {
Logger.LogInfo("PowerToys Runner exited. Exiting PowerDisplay"); Logger.LogError($"[EVENT] _mainWindow type mismatch, actual type: {_mainWindow?.GetType().Name}");
ForceExit(); }
});
NativeEventWaiter.WaitForEventLoop(
TerminatePowerDisplayEvent,
() =>
{
Logger.LogInfo("Received terminate event - exiting immediately");
Environment.Exit(0);
});
NativeEventWaiter.WaitForEventLoop(
RefreshMonitorsEvent,
() =>
{
Logger.LogInfo("Received refresh monitors event");
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
{
if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
{
mainWindow.ViewModel.RefreshCommand?.Execute(null);
}
}); });
} });
NativeEventWaiter.WaitForEventLoop(
SettingsUpdatedEvent,
() =>
{
Logger.LogInfo("Received settings updated event");
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
{
if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
{
_ = mainWindow.ViewModel.ReloadMonitorSettingsAsync();
}
});
});
// Monitor Runner process (backup exit mechanism)
if (_powerToysRunnerPid > 0)
{
Logger.LogInfo($"PowerDisplay started from PowerToys Runner. Runner pid={_powerToysRunnerPid}");
RunnerHelper.WaitForPowerToysRunner(_powerToysRunnerPid, () =>
{
Logger.LogInfo("PowerToys Runner exited. Exiting PowerDisplay");
Environment.Exit(0);
});
} }
else else
{ {
// Standalone mode Logger.LogInfo("PowerDisplay started in standalone mode");
Logger.LogInfo("PowerDisplay started detached from PowerToys Runner.");
_powerToysRunnerPid = -1;
}
// Initialize IPC in background (non-blocking)
// Only connect pipes when launched from PowerToys (not standalone)
bool isIPCMode = !string.IsNullOrEmpty(_pipeUuid) && _powerToysRunnerPid != -1;
if (isIPCMode)
{
// Start IPC message listener in background
_ipcCancellationTokenSource = new CancellationTokenSource();
_ = Task.Run(() => StartIPCListener(_pipeUuid, _ipcCancellationTokenSource.Token));
Logger.LogInfo("Starting IPC pipe listener in background");
}
else
{
Logger.LogInfo("Running in standalone mode, IPC disabled");
} }
// Create main window // Create main window
_mainWindow = new MainWindow(); _mainWindow = new MainWindow();
// Window visibility depends on launch mode // Window visibility depends on launch mode
// - IPC mode (launched by PowerToys Runner): Start hidden, wait for show_window IPC command bool isStandaloneMode = _powerToysRunnerPid <= 0;
// - Standalone mode (no command-line args): Show window immediately
if (!isIPCMode) if (isStandaloneMode)
{ {
// Standalone mode - activate and show window // Standalone mode - activate and show window immediately
_mainWindow.Activate(); _mainWindow.Activate();
Logger.LogInfo("Window activated (standalone mode)"); Logger.LogInfo("Window activated (standalone mode)");
} }
else else
{ {
// IPC mode - window remains inactive (hidden) until show_window command received // PowerToys mode - window remains hidden until show event received
Logger.LogInfo("Window created but not activated (IPC mode - waiting for show_window command)"); Logger.LogInfo("Window created, waiting for show event (PowerToys mode)");
// Start background initialization to scan monitors even when hidden // Start background initialization to scan monitors even when hidden
_ = Task.Run(async () => _ = Task.Run(async () =>
@@ -175,7 +195,7 @@ namespace PowerDisplay
if (_mainWindow is MainWindow mainWindow) if (_mainWindow is MainWindow mainWindow)
{ {
await mainWindow.EnsureInitializedAsync(); await mainWindow.EnsureInitializedAsync();
Logger.LogInfo("Background initialization completed (IPC mode)"); Logger.LogInfo("Background initialization completed");
} }
}); });
}); });
@@ -187,122 +207,6 @@ namespace PowerDisplay
} }
} }
/// <summary>
/// Start IPC listener to receive commands from ModuleInterface
/// </summary>
private async Task StartIPCListener(string pipeUuid, CancellationToken cancellationToken)
{
try
{
string pipeName = $"powertoys_powerdisplay_{pipeUuid}";
Logger.LogInfo($"Connecting to pipe: {pipeName}");
await NamedPipeProcessor.ProcessNamedPipeAsync(
pipeName,
TimeSpan.FromSeconds(5),
OnIPCMessageReceived,
cancellationToken);
}
catch (OperationCanceledException)
{
Logger.LogInfo("IPC listener cancelled");
}
catch (Exception ex)
{
Logger.LogError($"Error in IPC listener: {ex.Message}");
}
}
/// <summary>
/// Handle IPC messages received from ModuleInterface
/// </summary>
private void OnIPCMessageReceived(string message)
{
try
{
Logger.LogInfo($"Received IPC message: {message}");
// Parse JSON command
var json = System.Text.Json.JsonDocument.Parse(message);
var root = json.RootElement;
if (root.TryGetProperty("action", out var actionElement))
{
string action = actionElement.GetString() ?? string.Empty;
switch (action)
{
case "show_window":
Logger.LogInfo("Received show_window command");
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
{
if (_mainWindow is MainWindow mainWindow)
{
mainWindow.ShowWindow();
}
});
break;
case "toggle_window":
Logger.LogInfo("Received toggle_window command");
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
{
if (_mainWindow is MainWindow mainWindow)
{
if (mainWindow.IsWindowVisible())
{
mainWindow.HideWindow();
}
else
{
mainWindow.ShowWindow();
}
}
});
break;
case "refresh_monitors":
Logger.LogInfo("Received refresh_monitors command");
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
{
if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
{
mainWindow.ViewModel.RefreshCommand?.Execute(null);
}
});
break;
case "settings_updated":
Logger.LogInfo("Received settings_updated command");
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
{
if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
{
_ = mainWindow.ViewModel.ReloadMonitorSettingsAsync();
}
});
break;
case "terminate":
Logger.LogInfo("Received terminate command");
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
{
Shutdown();
});
break;
default:
Logger.LogWarning($"Unknown action: {action}");
break;
}
}
}
catch (Exception ex)
{
Logger.LogError($"Error processing IPC message: {ex.Message}");
}
}
/// <summary> /// <summary>
/// Show startup error /// Show startup error
/// </summary> /// </summary>
@@ -377,32 +281,11 @@ namespace PowerDisplay
} }
/// <summary> /// <summary>
/// Shutdown application (simplified version following other PowerToys modules pattern) /// Shutdown application (Awake pattern - simple and clean)
/// </summary> /// </summary>
public void Shutdown() public void Shutdown()
{ {
Logger.LogInfo("PowerDisplay shutting down"); Logger.LogInfo("PowerDisplay shutting down");
// Cancel IPC listener
_ipcCancellationTokenSource?.Cancel();
_ipcCancellationTokenSource?.Dispose();
// Exit immediately - OS will clean up all resources (pipes, threads, windows, etc.)
// Single instance is managed by AppInstance in Program.cs, no manual cleanup needed
Environment.Exit(0);
}
/// <summary>
/// Force exit when PowerToys Runner exits
/// </summary>
private void ForceExit()
{
Logger.LogInfo("PowerToys Runner exited, forcing shutdown");
// Cancel IPC listener
_ipcCancellationTokenSource?.Cancel();
_ipcCancellationTokenSource?.Dispose();
Environment.Exit(0); Environment.Exit(0);
} }
} }

View File

@@ -240,39 +240,83 @@ namespace PowerDisplay
} }
} }
public async void ShowWindow() public void ShowWindow()
{ {
// Ensure window is initialized before showing Logger.LogInfo("[SHOWWINDOW] Method entry");
if (!_hasInitialized) Logger.LogInfo($"[SHOWWINDOW] _hasInitialized: {_hasInitialized}");
Logger.LogInfo($"[SHOWWINDOW] Current thread ID: {Environment.CurrentManagedThreadId}");
try
{ {
await EnsureInitializedAsync(); // If not initialized, log warning but continue showing
if (!_hasInitialized)
{
Logger.LogWarning("[SHOWWINDOW] Window not fully initialized yet, showing anyway");
}
Logger.LogInfo("[SHOWWINDOW] Getting window handle");
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
Logger.LogInfo($"[SHOWWINDOW] Window handle: 0x{hWnd:X}");
Logger.LogInfo("[SHOWWINDOW] Adjusting window size");
AdjustWindowSizeToContent();
Logger.LogInfo("[SHOWWINDOW] Repositioning window");
if (_appWindow != null)
{
PositionWindowAtBottomRight(_appWindow);
Logger.LogInfo("[SHOWWINDOW] Window repositioned");
}
else
{
Logger.LogWarning("[SHOWWINDOW] _appWindow is null, skipping reposition");
}
Logger.LogInfo("[SHOWWINDOW] Setting opacity to 0 for animation");
RootGrid.Opacity = 0;
Logger.LogInfo("[SHOWWINDOW] Calling this.Activate()");
this.Activate();
Logger.LogInfo("[SHOWWINDOW] Calling WindowHelper.ShowWindow");
WindowHelper.ShowWindow(hWnd, true);
Logger.LogInfo("[SHOWWINDOW] Calling WindowHelpers.BringToForeground");
WindowHelpers.BringToForeground(hWnd);
Logger.LogInfo("[SHOWWINDOW] Checking for animation storyboard");
if (RootGrid.Resources.ContainsKey("SlideInStoryboard"))
{
Logger.LogInfo("[SHOWWINDOW] Starting SlideInStoryboard animation");
var slideInStoryboard = RootGrid.Resources["SlideInStoryboard"] as Storyboard;
slideInStoryboard?.Begin();
}
else
{
Logger.LogWarning("[SHOWWINDOW] SlideInStoryboard not found, setting opacity=1");
RootGrid.Opacity = 1;
}
Logger.LogInfo("[SHOWWINDOW] Verifying window visibility");
bool isVisible = IsWindowVisible();
Logger.LogInfo($"[SHOWWINDOW] IsWindowVisible result: {isVisible}");
if (!isVisible)
{
Logger.LogError("[SHOWWINDOW] Window not visible after show, forcing visibility");
RootGrid.Opacity = 1;
this.Activate();
WindowHelpers.BringToForeground(hWnd);
}
Logger.LogInfo("[SHOWWINDOW] Method completed successfully");
} }
catch (Exception ex)
var hWnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
// Adjust window size before showing
AdjustWindowSizeToContent();
// Reposition to bottom right (set position before showing)
if (_appWindow != null)
{ {
PositionWindowAtBottomRight(_appWindow); Logger.LogError($"[SHOWWINDOW] Exception: {ex.GetType().Name}");
} Logger.LogError($"[SHOWWINDOW] Exception message: {ex.Message}");
Logger.LogError($"[SHOWWINDOW] Stack trace: {ex.StackTrace}");
// Set initial state for animation throw;
RootGrid.Opacity = 0;
// Show window
WindowHelper.ShowWindow(hWnd, true);
// Bring window to foreground
PInvoke.SetForegroundWindow(hWnd);
// Use storyboard animation for window entrance
if (RootGrid.Resources.ContainsKey("SlideInStoryboard"))
{
var slideInStoryboard = RootGrid.Resources["SlideInStoryboard"] as Storyboard;
slideInStoryboard?.Begin();
} }
} }

View File

@@ -19,24 +19,24 @@ namespace PowerDisplay
WinRT.ComWrappersSupport.InitializeComWrappers(); WinRT.ComWrappersSupport.InitializeComWrappers();
// Parse command line arguments: args[0] = runner_pid, args[1] = pipe_uuid // Parse command line arguments: args[0] = runner_pid (Awake pattern)
int runnerPid = -1; int runnerPid = -1;
string pipeUuid = string.Empty;
if (args.Length >= 2) if (args.Length >= 1)
{ {
if (int.TryParse(args[0], out int parsedPid)) if (int.TryParse(args[0], out int parsedPid))
{ {
runnerPid = parsedPid; runnerPid = parsedPid;
Logger.LogInfo($"PowerDisplay started with runner_pid={runnerPid}");
}
else
{
Logger.LogWarning($"Failed to parse PID from args[0]: {args[0]}");
} }
pipeUuid = args[1];
Logger.LogInfo($"PowerDisplay started with runner_pid={runnerPid}, pipe_uuid={pipeUuid}");
} }
else else
{ {
Logger.LogWarning("PowerDisplay started without command line arguments"); Logger.LogWarning("PowerDisplay started without runner PID. Running in standalone mode.");
Logger.LogWarning($"PowerDisplay started with insufficient arguments (expected 2, got {args.Length}). Running in standalone mode.");
} }
var instanceKey = AppInstance.FindOrRegisterForKey("PowerToys_PowerDisplay_Instance"); var instanceKey = AppInstance.FindOrRegisterForKey("PowerToys_PowerDisplay_Instance");
@@ -47,7 +47,7 @@ namespace PowerDisplay
{ {
var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread()); var context = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread());
SynchronizationContext.SetSynchronizationContext(context); SynchronizationContext.SetSynchronizationContext(context);
_ = new App(runnerPid, pipeUuid); _ = new App(runnerPid);
}); });
} }
else else

View File

@@ -90,7 +90,6 @@
<ItemGroup> <ItemGroup>
<ClInclude Include="Constants.h" /> <ClInclude Include="Constants.h" />
<ClInclude Include="pch.h" /> <ClInclude Include="pch.h" />
<ClInclude Include="PowerDisplayProcessManager.h" />
<ClInclude Include="resource.h" /> <ClInclude Include="resource.h" />
<ClInclude Include="trace.h" /> <ClInclude Include="trace.h" />
</ItemGroup> </ItemGroup>
@@ -99,7 +98,6 @@
<ClCompile Include="pch.cpp"> <ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader> <PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
</ClCompile> </ClCompile>
<ClCompile Include="PowerDisplayProcessManager.cpp" />
<ClCompile Include="trace.cpp" /> <ClCompile Include="trace.cpp" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -105,18 +105,46 @@ bool PowerDisplayProcessManager::is_process_running() const
void PowerDisplayProcessManager::terminate_process() void PowerDisplayProcessManager::terminate_process()
{ {
// Close pipe // Terminate process if still running
m_write_pipe.reset();
// Terminate process
if (m_hProcess != nullptr) if (m_hProcess != nullptr)
{ {
TerminateProcess(m_hProcess, 1); // Check if process is still running
if (WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT)
{
Logger::trace(L"Process still running, calling TerminateProcess");
// Force terminate the process
if (TerminateProcess(m_hProcess, 1))
{
// Wait a bit to ensure process is terminated
DWORD wait_result = WaitForSingleObject(m_hProcess, 1000);
if (wait_result == WAIT_OBJECT_0)
{
Logger::trace(L"PowerDisplay process successfully terminated");
}
else
{
Logger::error(L"TerminateProcess succeeded but process did not exit within timeout");
}
}
else
{
Logger::error(L"TerminateProcess failed: {}", get_last_error_or_default(GetLastError()));
}
}
else
{
Logger::trace(L"PowerDisplay process already exited gracefully");
}
// Clean up process handle
CloseHandle(m_hProcess); CloseHandle(m_hProcess);
m_hProcess = nullptr; m_hProcess = nullptr;
} }
Logger::trace(L"PowerDisplay process terminated"); // Close pipe after process is terminated
m_write_pipe.reset();
Logger::trace(L"PowerDisplay process cleanup complete");
} }
HRESULT PowerDisplayProcessManager::start_process(const std::wstring& pipe_uuid) HRESULT PowerDisplayProcessManager::start_process(const std::wstring& pipe_uuid)
@@ -256,24 +284,60 @@ void PowerDisplayProcessManager::refresh()
// Stop PowerDisplay process // Stop PowerDisplay process
Logger::trace(L"Stopping PowerDisplay process"); Logger::trace(L"Stopping PowerDisplay process");
// Send terminate message // Send terminate message synchronously (not through thread executor)
send_message_to_powerdisplay(L"{\"action\":\"terminate\"}"); // This ensures the message is sent before we wait for process exit
if (m_write_pipe)
// Wait for graceful exit
if (m_hProcess != nullptr)
{ {
WaitForSingleObject(m_hProcess, 2000); try
} {
const auto message = L"{\"action\":\"terminate\"}";
const auto formatted = std::format(L"{}\r\n", message);
if (is_process_running()) // Match WinUI side which reads the pipe using UTF-16 (Encoding.Unicode)
{ const CString payload(formatted.c_str());
Logger::warn(L"PowerDisplay process failed to gracefully exit, terminating"); const DWORD bytes_to_write = static_cast<DWORD>(payload.GetLength() * sizeof(wchar_t));
DWORD bytes_written = 0;
if (SUCCEEDED(m_write_pipe->Write(payload, bytes_to_write, &bytes_written)))
{
Logger::trace(L"Sent terminate message to PowerDisplay");
}
else
{
Logger::warn(L"Failed to send terminate message to PowerDisplay");
}
}
catch (...)
{
Logger::warn(L"Exception while sending terminate message to PowerDisplay");
}
} }
else else
{ {
Logger::trace(L"PowerDisplay process successfully exited"); Logger::warn(L"Cannot send terminate message: pipe not connected");
} }
// Wait for graceful exit (use longer timeout like AdvancedPaste)
if (m_hProcess != nullptr)
{
Logger::trace(L"Waiting for PowerDisplay process to exit gracefully");
DWORD wait_result = WaitForSingleObject(m_hProcess, 5000);
if (wait_result == WAIT_OBJECT_0)
{
Logger::trace(L"PowerDisplay process exited gracefully");
}
else if (wait_result == WAIT_TIMEOUT)
{
Logger::warn(L"PowerDisplay process failed to exit within timeout, will force terminate");
}
else
{
Logger::error(L"WaitForSingleObject failed with error: {}", get_last_error_or_default(GetLastError()));
}
}
// Clean up (will force terminate if still running)
terminate_process(); terminate_process();
} }
} }

View File

@@ -11,7 +11,6 @@
#include "resource.h" #include "resource.h"
#include "Constants.h" #include "Constants.h"
#include "PowerDisplayProcessManager.h"
extern "C" IMAGE_DOS_HEADER __ImageBase; extern "C" IMAGE_DOS_HEADER __ImageBase;
@@ -55,8 +54,12 @@ private:
bool m_hotkey_enabled = false; bool m_hotkey_enabled = false;
Hotkey m_activation_hotkey = { .win = true, .ctrl = false, .shift = false, .alt = true, .key = 'M' }; Hotkey m_activation_hotkey = { .win = true, .ctrl = false, .shift = false, .alt = true, .key = 'M' };
// Process manager for handling PowerDisplay.exe lifecycle and IPC // Windows Events for IPC (persistent handles - ColorPicker pattern)
PowerDisplayProcessManager m_process_manager; HANDLE m_hProcess = nullptr;
HANDLE m_hInvokeEvent = nullptr;
HANDLE m_hTerminateEvent = nullptr;
HANDLE m_hRefreshEvent = nullptr;
HANDLE m_hSettingsUpdatedEvent = nullptr;
void parse_hotkey_settings(PowerToysSettings::PowerToyValues settings) void parse_hotkey_settings(PowerToysSettings::PowerToyValues settings)
{ {
@@ -143,6 +146,42 @@ private:
} }
} }
// Helper method to check if PowerDisplay.exe process is still running
bool is_process_running()
{
if (m_hProcess == nullptr)
{
return false;
}
return WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
}
// Helper method to launch PowerDisplay.exe process
void launch_process()
{
Logger::trace(L"Starting PowerDisplay process");
unsigned long powertoys_pid = GetCurrentProcessId();
std::wstring executable_args = std::to_wstring(powertoys_pid);
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI;
sei.lpFile = L"WinUI3Apps\\PowerToys.PowerDisplay.exe";
sei.nShow = SW_SHOWNORMAL;
sei.lpParameters = executable_args.data();
if (ShellExecuteExW(&sei))
{
Logger::trace(L"Successfully started PowerDisplay process");
m_hProcess = sei.hProcess;
}
else
{
Logger::error(L"PowerDisplay process failed to start. {}",
get_last_error_or_default(GetLastError()));
}
}
public: public:
PowerDisplayModule() PowerDisplayModule()
{ {
@@ -151,27 +190,51 @@ public:
init_settings(); init_settings();
// Note: PowerDisplay.exe will send messages directly to runner via named pipes // Create all Windows Events (persistent handles - ColorPicker pattern)
// The runner's message_receiver_thread will handle routing to Settings UI m_hInvokeEvent = CreateDefaultEvent(CommonSharedConstants::SHOW_POWER_DISPLAY_EVENT);
// No need to set a callback here - the process manager just manages lifecycle m_hTerminateEvent = CreateDefaultEvent(CommonSharedConstants::TERMINATE_POWER_DISPLAY_EVENT);
m_hRefreshEvent = CreateDefaultEvent(CommonSharedConstants::REFRESH_POWER_DISPLAY_MONITORS_EVENT);
m_hSettingsUpdatedEvent = CreateDefaultEvent(CommonSharedConstants::SETTINGS_UPDATED_POWER_DISPLAY_EVENT);
if (!m_hInvokeEvent || !m_hTerminateEvent || !m_hRefreshEvent || !m_hSettingsUpdatedEvent)
{
Logger::error(L"Failed to create one or more event handles");
}
} }
~PowerDisplayModule() ~PowerDisplayModule()
{ {
if (m_enabled) if (m_enabled)
{ {
m_process_manager.stop(); disable();
}
// Clean up all event handles
if (m_hInvokeEvent)
{
CloseHandle(m_hInvokeEvent);
m_hInvokeEvent = nullptr;
}
if (m_hTerminateEvent)
{
CloseHandle(m_hTerminateEvent);
m_hTerminateEvent = nullptr;
}
if (m_hRefreshEvent)
{
CloseHandle(m_hRefreshEvent);
m_hRefreshEvent = nullptr;
}
if (m_hSettingsUpdatedEvent)
{
CloseHandle(m_hSettingsUpdatedEvent);
m_hSettingsUpdatedEvent = nullptr;
} }
m_enabled = false;
} }
virtual void destroy() override virtual void destroy() override
{ {
Logger::trace("PowerDisplay::destroy()"); Logger::trace("PowerDisplay::destroy()");
if (m_enabled)
{
m_process_manager.stop();
}
delete this; delete this;
} }
@@ -209,14 +272,33 @@ public:
if (action_object.get_name() == L"Launch") if (action_object.get_name() == L"Launch")
{ {
Logger::trace(L"Launch action received, sending show_window command"); Logger::trace(L"Launch action received");
m_process_manager.send_message_to_powerdisplay(L"{\"action\":\"show_window\"}");
// ColorPicker pattern: check if process is running, re-launch if needed
if (!is_process_running())
{
Logger::trace(L"PowerDisplay process not running, re-launching");
launch_process();
}
if (m_hInvokeEvent)
{
Logger::trace(L"Signaling show event");
SetEvent(m_hInvokeEvent);
}
Trace::ActivatePowerDisplay(); Trace::ActivatePowerDisplay();
} }
else if (action_object.get_name() == L"RefreshMonitors") else if (action_object.get_name() == L"RefreshMonitors")
{ {
Logger::trace(L"RefreshMonitors action received"); Logger::trace(L"RefreshMonitors action received, signaling refresh event");
m_process_manager.send_message_to_powerdisplay(L"{\"action\":\"refresh_monitors\"}"); if (m_hRefreshEvent)
{
SetEvent(m_hRefreshEvent);
}
else
{
Logger::warn(L"Refresh event handle is null");
}
} }
} }
catch (std::exception&) catch (std::exception&)
@@ -236,9 +318,16 @@ public:
parse_activation_hotkey(values); parse_activation_hotkey(values);
values.save_to_settings_file(); values.save_to_settings_file();
// Notify PowerDisplay.exe that settings have been updated (signal only, no config data) // Signal settings updated event
// PowerDisplay will read the updated settings.json file itself if (m_hSettingsUpdatedEvent)
m_process_manager.send_message_to_powerdisplay(L"{\"action\":\"settings_updated\"}"); {
Logger::trace(L"Signaling settings updated event");
SetEvent(m_hSettingsUpdatedEvent);
}
else
{
Logger::warn(L"Settings updated event handle is null");
}
} }
catch (std::exception&) catch (std::exception&)
{ {
@@ -248,19 +337,43 @@ public:
virtual void enable() override virtual void enable() override
{ {
Logger::trace(L"PowerDisplay::enable()");
m_enabled = true; m_enabled = true;
Trace::EnablePowerDisplay(true); Trace::EnablePowerDisplay(true);
Logger::trace(L"PowerDisplay enabled, starting process manager"); // Launch PowerDisplay.exe with PID only (Awake pattern)
m_process_manager.start(); launch_process();
} }
virtual void disable() override virtual void disable() override
{ {
Logger::trace(L"PowerDisplay::disable()");
if (m_enabled) if (m_enabled)
{ {
Logger::trace(L"Disabling Power Display..."); // Reset invoke event to prevent accidental activation during shutdown
m_process_manager.stop(); if (m_hInvokeEvent)
{
ResetEvent(m_hInvokeEvent);
}
// Signal terminate event
if (m_hTerminateEvent)
{
Logger::trace(L"Signaling PowerDisplay to exit");
SetEvent(m_hTerminateEvent);
}
else
{
Logger::warn(L"Terminate event handle is null");
}
// Close process handle (don't wait, don't force terminate - Awake pattern)
if (m_hProcess)
{
CloseHandle(m_hProcess);
m_hProcess = nullptr;
}
} }
m_enabled = false; m_enabled = false;
@@ -274,11 +387,19 @@ public:
virtual bool on_hotkey(size_t /*hotkeyId*/) override virtual bool on_hotkey(size_t /*hotkeyId*/) override
{ {
if (m_enabled) if (m_enabled && m_hInvokeEvent)
{ {
Logger::trace(L"Power Display hotkey pressed"); Logger::trace(L"Power Display hotkey pressed");
// Send toggle window command
m_process_manager.send_message_to_powerdisplay(L"{\"action\":\"toggle_window\"}"); // ColorPicker pattern: check if process is running, re-launch if needed
if (!is_process_running())
{
Logger::trace(L"PowerDisplay process not running, re-launching");
launch_process();
}
Logger::trace(L"Signaling show event");
SetEvent(m_hInvokeEvent);
return true; return true;
} }