Created a new way to treat process creation from module interfaces

This commit is contained in:
Noraa Junker
2025-12-11 01:43:04 +01:00
parent 09a79ab692
commit 123d318d6a
12 changed files with 208 additions and 188 deletions

View File

@@ -17,7 +17,7 @@ using RunnerV2.Helpers;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class AdvancedPasteModuleInterface : IPowerToysModule, IDisposable
internal sealed class AdvancedPasteModuleInterface : ProcessModuleAbstractClass, IPowerToysModule, IDisposable
{
public string Name => "AdvancedPaste";
@@ -33,29 +33,17 @@ namespace RunnerV2.ModuleInterfaces
_ipc.End();
_ipc = null;
}
ProcessHelper.ScheudleProcessKill("PowerToys.AdvancedPaste");
}
private TwoWayPipeMessageIPCManaged? _ipc;
private string _ipcName = @"\\.\pipe\PowerToys.AdvancedPaste";
public void Enable()
{
if (Process.GetProcessesByName("PowerToys.AdvancedPaste.exe").Length > 0)
{
return;
}
string ipcName = @"\\.\pipe\PowerToys.AdvancedPaste";
_ipc = new TwoWayPipeMessageIPCManaged(string.Empty, ipcName, (_) => { });
_ipc = new TwoWayPipeMessageIPCManaged(string.Empty, _ipcName, (_) => { });
_ipc.Start();
if (Shortcuts.Count == 0)
{
PopulateShortcuts();
}
Process.Start("WinUI3Apps\\PowerToys.AdvancedPaste.exe", $"{Environment.ProcessId} {ipcName}");
PopulateShortcuts();
}
public void OnSettingsChanged(string settingsKind, JsonElement jsonProperties)
@@ -65,10 +53,7 @@ namespace RunnerV2.ModuleInterfaces
public void PopulateShortcuts()
{
if (_ipc is null)
{
_ipc = new TwoWayPipeMessageIPCManaged(string.Empty, @"\\.\pipe\PowerToys.AdvancedPaste", (_) => { });
}
_ipc ??= new TwoWayPipeMessageIPCManaged(string.Empty, @"\\.\pipe\PowerToys.AdvancedPaste", (_) => { });
Shortcuts.Clear();
@@ -112,5 +97,13 @@ namespace RunnerV2.ModuleInterfaces
}
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; } = [];
public override string ProcessPath => "WinUI3Apps\\PowerToys.AdvancedPaste.exe";
public override string ProcessName => "PowerToys.AdvancedPaste";
public override string ProcessArguments => _ipcName;
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess | ProcessLaunchOptions.RunnerProcessIdAsFirstArgument;
}
}

View File

@@ -14,7 +14,7 @@ using RunnerV2.Helpers;
namespace RunnerV2.ModuleInterfaces
{
public partial class AlwaysOnTopModuleInterface : IPowerToysModule, IDisposable
internal sealed partial class AlwaysOnTopModuleInterface : ProcessModuleAbstractClass, IPowerToysModule, IDisposable
{
public bool Enabled => new SettingsUtils().GetSettings<GeneralSettings>().Enabled.AlwaysOnTop;
@@ -22,8 +22,6 @@ namespace RunnerV2.ModuleInterfaces
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredAlwaysOnTopEnabledValue();
private Process? _process;
private InteropEvent? _pinEventWrapper;
public void Disable()
@@ -33,44 +31,22 @@ namespace RunnerV2.ModuleInterfaces
terminateEventWrapper.Dispose();
_pinEventWrapper?.Dispose();
_pinEventWrapper = null;
ProcessHelper.ScheudleProcessKill("PowerToys.AlwaysOnTop");
}
public void Enable()
{
if (_process?.HasExited == false)
{
return;
}
_pinEventWrapper = new InteropEvent(InteropEvent.AlwaysOnTopPin);
var psi = new ProcessStartInfo
{
FileName = "PowerToys.AlwaysOnTop.exe",
Arguments = Environment.ProcessId.ToString(CultureInfo.InvariantCulture),
UseShellExecute = true,
};
_process = Process.Start(psi);
}
public AlwaysOnTopModuleInterface()
{
InitializeHotkey();
_pinEventWrapper = new InteropEvent(InteropEvent.AlwaysOnTopPin);
}
private void InitializeHotkey()
{
Shortcuts.Clear();
Shortcuts.Add((new SettingsUtils().GetSettings<AlwaysOnTopSettings>(Name).Properties.Hotkey.Value, () =>
{
if (!_process?.HasExited ?? false)
{
_pinEventWrapper?.Fire();
}
}));
));
}
public void OnSettingsChanged(string settingsKind, JsonElement jsonProperties)
@@ -80,9 +56,14 @@ namespace RunnerV2.ModuleInterfaces
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; } = [];
public override string ProcessPath => "PowerToys.AlwaysOnTop.exe";
public override string ProcessName => "PowerToys.AlwaysOnTop";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess | ProcessLaunchOptions.RunnerProcessIdAsFirstArgument | ProcessLaunchOptions.ElevateIfApplicable;
public void Dispose()
{
_process?.Dispose();
_pinEventWrapper?.Dispose();
GC.SuppressFinalize(this);
}

