From c495d788f6c2df05bc235d8be8223655a675cfce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20=22Wizard=22=20Pol=C3=A1=C5=A1ek?= Date: Tue, 6 May 2025 04:05:54 +0200 Subject: [PATCH 1/6] Add a prototype for the 'Restart Windows Explorer' command, implemented using the Restart Manager --- .../Helpers/Commands.cs | 8 + .../Helpers/Icons.cs | 2 + .../Helpers/NativeMethods.cs | 45 ++++ .../Helpers/ProcessRestarter.cs | 205 ++++++++++++++++++ .../Properties/Resources.Designer.cs | 36 +++ .../Properties/Resources.resx | 12 + 6 files changed, 308 insertions(+) create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestarter.cs diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs index a981b2ed003c..4ed35d521438 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.Net.NetworkInformation; using System.Text; +using System.Threading.Tasks; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -110,6 +111,13 @@ public static List GetSystemCommands(bool isUefi, bool hideEmptyRecyc }); } + results.Add(new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_sys_RestartShell_name!, confirmCommands, Resources.Microsoft_plugin_sys_RestartShell_confirmation!, static () => Task.Run(static () => ProcessRestarter.RestartAsync("explorer", TimeSpan.FromSeconds(10))))) + { + Title = Resources.Microsoft_plugin_sys_RestartShell!, + Subtitle = Resources.Microsoft_plugin_sys_RestartShell_description!, + Icon = Icons.RestartShellIcon, + }); + // UEFI command/result. It is only available on systems booted in UEFI mode. if (isUefi) { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Icons.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Icons.cs index be64ddb18160..313ba773f80d 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Icons.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Icons.cs @@ -20,6 +20,8 @@ public static partial class Icons public static IconInfo RestartIcon { get; } = new IconInfo("\uE777"); + public static IconInfo RestartShellIcon { get; } = new IconInfo("\uEC50"); + public static IconInfo ShutdownIcon { get; } = new IconInfo("\uE7E8"); public static IconInfo SleepIcon { get; } = new IconInfo("\uE708"); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NativeMethods.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NativeMethods.cs index e620ab330b05..0ff9ebddc644 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NativeMethods.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NativeMethods.cs @@ -35,6 +35,28 @@ public static class NativeMethods [DllImport("Shell32.dll", CharSet = CharSet.Unicode)] public static extern uint SHEmptyRecycleBin(IntPtr hWnd, uint dwFlags); + + [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)] + public static extern int RmStartSession(out IntPtr pSessionHandle, int dwSessionFlags, string strSessionKey); + + [DllImport("rstrtmgr.dll")] + public static extern int RmEndSession(IntPtr pSessionHandle); + + [DllImport("rstrtmgr.dll", CharSet = CharSet.Auto)] + public static extern int RmRegisterResources( + IntPtr pSessionHandle, + uint nFiles, + string[]? rgsFilenames, + uint applications, + RM_UNIQUE_PROCESS[]? rgApplications, + uint nServices, + string[]? rgsServiceNames); + + [DllImport("rstrtmgr.dll")] + public static extern int RmShutdown(IntPtr pSessionHandle, RM_SHUTDOWN_TYPE lActionFlags, [Optional] RM_WRITE_STATUS_CALLBACK? fnStatus); + + [DllImport("rstrtmgr.dll")] + public static extern int RmRestart(IntPtr pSessionHandle, int dwRestartFlags, [Optional] RM_WRITE_STATUS_CALLBACK? fnStatus); } public enum HRESULT : uint @@ -119,3 +141,26 @@ public enum FirmwareType } public delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam); + +[StructLayout(LayoutKind.Sequential)] +public struct FILETIME +{ + public uint DateTimeLow; + public uint DateTimeHigh; +} + +[StructLayout(LayoutKind.Sequential)] +public struct RM_UNIQUE_PROCESS +{ + public int ProcessId; + public FILETIME ProcessStartTime; +} + +[Flags] +public enum RM_SHUTDOWN_TYPE : uint +{ + RmForceShutdown = 0x1, + RmShutdownOnlyRegistered = 0x10, +} + +public delegate void RM_WRITE_STATUS_CALLBACK(uint nPercentComplete); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestarter.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestarter.cs new file mode 100644 index 000000000000..0d33401e33ed --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestarter.cs @@ -0,0 +1,205 @@ +// 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.Diagnostics; +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.System.Helpers; + +/// +/// Restarts running instances of a specified process using the Windows Restart Manager. +/// +internal static class ProcessRestarter +{ + private static readonly TimeSpan DefaultShutdownTimeout = TimeSpan.FromSeconds(30); + + /// + /// Restarts all instances of the specified process name. + /// + /// The name of the process to restart (without .exe extension). + /// Optional timeout for shutdown operation. Default is 30 seconds. + /// True if processes were restarted successfully; otherwise, false. + /// Thrown when processName is null or empty. + /// Thrown when Restart Manager operations fail. + internal static async Task RestartAsync( + string processName, + TimeSpan? shutdownTimeout = null) + { + ArgumentException.ThrowIfNullOrWhiteSpace(processName); + + var timeout = shutdownTimeout ?? DefaultShutdownTimeout; + var sessionHandle = nint.Zero; + + try + { + var processes = GetProcesses(processName); + if (processes.Length == 0) + { + return false; + } + + sessionHandle = StartRestartManagerSession(); + + RegisterResources(sessionHandle, processes); + + await ShutdownOrKillAsync(sessionHandle, processes, timeout); + + RestartSession(sessionHandle); + + return true; + } + catch (Exception ex) when (ex is not ArgumentNullException) + { + ExtensionHost.LogMessage($"Critical failure: {ex.Message}"); + return false; + } + finally + { + EndRestartManagerSession(sessionHandle); + } + } + + private static nint StartRestartManagerSession() + { + var sessionKey = $"PT_{Guid.NewGuid()}"; + var result = NativeMethods.RmStartSession(out var handle, 0, sessionKey); + if (result != 0) + { + throw new InvalidOperationException($"Failed to start Restart Manager session. Error code: {result}"); + } + + return handle; + } + + private static void RegisterResources(nint sessionHandle, RM_UNIQUE_PROCESS[] processes) + { + var result = NativeMethods.RmRegisterResources( + sessionHandle, + 0, + null, + (uint)processes.Length, + processes, + 0, + null); + + if (result != 0) + { + throw new InvalidOperationException($"Failed to register resources with Restart Manager. Error code: {result}"); + } + } + + private static async Task ShutdownOrKillAsync( + nint sessionHandle, + RM_UNIQUE_PROCESS[] processes, + TimeSpan timeout) + { + // RmShutdown is a blocking call that can take a long time to return. We can run it in a separate thread + // and kill the processes when we run out of patience. + // https://learn.microsoft.com/en-us/windows/win32/api/restartmanager/nf-restartmanager-rmshutdown + var shutdownTask = Task.Run(() => NativeMethods.RmShutdown(sessionHandle, RM_SHUTDOWN_TYPE.RmForceShutdown)); + + try + { + var result = await shutdownTask.WaitAsync(timeout); + if (result != 0) + { + ExtensionHost.LogMessage($"RmShutdown returned error {result}, performing force kill."); + KillProcesses(processes); + } + } + catch (TimeoutException) + { + KillProcesses(processes); + try + { + await shutdownTask; + } + catch + { + // ignore + } + } + } + + private static void RestartSession(nint sessionHandle) + { + var result = NativeMethods.RmRestart(sessionHandle, 0); + if (result != 0) + { + throw new InvalidOperationException($"Failed to restart processes. Error code: {result}"); + } + } + + private static void EndRestartManagerSession(nint sessionHandle) + { + if (sessionHandle == 0) + { + return; + } + + try + { + _ = NativeMethods.RmEndSession(sessionHandle); + } + catch + { + // Suppress cleanup exceptions + } + } + + private static RM_UNIQUE_PROCESS[] GetProcesses(string name) + { + try + { + return Process + .GetProcessesByName(name) + .Select(static p => new RM_UNIQUE_PROCESS + { + ProcessId = p.Id, + ProcessStartTime = ToFileTimeStruct(p.StartTime), + }) + .ToArray(); + } + catch (Exception ex) + { + ExtensionHost.LogMessage( + $"Error retrieving processes '{name}': {ex.Message}"); + return []; + } + + static FILETIME ToFileTimeStruct(DateTime dt) + { + var ft = dt.ToUniversalTime().ToFileTimeUtc(); + return new FILETIME + { + DateTimeLow = (uint)(ft & 0xFFFFFFFF), + DateTimeHigh = (uint)(ft >> 32), + }; + } + } + + private static void KillProcesses(RM_UNIQUE_PROCESS[] processes) + { + foreach (var proc in processes) + { + try + { + Process.GetProcessById(proc.ProcessId).Kill(); + } + catch (ArgumentException) + { + // Process might have exited already + } + catch (Exception ex) + { + ExtensionHost.LogMessage( + $"Failed to kill process ID {proc.ProcessId}: {ex.Message}"); + } + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Properties/Resources.Designer.cs index 1cfa1a6353b0..f2ec8c12184e 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Properties/Resources.Designer.cs @@ -645,6 +645,42 @@ public static string Microsoft_plugin_sys_restart_computer_description { } } + /// + /// Looks up a localized string similar to Restart Windows Explorer. + /// + public static string Microsoft_plugin_sys_RestartShell { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_RestartShell", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to You are about to restart Windows Explorer, are you sure?. + /// + public static string Microsoft_plugin_sys_RestartShell_confirmation { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_RestartShell_confirmation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to End and restart the Windows Explorer shell process. + /// + public static string Microsoft_plugin_sys_RestartShell_description { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_RestartShell_description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Restart. + /// + public static string Microsoft_plugin_sys_RestartShell_name { + get { + return ResourceManager.GetString("Microsoft_plugin_sys_RestartShell_name", resourceCulture); + } + } + /// /// Looks up a localized string similar to ip; mac; address. /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Properties/Resources.resx index 23d27a86a2bb..bf7ab04da0b6 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Properties/Resources.resx @@ -417,4 +417,16 @@ Open System Command + + Restart Windows Explorer + + + End and restart the Windows Explorer shell process + + + Restart + + + You are about to restart Windows Explorer, are you sure? + \ No newline at end of file From dfe135a3ac63dd74ca65d9615060e7473b332a43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Thu, 8 May 2025 14:25:20 +0200 Subject: [PATCH 2/6] Fix spellcheck issues --- .github/actions/spell-check/expect.txt | 1 + .../cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs | 2 +- .../Helpers/{ProcessRestarter.cs => ProcessRestartHelper.cs} | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) rename src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/{ProcessRestarter.cs => ProcessRestartHelper.cs} (99%) diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 43447793a07d..c4f81c2ed47d 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1296,6 +1296,7 @@ pwcs PWSTR pwsz pwtd +rstrtmgr QDC qianlifeng qit diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs index 4ed35d521438..b2eeb59ab183 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs @@ -111,7 +111,7 @@ public static List GetSystemCommands(bool isUefi, bool hideEmptyRecyc }); } - results.Add(new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_sys_RestartShell_name!, confirmCommands, Resources.Microsoft_plugin_sys_RestartShell_confirmation!, static () => Task.Run(static () => ProcessRestarter.RestartAsync("explorer", TimeSpan.FromSeconds(10))))) + results.Add(new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_sys_RestartShell_name!, confirmCommands, Resources.Microsoft_plugin_sys_RestartShell_confirmation!, static () => Task.Run(static () => ProcessRestartHelper.RestartAsync("explorer", TimeSpan.FromSeconds(10))))) { Title = Resources.Microsoft_plugin_sys_RestartShell!, Subtitle = Resources.Microsoft_plugin_sys_RestartShell_description!, diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestarter.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestartHelper.cs similarity index 99% rename from src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestarter.cs rename to src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestartHelper.cs index 0d33401e33ed..4929eb0f00d9 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestarter.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestartHelper.cs @@ -14,7 +14,7 @@ namespace Microsoft.CmdPal.Ext.System.Helpers; /// /// Restarts running instances of a specified process using the Windows Restart Manager. /// -internal static class ProcessRestarter +internal static class ProcessRestartHelper { private static readonly TimeSpan DefaultShutdownTimeout = TimeSpan.FromSeconds(30); From a26e30ac89ae6c54dcb7a3cadb99309d5c0c64cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Fri, 9 May 2025 16:24:30 +0200 Subject: [PATCH 3/6] Limit process restarts to the current session Restricts process restarts to only affect the processes associated with the current session. This change prevents cross-session interference and supports multi-user scenarios more reliably. --- .../Helpers/ProcessRestartHelper.cs | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestartHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestartHelper.cs index 4929eb0f00d9..cf1e92a5f0ff 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestartHelper.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestartHelper.cs @@ -152,24 +152,36 @@ private static void EndRestartManagerSession(nint sessionHandle) } } - private static RM_UNIQUE_PROCESS[] GetProcesses(string name) + private static RM_UNIQUE_PROCESS[] GetProcesses(string processName) { - try + var currentSessionId = Process.GetCurrentProcess().SessionId; + + return Process.GetProcessesByName(processName) + .Select(process => GetProcessInfoSafe(process, currentSessionId)) + .Where(static processInfo => processInfo != null) + .Select(static processInfo => processInfo!.Value) + .ToArray(); + + static RM_UNIQUE_PROCESS? GetProcessInfoSafe(Process process, int targetSessionId) { - return Process - .GetProcessesByName(name) - .Select(static p => new RM_UNIQUE_PROCESS + try + { + if (process.HasExited || process.SessionId != targetSessionId) { - ProcessId = p.Id, - ProcessStartTime = ToFileTimeStruct(p.StartTime), - }) - .ToArray(); - } - catch (Exception ex) - { - ExtensionHost.LogMessage( - $"Error retrieving processes '{name}': {ex.Message}"); - return []; + return null; + } + + return new RM_UNIQUE_PROCESS + { + ProcessId = process.Id, + ProcessStartTime = ToFileTimeStruct(process.StartTime), + }; + } + catch (Exception ex) + { + ExtensionHost.LogMessage($"Error retrieving processes (Process ID {process.Id}): {ex.Message}"); + return null; + } } static FILETIME ToFileTimeStruct(DateTime dt) From 5481559d6eb1c01d0a93fa670727c6f667ce8654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Fri, 9 May 2025 17:35:32 +0200 Subject: [PATCH 4/6] Cleanup and simplify --- .../Helpers/Commands.cs | 2 +- .../Helpers/NativeMethods.cs | 18 +- .../Helpers/ProcessRestartHelper.cs | 217 ---------------- .../Helpers/ShellRestartHelper.cs | 231 ++++++++++++++++++ 4 files changed, 245 insertions(+), 223 deletions(-) delete mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestartHelper.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ShellRestartHelper.cs diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs index b2eeb59ab183..a9daa188c83a 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs @@ -111,7 +111,7 @@ public static List GetSystemCommands(bool isUefi, bool hideEmptyRecyc }); } - results.Add(new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_sys_RestartShell_name!, confirmCommands, Resources.Microsoft_plugin_sys_RestartShell_confirmation!, static () => Task.Run(static () => ProcessRestartHelper.RestartAsync("explorer", TimeSpan.FromSeconds(10))))) + results.Add(new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_sys_RestartShell_name!, confirmCommands, Resources.Microsoft_plugin_sys_RestartShell_confirmation!, static () => Task.Run(static () => ShellRestartHelper.RestartAllInCurrentSession()))) { Title = Resources.Microsoft_plugin_sys_RestartShell!, Subtitle = Resources.Microsoft_plugin_sys_RestartShell_description!, diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NativeMethods.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NativeMethods.cs index 0ff9ebddc644..63cffe5398b1 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NativeMethods.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NativeMethods.cs @@ -37,13 +37,13 @@ public static class NativeMethods public static extern uint SHEmptyRecycleBin(IntPtr hWnd, uint dwFlags); [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)] - public static extern int RmStartSession(out IntPtr pSessionHandle, int dwSessionFlags, string strSessionKey); + public static extern uint RmStartSession(out IntPtr pSessionHandle, uint dwSessionFlags, string strSessionKey); [DllImport("rstrtmgr.dll")] - public static extern int RmEndSession(IntPtr pSessionHandle); + public static extern uint RmEndSession(IntPtr pSessionHandle); [DllImport("rstrtmgr.dll", CharSet = CharSet.Auto)] - public static extern int RmRegisterResources( + public static extern uint RmRegisterResources( IntPtr pSessionHandle, uint nFiles, string[]? rgsFilenames, @@ -53,10 +53,18 @@ public static extern int RmRegisterResources( string[]? rgsServiceNames); [DllImport("rstrtmgr.dll")] - public static extern int RmShutdown(IntPtr pSessionHandle, RM_SHUTDOWN_TYPE lActionFlags, [Optional] RM_WRITE_STATUS_CALLBACK? fnStatus); + public static extern uint RmShutdown(IntPtr pSessionHandle, RM_SHUTDOWN_TYPE lActionFlags, [Optional] RM_WRITE_STATUS_CALLBACK? fnStatus); [DllImport("rstrtmgr.dll")] - public static extern int RmRestart(IntPtr pSessionHandle, int dwRestartFlags, [Optional] RM_WRITE_STATUS_CALLBACK? fnStatus); + public static extern uint RmRestart(IntPtr pSessionHandle, uint dwRestartFlags, [Optional] RM_WRITE_STATUS_CALLBACK? fnStatus); +} + +public enum SystemErrorCode : uint +{ + /// + /// The function succeeds and returns. + /// + ERROR_SUCCESS = 0, } public enum HRESULT : uint diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestartHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestartHelper.cs deleted file mode 100644 index cf1e92a5f0ff..000000000000 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ProcessRestartHelper.cs +++ /dev/null @@ -1,217 +0,0 @@ -// 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.Diagnostics; -using System.Linq; -using System.Threading.Tasks; - -using Microsoft.CommandPalette.Extensions.Toolkit; - -namespace Microsoft.CmdPal.Ext.System.Helpers; - -/// -/// Restarts running instances of a specified process using the Windows Restart Manager. -/// -internal static class ProcessRestartHelper -{ - private static readonly TimeSpan DefaultShutdownTimeout = TimeSpan.FromSeconds(30); - - /// - /// Restarts all instances of the specified process name. - /// - /// The name of the process to restart (without .exe extension). - /// Optional timeout for shutdown operation. Default is 30 seconds. - /// True if processes were restarted successfully; otherwise, false. - /// Thrown when processName is null or empty. - /// Thrown when Restart Manager operations fail. - internal static async Task RestartAsync( - string processName, - TimeSpan? shutdownTimeout = null) - { - ArgumentException.ThrowIfNullOrWhiteSpace(processName); - - var timeout = shutdownTimeout ?? DefaultShutdownTimeout; - var sessionHandle = nint.Zero; - - try - { - var processes = GetProcesses(processName); - if (processes.Length == 0) - { - return false; - } - - sessionHandle = StartRestartManagerSession(); - - RegisterResources(sessionHandle, processes); - - await ShutdownOrKillAsync(sessionHandle, processes, timeout); - - RestartSession(sessionHandle); - - return true; - } - catch (Exception ex) when (ex is not ArgumentNullException) - { - ExtensionHost.LogMessage($"Critical failure: {ex.Message}"); - return false; - } - finally - { - EndRestartManagerSession(sessionHandle); - } - } - - private static nint StartRestartManagerSession() - { - var sessionKey = $"PT_{Guid.NewGuid()}"; - var result = NativeMethods.RmStartSession(out var handle, 0, sessionKey); - if (result != 0) - { - throw new InvalidOperationException($"Failed to start Restart Manager session. Error code: {result}"); - } - - return handle; - } - - private static void RegisterResources(nint sessionHandle, RM_UNIQUE_PROCESS[] processes) - { - var result = NativeMethods.RmRegisterResources( - sessionHandle, - 0, - null, - (uint)processes.Length, - processes, - 0, - null); - - if (result != 0) - { - throw new InvalidOperationException($"Failed to register resources with Restart Manager. Error code: {result}"); - } - } - - private static async Task ShutdownOrKillAsync( - nint sessionHandle, - RM_UNIQUE_PROCESS[] processes, - TimeSpan timeout) - { - // RmShutdown is a blocking call that can take a long time to return. We can run it in a separate thread - // and kill the processes when we run out of patience. - // https://learn.microsoft.com/en-us/windows/win32/api/restartmanager/nf-restartmanager-rmshutdown - var shutdownTask = Task.Run(() => NativeMethods.RmShutdown(sessionHandle, RM_SHUTDOWN_TYPE.RmForceShutdown)); - - try - { - var result = await shutdownTask.WaitAsync(timeout); - if (result != 0) - { - ExtensionHost.LogMessage($"RmShutdown returned error {result}, performing force kill."); - KillProcesses(processes); - } - } - catch (TimeoutException) - { - KillProcesses(processes); - try - { - await shutdownTask; - } - catch - { - // ignore - } - } - } - - private static void RestartSession(nint sessionHandle) - { - var result = NativeMethods.RmRestart(sessionHandle, 0); - if (result != 0) - { - throw new InvalidOperationException($"Failed to restart processes. Error code: {result}"); - } - } - - private static void EndRestartManagerSession(nint sessionHandle) - { - if (sessionHandle == 0) - { - return; - } - - try - { - _ = NativeMethods.RmEndSession(sessionHandle); - } - catch - { - // Suppress cleanup exceptions - } - } - - private static RM_UNIQUE_PROCESS[] GetProcesses(string processName) - { - var currentSessionId = Process.GetCurrentProcess().SessionId; - - return Process.GetProcessesByName(processName) - .Select(process => GetProcessInfoSafe(process, currentSessionId)) - .Where(static processInfo => processInfo != null) - .Select(static processInfo => processInfo!.Value) - .ToArray(); - - static RM_UNIQUE_PROCESS? GetProcessInfoSafe(Process process, int targetSessionId) - { - try - { - if (process.HasExited || process.SessionId != targetSessionId) - { - return null; - } - - return new RM_UNIQUE_PROCESS - { - ProcessId = process.Id, - ProcessStartTime = ToFileTimeStruct(process.StartTime), - }; - } - catch (Exception ex) - { - ExtensionHost.LogMessage($"Error retrieving processes (Process ID {process.Id}): {ex.Message}"); - return null; - } - } - - static FILETIME ToFileTimeStruct(DateTime dt) - { - var ft = dt.ToUniversalTime().ToFileTimeUtc(); - return new FILETIME - { - DateTimeLow = (uint)(ft & 0xFFFFFFFF), - DateTimeHigh = (uint)(ft >> 32), - }; - } - } - - private static void KillProcesses(RM_UNIQUE_PROCESS[] processes) - { - foreach (var proc in processes) - { - try - { - Process.GetProcessById(proc.ProcessId).Kill(); - } - catch (ArgumentException) - { - // Process might have exited already - } - catch (Exception ex) - { - ExtensionHost.LogMessage( - $"Failed to kill process ID {proc.ProcessId}: {ex.Message}"); - } - } - } -} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ShellRestartHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ShellRestartHelper.cs new file mode 100644 index 000000000000..baa1b85411a7 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ShellRestartHelper.cs @@ -0,0 +1,231 @@ +// 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.Diagnostics; +using System.IO; +using System.Linq; +using System.Threading.Tasks; + +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.System.Helpers; + +/// +/// Restarts running instances of system shell (Windows Explorer). +/// +internal static class ShellRestartHelper +{ + private static readonly TimeSpan DefaultShutdownTimeout = TimeSpan.FromSeconds(10); + private static readonly TimeSpan PostRestartCheckDelay = TimeSpan.FromSeconds(10); + + /// + /// Restarts all instances of the "explorer.exe" process in the current session. + /// + internal static async Task RestartAllInCurrentSession() + { + // Restarting Windows Explorer: + // - Explorer can have multiple processes running in the same session. Let's not speculate why the user + // wants to restart it and terminate them all. + // - Explorer should always run un-elevated. If started elevated, it restarts itself (CreateExplorerShellUnelevatedTask). + // That means we don't have to worry about elevated processes. + // - Restart Manager will restore opened folder windows after restart (only if enabled in Folder Options). + // - Restarting by will make the new explorer.exe process a child process of CmdPal. This is not much of a + // problem unless something kills the entire CmdPal process tree. + await RestartProcessesInCurrentSessionAsync("explorer.exe"); + + // - Windows can automatically restart the shell if it detects that it has crashed. This can be disabled + // in registry (key AutoRestartShell). + // - Restart Manager is not guaranteed to restart all the processes it closes. + await EnsureProcessIsRunning("explorer.exe"); + } + + /// + /// Restarts all instances of the specified process name. + /// + /// The name of the process to restart. + /// A task that represents the asynchronous operation. + /// Thrown when processName is null. + /// Thrown when processName is null or consists only of white-space characters. + private static async Task RestartProcessesInCurrentSessionAsync(string processExecutableName) + { + ArgumentException.ThrowIfNullOrWhiteSpace(processExecutableName); + + var restartManagerSessionHandle = nint.Zero; + + try + { + var processName = Path.GetFileNameWithoutExtension(processExecutableName); + var uniqueProcesses = GetProcesses(processName); + if (uniqueProcesses.Length == 0) + { + return; + } + + var result = NativeMethods.RmStartSession(out restartManagerSessionHandle, 0, $"PT_{Guid.NewGuid()}"); + ThrowIfError(result, "start Restart Manager session"); + + result = NativeMethods.RmRegisterResources(restartManagerSessionHandle, 0, null, (uint)uniqueProcesses.Length, uniqueProcesses, 0, null); + ThrowIfError(result, "register resources with Restart Manager"); + + await ShutdownOrKillAsync(restartManagerSessionHandle, uniqueProcesses, DefaultShutdownTimeout); + + result = NativeMethods.RmRestart(restartManagerSessionHandle, 0); + ThrowIfError(result, "restart processes"); + } + catch (Exception ex) when (ex is not ArgumentNullException) + { + ExtensionHost.LogMessage($"Critical failure: {ex.Message}"); + } + finally + { + if (restartManagerSessionHandle != 0) + { + try + { + _ = NativeMethods.RmEndSession(restartManagerSessionHandle); + } + catch + { + // Suppress cleanup exceptions + } + } + } + } + + private static async Task ShutdownOrKillAsync( + nint sessionHandle, + RM_UNIQUE_PROCESS[] processes, + TimeSpan timeout) + { + // RmShutdown is a blocking call that can take a long time to return. We can run it in a separate thread + // and kill the processes when we run out of patience. + // https://learn.microsoft.com/en-us/windows/win32/api/restartmanager/nf-restartmanager-rmshutdown + var shutdownTask = Task.Run(() => NativeMethods.RmShutdown(sessionHandle, RM_SHUTDOWN_TYPE.RmForceShutdown)); + + try + { + var result = await shutdownTask.WaitAsync(timeout); + if (result != 0) + { + ExtensionHost.LogMessage($"RmShutdown returned error {result}, performing force kill."); + KillProcesses(processes); + } + } + catch (TimeoutException) + { + KillProcesses(processes); + try + { + await shutdownTask; + } + catch + { + // ignore + } + } + } + + private static RM_UNIQUE_PROCESS[] GetProcesses(string processName) + { + var currentSessionId = Process.GetCurrentProcess().SessionId; + + return Process.GetProcessesByName(processName) + .Select(process => GetProcessInfoSafe(process, currentSessionId)) + .OfType() + .ToArray(); + + static RM_UNIQUE_PROCESS? GetProcessInfoSafe(Process process, int targetSessionId) + { + try + { + if (process.HasExited || process.SessionId != targetSessionId) + { + return null; + } + + return new RM_UNIQUE_PROCESS + { + ProcessId = process.Id, + ProcessStartTime = ToFileTimeStruct(process.StartTime), + }; + } + catch (Exception ex) + { + ExtensionHost.LogMessage($"Error retrieving processes (Process ID {process.Id}): {ex.Message}"); + return null; + } + } + + static FILETIME ToFileTimeStruct(DateTime dateTime) + { + var fileTime = dateTime.ToFileTimeUtc(); + return new FILETIME + { + DateTimeLow = (uint)fileTime, + DateTimeHigh = (uint)(fileTime >> 32), + }; + } + } + + private static void KillProcesses(RM_UNIQUE_PROCESS[] processes) + { + foreach (var process in processes) + { + try + { + Process.GetProcessById(process.ProcessId).Kill(); + } + catch (ArgumentException) + { + // Process might have exited already + } + catch (Exception ex) + { + ExtensionHost.LogMessage($"Failed to kill process ID {process.ProcessId}: {ex.Message}"); + } + } + } + + private static void ThrowIfError(uint result, string operation) + { + if ((SystemErrorCode)result != SystemErrorCode.ERROR_SUCCESS) + { + throw new InvalidOperationException($"Failed to {operation}. Error code: {result}"); + } + } + + /// + /// Ensures that the specified process is running. If the process is not running, it attempts to start it. + /// + /// The name of the process to restart. + /// A task that represents the asynchronous operation. + private static async Task EnsureProcessIsRunning(string processExecutableName) + { + ArgumentException.ThrowIfNullOrWhiteSpace(processExecutableName); + + var processName = Path.GetFileNameWithoutExtension(processExecutableName); + + if (GetProcesses(processName).Length > 0) + { + return; + } + + await Task.Delay(PostRestartCheckDelay); + + if (GetProcesses(processName).Length > 0) + { + return; + } + + try + { + Process.Start(new ProcessStartInfo(processExecutableName) { UseShellExecute = true }); + } + catch (Exception ex) + { + ExtensionHost.LogMessage($"Fail-safe failed to start {processExecutableName}: {ex.Message}"); + } + } +} From f620860edfc3c65d466dd8944944e754d2e09b23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Mon, 16 Jun 2025 05:09:42 +0200 Subject: [PATCH 5/6] Simplify Explorer restart by removing Restart Manager and using tskill/start instead Trading the ability to reopen the previous folder in exchange for a simpler solution and more consistent behavior across Windows versions. - Reverted addition of Restart Manager - Replaced with `tskill` and `start` commands to restart Windows Explorer --- .github/actions/spell-check/expect.txt | 1 - .../Helpers/Commands.cs | 3 +- .../Helpers/NativeMethods.cs | 53 ---- .../Helpers/ShellRestartHelper.cs | 231 ------------------ 4 files changed, 1 insertion(+), 287 deletions(-) delete mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ShellRestartHelper.cs diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index c4f81c2ed47d..43447793a07d 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1296,7 +1296,6 @@ pwcs PWSTR pwsz pwtd -rstrtmgr QDC qianlifeng qit diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs index a9daa188c83a..70c1b7f8c640 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/Commands.cs @@ -7,7 +7,6 @@ using System.Globalization; using System.Net.NetworkInformation; using System.Text; -using System.Threading.Tasks; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -111,7 +110,7 @@ public static List GetSystemCommands(bool isUefi, bool hideEmptyRecyc }); } - results.Add(new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_sys_RestartShell_name!, confirmCommands, Resources.Microsoft_plugin_sys_RestartShell_confirmation!, static () => Task.Run(static () => ShellRestartHelper.RestartAllInCurrentSession()))) + results.Add(new ListItem(new ExecuteCommandConfirmation(Resources.Microsoft_plugin_sys_RestartShell_name!, confirmCommands, Resources.Microsoft_plugin_sys_RestartShell_confirmation!, static () => OpenInShellHelper.OpenInShell("cmd", "/C tskill explorer && start explorer", runWithHiddenWindow: true))) { Title = Resources.Microsoft_plugin_sys_RestartShell!, Subtitle = Resources.Microsoft_plugin_sys_RestartShell_description!, diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NativeMethods.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NativeMethods.cs index 63cffe5398b1..e620ab330b05 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NativeMethods.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NativeMethods.cs @@ -35,36 +35,6 @@ public static class NativeMethods [DllImport("Shell32.dll", CharSet = CharSet.Unicode)] public static extern uint SHEmptyRecycleBin(IntPtr hWnd, uint dwFlags); - - [DllImport("rstrtmgr.dll", CharSet = CharSet.Unicode)] - public static extern uint RmStartSession(out IntPtr pSessionHandle, uint dwSessionFlags, string strSessionKey); - - [DllImport("rstrtmgr.dll")] - public static extern uint RmEndSession(IntPtr pSessionHandle); - - [DllImport("rstrtmgr.dll", CharSet = CharSet.Auto)] - public static extern uint RmRegisterResources( - IntPtr pSessionHandle, - uint nFiles, - string[]? rgsFilenames, - uint applications, - RM_UNIQUE_PROCESS[]? rgApplications, - uint nServices, - string[]? rgsServiceNames); - - [DllImport("rstrtmgr.dll")] - public static extern uint RmShutdown(IntPtr pSessionHandle, RM_SHUTDOWN_TYPE lActionFlags, [Optional] RM_WRITE_STATUS_CALLBACK? fnStatus); - - [DllImport("rstrtmgr.dll")] - public static extern uint RmRestart(IntPtr pSessionHandle, uint dwRestartFlags, [Optional] RM_WRITE_STATUS_CALLBACK? fnStatus); -} - -public enum SystemErrorCode : uint -{ - /// - /// The function succeeds and returns. - /// - ERROR_SUCCESS = 0, } public enum HRESULT : uint @@ -149,26 +119,3 @@ public enum FirmwareType } public delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam); - -[StructLayout(LayoutKind.Sequential)] -public struct FILETIME -{ - public uint DateTimeLow; - public uint DateTimeHigh; -} - -[StructLayout(LayoutKind.Sequential)] -public struct RM_UNIQUE_PROCESS -{ - public int ProcessId; - public FILETIME ProcessStartTime; -} - -[Flags] -public enum RM_SHUTDOWN_TYPE : uint -{ - RmForceShutdown = 0x1, - RmShutdownOnlyRegistered = 0x10, -} - -public delegate void RM_WRITE_STATUS_CALLBACK(uint nPercentComplete); diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ShellRestartHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ShellRestartHelper.cs deleted file mode 100644 index baa1b85411a7..000000000000 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/ShellRestartHelper.cs +++ /dev/null @@ -1,231 +0,0 @@ -// 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.Diagnostics; -using System.IO; -using System.Linq; -using System.Threading.Tasks; - -using Microsoft.CommandPalette.Extensions.Toolkit; - -namespace Microsoft.CmdPal.Ext.System.Helpers; - -/// -/// Restarts running instances of system shell (Windows Explorer). -/// -internal static class ShellRestartHelper -{ - private static readonly TimeSpan DefaultShutdownTimeout = TimeSpan.FromSeconds(10); - private static readonly TimeSpan PostRestartCheckDelay = TimeSpan.FromSeconds(10); - - /// - /// Restarts all instances of the "explorer.exe" process in the current session. - /// - internal static async Task RestartAllInCurrentSession() - { - // Restarting Windows Explorer: - // - Explorer can have multiple processes running in the same session. Let's not speculate why the user - // wants to restart it and terminate them all. - // - Explorer should always run un-elevated. If started elevated, it restarts itself (CreateExplorerShellUnelevatedTask). - // That means we don't have to worry about elevated processes. - // - Restart Manager will restore opened folder windows after restart (only if enabled in Folder Options). - // - Restarting by will make the new explorer.exe process a child process of CmdPal. This is not much of a - // problem unless something kills the entire CmdPal process tree. - await RestartProcessesInCurrentSessionAsync("explorer.exe"); - - // - Windows can automatically restart the shell if it detects that it has crashed. This can be disabled - // in registry (key AutoRestartShell). - // - Restart Manager is not guaranteed to restart all the processes it closes. - await EnsureProcessIsRunning("explorer.exe"); - } - - /// - /// Restarts all instances of the specified process name. - /// - /// The name of the process to restart. - /// A task that represents the asynchronous operation. - /// Thrown when processName is null. - /// Thrown when processName is null or consists only of white-space characters. - private static async Task RestartProcessesInCurrentSessionAsync(string processExecutableName) - { - ArgumentException.ThrowIfNullOrWhiteSpace(processExecutableName); - - var restartManagerSessionHandle = nint.Zero; - - try - { - var processName = Path.GetFileNameWithoutExtension(processExecutableName); - var uniqueProcesses = GetProcesses(processName); - if (uniqueProcesses.Length == 0) - { - return; - } - - var result = NativeMethods.RmStartSession(out restartManagerSessionHandle, 0, $"PT_{Guid.NewGuid()}"); - ThrowIfError(result, "start Restart Manager session"); - - result = NativeMethods.RmRegisterResources(restartManagerSessionHandle, 0, null, (uint)uniqueProcesses.Length, uniqueProcesses, 0, null); - ThrowIfError(result, "register resources with Restart Manager"); - - await ShutdownOrKillAsync(restartManagerSessionHandle, uniqueProcesses, DefaultShutdownTimeout); - - result = NativeMethods.RmRestart(restartManagerSessionHandle, 0); - ThrowIfError(result, "restart processes"); - } - catch (Exception ex) when (ex is not ArgumentNullException) - { - ExtensionHost.LogMessage($"Critical failure: {ex.Message}"); - } - finally - { - if (restartManagerSessionHandle != 0) - { - try - { - _ = NativeMethods.RmEndSession(restartManagerSessionHandle); - } - catch - { - // Suppress cleanup exceptions - } - } - } - } - - private static async Task ShutdownOrKillAsync( - nint sessionHandle, - RM_UNIQUE_PROCESS[] processes, - TimeSpan timeout) - { - // RmShutdown is a blocking call that can take a long time to return. We can run it in a separate thread - // and kill the processes when we run out of patience. - // https://learn.microsoft.com/en-us/windows/win32/api/restartmanager/nf-restartmanager-rmshutdown - var shutdownTask = Task.Run(() => NativeMethods.RmShutdown(sessionHandle, RM_SHUTDOWN_TYPE.RmForceShutdown)); - - try - { - var result = await shutdownTask.WaitAsync(timeout); - if (result != 0) - { - ExtensionHost.LogMessage($"RmShutdown returned error {result}, performing force kill."); - KillProcesses(processes); - } - } - catch (TimeoutException) - { - KillProcesses(processes); - try - { - await shutdownTask; - } - catch - { - // ignore - } - } - } - - private static RM_UNIQUE_PROCESS[] GetProcesses(string processName) - { - var currentSessionId = Process.GetCurrentProcess().SessionId; - - return Process.GetProcessesByName(processName) - .Select(process => GetProcessInfoSafe(process, currentSessionId)) - .OfType() - .ToArray(); - - static RM_UNIQUE_PROCESS? GetProcessInfoSafe(Process process, int targetSessionId) - { - try - { - if (process.HasExited || process.SessionId != targetSessionId) - { - return null; - } - - return new RM_UNIQUE_PROCESS - { - ProcessId = process.Id, - ProcessStartTime = ToFileTimeStruct(process.StartTime), - }; - } - catch (Exception ex) - { - ExtensionHost.LogMessage($"Error retrieving processes (Process ID {process.Id}): {ex.Message}"); - return null; - } - } - - static FILETIME ToFileTimeStruct(DateTime dateTime) - { - var fileTime = dateTime.ToFileTimeUtc(); - return new FILETIME - { - DateTimeLow = (uint)fileTime, - DateTimeHigh = (uint)(fileTime >> 32), - }; - } - } - - private static void KillProcesses(RM_UNIQUE_PROCESS[] processes) - { - foreach (var process in processes) - { - try - { - Process.GetProcessById(process.ProcessId).Kill(); - } - catch (ArgumentException) - { - // Process might have exited already - } - catch (Exception ex) - { - ExtensionHost.LogMessage($"Failed to kill process ID {process.ProcessId}: {ex.Message}"); - } - } - } - - private static void ThrowIfError(uint result, string operation) - { - if ((SystemErrorCode)result != SystemErrorCode.ERROR_SUCCESS) - { - throw new InvalidOperationException($"Failed to {operation}. Error code: {result}"); - } - } - - /// - /// Ensures that the specified process is running. If the process is not running, it attempts to start it. - /// - /// The name of the process to restart. - /// A task that represents the asynchronous operation. - private static async Task EnsureProcessIsRunning(string processExecutableName) - { - ArgumentException.ThrowIfNullOrWhiteSpace(processExecutableName); - - var processName = Path.GetFileNameWithoutExtension(processExecutableName); - - if (GetProcesses(processName).Length > 0) - { - return; - } - - await Task.Delay(PostRestartCheckDelay); - - if (GetProcesses(processName).Length > 0) - { - return; - } - - try - { - Process.Start(new ProcessStartInfo(processExecutableName) { UseShellExecute = true }); - } - catch (Exception ex) - { - ExtensionHost.LogMessage($"Fail-safe failed to start {processExecutableName}: {ex.Message}"); - } - } -} From beecfd5ddfba609dfa1f660ed3502e3d0f4b15fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Mon, 16 Jun 2025 05:16:23 +0200 Subject: [PATCH 6/6] Add "tskill" to spellchecker dictionary --- .github/actions/spell-check/expect.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 43447793a07d..7b9371cf9e90 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1712,6 +1712,7 @@ trx tsa TSender TServer +tskill tstoi TStr tweakme