From 93d80f542c6c111a318e9f43370f4e01dafd0a4c Mon Sep 17 00:00:00 2001 From: Michael Clayton Date: Wed, 11 Oct 2023 15:58:19 +0100 Subject: [PATCH] [MouseJump]Long lived background process (#28380) * [MouseJump] Long lived background exe * [MouseJump] Long lived background exe * [MouseJump] Long lived background exe * [MouseJump] Long lived background exe * [MouseJump] Close long lived background exe when parent runner exits * [MouseJump] Close long lived background exe when parent runner exits * [MouseJump] long lived background exe - fixing build * [MouseJump] - add FileSystemWatcher for config (#26703) * Fix telemetry event --- src/common/interop/interop.cpp | 4 + src/common/interop/shared_constants.h | 3 + .../MouseJump/MouseJump.vcxproj.filters | 56 ++++++++++ src/modules/MouseUtils/MouseJump/dllmain.cpp | 101 +++++++++--------- .../MouseJumpUI/Helpers/SettingsHelper.cs | 95 ++++++++++++++++ .../Helpers/ThrottledActionInvoker.cs | 47 ++++++++ .../MouseUtils/MouseJumpUI/MainForm.cs | 61 +++++++---- .../MouseUtils/MouseJumpUI/MouseJumpUI.csproj | 1 + src/modules/MouseUtils/MouseJumpUI/Program.cs | 46 +++++++- .../Settings.UI.Library/MouseJumpSettings.cs | 18 ++++ 10 files changed, 359 insertions(+), 73 deletions(-) create mode 100644 src/modules/MouseUtils/MouseJump/MouseJump.vcxproj.filters create mode 100644 src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs create mode 100644 src/modules/MouseUtils/MouseJumpUI/Helpers/ThrottledActionInvoker.cs diff --git a/src/common/interop/interop.cpp b/src/common/interop/interop.cpp index aebf044f21..46ce729219 100644 --- a/src/common/interop/interop.cpp +++ b/src/common/interop/interop.cpp @@ -203,6 +203,10 @@ public return gcnew String(CommonSharedConstants::SHOW_POWEROCR_SHARED_EVENT); } + static String ^ MouseJumpShowPreviewEvent() { + return gcnew String(CommonSharedConstants::MOUSE_JUMP_SHOW_PREVIEW_EVENT); + } + static String ^ AwakeExitEvent() { return gcnew String(CommonSharedConstants::AWAKE_EXIT_EVENT); } diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h index 1535a3aa87..4761d3c22c 100644 --- a/src/common/interop/shared_constants.h +++ b/src/common/interop/shared_constants.h @@ -50,6 +50,9 @@ namespace CommonSharedConstants // Path to the event used by PowerOCR const wchar_t SHOW_POWEROCR_SHARED_EVENT[] = L"Local\\PowerOCREvent-dc864e06-e1af-4ecc-9078-f98bee745e3a"; + // Path to the events used by Mouse Jump + const wchar_t MOUSE_JUMP_SHOW_PREVIEW_EVENT[] = L"Local\\MouseJumpEvent-aa0be051-3396-4976-b7ba-1a9cc7d236a5"; + // Path to the event used by RegistryPreview const wchar_t REGISTRY_PREVIEW_TRIGGER_EVENT[] = L"Local\\RegistryPreviewEvent-4C559468-F75A-4E7F-BC4F-9C9688316687"; diff --git a/src/modules/MouseUtils/MouseJump/MouseJump.vcxproj.filters b/src/modules/MouseUtils/MouseJump/MouseJump.vcxproj.filters new file mode 100644 index 0000000000..1d00e4c5b3 --- /dev/null +++ b/src/modules/MouseUtils/MouseJump/MouseJump.vcxproj.filters @@ -0,0 +1,56 @@ + + + + + {4FC737F1-C7A5-4376-A066-2A32D752A2FF} + cpp;c;cc;cxx;c++;def;odl;idl;hpj;bat;asm;asmx + + + {93995380-89BD-4b04-88EB-625FBE52EBFB} + h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd + + + {67DA6AB6-F800-4c08-8B7A-83BB121AAD01} + rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms + + + {875a08c6-f610-4667-bd0f-80171ed96072} + + + + + Header Files + + + Generated Files + + + Header Files + + + + + Source Files + + + Source Files + + + Source Files + + + + + Header Files + + + Resource Files + + + + + + Resource Files + + + \ No newline at end of file diff --git a/src/modules/MouseUtils/MouseJump/dllmain.cpp b/src/modules/MouseUtils/MouseJump/dllmain.cpp index 8699fc4fad..d687caa063 100644 --- a/src/modules/MouseUtils/MouseJump/dllmain.cpp +++ b/src/modules/MouseUtils/MouseJump/dllmain.cpp @@ -1,32 +1,32 @@ +// dllmain.cpp : Defines the entry point for the DLL application. #include "pch.h" #include -//#include -//#include -#include #include "trace.h" -#include +#include +#include +#include #include +#include -extern "C" IMAGE_DOS_HEADER __ImageBase; - -HMODULE m_hModule; - -BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID /*lpReserved*/) +BOOL APIENTRY DllMain(HMODULE /*hModule*/, + DWORD ul_reason_for_call, + LPVOID /*lpReserved*/) { - m_hModule = hModule; switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: Trace::RegisterProvider(); break; case DLL_THREAD_ATTACH: + break; case DLL_THREAD_DETACH: break; case DLL_PROCESS_DETACH: Trace::UnregisterProvider(); break; } + return TRUE; } @@ -54,9 +54,17 @@ private: bool m_enabled = false; // Hotkey to invoke the module - Hotkey m_hotkey; + HANDLE m_hProcess; + // Time to wait for process to close after sending WM_CLOSE signal + static const int MAX_WAIT_MILLISEC = 10000; + + Hotkey m_hotkey; + + // Handle to event used to invoke PowerOCR + HANDLE m_hInvokeEvent; + void parse_hotkey(PowerToysSettings::PowerToyValues& settings) { auto settingsObject = settings.get_raw_json(); @@ -123,11 +131,21 @@ private: } // Load initial settings from the persisted values. - void init_settings(); - - void terminate_process() + void init_settings() { - TerminateProcess(m_hProcess, 1); + try + { + // Load and parse the settings file for this PowerToy. + PowerToysSettings::PowerToyValues settings = + PowerToysSettings::PowerToyValues::load_from_settings_file(get_key()); + + parse_hotkey(settings); + } + catch (std::exception&) + { + Logger::warn(L"An exception occurred while loading the settings file"); + // Error while loading from the settings file. Let default values stay as they are. + } } public: @@ -135,6 +153,7 @@ public: MouseJump() { LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mouseJumpLoggerName); + m_hInvokeEvent = CreateDefaultEvent(CommonSharedConstants::MOUSE_JUMP_SHOW_PREVIEW_EVENT); init_settings(); }; @@ -142,7 +161,6 @@ public: { if (m_enabled) { - terminate_process(); } m_enabled = false; } @@ -150,6 +168,7 @@ public: // Destroy the powertoy and free memory virtual void destroy() override { + Logger::trace("MouseJump::destroy()"); delete this; } @@ -180,12 +199,14 @@ public: PowerToysSettings::Settings settings(hinstance, get_name()); settings.set_description(MODULE_DESC); + settings.set_overview_link(L"https://aka.ms/PowerToysOverview_MouseUtilities/#mouse-jump"); + return settings.serialize_to_buffer(buffer, buffer_size); } // Signal from the Settings editor to call a custom action. // This can be used to spawn more complex editors. - virtual void call_custom_action(const wchar_t* action) override + virtual void call_custom_action(const wchar_t* /*action*/) override { } @@ -199,7 +220,6 @@ public: PowerToysSettings::PowerToyValues::from_json_string(config, get_key()); parse_hotkey(values); - values.save_to_settings_file(); } catch (std::exception&) @@ -211,6 +231,9 @@ public: // Enable the powertoy virtual void enable() { + Logger::trace("MouseJump::enable()"); + ResetEvent(m_hInvokeEvent); + launch_process(); m_enabled = true; Trace::EnableJumpTool(true); } @@ -218,33 +241,29 @@ public: // Disable the powertoy virtual void disable() { + Logger::trace("MouseJump::disable()"); if (m_enabled) { - terminate_process(); + ResetEvent(m_hInvokeEvent); + TerminateProcess(m_hProcess, 1); } m_enabled = false; Trace::EnableJumpTool(false); } - // Returns if the powertoys is enabled - virtual bool is_enabled() override - { - return m_enabled; - } - virtual bool on_hotkey(size_t /*hotkeyId*/) override { if (m_enabled) { Logger::trace(L"MouseJump hotkey pressed"); Trace::InvokeJumpTool(); - if (is_process_running()) + if (!is_process_running()) { - terminate_process(); + launch_process(); } - launch_process(); + SetEvent(m_hInvokeEvent); return true; } @@ -268,6 +287,12 @@ public: } } + // Returns if the powertoys is enabled + virtual bool is_enabled() override + { + return m_enabled; + } + // Returns whether the PowerToys should be enabled by default virtual bool is_enabled_by_default() const override { @@ -276,26 +301,6 @@ public: }; -// Load the settings file. -void MouseJump::init_settings() -{ - try - { - // Load and parse the settings file for this PowerToy. - PowerToysSettings::PowerToyValues settings = - PowerToysSettings::PowerToyValues::load_from_settings_file(MouseJump::get_name()); - - parse_hotkey(settings); - - } - catch (std::exception&) - { - Logger::warn(L"An exception occurred while loading the settings file"); - // Error while loading from the settings file. Let default values stay as they are. - } -} - - extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() { return new MouseJump(); diff --git a/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs b/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs new file mode 100644 index 0000000000..c2602ad700 --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Helpers/SettingsHelper.cs @@ -0,0 +1,95 @@ +// 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.IO; +using System.IO.Abstractions; +using System.Threading; +using ManagedCommon; +using Microsoft.PowerToys.Settings.UI.Library; +using Microsoft.PowerToys.Settings.UI.Library.Utilities; + +namespace MouseJumpUI.Helpers; + +internal class SettingsHelper +{ + public SettingsHelper() + { + this.LockObject = new(); + this.CurrentSettings = this.LoadSettings(); + + // delay loading settings on change by some time to avoid file in use exception + var throttledActionInvoker = new ThrottledActionInvoker(); + this.FileSystemWatcher = Helper.GetFileWatcher( + moduleName: MouseJumpSettings.ModuleName, + fileName: "settings.json", + onChangedCallback: () => throttledActionInvoker.ScheduleAction(this.ReloadSettings, 250)); + } + + private IFileSystemWatcher FileSystemWatcher + { + get; + } + + private object LockObject + { + get; + } + + public MouseJumpSettings CurrentSettings + { + get; + private set; + } + + private MouseJumpSettings LoadSettings() + { + lock (this.LockObject) + { + { + var settingsUtils = new SettingsUtils(); + + // set this to 1 to disable retries + var remainingRetries = 5; + + while (remainingRetries > 0) + { + try + { + if (!settingsUtils.SettingsExists(MouseJumpSettings.ModuleName)) + { + Logger.LogInfo("MouseJump settings.json was missing, creating a new one"); + var defaultSettings = new MouseJumpSettings(); + defaultSettings.Save(settingsUtils); + } + + var settings = settingsUtils.GetSettingsOrDefault(MouseJumpSettings.ModuleName); + return settings; + } + catch (IOException ex) + { + Logger.LogError("Failed to read changed settings", ex); + Thread.Sleep(250); + } + catch (Exception ex) + { + Logger.LogError("Failed to read changed settings", ex); + Thread.Sleep(250); + } + + remainingRetries--; + } + } + } + + const string message = "Failed to read changed settings - ran out of retries"; + Logger.LogError(message); + throw new InvalidOperationException(message); + } + + public void ReloadSettings() + { + this.CurrentSettings = this.LoadSettings(); + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/Helpers/ThrottledActionInvoker.cs b/src/modules/MouseUtils/MouseJumpUI/Helpers/ThrottledActionInvoker.cs new file mode 100644 index 0000000000..ecd17cfa4c --- /dev/null +++ b/src/modules/MouseUtils/MouseJumpUI/Helpers/ThrottledActionInvoker.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 System.Windows.Threading; + +namespace MouseJumpUI.Helpers; + +internal sealed class ThrottledActionInvoker +{ + private readonly object _invokerLock = new(); + private readonly DispatcherTimer _timer; + + private Action? _actionToRun; + + public ThrottledActionInvoker() + { + _timer = new DispatcherTimer(); + _timer.Tick += Timer_Tick; + } + + public void ScheduleAction(Action action, int milliseconds) + { + lock (_invokerLock) + { + if (_timer.IsEnabled) + { + _timer.Stop(); + } + + _actionToRun = action; + _timer.Interval = new TimeSpan(0, 0, 0, 0, milliseconds); + + _timer.Start(); + } + } + + private void Timer_Tick(object? sender, EventArgs? e) + { + lock (_invokerLock) + { + _timer.Stop(); + _actionToRun?.Invoke(); + } + } +} diff --git a/src/modules/MouseUtils/MouseJumpUI/MainForm.cs b/src/modules/MouseUtils/MouseJumpUI/MainForm.cs index 7dbc1c6dd4..2b055aa862 100644 --- a/src/modules/MouseUtils/MouseJumpUI/MainForm.cs +++ b/src/modules/MouseUtils/MouseJumpUI/MainForm.cs @@ -19,14 +19,13 @@ namespace MouseJumpUI; internal partial class MainForm : Form { - public MainForm(MouseJumpSettings settings) + public MainForm(SettingsHelper settingsHelper) { this.InitializeComponent(); - this.Settings = settings ?? throw new ArgumentNullException(nameof(settings)); - this.ShowThumbnail(); + this.SettingsHelper = settingsHelper ?? throw new ArgumentNullException(nameof(settingsHelper)); } - public MouseJumpSettings Settings + public SettingsHelper SettingsHelper { get; } @@ -104,14 +103,8 @@ internal partial class MainForm : Form private void MainForm_Deactivate(object sender, EventArgs e) { - this.Close(); - - if (this.Thumbnail.Image is not null) - { - var tmp = this.Thumbnail.Image; - this.Thumbnail.Image = null; - tmp.Dispose(); - } + this.Hide(); + this.ClearPreview(); } private void Thumbnail_Click(object sender, EventArgs e) @@ -139,8 +132,11 @@ internal partial class MainForm : Form this.OnDeactivate(EventArgs.Empty); } - public void ShowThumbnail() + public void ShowPreview() { + // hide the form while we redraw it... + this.Visible = false; + var stopwatch = Stopwatch.StartNew(); var layoutInfo = MainForm.GetLayoutInfo(this); LayoutHelper.PositionForm(this, layoutInfo.FormBounds); @@ -148,7 +144,7 @@ internal partial class MainForm : Form stopwatch.Stop(); // we have to activate the form to make sure the deactivate event fires - Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpTeleportCursorEvent()); + Microsoft.PowerToys.Telemetry.PowerToysTelemetry.Log.WriteEvent(new Telemetry.MouseJumpShowEvent()); this.Activate(); } @@ -175,6 +171,9 @@ internal partial class MainForm : Form .Single(item => item.Screen.Handle == activatedScreenHandle.Value) .Index; + // avoid a race condition - cache the current settings in case they change + var currentSettings = form.SettingsHelper.CurrentSettings; + var layoutConfig = new LayoutConfig( virtualScreenBounds: ScreenHelper.GetVirtualScreen(), screens: screens.Select(item => item.Screen).ToList(), @@ -182,13 +181,13 @@ internal partial class MainForm : Form activatedScreenIndex: activatedScreenIndex, activatedScreenNumber: activatedScreenIndex + 1, maximumFormSize: new( - form.Settings.Properties.ThumbnailSize.Width, - form.Settings.Properties.ThumbnailSize.Height), - formPadding: new( - form.panel1.Padding.Left, - form.panel1.Padding.Top, - form.panel1.Padding.Right, - form.panel1.Padding.Bottom), + currentSettings.Properties.ThumbnailSize.Width, + currentSettings.Properties.ThumbnailSize.Height), + /* + don't read the panel padding values because they are affected by dpi scaling + and can give wrong values when moving between monitors with different dpi scaling + */ + formPadding: new(5, 5, 5, 5), previewPadding: new(0)); Logger.LogInfo(string.Join( '\n', @@ -295,10 +294,30 @@ internal partial class MainForm : Form stopwatch.Stop(); } + private void ClearPreview() + { + if (this.Thumbnail.Image is null) + { + return; + } + + var tmp = this.Thumbnail.Image; + this.Thumbnail.Image = null; + tmp.Dispose(); + + // force preview image memory to be released, otherwise + // all the disposed images can pile up without being GC'ed + GC.Collect(); + } + private static void RefreshPreview(MainForm form) { if (!form.Visible) { + // we seem to need to turn off topmost and then re-enable it again + // when we show the form, otherwise it doesn't get shown topmost... + form.TopMost = false; + form.TopMost = true; form.Show(); } diff --git a/src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj b/src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj index 478e7afe1e..d8cbe38df9 100644 --- a/src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj +++ b/src/modules/MouseUtils/MouseJumpUI/MouseJumpUI.csproj @@ -62,6 +62,7 @@ + diff --git a/src/modules/MouseUtils/MouseJumpUI/Program.cs b/src/modules/MouseUtils/MouseJumpUI/Program.cs index 7d3c385730..d70ebfa454 100644 --- a/src/modules/MouseUtils/MouseJumpUI/Program.cs +++ b/src/modules/MouseUtils/MouseJumpUI/Program.cs @@ -4,10 +4,16 @@ using System; using System.IO; +using System.Reflection; using System.Text.Json; +using System.Threading; using System.Windows.Forms; +using System.Windows.Threading; +using Common.UI; +using interop; using ManagedCommon; using Microsoft.PowerToys.Settings.UI.Library; +using MouseJumpUI.Helpers; namespace MouseJumpUI; @@ -17,7 +23,7 @@ internal static class Program /// The main entry point for the application. /// [STAThread] - private static void Main() + private static void Main(string[] args) { Logger.InitializeLogger("\\MouseJump\\Logs"); @@ -38,10 +44,42 @@ internal static class Program return; } - var settings = Program.ReadSettings(); - var mainForm = new MainForm(settings); + // validate command line arguments - we're expecting + // a single argument containing the runner pid + if ((args.Length != 1) || !int.TryParse(args[0], out var runnerPid)) + { + var message = string.Join("\r\n", new[] + { + "Invalid command line arguments.", + "Expected usage is:", + string.Empty, + $"{Assembly.GetExecutingAssembly().GetName().Name} ", + }); + Logger.LogInfo(message); + throw new InvalidOperationException(message); + } - Application.Run(mainForm); + Logger.LogInfo($"Mouse Jump started from the PowerToys Runner. Runner pid={runnerPid}"); + + var cancellationTokenSource = new CancellationTokenSource(); + + RunnerHelper.WaitForPowerToysRunner(runnerPid, () => + { + Logger.LogInfo("PowerToys Runner exited. Exiting Mouse Jump"); + cancellationTokenSource.Cancel(); + Application.Exit(); + }); + + var settingsHelper = new SettingsHelper(); + var mainForm = new MainForm(settingsHelper); + + NativeEventWaiter.WaitForEventLoop( + Constants.MouseJumpShowPreviewEvent(), + mainForm.ShowPreview, + Dispatcher.CurrentDispatcher, + cancellationTokenSource.Token); + + Application.Run(); } private static MouseJumpSettings ReadSettings() diff --git a/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs b/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs index 73b15f5086..2c0cf9eb3b 100644 --- a/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs +++ b/src/settings-ui/Settings.UI.Library/MouseJumpSettings.cs @@ -2,6 +2,8 @@ // 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.Text.Json; using System.Text.Json.Serialization; using Microsoft.PowerToys.Settings.UI.Library.Interfaces; @@ -21,6 +23,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library Version = "1.0"; } + public void Save(ISettingsUtils settingsUtils) + { + // Save settings to file + var options = new JsonSerializerOptions + { + WriteIndented = true, + }; + + if (settingsUtils == null) + { + throw new ArgumentNullException(nameof(settingsUtils)); + } + + settingsUtils.SaveSettings(JsonSerializer.Serialize(this, options), ModuleName); + } + public string GetModuleName() { return Name;