View File

@@ -9,10 +9,11 @@ using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Library;
using PowerToys.GPOWrapper;
using RunnerV2.Helpers;
using Windows.Media.Capture;
namespace RunnerV2.ModuleInterfaces
{
internal sealed partial class AwakeModuleInterface : IPowerToysModule, IDisposable
internal sealed class AwakeModuleInterface : ProcessModuleAbstractClass, IPowerToysModule
{
public string Name => "Awake";
@@ -20,38 +21,23 @@ namespace RunnerV2.ModuleInterfaces
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredAwakeEnabledValue();
private Process? _process;
public override string ProcessPath => "PowerToys.Awake.exe";
public override string ProcessName => "PowerToys.Awake";
public override string ProcessArguments => $"--use-pt-config --pid {Environment.ProcessId.ToString(CultureInfo.InvariantCulture)}";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess;
public void Disable()
{
InteropEvent terminateEventWrapper = new(InteropEvent.AwakeTerminate);
terminateEventWrapper.Fire();
terminateEventWrapper.Dispose();
ProcessHelper.ScheudleProcessKill("PowerToys.Awake");
}
public void Enable()
{
if (_process?.HasExited == false)
{
return;
}
var psi = new ProcessStartInfo
{
FileName = "PowerToys.Awake.exe",
Arguments = $"--use-pt-config --pid {Environment.ProcessId.ToString(CultureInfo.InvariantCulture)}",
UseShellExecute = true,
};
_process = Process.Start(psi);
}
public void Dispose()
{
_process?.Dispose();
GC.SuppressFinalize(this);
}
}
}

View File

@@ -17,7 +17,7 @@ using RunnerV2.Helpers;
namespace RunnerV2.ModuleInterfaces
{
internal sealed partial class ColorPickerModuleInterface : IPowerToysModule, IDisposable
internal sealed partial class ColorPickerModuleInterface : ProcessModuleAbstractClass, IPowerToysModule, IDisposable
{
public string Name => "ColorPicker";
@@ -25,8 +25,6 @@ namespace RunnerV2.ModuleInterfaces
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredColorPickerEnabledValue();
private Process? _process;
private InteropEvent? _showUiEventWrapper;
public void Disable()
@@ -36,56 +34,40 @@ namespace RunnerV2.ModuleInterfaces
terminateEventWrapper.Dispose();
_showUiEventWrapper?.Dispose();
_showUiEventWrapper = null;
ProcessHelper.ScheudleProcessKill("PowerToys.ColorPickerUI");
}
public void Enable()
{
if (_process?.HasExited == false)
{
return;
}
InitializeShortcuts();
_showUiEventWrapper = new InteropEvent(InteropEvent.ColorPickerShow);
var psi = new ProcessStartInfo
{
FileName = "PowerToys.ColorPickerUI.exe",
Arguments = Environment.ProcessId.ToString(CultureInfo.InvariantCulture),
UseShellExecute = true,
};
_process = Process.Start(psi);
}
public ColorPickerModuleInterface()
{
InitializeHotkey();
}
private void InitializeHotkey()
private void InitializeShortcuts()
{
Shortcuts.Clear();
Shortcuts.Add((new SettingsUtils().GetSettings<ColorPickerSettings>(Name).Properties.ActivationShortcut, () =>
{
if (!_process?.HasExited ?? false)
{
_showUiEventWrapper?.Fire();
}
}));
_showUiEventWrapper?.Fire();
}
));
}
public void OnSettingsChanged(string settingsKind, JsonElement jsonProperties)
{
InitializeHotkey();
InitializeShortcuts();
}
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; } = [];
public override string ProcessPath => "PowerToys.ColorPickerUI.exe";
public override string ProcessName => "PowerToys.ColorPickerUI";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess | ProcessLaunchOptions.RunnerProcessIdAsFirstArgument;
public void Dispose()
{
_process?.Dispose();
_showUiEventWrapper?.Dispose();
GC.SuppressFinalize(this);
}

