1

I'm trying to make a little program that'll run a bat command on a background thread - that's working but I'm trying to implement a timeout "safety". Ie if the background command hangs it'll be terminated after a certain amount of time. There's no problem running the code... I just can't terminate the process once it's started. I've ended up butchering my code into this test program:

public void ExecutePostProcess(string cmd)
{
    //...
    WriteToDebugTextBox("Executing Threaded Post Process '" + cmd + "'");
    //WriteToDebugTextBox() simply writes to a TextBox across threads with a timestamp
    var t = new Thread(delegate()
    {
        var processInfo = new ProcessStartInfo("cmd.exe", "/c " + cmd);
        processInfo.CreateNoWindow = true;
        processInfo.UseShellExecute = false;
        processInfo.RedirectStandardError = true;
        processInfo.RedirectStandardOutput = true;

        var process = Process.Start(processInfo);
        process.OutputDataReceived += (object sender, DataReceivedEventArgs e) => WriteToDebugTextBox(e.Data);
        process.BeginOutputReadLine();
        process.WaitForExit(3000);
        process.Close();
        //process.Kill();
        //process.CloseMainWindow(); 
        WriteToDebugTextBox("Finished Post Process");
    });
    t.Start();
    //...
}

At the moment I have it running this console "TestApp" which looks like this:

class Program
{
    static void Main(string[] args)
    {
        int hangTime = int.Parse(args[0]);

        Console.WriteLine("This is the TestApp");
        Console.WriteLine("TestApp is going to have a little 'sleep' for {0} seconds", hangTime);
        Thread.Sleep(hangTime * 1000);
        Console.WriteLine("Test App has woken up!");
    }
}

which I'm giving a 10 second hang time. The process is given a timeout; process.WaitForExit(3000); so that should give up and terminate after 3 seconds. However the output to my TextBox is always this:

16:09:22.175 Executing Threaded Post Process 'test.bat'

16:09:22.257 This is the TestApp

16:09:22.261 TestApp is going to have a little 'sleep' for 10 seconds

16:09:25.191 Finished Post Process

16:09:32.257 Test App has woken up!

I've tried numerous answers from all over the place but to no avail. How do I properly kill off the process?

Hugo Yates
  • 2,081
  • 2
  • 26
  • 24
  • You are killing Cmd.exe, not whatever processes it started. It isn't very clear why you are using Cmd.exe at all, it is better when you don't of course. Finding back the correct processes would require [interrogating their parent](http://stackoverflow.com/a/2533287/17034). – Hans Passant Feb 29 '16 at 16:49
  • The program it's intended for is meant to fire off some complex batch commands. I just tried bypassing cmd.exe; `var processInfo = new ProcessStartInfo(@"C:\PathTo\TestApp.exe", "10");` but sadly still getting to the point where "Test App has woken up!" . – Hugo Yates Feb 29 '16 at 17:04
  • Post the actual code you tested, right now we are just guessing at it. You cannot call Kill() after calling Close(). – Hans Passant Feb 29 '16 at 17:09
  • You are killing the cmd.exe but not your target process. In order to kill your target app you need to know its Process ID. If it's a complex batch script - maybe it's time to revise it and make a proper C# app instead (or PowerShell). – Serge Semenov Feb 29 '16 at 17:17
  • Sadly this is the actual code. TestApp compiles into TestApp.exe and the main method is in a WinForms hinged off a button click event. The batch file it's calling is simply to `TestApp.Exe 10` – Hugo Yates Feb 29 '16 at 17:21
  • @SergeSemenov thanks, you helped me get the right answer :) – Hugo Yates Mar 01 '16 at 12:42
  • np @HugoYates, if you are looking for a process ID of the TestApp, make sure that your cmd.exe started it, otherwise you can kill wrong instance of the TestApp – Serge Semenov Mar 01 '16 at 17:03

4 Answers4

2

Start your process in an NT Job Object. Set the JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE job limitation on this job. Start your process and assign it to this job (normally you create the process suspended, assign it to the job, then resume the process, to prevent it from escaping the job context before is added). When the time is up, kill the job. This way you kill not only the process, but also any child process spawned by your process which is the big missing part on your current attempt. As there is no managed NT Jobs API, have a look at Working example of CreateJobObject/SetInformationJobObject pinvoke in .net?

