Program.cs
1 // Copyright (c) Microsoft Corporation 2 // The Microsoft Corporation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System.Runtime.InteropServices; 6 using ManagedCommon; 7 using Microsoft.CmdPal.UI.Events; 8 using Microsoft.PowerToys.Telemetry; 9 using Microsoft.UI.Dispatching; 10 using Microsoft.Windows.AppLifecycle; 11 using Windows.Win32; 12 using Windows.Win32.Foundation; 13 using Windows.Win32.System.Com; 14 using Windows.Win32.UI.WindowsAndMessaging; 15 16 namespace Microsoft.CmdPal.UI; 17 18 // cribbed heavily from 19 // 20 // https://github.com/microsoft/WindowsAppSDK-Samples/tree/main/Samples/AppLifecycle/Instancing/cs2/cs-winui-packaged/CsWinUiDesktopInstancing 21 internal sealed class Program 22 { 23 private static DispatcherQueueSynchronizationContext? uiContext; 24 private static App? app; 25 26 // LOAD BEARING 27 // 28 // Main cannot be async. If it is, then the clipboard won't work, and neither will narrator. 29 // That means you, the person thinking about making this a MTA thread. Don't 30 // do it. It won't work. That's not the solution. 31 [STAThread] 32 private static int Main(string[] args) 33 { 34 if (Helpers.GpoValueChecker.GetConfiguredCmdPalEnabledValue() == Helpers.GpoRuleConfiguredValue.Disabled) 35 { 36 // There's a GPO rule configured disabling CmdPal. Exit as soon as possible. 37 return 0; 38 } 39 40 try 41 { 42 Logger.InitializeLogger("\\CmdPal\\Logs\\"); 43 } 44 catch (COMException e) 45 { 46 // This is unexpected. For the sake of debugging: 47 // pop a message box 48 PInvoke.MessageBox( 49 (HWND)IntPtr.Zero, 50 $"Failed to initialize the logger. COMException: \r{e.Message}", 51 "Command Palette", 52 MESSAGEBOX_STYLE.MB_OK | MESSAGEBOX_STYLE.MB_ICONERROR); 53 return 0; 54 } 55 catch (Exception e2) 56 { 57 // This is unexpected. For the sake of debugging: 58 // pop a message box 59 PInvoke.MessageBox( 60 (HWND)IntPtr.Zero, 61 $"Failed to initialize the logger. Unknown Exception: \r{e2.Message}", 62 "Command Palette", 63 MESSAGEBOX_STYLE.MB_OK | MESSAGEBOX_STYLE.MB_ICONERROR); 64 return 0; 65 } 66 67 Logger.LogDebug($"Starting at {DateTime.UtcNow}"); 68 PowerToysTelemetry.Log.WriteEvent(new CmdPalProcessStarted()); 69 70 WinRT.ComWrappersSupport.InitializeComWrappers(); 71 var isRedirect = DecideRedirection(); 72 if (!isRedirect) 73 { 74 Microsoft.UI.Xaml.Application.Start((p) => 75 { 76 uiContext = new DispatcherQueueSynchronizationContext(DispatcherQueue.GetForCurrentThread()); 77 SynchronizationContext.SetSynchronizationContext(uiContext); 78 app = new App(); 79 }); 80 } 81 82 return 0; 83 } 84 85 private static bool DecideRedirection() 86 { 87 var isRedirect = false; 88 var args = AppInstance.GetCurrent().GetActivatedEventArgs(); 89 var keyInstance = AppInstance.FindOrRegisterForKey("randomKey"); 90 91 if (keyInstance.IsCurrent) 92 { 93 PowerToysTelemetry.Log.WriteEvent(new ColdLaunch()); 94 keyInstance.Activated += OnActivated; 95 } 96 else 97 { 98 isRedirect = true; 99 PowerToysTelemetry.Log.WriteEvent(new ReactivateInstance()); 100 RedirectActivationTo(args, keyInstance); 101 } 102 103 return isRedirect; 104 } 105 106 private static void RedirectActivationTo(AppActivationArguments args, AppInstance keyInstance) 107 { 108 // Do the redirection on another thread, and use a non-blocking 109 // wait method to wait for the redirection to complete. 110 using var redirectSemaphore = new Semaphore(0, 1); 111 var redirectTimeout = TimeSpan.FromSeconds(32); 112 113 _ = Task.Run(() => 114 { 115 using var cts = new CancellationTokenSource(redirectTimeout); 116 try 117 { 118 keyInstance.RedirectActivationToAsync(args) 119 .AsTask(cts.Token) 120 .GetAwaiter() 121 .GetResult(); 122 } 123 catch (OperationCanceledException) 124 { 125 Logger.LogError($"Failed to activate existing instance; timed out after {redirectTimeout}."); 126 } 127 catch (Exception ex) 128 { 129 Logger.LogError("Failed to activate existing instance", ex); 130 } 131 finally 132 { 133 redirectSemaphore.Release(); 134 } 135 }); 136 137 _ = PInvoke.CoWaitForMultipleObjects( 138 (uint)CWMO_FLAGS.CWMO_DEFAULT, 139 PInvoke.INFINITE, 140 [new HANDLE(redirectSemaphore.SafeWaitHandle.DangerousGetHandle())], 141 out _); 142 } 143 144 private static void OnActivated(object? sender, AppActivationArguments args) 145 { 146 // If we already have a form, display the message now. 147 // Otherwise, add it to the collection for displaying later. 148 if (App.Current?.AppWindow is MainWindow mainWindow) 149 { 150 // LOAD BEARING 151 // This must be synchronous to ensure the method does not return 152 // before the activation is fully handled and the parameters are processed. 153 // The sending instance remains blocked until this returns; afterward it may quit, 154 // causing the activation arguments to be lost. 155 mainWindow.HandleLaunchNonUI(args); 156 } 157 } 158 }