My first experience with the .NET Framework was co-writing a custom build system in C# a few years ago. The build system ran as an add-in for Visual Studio 2003 and 2005. It enabled the developer to port a single code/content base to any number of Java ME and Brew handsets.

The core build system was pretty straight forward, but we ran in to some problems with the .NET Process object. Each step of the build system was executed as either an internal command, or an external process. The external processes were kicked off using the .NET Process object. These external processes could kick off a single executable file. Or, they could run a batch file, potentially spawning several child processes.

Under normal operation this was completely fine. However, canceling a build half way through exposed some limitations of the .NET Process object.

It turns out that Win32 doesn't maintain a parent/child hierarchy of processes. Killing the parent process wont automatically kill the child processes. So, if you create a .NET Process object, and that object creates a child process, killing the parent process just orphans the child process.

Process Groups

Visual Studio 2003 ships with a command-line tool called VCSPAWN.EXE. This tool was designed to solve the above problem for console applications. It works by exploiting the fact that all console processes run in a console group. When a message is sent to the group, all processes in the group receive the message. Under the hood VCSPAWN.EXE ensures that each process it spawns belongs to the same group. Terminate VCSPAWN.EXE and you terminate any processes it has spawned.

This is a good solution but VCSPAWN.EXE does not ship with Visual Studio 2005 or 2008.  Also, it does not handle GUI applications as far as I know.

Job Objects

Windows 2000 (and up) provide Job Kernel Objects. Job Kernel Objects offer a much better solution to the above problem, allowing you to group several processes together. Terminate the Job Object and all processes assigned to the Job Object are terminated also. This works for child processes because child processes are automatically assigned to the same job as the parent process.

There's one problem. The .NET Framework does not expose Job Kernel Objects as standard so we have to use Interoperation (specifically P/Invoke) to use this functionality. Below is some code that shows how to use a Job Object to run a .NET Process object. The idea is simple. Create your process in a suspended state. Assign it to a Job Object. Then, to terminate the process and all child processes, just terminate the Job Object.

class JobObjectProcess
{
    protected Process _process;
    protected IntPtr _jobHandle;
 
    public JobObjectProcess(ProcessStartInfo startInfo)
    {
        _process = new Process();
        _process.StartInfo = startInfo;
 
        CreateJobObject();
    }
 
    private void CreateJobObject()
    {
        win32.SECURITY_ATTRIBUTES SA = new win32.SECURITY_ATTRIBUTES();
        SA.nLength = 12;
        SA.SecurityDescriptor = IntPtr.Zero;
        SA.bInheritHandle = true;
        _jobHandle = win32.CreateJobObject(ref SA, "");
    }
 
    public bool Start()
    {
        bool started = _process.Start();
 
        if (started)
        {
            bool success = win32.AssignProcessToJobObject(_jobHandle, _process.Handle);
            if (!success)
            {
                string error = new Win32Exception(Marshal.GetLastWin32Error()).Message;
                throw new ApplicationException(error);
            }
        }
 
        return started;
    }
 
    public void Kill()
    {
        bool success = win32.TerminateJobObject(_jobHandle, 0);
        if (!success)
        {
            string error = new Win32Exception(Marshal.GetLastWin32Error()).Message;
            throw new ApplicationException(error);
        }
 
        if (_jobHandle != IntPtr.Zero)
        {
            if (0 == win32.CloseHandle(_jobHandle))
            {
                string error = new Win32Exception(Marshal.GetLastWin32Error()).Message;
                throw new ApplicationException(error);
            }
            _jobHandle = IntPtr.Zero;
        }
    }
}

There's a problem with the code above. It starts the .NET Process object before it assigns it to the Job Object. For processes that don't exit quickly this is fine. But for processes that terminate quickly we could still end up with orphaned child processes. So why not just start the process suspended like I mentioned earlier? Well, the .NET Framework Process object doesn't expose a way to do this.

The only way around this is to use interoperation to call CreateProcess to create a process in a suspended state. The good news is this works fine. On Windows XP.

Vista

It doesn't work on Windows Vista due to the enhanced security features. Luckily, there is also a way around this. In order to get this to work on Vista I had to replace the CreateProcess call with a call to CreateProcessAsUser. This call will create a process under the security token that is specified as a parameter. This token must be a primary token. I used the DuplicateTokenEx API method to convert the token of the current user to a primary token. The process is then created in a suspended state and assigned to a Job Object. Finally the main process thread is resumed. For more information on how CreateProcessAsUser is being used in this context, see this article.

If you want to know more about Job Kernel Objects, Jeffrey Richter has written several articles on this subject. You can find out more here and here.

Below is the C# source code for the custom JobObjectProcess object. Calling Kill on the JobObjectProcess will terminate the parent process and any child processes it has spawned.

[Update: For a much more complete solution that uses Job Objects in managed code, check the JobObjectWrapper up on CodePlex.]