You can even experiment with setting JOB_OBJECT_LIMIT_JOB_TIME to make sure the process does not runaway CPU/resources before you kill it.

Community
  • 1
  • 1
Remus Rusanu
  • 288,378
  • 40
  • 442
  • 569
  • Thanks this has put me on the right track - though I didn't go with this in the end – Hugo Yates Mar 01 '16 at 12:09
  • 1
    Job objects are an underutilized feature. A terrific OS capability. Was about to give the same answer. – usr Mar 01 '16 at 12:57
1

Edit: While this answer does solve the problem it's not the ideal solution

Ok, hate to answer my own question but I find it worse to leave no answer when a solution was found so here it is:

Serge was right, the cmd process was the process being watched/killed not the child process. For some odd reason I got it in my head that terminating the parent would subsequently terminate any sub-processes.

So, I found this answer on how to get the child processes and built the extension class:

//requires you add a reference to System.Management + using System.Diagnostics and System.Management
public static class ProcessExtensions
{
    public static IEnumerable<Process> GetChildProcesses(this Process process)
    {
        List<Process> children = new List<Process>();
        ManagementObjectSearcher mos = new ManagementObjectSearcher(String.Format("Select * From Win32_Process Where ParentProcessID={0}", process.Id));

        foreach (ManagementObject mo in mos.Get())
        {
            children.Add(Process.GetProcessById(Convert.ToInt32(mo["ProcessID"])));
        }

        return children;
    }
}

I built this recursive method:

private void KillChildProcesses(Process process)
{
    foreach(Process childProcess in process.GetChildProcesses())
    {
        KillChildProcesses(childProcess);
        WriteToDebugTextBox("killed process: " + childProcess.ProcessName);
        childProcess.Kill();
    }
}

And messed around with my main function

var t = new Thread(delegate()
{

    try
    {
        var processInfo = new ProcessStartInfo("cmd.exe", "/c " + cmd);

        processInfo.CreateNoWindow = true;
        processInfo.UseShellExecute = false;
        processInfo.RedirectStandardError = true;
        processInfo.RedirectStandardOutput = true;
        using (Process process = Process.Start(processInfo))
        {
            process.EnableRaisingEvents = true;
            process.OutputDataReceived += (object sender, DataReceivedEventArgs e) => WriteToDebugTextBox(e.Data);
            process.BeginOutputReadLine();
            process.WaitForExit(3000);
            KillChildProcesses(process);
        }
    }
    catch(Exception ex)
    {
        WriteToDebugTextBox(ex.Message);
    }
    WriteToDebugTextBox("Finished Post Process");
});
t.Start();

Now once the WaitForExit timeout is reached any child processes created under cmd are killed - which hopefully won't come to that.

12:17:43.005 Executing Threaded Post Process 'test.bat'

12:17:43.088 This is the TestApp

12:17:43.093 TestApp is going to have a little 'sleep' for 10 seconds

12:17:46.127 killed process: TestApp

12:17:46.133 Finished Post Process

Concept proven, I can now move into making this into a proper application.

Community
  • 1
  • 1
Hugo Yates
  • 2,081
  • 2
  • 26
  • 24
  • Compared to Remus' answer this does not kill childrens children. Launcher executables are a common case that can happen. – usr Mar 01 '16 at 14:44
  • @usr I've been going over this and yes it does kill off any childrens children. I modified my TestApp to call itself again after a 1 second delay via cmd. As soon as the WaitForExit timeout is reached the method kills off all child elements. – Hugo Yates Mar 01 '16 at 16:02
  • 2
    OK, I didn't spot the recursive call. This only works as long as there is a parent chain connecting all children. If A launches B which launches C, and B exits immediately this will not kill C. (You might not care about this, but it highlights how fragile this is and why the Job method is a better way to do it by principle.) – usr Mar 01 '16 at 16:07
  • Yup, I think you're right. I'm extending my testing. At the moment it meets requirements as the processes called are only one level deep however we all know requirements can change. – Hugo Yates Mar 01 '16 at 16:56
1

Ok so I believe I've gotten to the ideal solution, many thanks to Remus and Usr. I'm leaving my previous solution up as it's fairly viable for small operations.

The biggest problem is when the lineage chain is broken, terminating all child processes becomes quite difficult. Ie A creates B, B creates C but then B ends - A looses any scope over C.

