使用超时终止进程和任何子进程

Terminating a Process and any Child Processes with a Timeout

我正在尝试制作一个小程序,该程序将 运行 在后台线程上执行 bat 命令 - 这可行,但我正在尝试实现超时 "safety"。也就是说,如果后台命令挂起,它会在一定时间后终止。 运行 代码没有问题...我只是无法在进程启动后终止它。我最终将我的代码添加到这个测试程序中:

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();
    //...
}

目前我运行在这个控制台"TestApp"上有它,它看起来像这样:

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!");
    }
}

我给了 10 秒的挂起时间。进程超时; process.WaitForExit(3000); 这样应该会在 3 秒后放弃并终止。但是,我的 TextBox 的输出始终是这样的:

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!

我试过来自各地的无数答案,但无济于事。我如何正确终止进程?

我认为您需要为退出事件添加一个事件处理程序。否则 "WaitForExit()" 可能永远不会触发。 Process.OnExited Method ()

 processObject.Exited += new EventHandler(myProcess_HasExited);

有了事件处理程序后,您应该能够执行 (x) 时间量的简单循环,最后您只需向进程发送一个关闭命令。

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

如果生成的进程最终挂起,老实说我认为您对此无能为力,因为它无法接收或执行命令。 (因此发送关闭将不起作用,也不会触发事件处理程序确认)

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?

中开始您的流程

您甚至可以尝试设置 JOB_OBJECT_LIMIT_JOB_TIME 以确保进程在终止之前不会失控 CPU/resources。

编辑:虽然这个答案确实解决了问题,但它不是理想的解决方案

好吧,我不想回答我自己的问题,但我发现在找到解决方案时不留下答案更糟糕,所以这里是:

Serge 是对的,cmd 进程是 watched/killed 而不是子进程。出于某种奇怪的原因,我想到终止父进程将随后终止所有子进程。

所以,我发现 this answer 如何获取子进程并构建扩展 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;
    }
}

我构建了这个递归方法:

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

并且弄乱了我的主要功能

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();

现在,一旦达到 WaitForExit 超时,所有在 cmd 下创建的子进程都会被终止 - 希望不会发生这种情况。

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

概念得到验证,我现在可以开始将其制作成合适的应用程序。

好的,我相信我已经找到了理想的解决方案,非常感谢 Remus 和 Usr。我将保留我以前的解决方案,因为它对于小型操作来说相当可行。

最大的问题是当沿袭链断裂时,终止所有子进程变得相当困难。即 A 创建 B, B 创建 C 但随后B 结束 - A 失去 C.

的任何范围

为了我的测试,我将我的 TestApp 修改成了一个相当可怕的东西,一个带有自我终止触发器的自我产生的噩梦。它的讨厌代码在这个答案的底部,我建议任何人只看它仅供参考。

控制这场噩梦的唯一答案似乎是通过作业对象。我使用了 this answer 中的 class(归功于 Alexander Yezutov -> Matt Howells -> 'Josh')但必须稍微修改它才能工作(因此我发布了它的代码)。我将此 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)]
        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

}

将我的主要方法的内容更改为如下所示:

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();

通过该方法的反馈看起来像这样(注意:它部分地失去了范围,但 TestApp 继续 运行 并传播):

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

请注意,PID 会有所不同,因为 TestApp 没有被直接调用,而是通过 CMD 传递 - 我在这里走极端 ;)

这是 TestApp,我强烈建议仅供参考,因为它会 运行 疯狂地创建自己的新实例(如果有人 [=50],它确实有一个 'kill' 清理参数=]就这样了!)。

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