From fa23e29d5b90471ff057006fb08e64fe2889845d Mon Sep 17 00:00:00 2001 From: Jarr3 Date: Sat, 30 Mar 2024 16:08:31 +0100 Subject: [PATCH] Add VPin Process to ChildProcessTracker #11 --- .github/workflows/prerelease.yml | 2 +- VPinballX.starter/App.xaml.cs | 140 +++++++++++++++++++++ VPinballX.starter/VPinballX.starter.csproj | 12 +- 3 files changed, 145 insertions(+), 9 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index 4d5e9f2..ec5ea9c 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -23,7 +23,7 @@ jobs: echo "sha7=${SHA7}" echo "sha=${SHA}" >> $GITHUB_OUTPUT echo "sha7=${SHA7}" >> $GITHUB_OUTPUT - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 with: ref: ${{ steps.sha.outputs.sha }} fetch-depth: 0 diff --git a/VPinballX.starter/App.xaml.cs b/VPinballX.starter/App.xaml.cs index dfa42bc..ad3ebd7 100644 --- a/VPinballX.starter/App.xaml.cs +++ b/VPinballX.starter/App.xaml.cs @@ -21,6 +21,8 @@ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the using OpenMcdf; using System.Runtime.InteropServices; using Salaros.Configuration; +using System.ComponentModel; + namespace VPinballX.starter { @@ -35,6 +37,141 @@ public partial class App : Application public static string strIniConfigFilename = "VPinballX.starter.ini"; public static string strLogFilename = Path.Combine(App.strExeFilePath, "VPinballX.starter.log"); + /// + /// Allows processes to be automatically killed if this parent process unexpectedly quits. + /// This feature requires Windows 8 or greater. On Windows 7, nothing is done. + /// References: + /// https://stackoverflow.com/a/4657392/386091 + /// https://stackoverflow.com/a/9164742/386091 + public static class ChildProcessTracker + { + /// + /// Add the process to be tracked. If our current process is killed, the child processes + /// that we are tracking will be automatically killed, too. If the child process terminates + /// first, that's fine, too. + /// + public static void AddProcess(Process process) + { + if (s_jobHandle != IntPtr.Zero) + { + bool success = AssignProcessToJobObject(s_jobHandle, process.Handle); + if (!success && !process.HasExited) + throw new Win32Exception(); + } + } + + static ChildProcessTracker() + { + // This feature requires Windows 8 or later. To support Windows 7 requires + // registry settings to be added if you are using Visual Studio plus an + // app.manifest change. + // https://stackoverflow.com/a/4232259/386091 + // https://stackoverflow.com/a/9507862/386091 + if (Environment.OSVersion.Version < new Version(6, 2)) + return; + + // The job name is optional (and can be null) but it helps with diagnostics. + // If it's not null, it has to be unique. Use SysInternals' Handle command-line + // utility: handle -a ChildProcessTracker + string jobName = "ChildProcessTracker" + Process.GetCurrentProcess().Id; + s_jobHandle = CreateJobObject(IntPtr.Zero, jobName); + + var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION(); + + // This is the key flag. When our process is killed, Windows will automatically + // close the job handle, and when that happens, we want the child processes to + // be killed, too. + info.LimitFlags = JOBOBJECTLIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE; + + var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION(); + extendedInfo.BasicLimitInformation = info; + + int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); + IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length); + try + { + Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false); + + if (!SetInformationJobObject(s_jobHandle, JobObjectInfoType.ExtendedLimitInformation, + extendedInfoPtr, (uint)length)) + { + throw new Win32Exception(); + } + } + finally + { + Marshal.FreeHGlobal(extendedInfoPtr); + } + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string name); + + [DllImport("kernel32.dll")] + static extern bool SetInformationJobObject(IntPtr job, JobObjectInfoType infoType, + IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength); + + [DllImport("kernel32.dll", SetLastError = true)] + static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process); + + // Windows will automatically close any open job handles when our process terminates. + // This can be verified by using SysInternals' Handle utility. When the job handle + // is closed, the child processes will be killed. + private static readonly IntPtr s_jobHandle; + } + + public enum JobObjectInfoType + { + AssociateCompletionPortInformation = 7, + BasicLimitInformation = 2, + BasicUIRestrictions = 4, + EndOfJobTimeInformation = 6, + ExtendedLimitInformation = 9, + SecurityLimitInformation = 5, + GroupInformation = 11 + } + + [StructLayout(LayoutKind.Sequential)] + public struct JOBOBJECT_BASIC_LIMIT_INFORMATION + { + public Int64 PerProcessUserTimeLimit; + public Int64 PerJobUserTimeLimit; + public JOBOBJECTLIMIT LimitFlags; + public UIntPtr MinimumWorkingSetSize; + public UIntPtr MaximumWorkingSetSize; + public UInt32 ActiveProcessLimit; + public Int64 Affinity; + public UInt32 PriorityClass; + public UInt32 SchedulingClass; + } + + [Flags] + public enum JOBOBJECTLIMIT : uint + { + JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000 + } + + [StructLayout(LayoutKind.Sequential)] + public struct IO_COUNTERS + { + public UInt64 ReadOperationCount; + public UInt64 WriteOperationCount; + public UInt64 OtherOperationCount; + public UInt64 ReadTransferCount; + public UInt64 WriteTransferCount; + public UInt64 OtherTransferCount; + } + + [StructLayout(LayoutKind.Sequential)] + public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION + { + public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation; + public IO_COUNTERS IoInfo; + public UIntPtr ProcessMemoryLimit; + public UIntPtr JobMemoryLimit; + public UIntPtr PeakProcessMemoryUsed; + public UIntPtr PeakJobMemoryUsed; + } public static class Native { public const int MB_OK = (int)0x00000000L; @@ -311,9 +448,12 @@ void StartAnotherProgram(string programPath, string[] programArgs) } process.StartInfo = startInfo; process.Start(); + // Add the Process to ChildProcessTracker. + ChildProcessTracker.AddProcess(process); process.WaitForInputIdle(10000); + process.WaitForExit(); process.Close(); } diff --git a/VPinballX.starter/VPinballX.starter.csproj b/VPinballX.starter/VPinballX.starter.csproj index ca4884c..75b689f 100644 --- a/VPinballX.starter/VPinballX.starter.csproj +++ b/VPinballX.starter/VPinballX.starter.csproj @@ -9,14 +9,14 @@ VPinballX.starter.ico VPinballX version starter using an inifile VPinballX.starter - 1.5 - 1.5 - 1.5 + 1.6 + 1.6 + 1.6 ©COPYRIGHTYEAR Richard Ludwig VPinballX.exe version starter using an inifile false false - Debug;Release;Release and Deploy + Debug;Release VPinballX.starter.App https://github.com/JockeJarre/VPinballX.starter @@ -25,10 +25,6 @@ False - - True - -