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  }