From e31bbbd4aa10579cb10ba61f6c9c4c497554d7c7 Mon Sep 17 00:00:00 2001 From: Terry MacDonald Date: Wed, 2 Jun 2021 21:53:18 +1200 Subject: [PATCH] Added stronger start program closing Any start program that is started then tries to doubly make sure that the program is also closed. Now checks for something to close in the following order: - tries to close the process it opened - if that was closed it tries to find any child processes started by the original process in case it was a 'launcher' process - If there are no children with the same parent process then it just tries to close all processes with the same name as the one it opened. That final close also is a forced kill, as I've found programs like SimHub resist closing.... --- DisplayMagician/DisplayMagician.csproj | 1 + DisplayMagician/ProcessUtils.cs | 122 +++++++++++++++++++++++++ DisplayMagician/ShortcutRepository.cs | 119 ++++++++++++++++++++++-- 3 files changed, 236 insertions(+), 6 deletions(-) create mode 100644 DisplayMagician/ProcessUtils.cs diff --git a/DisplayMagician/DisplayMagician.csproj b/DisplayMagician/DisplayMagician.csproj index da08356..d8f73a6 100644 --- a/DisplayMagician/DisplayMagician.csproj +++ b/DisplayMagician/DisplayMagician.csproj @@ -116,6 +116,7 @@ + True diff --git a/DisplayMagician/ProcessUtils.cs b/DisplayMagician/ProcessUtils.cs new file mode 100644 index 0000000..6cbaeda --- /dev/null +++ b/DisplayMagician/ProcessUtils.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Management; +using System.Text; +using System.Threading.Tasks; + +namespace DisplayMagician +{ + + class ProcessInfo : IComparable + { + public Process TheProcess; + public ProcessInfo Parent; + public List Children = new List(); + + public ProcessInfo(Process the_process) + { + TheProcess = the_process; + } + + public override string ToString() + { + return string.Format("{0} [{1}]", + TheProcess.ProcessName, TheProcess.Id); + } + + public int CompareTo(ProcessInfo other) + { + return TheProcess.ProcessName.CompareTo( + other.TheProcess.ProcessName); + } + } + + static class ProcessUtils + { + private static Dictionary allProcessInfosDict; + private static readonly NLog.Logger logger = NLog.LogManager.GetCurrentClassLogger(); + + public static void Initialise() + { + allProcessInfosDict = new Dictionary(); + + try + { + // Get the parent/child info. + ManagementObjectSearcher searcher = new ManagementObjectSearcher( + "SELECT ProcessId, ParentProcessId FROM Win32_Process"); + ManagementObjectCollection collection = searcher.Get(); + + // Get the processes. + foreach (Process process in Process.GetProcesses()) + { + allProcessInfosDict.Add(process.Id, new ProcessInfo(process)); + } + + // Create the child lists. + foreach (var item in collection) + { + // Find the parent and child in the dictionary. + int child_id = Convert.ToInt32(item["ProcessId"]); + int parent_id = Convert.ToInt32(item["ParentProcessId"]); + + ProcessInfo child_info = null; + ProcessInfo parent_info = null; + if (allProcessInfosDict.ContainsKey(child_id)) + child_info = allProcessInfosDict[child_id]; + if (allProcessInfosDict.ContainsKey(parent_id)) + parent_info = allProcessInfosDict[parent_id]; + + if (child_info == null) + Console.WriteLine( + "Cannot find child " + child_id.ToString() + + " for parent " + parent_id.ToString()); + + if (parent_info == null) + Console.WriteLine( + "Cannot find parent " + parent_id.ToString() + + " for child " + child_id.ToString()); + + if ((child_info != null) && (parent_info != null)) + { + parent_info.Children.Add(child_info); + child_info.Parent = parent_info; + } + } + + } + catch(Exception ex) + { + logger.Error(ex,$"ProcessUtils/Initialise: Exception (re)initialising the process information to figure out process hierarchy"); + } + + } + + public static List FindChildProcesses(Process parentProcess) + { + List childProcesses = new List() { }; + + try + { + int parentId = parentProcess.Id; + if (allProcessInfosDict.ContainsKey(parentId)) + { + foreach (ProcessInfo childProcess in allProcessInfosDict[parentId].Children) + { + childProcesses.Add(childProcess.TheProcess); + } + + } + } + catch (Exception ex) + { + logger.Error(ex, $"ProcessUtils/FindChildProcesses: Exception finding the child processes of the parentProcess"); + } + + return childProcesses; + } + + } +} diff --git a/DisplayMagician/ShortcutRepository.cs b/DisplayMagician/ShortcutRepository.cs index 95966ea..9cbd91b 100644 --- a/DisplayMagician/ShortcutRepository.cs +++ b/DisplayMagician/ShortcutRepository.cs @@ -1608,16 +1608,123 @@ namespace DisplayMagician { logger.Debug($"ShortcutRepository/RunShortcut: We started {startProgramsToStart.Count} programs before the main executable or game, and now we want to stop {startProgramsToStop.Count } of them"); + // Prepare the processInfos we need for finding child processes. + ProcessUtils.Initialise(); + // Stop the programs in the reverse order we started them foreach (Process processToStop in startProgramsToStop.Reverse()) { if (processToStop.HasExited) { - // if the process has already exited (e.g. the user exited it themselves) - // then stop trying to stop the process, and instead log the fact it already stopped. - Console.WriteLine($"Stopping process {processToStop.StartInfo.FileName} but was already stopped by user or another process."); - logger.Debug($"ShortcutRepository/RunShortcut: Stopping process {processToStop.StartInfo.FileName} but was already stopped by user or another process."); - continue; + // First check whether it was a loader process that started a subprocess + // If so, we need to go through and find and close all subprocesses + List childProcesses = ProcessUtils.FindChildProcesses(processToStop); + if (childProcesses.Count > 0) + { + foreach (Process childProcessToStop in childProcesses) + { + if (processToStop.HasExited) + { + // if there were no child processes, and the only process has already exited (e.g. the user exited it themselves) + // then stop trying to stop the process, and instead log the fact it already stopped. + Console.WriteLine($"Stopping child process {childProcessToStop.StartInfo.FileName} but was already stopped by user or another process."); + logger.Warn($"ShortcutRepository/RunShortcut: Stopping child process {childProcessToStop.StartInfo.FileName} but was already stopped by user or another process."); + continue; + } + + Console.WriteLine($"Stopping child process {childProcessToStop.StartInfo.FileName} of parent process {processToStop.StartInfo.FileName}"); + logger.Debug($"ShortcutRepository/RunShortcut: Stopping child process {childProcessToStop.StartInfo.FileName} of parent process {processToStop.StartInfo.FileName}"); + try + { + // Stop the program + childProcessToStop.CloseMainWindow(); + childProcessToStop.WaitForExit(5000); + if (!childProcessToStop.HasExited) + { + Console.WriteLine($"- Process {childProcessToStop.StartInfo.FileName} wouldn't stop cleanly. Forcing program close."); + logger.Warn($"ShortcutRepository/RunShortcut: Process {childProcessToStop.StartInfo.FileName} wouldn't stop cleanly. Forcing program close."); + childProcessToStop.Kill(); + childProcessToStop.WaitForExit(5000); + } + childProcessToStop.Close(); + } + catch (Win32Exception ex) + { + logger.Error(ex, $"ShortcutRepository/RunShortcut: Couldn't access the wait status for a child process we're trying to stop."); + } + catch (InvalidOperationException ex) + { + logger.Error(ex, $"ShortcutRepository/RunShortcut: Couldn't kill the child process as the child process appears to have closed already. This can be caused if your {childProcessToStop.StartInfo.FileName} loaded another exe then closed itself. DisplayMagician cannot track that sort of behaviour."); + } + catch (SystemException ex) + { + logger.Error(ex, $"ShortcutRepository/RunShortcut: Couldn't WaitForExit the child process as there is no child process associated with the Process object (or cannot get the ID from the process handle)."); + } + + catch (AggregateException ae) + { + logger.Error(ae, $"ShortcutRepository/RunShortcut: Got an AggregateException."); + } + + } + } + else + { + // if there were no child processes found, and the only process has already exited (e.g. the user exited it themselves) + // then we try to stop any processes with the same name as the application we started + // Look for the processes with the ProcessName we sorted out earlier + string processName = Path.GetFileNameWithoutExtension(processToStop.StartInfo.FileName); + List namedProcessesToStop = Process.GetProcessesByName(processName).ToList(); + + // If we have found one or more processes then we should be good to go + if (namedProcessesToStop.Count > 0) + { + logger.Warn($"ShortcutRepository/RunShortcut: We couldn't find any children processes so we've looked for named processes with the name '{processToStop.StartInfo.FileName}' and we found {namedProcessesToStop.Count}. Closing them."); + foreach (Process namedProcessToStop in namedProcessesToStop) + { + try + { + // Stop the named process + namedProcessToStop.CloseMainWindow(); + namedProcessToStop.WaitForExit(5000); + namedProcessToStop.Kill(); + /*if (!namedProcessToStop.HasExited) + { + logger.Warn($"ShortcutRepository/RunShortcut: Named process {namedProcessToStop.StartInfo.FileName} wouldn't stop cleanly. Forcing program close."); + namedProcessToStop.Kill(); + namedProcessToStop.WaitForExit(5000); + }*/ + namedProcessToStop.Close(); + } + catch (Win32Exception ex) + { + logger.Error(ex, $"ShortcutRepository/RunShortcut: Couldn't access the wait status for a named process we're trying to stop."); + } + catch (InvalidOperationException ex) + { + logger.Error(ex, $"ShortcutRepository/RunShortcut: Couldn't kill the named process as the process appears to have closed already."); + } + catch (SystemException ex) + { + logger.Error(ex, $"ShortcutRepository/RunShortcut: Couldn't WaitForExit the named process as there is no process associated with the Process object (or cannot get the ID from the named process handle)."); + } + + catch (AggregateException ae) + { + logger.Error(ae, $"ShortcutRepository/RunShortcut: Got an AggregateException."); + } + } + } + else + { + // then stop trying to stop the process, and instead log the fact it already stopped. + Console.WriteLine($"Stopping only process {processToStop.StartInfo.FileName} but was already stopped by user or another process."); + logger.Debug($"ShortcutRepository/RunShortcut: Stopping only process {processToStop.StartInfo.FileName} but was already stopped by user or another process."); + + } + continue; + } + } Console.WriteLine($"Stopping process {processToStop.StartInfo.FileName}"); logger.Debug($"ShortcutRepository/RunShortcut: Stopping process {processToStop.StartInfo.FileName}"); @@ -1639,7 +1746,7 @@ namespace DisplayMagician logger.Error(ex, $"ShortcutRepository/RunShortcut: Couldn't access the wait status for a process we're trying to stop."); } catch (InvalidOperationException ex) { - logger.Error(ex, $"ShortcutRepository/RunShortcut: Couldn't kill the process as the proicess appears to have closed already. This can be caused if your {processToStop.ProcessName}.exe loaded another exe then closed itself. DisplayMagician cannot track that sort of behaviour."); + logger.Error(ex, $"ShortcutRepository/RunShortcut: Couldn't kill the process as the proicess appears to have closed already. This can be caused if your {processToStop.StartInfo.FileName} loaded another exe then closed itself. DisplayMagician cannot track that sort of behaviour."); } catch (SystemException ex) {