// 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.Runtime.InteropServices; using ManagedCommon; using Microsoft.CmdPal.UI.Events; using Microsoft.PowerToys.Telemetry; using Microsoft.UI.Dispatching; using Microsoft.Windows.AppLifecycle; using Windows.Win32; using Windows.Win32.Foundation; using Windows.Win32.System.Com; using Windows.Win32.UI.WindowsAndMessaging; namespace Microsoft.CmdPal.UI; // cribbed heavily from // // https://github.com/microsoft/WindowsAppSDK-Samples/tree/main/Samples/AppLifecycle/Instancing/cs2/cs-winui-packaged/CsWinUiDesktopInstancing internal sealed class Program { private static DispatcherQueueSynchronizationContext? uiContext; private static App? app; // LOAD BEARING // // Main cannot be async. If it is, then the clipboard won't work, and neither will narrator. // That means you, the person thinking about making this a MTA thread. Don't // do it. It won't work. That's not the solution. [STAThread] private static int Main(string[] args) { if (Helpers.GpoValueChecker.GetConfiguredCmdPalEnabledValue() == Helpers.GpoRuleConfiguredValue.Disabled) { // There's a GPO rule configured disabling CmdPal. Exit as soon as possible. return 0; } try { Logger.InitializeLogger("\\CmdPal\\Logs\\"); } catch (COMException e) { // This is unexpected. For the sake of debugging: // pop a message box PInvoke.MessageBox( (HWND)IntPtr.Zero, $"Failed to initialize the logger. COMException: \r{e.Message}", "Command Palette", MESSAGEBOX_STYLE.MB_OK | MESSAGEBOX_STYLE.MB_ICONERROR); return 0; } catch (Exception e2) { // This is unexpected. For the sake of debugging: // pop a message box PInvoke.MessageBox( (HWND)IntPtr.Zero, $"Failed to initialize the logger. Unknown Exception: \r{e2.Message}", "Command Palette", MESSAGEBOX_STYLE.MB_OK | MESSAGEBOX_STYLE.MB_ICONERROR); return 0; } Logger.LogDebug($"Starting at {DateTime.UtcNow}"); PowerToysTelemetry.Log.WriteEvent(new CmdPalProcessStarted()); WinRT.ComWrappersSupport.InitializeComWrappers(); var isRedirect = DecideRedirection(); if (!isRedirect) { Microsoft.UI.Xaml.Application.Start((p) => { uiContext = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread()); SynchronizationContext.SetSynchronizationContext(uiContext); app = new App(); }); } return 0; } private static bool DecideRedirection() { var isRedirect = false; var args = AppInstance.GetCurrent().GetActivatedEventArgs(); var keyInstance = AppInstance.FindOrRegisterForKey("randomKey"); if (keyInstance.IsCurrent) { PowerToysTelemetry.Log.WriteEvent(new ColdLaunch()); keyInstance.Activated += OnActivated; } else { isRedirect = true; PowerToysTelemetry.Log.WriteEvent(new ReactivateInstance()); RedirectActivationTo(args, keyInstance); } return isRedirect; } private static void RedirectActivationTo(AppActivationArguments args, AppInstance keyInstance) { // Do the redirection on another thread, and use a non-blocking // wait method to wait for the redirection to complete. using var redirectSemaphore = new Semaphore(0, 1); var redirectTimeout = TimeSpan.FromSeconds(32); _ = Task.Run(() => { using var cts = new CancellationTokenSource(redirectTimeout); try { keyInstance.RedirectActivationToAsync(args) .AsTask(cts.Token) .GetAwaiter() .GetResult(); } catch (OperationCanceledException) { Logger.LogError($"Failed to activate existing instance; timed out after {redirectTimeout}."); } catch (Exception ex) { Logger.LogError("Failed to activate existing instance", ex); } finally { redirectSemaphore.Release(); } }); _ = PInvoke.CoWaitForMultipleObjects( (uint)CWMO_FLAGS.CWMO_DEFAULT, PInvoke.INFINITE, [new HANDLE(redirectSemaphore.SafeWaitHandle.DangerousGetHandle())], out _); } private static void OnActivated(object? sender, AppActivationArguments args) { // If we already have a form, display the message now. // Otherwise, add it to the collection for displaying later. if (App.Current?.AppWindow is MainWindow mainWindow) { // LOAD BEARING // This must be synchronous to ensure the method does not return // before the activation is fully handled and the parameters are processed. // The sending instance remains blocked until this returns; afterward it may quit, // causing the activation arguments to be lost. mainWindow.HandleLaunchNonUI(args); } } }