[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
This commit is contained in:
Michael Clayton
2023-10-11 15:58:19 +01:00
committed by GitHub
parent 602a3ff090
commit 93d80f542c
10 changed files with 359 additions and 73 deletions

View File

@@ -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);
}

View File

@@ -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";

View File

@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Source Files">
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
<Extensions>cpp;c;cc;cxx;c++;def;odl;idl;hpj;bat;asm;asmx</Extensions>
</Filter>
<Filter Include="Header Files">
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
</Filter>
<Filter Include="Resource Files">
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
<Filter Include="Generated Files">
<UniqueIdentifier>{875a08c6-f610-4667-bd0f-80171ed96072}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Generated Files\resource.h">
<Filter>Generated Files</Filter>
</ClInclude>
<ClInclude Include="trace.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="trace.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<None Include="resource.h">
<Filter>Header Files</Filter>
</None>
<None Include="MouseJump.rc">
<Filter>Resource Files</Filter>
</None>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="Generated Files\MouseJump.rc">
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
</Project>

View File

@@ -1,32 +1,32 @@
// dllmain.cpp : Defines the entry point for the DLL application.
#include "pch.h"
#include <interface/powertoy_module_interface.h>
//#include <interface/lowlevel_keyboard_event_data.h>
//#include <interface/win_hook_event_data.h>
#include <common/SettingsAPI/settings_objects.h>
#include "trace.h"
#include <common/utils/winapi_error.h>
#include <common/SettingsAPI/settings_objects.h>
#include <common/utils/resources.h>
#include <common/interop/shared_constants.h>
#include <common/utils/logger_helper.h>
#include <common/utils/winapi_error.h>
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();

View File

@@ -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>(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();
}
}

View File

@@ -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();
}
}
}

View File

@@ -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();
}

View File

@@ -62,6 +62,7 @@
<PackageReference Include="Microsoft.Windows.CsWinRT" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\Common.UI\Common.UI.csproj" />
<ProjectReference Include="..\..\..\common\GPOWrapper\GPOWrapper.vcxproj" />
<ProjectReference Include="..\..\..\common\interop\PowerToys.Interop.vcxproj" />
<ProjectReference Include="..\..\..\common\ManagedCommon\ManagedCommon.csproj" />

View File

@@ -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.
/// </summary>
[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} <RunnerPid>",
});
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()

View File

@@ -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;