View File

@@ -16,7 +16,7 @@ using Windows.ApplicationModel;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class CommandPaletteModuleInterface : IPowerToysModule
internal sealed class CommandPaletteModuleInterface : ProcessModuleAbstractClass, IPowerToysModule
{
private const string PackageName = "Microsoft.CommandPalette"
#if DEBUG
@@ -30,13 +30,16 @@ namespace RunnerV2.ModuleInterfaces
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredCmdPalEnabledValue();
public override string ProcessPath => "explorer.exe";
public override string ProcessArguments => "x-cmdpal://background";
public override string ProcessName => string.Empty;
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.UseShellExecute | ProcessLaunchOptions.NeverExit;
public void Disable()
{
ProcessHelper.ScheudleProcessKill("Microsoft.CmdPal.UI");
lock (_launchedLock)
{
_launched = false;
}
}
public void Enable()
@@ -73,64 +76,6 @@ namespace RunnerV2.ModuleInterfaces
Logger.LogError("Command Palette package is not registered after attempting to enable it.");
return;
}
lock (_launchedLock)
{
if (_launched)
{
return;
}
}
LaunchApp("explorer.exe", "x-cmdpal://background", false);
}
private readonly object _launchedLock = new();
private bool _launched;
// TODO: Implement retry logic for launching the app
/*private static void TryLaunch(string path, string args)
{
int baseDelay = 1000;
int maxAttempts = 9;
int retryCount = 0;
do
{
if (LaunchApp)
}
}*/
private bool LaunchApp(string path, string args, bool elevated)
{
try
{
Process? process = Process.Start(new ProcessStartInfo
{
FileName = path,
CreateNoWindow = true,
UseShellExecute = true,
Verb = elevated ? "runas" : "open",
Arguments = args,
});
if (process is null)
{
Logger.LogError($"Failed to start process for {path} with args {args}");
}
}
catch (Exception ex)
{
Logger.LogError($"Exception occurred while launching app {path} with args {args}", ex);
return false;
}
lock (_launchedLock)
{
_launched = true;
}
return true;
}
}
}

View File

