diff --git a/src/RunnerV2/RunnerV2/Helpers/ElevationHelper.cs b/src/RunnerV2/RunnerV2/Helpers/ElevationHelper.cs index 80dc82631e..3f2c42bcbe 100644 --- a/src/RunnerV2/RunnerV2/Helpers/ElevationHelper.cs +++ b/src/RunnerV2/RunnerV2/Helpers/ElevationHelper.cs @@ -11,8 +11,56 @@ namespace RunnerV2.Helpers { internal static partial class ElevationHelper { + internal static RestartScheduledMode RestartScheduled { get; set; } = RestartScheduledMode.None; + + internal enum RestartScheduledMode + { + None, + RestartElevated, + RestartElevatedWithOpenSettings, + RestartNonElevated, + } + private static bool? _cachedValue; + internal static void RestartIfScheudled() + { + switch (RestartScheduled) + { + case RestartScheduledMode.None: + return; + case RestartScheduledMode.RestartElevated: + RestartAsAdministrator("--restartedElevated"); + break; + case RestartScheduledMode.RestartElevatedWithOpenSettings: + RestartAsAdministrator("--restartedElevated --open-settings"); + break; + case RestartScheduledMode.RestartNonElevated: + // Todo: restart unelevated + break; + } + } + + private static void RestartAsAdministrator(string arguments) + { + ProcessStartInfo processStartInfo = new() + { + Arguments = arguments, + Verb = "runas", + UseShellExecute = true, + FileName = Environment.ProcessPath, + }; + + try + { + Process.Start(processStartInfo); + } + catch (Exception ex) + { + Console.WriteLine("Failed to restart as administrator: " + ex); + } + } + internal static bool IsProcessElevated(bool useCachedValue = true) { if (_cachedValue is not null && useCachedValue) diff --git a/src/RunnerV2/RunnerV2/Helpers/SettingsHelper.cs b/src/RunnerV2/RunnerV2/Helpers/SettingsHelper.cs index 239832d7a0..2f77b44b7d 100644 --- a/src/RunnerV2/RunnerV2/Helpers/SettingsHelper.cs +++ b/src/RunnerV2/RunnerV2/Helpers/SettingsHelper.cs @@ -112,9 +112,32 @@ namespace RunnerV2.Helpers { switch (property.Name) { + case "action": + _settingsUtils.SaveSettings(property.Value.GetProperty("general").ToString(), string.Empty); + switch (property.Value.GetProperty("general").GetProperty("action_name").GetString()) + { + case "restart_elevation": + ElevationHelper.RestartScheduled = ElevationHelper.RestartScheduledMode.RestartElevatedWithOpenSettings; + Runner.Close(); + break; + case "request_update_state_date": + // Todo: + break; + } + + break; case "get_all_hotkey_conflicts": // Todo: Handle hotkey conflict break; + case "bugreport": + TrayIconManager.ProcessTrayMenuCommand((nuint)TrayIconManager.TrayButton.ReportBug); + break; + case "bug_report_status": + _ipc?.Send($@"{{""bug_report_running:"" {(TrayIconManager.IsBugReportToolRunning ? "true" : "false")}"); + break; + case "killrunner": + Runner.Close(); + break; case "general": _settingsUtils.SaveSettings(property.Value.ToString(), string.Empty); foreach (IPowerToysModule module in Runner.LoadedModules) @@ -124,25 +147,38 @@ namespace RunnerV2.Helpers } break; - case string s: - _settingsUtils.SaveSettings(property.Value.ToString(), s); + case "powertoys": + foreach (var powertoysSettingsPart in property.Value.EnumerateObject()) + { + _settingsUtils.SaveSettings(property.Value.ToString(), powertoysSettingsPart.Name); - if (Runner.LoadedModules.Find(m => m.Name == s) is IPowerToysModule moduleFound) - { - moduleFound.OnSettingsChanged(s, property.Value); - } - else - { - // If no specific module was found, notify all enabled modules - foreach (IPowerToysModule module in Runner.LoadedModules.Where(m => m.Enabled)) + if (Runner.LoadedModules.Find(m => m.Name == powertoysSettingsPart.Name) is IPowerToysModule module) { - module.OnSettingsChanged(s, property.Value); + module.OnSettingsChanged(powertoysSettingsPart.Name, powertoysSettingsPart.Value); + } + else + { + // If no specific module was found, notify all enabled modules + foreach (IPowerToysModule module2 in Runner.LoadedModules.Where(m => m.Enabled)) + { + module2.OnSettingsChanged(powertoysSettingsPart.Name, powertoysSettingsPart.Value); + } } } + break; + default: + Console.WriteLine($"Unknown message received from Settings: {property.Name}"); break; } } } + + public static void CloseSettingsWindow() + { + InteropEvent closeEventWrapper = new(InteropEvent.SettingsTerminate); + closeEventWrapper.Fire(); + closeEventWrapper.Dispose(); + } } } diff --git a/src/RunnerV2/RunnerV2/Helpers/TrayIconManager.cs b/src/RunnerV2/RunnerV2/Helpers/TrayIconManager.cs index 54f5d3a4a6..95c364bbac 100644 --- a/src/RunnerV2/RunnerV2/Helpers/TrayIconManager.cs +++ b/src/RunnerV2/RunnerV2/Helpers/TrayIconManager.cs @@ -44,7 +44,7 @@ namespace RunnerV2.Helpers Shell_NotifyIcon(NIMDELETE, ref notifyicondata); } - private enum TrayButton : uint + internal enum TrayButton : uint { Settings = 1, Documentation, @@ -102,6 +102,8 @@ namespace RunnerV2.Helpers } } + internal static bool IsBugReportToolRunning { get; set; } + internal static void ProcessTrayMenuCommand(nuint commandId) { switch ((TrayButton)commandId) @@ -132,9 +134,11 @@ namespace RunnerV2.Helpers { bugReportProcess.Dispose(); EnableMenuItem(_trayIconMenu, (uint)TrayButton.ReportBug, 0x00000000); + IsBugReportToolRunning = false; }; bugReportProcess.Start(); + IsBugReportToolRunning = true; break; case TrayButton.Close: diff --git a/src/RunnerV2/RunnerV2/Program.cs b/src/RunnerV2/RunnerV2/Program.cs index 99c5b0567c..1c30588d28 100644 --- a/src/RunnerV2/RunnerV2/Program.cs +++ b/src/RunnerV2/RunnerV2/Program.cs @@ -17,9 +17,8 @@ using Settings.UI.Library; internal sealed class Program { private static readonly SettingsUtils _settingsUtils = new(); - private static GeneralSettings _generalSettings = _settingsUtils.GetSettings(); - public static GeneralSettings GeneralSettings => _generalSettings; + internal static GeneralSettings GeneralSettings => _settingsUtils.GetSettings(); private static void Main(string[] args) { @@ -46,13 +45,13 @@ internal sealed class Program bool isElevated = ElevationHelper.IsProcessElevated(); bool hasDontElevateArgument = args.Contains("--dont-elevate"); - bool runElevatedSetting = _generalSettings.RunElevated; + bool runElevatedSetting = GeneralSettings.RunElevated; bool hasRestartedElevatedArgment = args.Contains("--restartedElevated"); Action afterInitializationAction = () => { }; Version version = Assembly.GetExecutingAssembly().GetName().Version!; - if ($"v{version.Major}.{version.Minor}.{version.Build}" != _settingsUtils.GetSettings(fileName: "last_version_run.json").LastVersion && (!_generalSettings.ShowWhatsNewAfterUpdates || GPOWrapper.GetDisableShowWhatsNewAfterUpdatesValue() != GpoRuleConfigured.Disabled)) + if ($"v{version.Major}.{version.Minor}.{version.Build}" != _settingsUtils.GetSettings(fileName: "last_version_run.json").LastVersion && (!GeneralSettings.ShowWhatsNewAfterUpdates || GPOWrapper.GetDisableShowWhatsNewAfterUpdatesValue() != GpoRuleConfigured.Disabled)) { afterInitializationAction += () => { @@ -68,6 +67,14 @@ internal sealed class Program }; } + if (shouldOpenSettings) + { + afterInitializationAction += () => + { + SettingsHelper.OpenSettingsWindow(additionalArguments: shouldOpenSettingsToSpecificPage ? args.First(s => s.StartsWith("--open-settings=", StringComparison.InvariantCulture)).Replace("--open-settings=", string.Empty, StringComparison.InvariantCulture) : null); + }; + } + // Set last version run _settingsUtils.SaveSettings(new LastVersionRunSettings() { LastVersion = $"v{version.Major}.{version.Minor}.{version.Build}" }.ToJsonString(), fileName: "last_version_run.json"); @@ -80,14 +87,18 @@ internal sealed class Program case (_, _, false, _): case (_, true, _, _): case (false, _, _, true): - _ = Runner.Run(afterInitializationAction); + GeneralSettings tempGeneralSettings = GeneralSettings; + tempGeneralSettings.IsElevated = isElevated; + _settingsUtils.SaveSettings(tempGeneralSettings.ToJsonString()); - // Todo: Save settings + _ = Runner.Run(afterInitializationAction); break; default: - // Todo: scheudle restart as elevated - throw new NotImplementedException(); + ElevationHelper.RestartScheduled = ElevationHelper.RestartScheduledMode.RestartElevated; + break; } + + ElevationHelper.RestartIfScheudled(); } private static SpecialMode ShouldRunInSpecialMode(string[] args) diff --git a/src/RunnerV2/RunnerV2/Runner.cs b/src/RunnerV2/RunnerV2/Runner.cs index 7a4f35fdf3..66087437a5 100644 --- a/src/RunnerV2/RunnerV2/Runner.cs +++ b/src/RunnerV2/RunnerV2/Runner.cs @@ -36,7 +36,6 @@ namespace RunnerV2 internal static bool Run(Action afterInitializationAction) { - // Todo: Start tray icon TrayIconManager.StartTrayIcon(); FrozenSet modulesToLoad = ["PowerToys.AlwaysOnTopModuleInterface.dll", "WinUI3Apps\\PowerToys.Hosts.dll"]; @@ -78,21 +77,24 @@ namespace RunnerV2 private static void MessageLoop() { - while (true) + while (GetMessageW(out MSG msg, IntPtr.Zero, 0, 0) != 0) { - if (GetMessageW(out MSG msg, IntPtr.Zero, 0, 0) != 0) - { - TranslateMessage(ref msg); - DispatchMessageW(ref msg); + TranslateMessage(ref msg); + DispatchMessageW(ref msg); - HandleMessage(msg.HWnd, msg.Message, (nint)msg.WParam, (nint)msg.LParam); - } + HandleMessage(msg.HWnd, msg.Message, (nint)msg.WParam, (nint)msg.LParam); } + + Close(); } [DoesNotReturn] internal static void Close() { + TrayIconManager.StopTrayIcon(); + SettingsHelper.CloseSettingsWindow(); + ElevationHelper.RestartIfScheudled(); + foreach (IPowerToysModule module in _successfullyAddedModules) { try @@ -193,12 +195,21 @@ namespace RunnerV2 } } + private static bool _handledShortcut; + private static IntPtr HandleMessage(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { switch (msg) { case (uint)WindowMessages.HOTKEY: + if (_handledShortcut) + { + _handledShortcut = false; + break; + } + HotkeyManager.ProcessHotkey((nuint)wParam); + _handledShortcut = true; break; case (uint)WindowMessages.ICON_NOTIFY: TrayIconManager.ProcessTrayIconMessage(lParam); @@ -210,7 +221,7 @@ namespace RunnerV2 TrayIconManager.StartTrayIcon(); break; case (uint)WindowMessages.DESTROY: - TrayIconManager.StopTrayIcon(); + Close(); break; default: if (msg == _taskbarCreatedMessage) diff --git a/src/common/ManagedCommon/InteropEvent.cs b/src/common/ManagedCommon/InteropEvent.cs new file mode 100644 index 0000000000..e5be24303f --- /dev/null +++ b/src/common/ManagedCommon/InteropEvent.cs @@ -0,0 +1,47 @@ +// 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 static ManagedCommon.NativeMethods; + +namespace ManagedCommon +{ + public partial class InteropEvent : IDisposable + { + public const string AlwaysOnTopPin = "Local\\AlwaysOnTopPinEvent-892e0aa2-cfa8-4cc4-b196-ddeb32314ce8"; + public const string AlwaysOnTopTerminate = "Local\\AlwaysOnTopTerminateEvent-cfdf1eae-791f-4953-8021-2f18f3837eae"; + public const string SettingsTerminate = "Local\\PowerToysRunnerTerminateSettingsEvent-c34cb661-2e69-4613-a1f8-4e39c25d7ef6"; + + private IntPtr _eventHandle; + + public InteropEvent(string eventName) + { + _eventHandle = CreateEventW(IntPtr.Zero, false, false, eventName); + } + + public void Fire() + { + if (_eventHandle != IntPtr.Zero) + { + SetEvent(_eventHandle); + } + } + + ~InteropEvent() + { + Dispose(); + } + + public void Dispose() + { + if (_eventHandle != IntPtr.Zero) + { + CloseHandle(_eventHandle); + _eventHandle = IntPtr.Zero; + } + + GC.SuppressFinalize(this); + } + } +} diff --git a/src/common/ManagedCommon/NativeMethods.cs b/src/common/ManagedCommon/NativeMethods.cs index a8d05b7a47..481c224129 100644 --- a/src/common/ManagedCommon/NativeMethods.cs +++ b/src/common/ManagedCommon/NativeMethods.cs @@ -7,7 +7,7 @@ using System.Runtime.InteropServices; namespace ManagedCommon { - internal static class NativeMethods + internal static partial class NativeMethods { [DllImport("kernel32.dll", SetLastError = true)] internal static extern IntPtr OpenProcess(uint processAccess, bool bInheritHandle, int processId); @@ -21,6 +21,13 @@ namespace ManagedCommon [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] internal static extern bool QueryFullProcessImageName(IntPtr hProcess, uint dwFlags, System.Text.StringBuilder lpExeName, ref uint lpdwSize); + [LibraryImport("kernel32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)] + internal static partial IntPtr CreateEventW(IntPtr lpEventAttributes, [MarshalAs(UnmanagedType.Bool)] bool bManualReset, [MarshalAs(UnmanagedType.Bool)] bool bInitialState, string lpName); + + [LibraryImport("kernel32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + internal static partial bool SetEvent(IntPtr hEvent); + [DllImport("kernel32.dll", SetLastError = true)] internal static extern bool GetExitCodeProcess(IntPtr hProcess, out uint lpExitCode); diff --git a/src/modules/alwaysontop/AlwaysOnTopModuleInterface/ModuleInterface.cs b/src/modules/alwaysontop/AlwaysOnTopModuleInterface/ModuleInterface.cs index 1b3bfb0b19..a4c26035c9 100644 --- a/src/modules/alwaysontop/AlwaysOnTopModuleInterface/ModuleInterface.cs +++ b/src/modules/alwaysontop/AlwaysOnTopModuleInterface/ModuleInterface.cs @@ -12,9 +12,9 @@ using PowerToys.GPOWrapper; namespace AlwaysOnTopModuleInterface { - public class ModuleInterface : IPowerToysModule + public partial class ModuleInterface : IPowerToysModule, IDisposable { - public bool Enabled => true; + public bool Enabled => new SettingsUtils().GetSettings().Enabled.AlwaysOnTop; public string Name => "AlwaysOnTop"; @@ -22,24 +22,22 @@ namespace AlwaysOnTopModuleInterface private Process? _process; - private IntPtr pinEvent = CreateEventW(IntPtr.Zero, false, false, "Local\\AlwaysOnTopPinEvent-892e0aa2-cfa8-4cc4-b196-ddeb32314ce8"); + private InteropEvent? _pinEventWrapper; public void Disable() { - if (_process is not null && !_process.HasExited) - { - _process.Kill(); - } - - if (pinEvent != IntPtr.Zero) - { - CloseHandle(pinEvent); - pinEvent = IntPtr.Zero; - } + InteropEvent terminateEventWrapper = new(InteropEvent.AlwaysOnTopTerminate); + terminateEventWrapper.Fire(); + terminateEventWrapper.Dispose(); + _process?.Dispose(); + _pinEventWrapper?.Dispose(); + _pinEventWrapper = null; } public void Enable() { + _pinEventWrapper = new InteropEvent(InteropEvent.AlwaysOnTopPin); + var psi = new ProcessStartInfo { FileName = "PowerToys.AlwaysOnTop.exe", @@ -54,19 +52,17 @@ namespace AlwaysOnTopModuleInterface public Action OnHotkey => () => { - if (_process is not null && !_process.HasExited && pinEvent != IntPtr.Zero) + if (!_process?.HasExited ?? false) { - _ = SetEvent(pinEvent); + _pinEventWrapper?.Fire(); } }; - [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] - internal static extern IntPtr CreateEventW(IntPtr lpEventAttributes, bool bManualReset, bool bInitialState, string lpName); - - [DllImport("kernel32.dll", SetLastError = true)] - internal static extern bool CloseHandle(IntPtr hObject); - - [DllImport("kernel32.dll", SetLastError = true)] - internal static extern bool SetEvent(IntPtr hEvent); + public void Dispose() + { + _process?.Dispose(); + _pinEventWrapper?.Dispose(); + GC.SuppressFinalize(this); + } } }