[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); return gcnew String(CommonSharedConstants::SHOW_POWEROCR_SHARED_EVENT);
} }
static String ^ MouseJumpShowPreviewEvent() {
return gcnew String(CommonSharedConstants::MOUSE_JUMP_SHOW_PREVIEW_EVENT);
}
static String ^ AwakeExitEvent() { static String ^ AwakeExitEvent() {
return gcnew String(CommonSharedConstants::AWAKE_EXIT_EVENT); return gcnew String(CommonSharedConstants::AWAKE_EXIT_EVENT);
} }

View File

@@ -50,6 +50,9 @@ namespace CommonSharedConstants
// Path to the event used by PowerOCR // Path to the event used by PowerOCR
const wchar_t SHOW_POWEROCR_SHARED_EVENT[] = L"Local\\PowerOCREvent-dc864e06-e1af-4ecc-9078-f98bee745e3a"; 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 // Path to the event used by RegistryPreview
const wchar_t REGISTRY_PREVIEW_TRIGGER_EVENT[] = L"Local\\RegistryPreviewEvent-4C559468-F75A-4E7F-BC4F-9C9688316687"; 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 "pch.h"
#include <interface/powertoy_module_interface.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 "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/logger_helper.h>
#include <common/utils/winapi_error.h>
extern "C" IMAGE_DOS_HEADER __ImageBase; BOOL APIENTRY DllMain(HMODULE /*hModule*/,
DWORD ul_reason_for_call,
HMODULE m_hModule; LPVOID /*lpReserved*/)
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{ {
m_hModule = hModule;
switch (ul_reason_for_call) switch (ul_reason_for_call)
{ {
case DLL_PROCESS_ATTACH: case DLL_PROCESS_ATTACH:
Trace::RegisterProvider(); Trace::RegisterProvider();
break; break;
case DLL_THREAD_ATTACH: case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH: case DLL_THREAD_DETACH:
break; break;
case DLL_PROCESS_DETACH: case DLL_PROCESS_DETACH:
Trace::UnregisterProvider(); Trace::UnregisterProvider();
break; break;
} }
return TRUE; return TRUE;
} }
@@ -54,9 +54,17 @@ private:
bool m_enabled = false; bool m_enabled = false;
// Hotkey to invoke the module // Hotkey to invoke the module
Hotkey m_hotkey;
HANDLE m_hProcess; 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) void parse_hotkey(PowerToysSettings::PowerToyValues& settings)
{ {
auto settingsObject = settings.get_raw_json(); auto settingsObject = settings.get_raw_json();
@@ -123,11 +131,21 @@ private:
} }
// Load initial settings from the persisted values. // Load initial settings from the persisted values.
void init_settings(); void init_settings()
void terminate_process()
{ {
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: public:
@@ -135,6 +153,7 @@ public:
MouseJump() MouseJump()
{ {
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mouseJumpLoggerName); LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mouseJumpLoggerName);
m_hInvokeEvent = CreateDefaultEvent(CommonSharedConstants::MOUSE_JUMP_SHOW_PREVIEW_EVENT);
init_settings(); init_settings();
}; };
@@ -142,7 +161,6 @@ public:
{ {
if (m_enabled) if (m_enabled)
{ {
terminate_process();
} }
m_enabled = false; m_enabled = false;
} }
@@ -150,6 +168,7 @@ public:
// Destroy the powertoy and free memory // Destroy the powertoy and free memory
virtual void destroy() override virtual void destroy() override
{ {
Logger::trace("MouseJump::destroy()");
delete this; delete this;
} }
@@ -180,12 +199,14 @@ public:
PowerToysSettings::Settings settings(hinstance, get_name()); PowerToysSettings::Settings settings(hinstance, get_name());
settings.set_description(MODULE_DESC); 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); return settings.serialize_to_buffer(buffer, buffer_size);
} }
// Signal from the Settings editor to call a custom action. // Signal from the Settings editor to call a custom action.
// This can be used to spawn more complex editors. // 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()); PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
parse_hotkey(values); parse_hotkey(values);
values.save_to_settings_file(); values.save_to_settings_file();
} }
catch (std::exception&) catch (std::exception&)
@@ -211,6 +231,9 @@ public:
// Enable the powertoy // Enable the powertoy
virtual void enable() virtual void enable()
{ {
Logger::trace("MouseJump::enable()");
ResetEvent(m_hInvokeEvent);
launch_process();
m_enabled = true; m_enabled = true;
Trace::EnableJumpTool(true); Trace::EnableJumpTool(true);
} }
@@ -218,33 +241,29 @@ public:
// Disable the powertoy // Disable the powertoy
virtual void disable() virtual void disable()
{ {
Logger::trace("MouseJump::disable()");
if (m_enabled) if (m_enabled)
{ {
terminate_process(); ResetEvent(m_hInvokeEvent);
TerminateProcess(m_hProcess, 1);
} }
m_enabled = false; m_enabled = false;
Trace::EnableJumpTool(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 virtual bool on_hotkey(size_t /*hotkeyId*/) override
{ {
if (m_enabled) if (m_enabled)
{ {
Logger::trace(L"MouseJump hotkey pressed"); Logger::trace(L"MouseJump hotkey pressed");
Trace::InvokeJumpTool(); Trace::InvokeJumpTool();
if (is_process_running()) if (!is_process_running())
{ {
terminate_process();
}
launch_process(); launch_process();
}
SetEvent(m_hInvokeEvent);
return true; 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 // Returns whether the PowerToys should be enabled by default
virtual bool is_enabled_by_default() const override 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() extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{ {
return new MouseJump(); 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 internal partial class MainForm : Form
{ {
public MainForm(MouseJumpSettings settings) public MainForm(SettingsHelper settingsHelper)
{ {
this.InitializeComponent(); this.InitializeComponent();
this.Settings = settings ?? throw new ArgumentNullException(nameof(settings)); this.SettingsHelper = settingsHelper ?? throw new ArgumentNullException(nameof(settingsHelper));
this.ShowThumbnail();
} }
public MouseJumpSettings Settings public SettingsHelper SettingsHelper
{ {
get; get;
} }
@@ -104,14 +103,8 @@ internal partial class MainForm : Form
private void MainForm_Deactivate(object sender, EventArgs e) private void MainForm_Deactivate(object sender, EventArgs e)
{ {
this.Close(); this.Hide();
this.ClearPreview();
if (this.Thumbnail.Image is not null)
{
var tmp = this.Thumbnail.Image;
this.Thumbnail.Image = null;
tmp.Dispose();
}
} }
private void Thumbnail_Click(object sender, EventArgs e) private void Thumbnail_Click(object sender, EventArgs e)
@@ -139,8 +132,11 @@ internal partial class MainForm : Form
this.OnDeactivate(EventArgs.Empty); 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 stopwatch = Stopwatch.StartNew();
var layoutInfo = MainForm.GetLayoutInfo(this); var layoutInfo = MainForm.GetLayoutInfo(this);
LayoutHelper.PositionForm(this, layoutInfo.FormBounds); LayoutHelper.PositionForm(this, layoutInfo.FormBounds);
@@ -148,7 +144,7 @@ internal partial class MainForm : Form
stopwatch.Stop(); stopwatch.Stop();
// we have to activate the form to make sure the deactivate event fires // 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(); this.Activate();
} }
@@ -175,6 +171,9 @@ internal partial class MainForm : Form
.Single(item => item.Screen.Handle == activatedScreenHandle.Value) .Single(item => item.Screen.Handle == activatedScreenHandle.Value)
.Index; .Index;
// avoid a race condition - cache the current settings in case they change
var currentSettings = form.SettingsHelper.CurrentSettings;
var layoutConfig = new LayoutConfig( var layoutConfig = new LayoutConfig(
virtualScreenBounds: ScreenHelper.GetVirtualScreen(), virtualScreenBounds: ScreenHelper.GetVirtualScreen(),
screens: screens.Select(item => item.Screen).ToList(), screens: screens.Select(item => item.Screen).ToList(),
@@ -182,13 +181,13 @@ internal partial class MainForm : Form
activatedScreenIndex: activatedScreenIndex, activatedScreenIndex: activatedScreenIndex,
activatedScreenNumber: activatedScreenIndex + 1, activatedScreenNumber: activatedScreenIndex + 1,
maximumFormSize: new( maximumFormSize: new(
form.Settings.Properties.ThumbnailSize.Width, currentSettings.Properties.ThumbnailSize.Width,
form.Settings.Properties.ThumbnailSize.Height), currentSettings.Properties.ThumbnailSize.Height),
formPadding: new( /*
form.panel1.Padding.Left, don't read the panel padding values because they are affected by dpi scaling
form.panel1.Padding.Top, and can give wrong values when moving between monitors with different dpi scaling
form.panel1.Padding.Right, */
form.panel1.Padding.Bottom), formPadding: new(5, 5, 5, 5),
previewPadding: new(0)); previewPadding: new(0));
Logger.LogInfo(string.Join( Logger.LogInfo(string.Join(
'\n', '\n',
@@ -295,10 +294,30 @@ internal partial class MainForm : Form
stopwatch.Stop(); 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) private static void RefreshPreview(MainForm form)
{ {
if (!form.Visible) 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(); form.Show();
} }

View File

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

View File

@@ -4,10 +4,16 @@
using System; using System;
using System.IO; using System.IO;
using System.Reflection;
using System.Text.Json; using System.Text.Json;
using System.Threading;
using System.Windows.Forms; using System.Windows.Forms;
using System.Windows.Threading;
using Common.UI;
using interop;
using ManagedCommon; using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library; using Microsoft.PowerToys.Settings.UI.Library;
using MouseJumpUI.Helpers;
namespace MouseJumpUI; namespace MouseJumpUI;
@@ -17,7 +23,7 @@ internal static class Program
/// The main entry point for the application. /// The main entry point for the application.
/// </summary> /// </summary>
[STAThread] [STAThread]
private static void Main() private static void Main(string[] args)
{ {
Logger.InitializeLogger("\\MouseJump\\Logs"); Logger.InitializeLogger("\\MouseJump\\Logs");
@@ -38,10 +44,42 @@ internal static class Program
return; return;
} }
var settings = Program.ReadSettings(); // validate command line arguments - we're expecting
var mainForm = new MainForm(settings); // 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() private static MouseJumpSettings ReadSettings()

View File

@@ -2,6 +2,8 @@
// The Microsoft Corporation licenses this file to you under the MIT license. // The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information. // See the LICENSE file in the project root for more information.
using System;
using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces; using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
@@ -21,6 +23,22 @@ namespace Microsoft.PowerToys.Settings.UI.Library
Version = "1.0"; 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() public string GetModuleName()
{ {
return Name; return Name;