@@ -15,7 +15,7 @@ using RunnerV2.Helpers;
namespace RunnerV2.ModuleInterfaces
{
internal sealed partial class CropAndLockModuleInterface : IPowerToysModule, IDisposable
internal sealed partial class CropAndLockModuleInterface : ProcessModuleAbstractClass, IPowerToysModule, IDisposable
{
public string Name => "CropAndLock";
@@ -33,27 +33,16 @@ namespace RunnerV2.ModuleInterfaces
_reparentEvent.Dispose();
_thumbnailEvent.Dispose();
_terminateEvent.Dispose();
ProcessHelper.ScheudleProcessKill("PowerToys.CropAndLock");
}
public void Enable()
{
EnsureLaunched();
_reparentEvent = new(InteropEvent.CropAndLockReparent);
_thumbnailEvent = new(InteropEvent.CropAndLockThumbnail);
_terminateEvent = new(InteropEvent.CropAndLockTerminate);
PopulateShortcuts();
}
private void EnsureLaunched()
{
Process[] processes = Process.GetProcessesByName("PowerToys.CropAndLock");
if (processes.Length == 0)
{
Process.Start("PowerToys.CropAndLock.exe", Environment.ProcessId.ToString(CultureInfo.InvariantCulture));
}
}
public void PopulateShortcuts()
{
Shortcuts.Clear();
@@ -69,6 +58,12 @@ namespace RunnerV2.ModuleInterfaces
public List<(HotkeySettings Hotkey, Action Action)> Shortcuts { get; } = [];
public override string ProcessPath => "PowerToys.CropAndLock.exe";
public override string ProcessName => "PowerToys.CropAndLock";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess | ProcessLaunchOptions.RunnerProcessIdAsFirstArgument;
public void Dispose()
{
GC.SuppressFinalize(this);

View File

@@ -9,7 +9,7 @@ using RunnerV2.Helpers;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class HostsModuleInterface : IPowerToysModule
internal sealed class HostsModuleInterface : ProcessModuleAbstractClass, IPowerToysModule
{
public bool Enabled => new SettingsUtils().GetSettingsOrDefault<GeneralSettings>().Enabled.Hosts;
@@ -17,9 +17,14 @@ namespace RunnerV2.ModuleInterfaces
public string Name => "Hosts";
public override string ProcessPath => string.Empty;
public override string ProcessName => "PowerToys.Hosts";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SupressLaunchOnModuleEnabled;
public void Disable()
{
ProcessHelper.ScheudleProcessKill("PowerToys.Hosts", 0);
}
public void Enable()

View File

@@ -10,7 +10,7 @@ using PowerToys.GPOWrapper;
namespace RunnerV2.ModuleInterfaces
{
internal sealed class PowerAccentModuleInterface : IPowerToysModule
internal sealed class PowerAccentModuleInterface : ProcessModuleAbstractClass, IPowerToysModule
{
public string Name => "PowerAccent";
@@ -18,17 +18,18 @@ namespace RunnerV2.ModuleInterfaces
public GpoRuleConfigured GpoRuleConfigured => GPOWrapper.GetConfiguredQuickAccentEnabledValue();
public override string ProcessPath => "PowerToys.PowerAccent.exe";
public override string ProcessName => "PowerToys.PowerAccent";
public override ProcessLaunchOptions LaunchOptions => ProcessLaunchOptions.SingletonProcess | ProcessLaunchOptions.RunnerProcessIdAsFirstArgument;
public void Disable()
{
foreach (var process in Process.GetProcessesByName("PowerToys.PowerAccent.exe"))
{
process.Kill();
}
}
public void Enable()
{
Process.Start("PowerToys.PowerAccent.exe", Environment.ProcessId.ToString(CultureInfo.InvariantCulture));
}
}
}

View File

@@ -0,0 +1,114 @@
// 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.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using PowerToys.GPOWrapper;
using RunnerV2.Helpers;
namespace RunnerV2.ModuleInterfaces
{
internal abstract class ProcessModuleAbstractClass
{
/// <summary>
/// Options for launching a process.
/// </summary>
[Flags]
public enum ProcessLaunchOptions
{
/// <summary>
/// Only a single instance of the process should be running.
/// </summary>
SingletonProcess = 1,
/// <summary>
/// Elevate the process if the current process is elevated.
/// </summary>
ElevateIfApplicable = 2,
/// <summary>
/// Provide the runner process ID as the first argument to the launched process.
/// </summary>
RunnerProcessIdAsFirstArgument = 4,
/// <summary>
/// Indicates that the application should not launch automatically when the specified module is enabled.
/// </summary>
SupressLaunchOnModuleEnabled = 8,
/// <summary>
/// Specifies that the process should be started using the operating system shell.
/// </summary>
UseShellExecute = 16,
/// <summary>
/// Indicates that the process should never exit automatically.
/// </summary>
/// <remarks>Use this value when using a helper process to launch an application like explorer.exe.</remarks>
NeverExit = 32,
}
public abstract string ProcessPath { get; }
public abstract string ProcessName { get; }
public virtual string ProcessArguments { get; } = string.Empty;
public abstract ProcessLaunchOptions LaunchOptions { get; }
public void EnsureLaunched()
{
Process[] processes = Process.GetProcessesByName(ProcessName);
if (processes.Length > 0)
{
return;
}
LaunchProcess();
}
public void LaunchProcess(bool isModuleEnableProcess = false)
{
if (isModuleEnableProcess && LaunchOptions.HasFlag(ProcessLaunchOptions.SupressLaunchOnModuleEnabled))
{
return;
}
if (LaunchOptions.HasFlag(ProcessLaunchOptions.SingletonProcess))
{
Process[] processes = Process.GetProcessesByName(ProcessName);
if (processes.Length > 0)
{
return;
}
}
string arguments = (LaunchOptions.HasFlag(ProcessLaunchOptions.RunnerProcessIdAsFirstArgument) ? Environment.ProcessId.ToString(CultureInfo.InvariantCulture) + (string.IsNullOrEmpty(ProcessArguments) ? string.Empty : " ") : string.Empty) + ProcessArguments;
Process.Start(new ProcessStartInfo()
{
UseShellExecute = LaunchOptions.HasFlag(ProcessLaunchOptions.UseShellExecute),
FileName = ProcessPath,
Arguments = arguments,
Verb = LaunchOptions.HasFlag(ProcessLaunchOptions.ElevateIfApplicable) && ElevationHelper.IsProcessElevated() ? "runas" : "open",
});
}
public void ProcessExit(int msDelay = 500)
{
if (LaunchOptions.HasFlag(ProcessLaunchOptions.NeverExit))
{
return;
}
ProcessHelper.ScheudleProcessKill(ProcessName, msDelay);
}
}
}

View File

@@ -8,6 +8,7 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Drawing;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Reflection;
@@ -34,13 +35,13 @@ namespace RunnerV2
public static FrozenSet<IPowerToysModule> ModulesToLoad { get; } =
[
new ColorPickerModuleInterface(),
new AlwaysOnTopModuleInterface(),
new HostsModuleInterface(),
new PowerAccentModuleInterface(),
new AdvancedPasteModuleInterface(),
new AwakeModuleInterface(),
new CmdNotFoundModuleInterface(),
new ColorPickerModuleInterface(),
new CommandPaletteModuleInterface(),
new CropAndLockModuleInterface(),
];
@@ -102,6 +103,12 @@ namespace RunnerV2
try
{
module.Disable();
if (module is ProcessModuleAbstractClass pmac)
{
pmac.ProcessExit();
}
foreach (var hotkey in module.Hotkeys)
{
HotkeyManager.DisableHotkey(hotkey.Key);
@@ -127,6 +134,11 @@ namespace RunnerV2
if (!LoadedModules.Contains(module))
{
module.Enable();
if (module is ProcessModuleAbstractClass pmac)
{
pmac.LaunchProcess(true);
}
LoadedModules.Add(module);
}
@@ -159,6 +171,11 @@ namespace RunnerV2
{
module.Disable();
if (module is ProcessModuleAbstractClass pmac)
{
pmac.ProcessExit();
}
foreach (var hotkey in module.Hotkeys)
{
HotkeyManager.DisableHotkey(hotkey.Key);

View File

@@ -16,6 +16,7 @@
<ItemGroup>
<ProjectReference Include="..\..\common\ManagedCommon\ManagedCommon.csproj" />
<ProjectReference Include="..\..\common\ManagedCsWin32\ManagedCsWin32.csproj" />
<ProjectReference Include="..\..\modules\poweraccent\PowerAccent.UI\PowerAccent.UI.csproj" />
<ProjectReference Include="..\..\settings-ui\Settings.UI.Library\Settings.UI.Library.csproj" />
<ProjectReference Include="..\..\Update\Update.csproj" />
</ItemGroup>

View File

@@ -3,7 +3,7 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Diagnostics;
using ColorPicker.Helpers;
using ColorPicker.Mouse;
using ColorPickerUI;