Files
PowerToys/src/modules/powerdisplay/PowerDisplay/PowerDisplayXAML/App.xaml.cs

410 lines
15 KiB
C#
Raw Normal View History

2025-10-20 16:22:47 +08:00
// 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 System.Threading.Tasks;
using ManagedCommon;
using Microsoft.PowerToys.Telemetry;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media;
using Microsoft.Windows.AppLifecycle;
using PowerDisplay.Helpers;
using PowerDisplay.Serialization;
namespace PowerDisplay
{
/// <summary>
/// PowerDisplay application main class
/// </summary>
#pragma warning disable CA1001 // CancellationTokenSource is disposed in Shutdown/ForceExit methods
2025-10-20 16:22:47 +08:00
public partial class App : Application
#pragma warning restore CA1001
2025-10-20 16:22:47 +08:00
{
private Window? _mainWindow;
private int _powerToysRunnerPid;
private string _pipeUuid = string.Empty;
private CancellationTokenSource? _ipcCancellationTokenSource;
2025-10-20 16:22:47 +08:00
public App(int runnerPid, string pipeUuid)
{
_powerToysRunnerPid = runnerPid;
_pipeUuid = pipeUuid;
this.InitializeComponent();
// Ensure types used in XAML are preserved for AOT compilation
TypePreservation.PreserveTypes();
// Initialize Logger
Logger.InitializeLogger("\\PowerDisplay\\Logs");
// Initialize PowerToys telemetry
try
{
PowerToysTelemetry.Log.WriteEvent(new Telemetry.Events.PowerDisplayStartEvent());
}
catch
{
// Telemetry errors should not crash the app
}
// Initialize language settings
string appLanguage = LanguageHelper.LoadLanguage();
if (!string.IsNullOrEmpty(appLanguage))
{
Microsoft.Windows.Globalization.ApplicationLanguages.PrimaryLanguageOverride = appLanguage;
}
// Handle unhandled exceptions
this.UnhandledException += OnUnhandledException;
}
/// <summary>
/// Handle unhandled exceptions
/// </summary>
private void OnUnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
// Try to display error information
ShowStartupError(e.Exception);
// Mark exception as handled to prevent app crash
e.Handled = true;
}
/// <summary>
/// Called when the application is launched
/// </summary>
/// <param name="args">Launch arguments</param>
protected override void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
try
{
// Single instance is already ensured by AppInstance.FindOrRegisterForKey() in Program.cs
// No need for additional Mutex check here
2025-10-20 16:22:47 +08:00
// Parse command line arguments
var cmdArgs = Environment.GetCommandLineArgs();
if (cmdArgs?.Length > 1)
{
// 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))
{
break;
}
}
// 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");
ForceExit();
});
}
}
else
{
// 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)
2025-10-20 16:22:47 +08:00
{
// Start IPC message listener in background
_ipcCancellationTokenSource = new CancellationTokenSource();
_ = Task.Run(() => StartIPCListener(_pipeUuid, _ipcCancellationTokenSource.Token));
Logger.LogInfo("Starting IPC pipe listener in background");
2025-10-20 16:22:47 +08:00
}
else
{
Logger.LogInfo("Running in standalone mode, IPC disabled");
}
// Create main window
2025-10-20 16:22:47 +08:00
_mainWindow = new MainWindow();
// Window visibility depends on launch mode
// - IPC mode (launched by PowerToys Runner): Start hidden, wait for show_window IPC command
// - Standalone mode (no command-line args): Show window immediately
if (!isIPCMode)
{
// Standalone mode - activate and show window
_mainWindow.Activate();
Logger.LogInfo("Window activated (standalone mode)");
}
else
{
// IPC mode - window remains inactive (hidden) until show_window command received
Logger.LogInfo("Window created but not activated (IPC mode - waiting for show_window command)");
// Start background initialization to scan monitors even when hidden
_ = Task.Run(async () =>
{
// Give window a moment to finish construction
await Task.Delay(500);
// Trigger initialization on UI thread
_mainWindow?.DispatcherQueue.TryEnqueue(async () =>
{
if (_mainWindow is MainWindow mainWindow)
{
await mainWindow.EnsureInitializedAsync();
Logger.LogInfo("Background initialization completed (IPC mode)");
}
});
});
}
2025-10-20 16:22:47 +08:00
}
catch (Exception ex)
{
ShowStartupError(ex);
}
}
/// <summary>
/// Start IPC listener to receive commands from ModuleInterface
2025-10-20 16:22:47 +08:00
/// </summary>
private async Task StartIPCListener(string pipeUuid, CancellationToken cancellationToken)
2025-10-20 16:22:47 +08:00
{
try
{
string pipeName = $"powertoys_powerdisplay_{pipeUuid}";
Logger.LogInfo($"Connecting to pipe: {pipeName}");
await NamedPipeProcessor.ProcessNamedPipeAsync(
pipeName,
TimeSpan.FromSeconds(5),
OnIPCMessageReceived,
cancellationToken);
2025-10-20 16:22:47 +08:00
}
catch (OperationCanceledException)
2025-10-20 16:22:47 +08:00
{
Logger.LogInfo("IPC listener cancelled");
2025-10-20 16:22:47 +08:00
}
catch (Exception ex)
{
Logger.LogError($"Error in IPC listener: {ex.Message}");
2025-10-20 16:22:47 +08:00
}
}
/// <summary>
/// Handle IPC messages received from ModuleInterface
2025-10-20 16:22:47 +08:00
/// </summary>
private void OnIPCMessageReceived(string message)
2025-10-20 16:22:47 +08:00
{
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))
2025-10-20 16:22:47 +08:00
{
string action = actionElement.GetString() ?? string.Empty;
2025-10-20 16:22:47 +08:00
switch (action)
{
case "show_window":
Logger.LogInfo("Received show_window command");
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
{
if (_mainWindow is MainWindow mainWindow)
{
mainWindow.ShowWindow();
}
});
2025-10-20 16:22:47 +08:00
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();
}
}
});
2025-10-20 16:22:47 +08:00
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);
}
});
2025-10-20 16:22:47 +08:00
break;
case "settings_updated":
Logger.LogInfo("Received settings_updated command");
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
{
if (_mainWindow is MainWindow mainWindow && mainWindow.ViewModel != null)
{
_ = mainWindow.ViewModel.ReloadMonitorSettingsAsync();
}
});
2025-10-20 16:22:47 +08:00
break;
case "terminate":
Logger.LogInfo("Received terminate command");
_mainWindow?.DispatcherQueue.TryEnqueue(() =>
{
Shutdown();
});
2025-10-20 16:22:47 +08:00
break;
default:
Logger.LogWarning($"Unknown action: {action}");
2025-10-20 16:22:47 +08:00
break;
}
}
}
catch (Exception ex)
{
Logger.LogError($"Error processing IPC message: {ex.Message}");
}
}
/// <summary>
/// Show startup error
/// </summary>
private void ShowStartupError(Exception ex)
{
try
{
Logger.LogError($"PowerDisplay startup failed: {ex.Message}");
var errorWindow = new Window { Title = "PowerDisplay - Startup Error" };
var panel = new StackPanel { Margin = new Thickness(20), Spacing = 16 };
panel.Children.Add(new TextBlock
{
Text = "PowerDisplay Startup Failed",
FontSize = 20,
FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
});
panel.Children.Add(new TextBlock
{
Text = $"Error: {ex.Message}",
FontSize = 14,
TextWrapping = TextWrapping.Wrap,
});
panel.Children.Add(new TextBlock
{
Text = $"Details:\n{ex}",
FontSize = 12,
TextWrapping = TextWrapping.Wrap,
Foreground = new SolidColorBrush(Microsoft.UI.Colors.Gray),
Margin = new Thickness(0, 10, 0, 0),
});
var closeButton = new Button
{
Content = "Close",
HorizontalAlignment = HorizontalAlignment.Right,
Margin = new Thickness(0, 10, 0, 0),
};
closeButton.Click += (_, _) => errorWindow.Close();
panel.Children.Add(closeButton);
errorWindow.Content = new ScrollViewer
{
Content = panel,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
MaxHeight = 600,
MaxWidth = 800,
};
errorWindow.Activate();
}
catch
{
Environment.Exit(1);
}
}
/// <summary>
/// Gets the main window instance
/// </summary>
public Window? MainWindow => _mainWindow;
/// <summary>
/// Check if running standalone (not launched from PowerToys Runner)
/// </summary>
public bool IsRunningDetachedFromPowerToys()
{
return _powerToysRunnerPid == -1;
}
/// <summary>
/// Shutdown application (simplified version following other PowerToys modules pattern)
2025-10-20 16:22:47 +08:00
/// </summary>
public void Shutdown()
{
Logger.LogInfo("PowerDisplay shutting down");
2025-10-20 16:22:47 +08:00
// Cancel IPC listener
_ipcCancellationTokenSource?.Cancel();
_ipcCancellationTokenSource?.Dispose();
2025-10-20 16:22:47 +08:00
// 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);
2025-10-20 16:22:47 +08:00
}
/// <summary>
/// Force exit when PowerToys Runner exits
2025-10-20 16:22:47 +08:00
/// </summary>
private void ForceExit()
{
Logger.LogInfo("PowerToys Runner exited, forcing shutdown");
2025-10-20 16:22:47 +08:00
// Cancel IPC listener
_ipcCancellationTokenSource?.Cancel();
_ipcCancellationTokenSource?.Dispose();
2025-10-20 16:22:47 +08:00
Environment.Exit(0);
2025-10-20 16:22:47 +08:00
}
}
}