终止进程和任何超时的子进程

本文关键字:子进程 超时 任何 进程 终止 | 更新日期: 2023-09-27 18:36:56

我正在尝试制作一个小程序,该程序将在后台线程上运行bat命令 - 这是有效的,但我正在尝试实现超时"安全"。即,如果后台命令挂起,它将在一定时间后终止。运行代码没有问题...一旦开始,我就无法终止该过程。我最终将我的代码屠宰到这个测试程序中:

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 秒后终止。但是,我的文本框的输出始终是这样的:

16:

09:22.175 执行线程后处理"测试.bat"

16:09:22.257 这是测试应用

16:09:22.261 TestApp 将有 10 秒的"睡眠"时间

16:

09:25.191 完成后处理

16:09:32.257 测试应用已唤醒!

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

终止进程和任何超时的子进程

在 NT 作业对象中启动进程。为此作业设置JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE作业限制。启动流程并将其分配给此作业(通常创建挂起的进程,将其分配给作业,然后恢复流程,以防止它在添加之前转义作业上下文)。时间到了,就干掉作业。这样,您不仅可以杀死进程,还可以杀死进程生成的任何子进程,这是当前尝试中缺少的最大部分。由于没有托管的NT Jobs API,请查看.net中CreateJobObject/SetInformationJobObject pinvoke的工作示例?

您甚至可以尝试设置JOB_OBJECT_LIMIT_JOB_TIME,以确保进程在杀死它之前不会耗尽 CPU/资源。

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

好的,讨厌回答我自己的问题,但是当找到解决方案时,我发现没有答案更糟,所以这里是:

Serge 是对的,cmd 进程是被监视/杀死的进程,而不是子进程。出于某种奇怪的原因,我脑海中浮现出终止父级将随后终止任何子进程。

所以,我找到了关于如何获取子进程并构建扩展类的答案:

//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 执行线程后处理"测试.bat"

12:

17:43.088 这是测试应用程序

12:

17:43.093 TestApp 将有一点"睡眠"10 秒

12:17:

46.127 已终止进程:TestApp

12:

17:46.133 完成后期处理

概念经过验证,我现在可以将其变成一个适当的应用程序。

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

最大的问题是,当世系链被破坏时,终止所有子进程变得非常困难。即 A 创建 B,B 创建 C,但随后 B 结束 - AC 上失去任何作用域。

在我的测试中,我将我的测试应用程序修改为相当可怕的东西,一个自我生成的噩梦,带有一个自我终止的人字拖。它令人讨厌的代码位于此答案的底部,我建议任何人仅查看以供参考。

它似乎控制这场噩梦的唯一答案是通过作业对象。我使用了这个答案中的类(归功于亚历山大·叶祖托夫 -> 马特·豪威尔斯 ->"乔希"),但不得不稍微修改它才能工作(因此我发布了它的代码)。 我将此类添加到我的项目中:

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 执行线程后处理"测试.bat"

13:06:31.214 24976 测试应用已启动

13:06:31.226 24976 现在在 1 秒内打电话给自己会把事情搞得一团糟......

13:06:32.213 24976 创建子进程 cmd 'TestApp.exe'

13:06:32.229 24976 已完成的子线程进程

13:06:32.285 24976 TestApp 将有一点"睡眠"10 秒

13:06:32.336 24976 创建新进程 26936

13:06:32.454 20344 测试应用已启动

13:06:32.500 20344 现在,在 1 秒内打电话给自己,会把事情搞得一团糟......

13:06:32.512 20344 !!我将在创建子进程以打破世系链后自行终止

13:06:33.521 20344 创建子进程 cmd 'TestApp.exe'

13:06:33.540 20344 已完成子线程进程

13:06:33.599 20344 创建新进程 24124

13:

06:33.707 19848 测试应用启动

13:

06:33.759 19848 现在在 1 秒内打电话给自己会把事情搞得一团糟......

13:06:34.540 20344 !!顶我自己!PID 20344 Scope lost after here

13:

06:41.139 达到超时,终止所有子进程

请注意,PID 各不相同,因为 TestApp 不是直接调用的,而是通过 CMD 传递的 - 我在这里走极端;)

这是 TestApp,我强烈建议它仅供参考,因为它将肆无忌惮地创建自身的新实例(如果有人运行它,它确实有一个"杀死"参数来清理!

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

您需要为退出事件添加一个事件处理程序。否则,"WaitForExit()"可能永远不会触发。进程.退出方法 ()

 processObject.Exited += new EventHandler(myProcess_HasExited);

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

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

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