class JobObjectProcess
{
    private IntPtr _jobHandle;
 
    public JobObjectProcess()
    {
    }
 
    public bool Start(string commandLinePath)
    {
        IntPtr Token = new IntPtr(0);
        IntPtr DupedToken = new IntPtr(0);
        bool ret;
 
        SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
        sa.bInheritHandle = false;
        sa.Length = Marshal.SizeOf(sa);
        sa.lpSecurityDescriptor = (IntPtr)0;
 
        Token = WindowsIdentity.GetCurrent().Token;
        const uint GENERIC_ALL = 0x10000000;
        const int SecurityImpersonation = 2;
        const int TokenType = 1;
        ret = DuplicateTokenEx(Token, GENERIC_ALL, ref sa, SecurityImpersonation, TokenType, ref DupedToken);
 
        STARTUPINFO si = new STARTUPINFO();
        si.cb = Marshal.SizeOf(si);
        si.lpDesktop = "";
 
        PROCESS_INFORMATION pi = new PROCESS_INFORMATION();
        const int CREATE_SUSPENDED = 0x00000004;
        ret = CreateProcessAsUser(DupedToken, null, commandLinePath, ref sa, ref sa, false, CREATE_SUSPENDED, (IntPtr)0, "c:/", ref si, out pi);
 
        SECURITY_ATTRIBUTES saJob = new SECURITY_ATTRIBUTES();
        saJob.Length = 12;
        saJob.lpSecurityDescriptor = IntPtr.Zero;
        saJob.bInheritHandle = true;
        _jobHandle = CreateJobObject(ref saJob, "");
 
        bool success = AssignProcessToJobObject(_jobHandle, pi.hProcess);
        int nSuspendCount = ResumeThread(pi.hThread);
 
        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
        CloseHandle(DupedToken);
 
        return true;
    }
 
    public bool Kill()
    {
        bool success = TerminateJobObject(_jobHandle, 0);
        CloseHandle(_jobHandle);
        return success;
    }
 
    [StructLayout(LayoutKind.Sequential)]
    public struct STARTUPINFO
    {
        public int cb;
        public String lpReserved;
        public String lpDesktop;
        public String lpTitle;
        public uint dwX;
        public uint dwY;
        public uint dwXSize;
        public uint dwYSize;
        public uint dwXCountChars;
        public uint dwYCountChars;
        public uint dwFillAttribute;
        public uint dwFlags;
        public short wShowWindow;
        public short cbReserved2;
        public IntPtr lpReserved2;
        public IntPtr hStdInput;
        public IntPtr hStdOutput;
        public IntPtr hStdError;
    }
 
    [StructLayout(LayoutKind.Sequential)]
    public struct PROCESS_INFORMATION
    {
        public IntPtr hProcess;
        public IntPtr hThread;
        public uint dwProcessId;
        public uint dwThreadId;
    }
 
    [StructLayout(LayoutKind.Sequential)]
    public struct SECURITY_ATTRIBUTES
    {
        public int Length;
        public IntPtr lpSecurityDescriptor;
        public bool bInheritHandle;
    }
 
    [DllImport("kernel32.dll", EntryPoint = "CloseHandle", SetLastError = true, CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
    public extern static bool CloseHandle(IntPtr handle);
 
    [DllImport("advapi32.dll", EntryPoint = "CreateProcessAsUser", SetLastError = true, CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)]
    public extern static bool CreateProcessAsUser(IntPtr hToken, String lpApplicationName, String lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes,
        ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandle, int dwCreationFlags, IntPtr lpEnvironment,
        String lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);
 
    [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")]
    public extern static bool DuplicateTokenEx(IntPtr ExistingTokenHandle, uint dwDesiredAccess,
        ref SECURITY_ATTRIBUTES lpThreadAttributes, int TokenType,
        int ImpersonationLevel, ref IntPtr DuplicateTokenHandle);
 
    [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
    public static extern IntPtr CreateJobObject(ref SECURITY_ATTRIBUTES JobAttributes,
        [MarshalAs(UnmanagedType.LPTStr)]string strName);
 
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess);
 
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern bool TerminateJobObject(IntPtr hJob, uint exitCode);
 
    [DllImport("kernel32.dll", SetLastError = true)]
    public static extern int ResumeThread(IntPtr hThread);
}

Comments

6/24/2008 9:20:21 PM

Alon Fliess

We developed an open source project that wrap the Win32 Job Object using C++/CLI and gives all Job Object capabilities to managed code.
http://www.codeplex.com/JobObjectWrapper

Alon.

Alon Fliess us

6/24/2008 10:32:16 PM

Aidan Doolan

Nice work Alon. I briefly checked it out just now. It looks great.

Aidan Doolan ie

7/10/2009 8:22:32 PM

pingback

Pingback from blog.yoot.be

Developing Multi-Process .Net applications | YOOT

blog.yoot.be