For my testing I modified my TestApp into something rather horrible, a self-spawning nightmare with a self-terminating flipflop. The nasty code for it is at the bottom of this answer, which I suggest anyone only looks at for reference only.

The only answer it seems to govern this nightmare is via Job Objects. I used the class from this answer (credit to Alexander Yezutov -> Matt Howells -> 'Josh') but had to modify it slight to work (therefore I'm posting it's code). I added this class to my project:

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)]
        static extern bool CloseHandle(IntPtr hObject);

        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
            };

            int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
            IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length);
            Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);

            if (!SetInformationJobObject(_handle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length))
            {
                throw new Exception(string.Format("Unable to set information.  Error: {0}", Marshal.GetLastWin32Error()));
            }
        }

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        private void Dispose(bool disposing)
        {
            if (_disposed)
            {
                return;
            }

            if (disposing) { }

            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)
        {
            return AddProcess(Process.GetProcessById(processId).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

}

Changed my main method's content to look like this:

var t = new Thread(delegate()
{
    try
    {
        using (var jobHandler = new Job())
        {
            var processInfo = new ProcessStartInfo("cmd.exe", "/c " + cmd);
            processInfo.CreateNoWindow = true;
            processInfo.UseShellExecute = false;
            processInfo.RedirectStandardError = true;
            processInfo.RedirectStandardOutput = true;
            using (Process process = Process.Start(processInfo))
            {
                DateTime started = process.StartTime;
                jobHandler.AddProcess(process.Id); //add the PID to the Job object
                process.EnableRaisingEvents = true;
                process.OutputDataReceived += (object sender, DataReceivedEventArgs e) => WriteToDebugTextBox(e.Data);
                process.BeginOutputReadLine();
                process.WaitForExit(_postProcessesTimeOut * 1000);

                TimeSpan tpt = (DateTime.Now - started);
                if (Math.Abs(tpt.TotalMilliseconds) > (_postProcessesTimeOut * 1000))
                {
                    WriteToDebugTextBox("Timeout reached, terminating all child processes"); //jobHandler.Close() will do this, just log that the timeout was reached
                }

            }
            jobHandler.Close(); //this will terminate all spawned processes 
        }
    }
    catch (Exception ex)
    {
        WriteToDebugTextBox("ERROR:" + ex.Message);
    }
    WriteToDebugTextBox("Finished Post Process");
});
t.Start();

Feedback through the method looks like this (note: it looses scope part way through but TestApp continues to run and propagate):

13:06:31.055 Executing Threaded Post Process 'test.bat'

13:06:31.214 24976 TestApp started

13:06:31.226 24976 Now going to make a horrible mess by calling myself in 1 second...

13:06:32.213 24976 Creating Child Process cmd 'TestApp.exe'

13:06:32.229 24976 Finished Child-Threaded Process

13:06:32.285 24976 TestApp is going to have a little 'sleep' for 10 seconds

13:06:32.336 24976 Created New Process 26936

13:06:32.454 20344 TestApp started

13:06:32.500 20344 Now going to make a horrible mess by calling myself in 1 second...

13:06:32.512 20344 !! I will self terminate after creating a child process to break the lineage chain

13:06:33.521 20344 Creating Child Process cmd 'TestApp.exe'

13:06:33.540 20344 Finished Child-Threaded Process

13:06:33.599 20344 Created New Process 24124

13:06:33.707 19848 TestApp started

13:06:33.759 19848 Now going to make a horrible mess by calling myself in 1 second...

13:06:34.540 20344 !! Topping myself! PID 20344 Scope lost after here

13:06:41.139 Timeout reached, terminating all child processes

Note the PIDs vary because TestApp isn't being directly called, it's being passed through CMD - I was going for extreme here ;)

Here's TestApp, I strongly suggest this is used for reference only as it will run amok creating new instances of itself (it does have a 'kill' argument for clean up if anyone runs it!).

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;

namespace TestApp
{
    class Program
    {
        /// <summary>
        /// TestApp.exe [hangtime(int)] [self-terminate(bool)]
        /// TestApp.exe kill --terminate all processes called TestApp
        /// </summary>
        /// <param name="args"></param>

