Monday, June 22, 2015

How To: Kill child process when parent process is killed

UPDATE (2/15/2016): There is now a v2 of this class!

Killing child all child process spawned by a parent process is an extremely useful trick that is not directly supported by the .NET framework. Fortunately the windows operating system, more specifically Kernel32.dll, does support the ability to link one process to another on shutdown. A huge thanks to Matt Howell for sharing this solution on Stack Overflow!

I took the liberty of cleaning up a few small things in the code and creating a demo:

Sample Program

using System;
using System.Diagnostics;
using JobManagement;
 
namespace ChildProcessDemo
{
    public static class Program
    {
        public static void Main(string[] args)
        {
            var isChild = args.Length == 1 && Boolean.Parse(args[0]);
 
            if (isChild)
                Child();
            else
                Parent();
        }
 
        private static void Parent()
        {
            Console.WriteLine("PARENT PROCESS");
            Console.WriteLine("Launching three child processes.");
 
            for (var i = 1; i <= 3; i++)
            {
                var process = new Process
                {
                    StartInfo = new ProcessStartInfo(
                        "ChildProcessDemo.exe", 
                        "true")
                };
                process.Start();
 
                var job = new Job();
                job.AddProcess(process.Handle);
 
                Console.WriteLine("Launched child process " + i);
            }
 
            Console.WriteLine("Press enter to close.");
            Console.ReadLine();
        }
 
        private static void Child()
        {
            Console.WriteLine("CHILD PROCESS");
            Console.WriteLine("Press enter to close.");
            Console.ReadLine();
        }
    }
}

Complete Job Class

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
 
namespace JobManagement
{
    public class Job : IDisposable
    {
        [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
        static extern IntPtr CreateJobObject(IntPtr a, string lpName);
 
        [DllImport("kernel32.dll")]
        static extern bool SetInformationJobObject(
            IntPtr hJob, 
            JobObjectInfoType infoType, 
            IntPtr lpJobObjectInfo, 
            UInt32 cbJobObjectInfoLength);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        static extern bool AssignProcessToJobObject(
            IntPtr job, 
            IntPtr process);
 
        [DllImport("kernel32.dll", SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        public static extern bool CloseHandle(IntPtr handle);
 
        private IntPtr _handle;
        private bool _disposed;
 
        public Job()
        {
            _handle = CreateJobObject(IntPtr.Zero, null);
 
            var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION
            {
                LimitFlags = 0x2000
            };
 
            var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
            {
                BasicLimitInformation = info
            };
 
            var infoType = typeof (JOBOBJECT_EXTENDED_LIMIT_INFORMATION);
            var length = Marshal.SizeOf(infoType);
            var extendedInfoPtr = Marshal.AllocHGlobal(length);
            Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);
 
            var setResult = SetInformationJobObject(
                _handle,
                JobObjectInfoType.ExtendedLimitInformation,
                extendedInfoPtr,
                (uint) length);
 
            if (setResult)
                return;
 
            var lastError = Marshal.GetLastWin32Error();
            var message = "Unable to set information. Error: " + lastError;
            throw new Exception(message);
        }
 
        ~Job()
        {
            Dispose(false);
        }
 
        public void Dispose()
        {
            Dispose(true);
        }
 
        private void Dispose(bool disposing)
        {
            if (_disposed)
                return;
 
            if (disposing)
                GC.SuppressFinalize(this);
 
            Close();
 
            _disposed = true;
        }
 
        public void Close()
        {
            CloseHandle(_handle);
            _handle = IntPtr.Zero;
        }
 
        public bool AddProcess(IntPtr processHandle)
        {
            return AssignProcessToJobObject(_handle, processHandle);
        }
 
        public bool AddProcess(int processId)
        {
            var process = Process.GetProcessById(processId);
            return AddProcess(process.Handle);
        }
 
    }
 
    #region Helper classes
 
    [StructLayout(LayoutKind.Sequential)]
    struct IO_COUNTERS
    {
        public UInt64 ReadOperationCount;
        public UInt64 WriteOperationCount;
        public UInt64 OtherOperationCount;
        public UInt64 ReadTransferCount;
        public UInt64 WriteTransferCount;
        public UInt64 OtherTransferCount;
    }
 
 
    [StructLayout(LayoutKind.Sequential)]
    struct JOBOBJECT_BASIC_LIMIT_INFORMATION
    {
        public Int64 PerProcessUserTimeLimit;
        public Int64 PerJobUserTimeLimit;
        public UInt32 LimitFlags;
        public UIntPtr MinimumWorkingSetSize;
        public UIntPtr MaximumWorkingSetSize;
        public UInt32 ActiveProcessLimit;
        public UIntPtr Affinity;
        public UInt32 PriorityClass;
        public UInt32 SchedulingClass;
    }
 
    [StructLayout(LayoutKind.Sequential)]
    public struct SECURITY_ATTRIBUTES
    {
        public UInt32 nLength;
        public IntPtr lpSecurityDescriptor;
        public Int32 bInheritHandle;
    }
 
    [StructLayout(LayoutKind.Sequential)]
    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 enum JobObjectInfoType
    {
        AssociateCompletionPortInformation = 7,
        BasicLimitInformation = 2,
        BasicUIRestrictions = 4,
        EndOfJobTimeInformation = 6,
        ExtendedLimitInformation = 9,
        SecurityLimitInformation = 5,
        GroupInformation = 11
    }
 
    #endregion
}

Enjoy,
Tom

2 comments:

  1. Now I haven't done too much with unmanaged code, but I think this implementation may have a minor memory leak. Please correct me if I am wrong though.

    I am pretty sure that you have to call FreeHGlobal on something that you use AllocHGlobal on. So in this case, the struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION may be leaking since you're never freeing that memory. It's minor since I'm pretty sure that you'll only have one instance of job running around.


    Some misc things to consider as well:
    1. Use SafeHandles (http://www.codeproject.com/Articles/29534/IDisposable-What-Your-Mother-Never-Told-You-About)
    2. Seal the class since your disposable method can't be overridden
    3. Move GC.SuppressFinalize(this); into the public void Dispose() method after the Dispose(true) call (https://msdn.microsoft.com/en-us/library/ms182269.aspx) - This is less of a problem right now since Dispose isn't virtual (https://msdn.microsoft.com/en-us/library/ms244737.aspx)
    4. Move your P/Invokes to a NativeMethods class (https://msdn.microsoft.com/en-us/library/ms182161.aspx)

    ReplyDelete
  2. This comment has been removed by the author.

    ReplyDelete

Real Time Web Analytics