        static void Main(string[] args)
        {
            int hangTime = 5; //5 second default
            bool selfTerminate = true;
            Process thisProcess = Process.GetCurrentProcess();
            if (args.Length > 0)
            {
                if (args[0] == "kill")
                {
                    KillAllTestApps(thisProcess);
                    return;
                }
                hangTime = int.Parse(args[0]);
                if (args.Length > 1)
                {
                    selfTerminate = bool.Parse(args[1]);
                }
            }

            Console.WriteLine("{0} TestApp started", thisProcess.Id);
            Console.WriteLine("{0} Now going to make a horrible mess by calling myself in 1 second...", thisProcess.Id);
            if (selfTerminate)
            {
                Console.WriteLine("{0} !! I will self terminate after creating a child process to break the lineage chain", thisProcess.Id);
            }
            Thread.Sleep(1000);
            ExecutePostProcess("TestApp.exe", thisProcess, selfTerminate);
            if (selfTerminate)
            {
                Thread.Sleep(1000);
                Console.WriteLine("{0} !! Topping myself! PID {0}", thisProcess.Id);
                thisProcess.Kill();
            }
            Console.WriteLine("{0} TestApp is going to have a little 'sleep' for {1} seconds", thisProcess.Id, hangTime);
            Thread.Sleep(hangTime * 1000);
            Console.WriteLine("{0} Test App has woken up!", thisProcess.Id);
        }


        public static void ExecutePostProcess(string cmd, Process thisProcess, bool selfTerminate)
        {
            Console.WriteLine("{0} Creating Child Process cmd '{1}'", thisProcess.Id, cmd);
            var t = new Thread(delegate()
            {
                try
                {
                    var processInfo = new ProcessStartInfo("cmd.exe", "/c " + cmd + " 10 " + (selfTerminate ? "false" : "true" ));
                    processInfo.CreateNoWindow = true;
                    processInfo.UseShellExecute = false;
                    processInfo.RedirectStandardError = true;
                    processInfo.RedirectStandardOutput = true;
                    using (Process process = Process.Start(processInfo))
                    {
                        Console.WriteLine("{0} Created New Process {1}", thisProcess.Id, process.Id);
                        process.EnableRaisingEvents = true;
                        process.OutputDataReceived += (object sender, DataReceivedEventArgs e) => Console.WriteLine(e.Data);
                        process.BeginOutputReadLine();

                    }
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
            });
            t.Start();
            Console.WriteLine("{0} Finished Child-Threaded Process", thisProcess.Id);
        }

        /// <summary>
        /// kill all TestApp processes regardless of parent
        /// </summary>
        private static void KillAllTestApps(Process thisProcess)
        {
            Process[] processes = Process.GetProcessesByName("TestApp");
            foreach(Process p in processes)
            {
                if (thisProcess.Id != p.Id)
                {
                    Console.WriteLine("Killing {0}:{1}", p.ProcessName, p.Id);
                    p.Kill();
                }
            }
        }
    }
}
Community
  • 1
  • 1
Hugo Yates
  • 2,081
  • 2
  • 26
  • 24
  • This is the right track, but your child process has plenty of time to escape the job between create and add to job. This may not even be on purpose, it can be as simple as cmd.exe actually launching the child process *before* is add to the job. This where [`CREATE_SUSPENDED`](https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863(v=vs.85).aspx) comes into picture. Not sure if `Process.Create` can do this, but you're already committed to pInvoke... http://stackoverflow.com/a/22570615/105929 – Remus Rusanu Mar 02 '16 at 18:03
  • BTW, Linux world doesn't have to deal with all this because [process groups](https://en.wikipedia.org/wiki/Process_group) can be killed. – Remus Rusanu Mar 02 '16 at 18:13
0

I believe you need to add an EventHandler for the Exit Event. Otherwise "WaitForExit()" will likely never fire. Process.OnExited Method ()

 processObject.Exited += new EventHandler(myProcess_HasExited);

Once you have an event Handler, you should be able to do a simple loop for (x) amount of time, at the end of which you simply send a close command to the process.

    myProcess.CloseMainWindow();
    // Free resources associated with process.
    myProcess.Close();

If the spawned process does end up hanging up, I don't think there is much you can do about it honestly, because it won't be capable of receiving commands, or executing them. (So sending a close won't work, nor will it fire back event handler confirmations)

Jscix
  • 86
  • 1
  • 6