Убить дочерний процесс, когда родительский процесс убит

156

Я создаю новые процессы, используя System.Diagnostics.Processкласс из моего приложения.

Я хочу, чтобы эти процессы были убиты, когда / если мое приложение потерпело крах. Но если я убью свое приложение из диспетчера задач, дочерние процессы не будут убиты.

Есть ли способ сделать дочерние процессы зависимыми от родительского процесса?

SiberianGuy
источник

Ответы:

176

От этого форума , кредит на «Джош».

Application.Quit()и Process.Kill()являются возможными решениями, но оказались ненадежными. Когда ваше основное приложение умирает, у вас все еще остаются запущенные дочерние процессы. Мы действительно хотим, чтобы дочерние процессы умирали, как только умирает основной процесс.

Решение состоит в том, чтобы использовать «объекты заданий» http://msdn.microsoft.com/en-us/library/ms682409(VS.85).aspx .

Идея состоит в том, чтобы создать «объект задания» для вашего основного приложения и зарегистрировать ваши дочерние процессы с объектом задания. Если основной процесс умирает, ОС позаботится о прекращении дочерних процессов.

public enum JobObjectInfoType
{
    AssociateCompletionPortInformation = 7,
    BasicLimitInformation = 2,
    BasicUIRestrictions = 4,
    EndOfJobTimeInformation = 6,
    ExtendedLimitInformation = 9,
    SecurityLimitInformation = 5,
    GroupInformation = 11
}

[StructLayout(LayoutKind.Sequential)]
public struct SECURITY_ATTRIBUTES
{
    public int nLength;
    public IntPtr lpSecurityDescriptor;
    public int bInheritHandle;
}

[StructLayout(LayoutKind.Sequential)]
struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
    public Int64 PerProcessUserTimeLimit;
    public Int64 PerJobUserTimeLimit;
    public Int16 LimitFlags;
    public UInt32 MinimumWorkingSetSize;
    public UInt32 MaximumWorkingSetSize;
    public Int16 ActiveProcessLimit;
    public Int64 Affinity;
    public Int16 PriorityClass;
    public Int16 SchedulingClass;
}

[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_EXTENDED_LIMIT_INFORMATION
{
    public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
    public IO_COUNTERS IoInfo;
    public UInt32 ProcessMemoryLimit;
    public UInt32 JobMemoryLimit;
    public UInt32 PeakProcessMemoryUsed;
    public UInt32 PeakJobMemoryUsed;
}

public class Job : IDisposable
{
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    static extern IntPtr CreateJobObject(object a, string lpName);

    [DllImport("kernel32.dll")]
    static extern bool SetInformationJobObject(IntPtr hJob, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);

    private IntPtr m_handle;
    private bool m_disposed = false;

    public Job()
    {
        m_handle = CreateJobObject(null, null);

        JOBOBJECT_BASIC_LIMIT_INFORMATION info = new JOBOBJECT_BASIC_LIMIT_INFORMATION();
        info.LimitFlags = 0x2000;

        JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
        extendedInfo.BasicLimitInformation = info;

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

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

    #region IDisposable Members

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

    #endregion

    private void Dispose(bool disposing)
    {
        if (m_disposed)
            return;

        if (disposing) {}

        Close();
        m_disposed = true;
    }

    public void Close()
    {
        Win32.CloseHandle(m_handle);
        m_handle = IntPtr.Zero;
    }

    public bool AddProcess(IntPtr handle)
    {
        return AssignProcessToJobObject(m_handle, handle);
    }

}

Глядя на конструктор ...

JOBOBJECT_BASIC_LIMIT_INFORMATION info = new JOBOBJECT_BASIC_LIMIT_INFORMATION();
info.LimitFlags = 0x2000;

Ключевым моментом здесь является правильная настройка объекта задания. В конструкторе я устанавливаю «пределы» на 0x2000, что является числовым значением для JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE.

MSDN определяет этот флаг как:

Вызывает завершение всех процессов, связанных с заданием, когда закрывается последний дескриптор задания.

После настройки этого класса ... вам просто нужно зарегистрировать каждый дочерний процесс в задании. Например:

[DllImport("user32.dll", SetLastError = true)]
public static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint lpdwProcessId);

Excel.Application app = new Excel.ApplicationClass();

uint pid = 0;
Win32.GetWindowThreadProcessId(new IntPtr(app.Hwnd), out pid);
 job.AddProcess(Process.GetProcessById((int)pid).Handle);
Мэтт Хауэллс
источник
4
Я хотел бы добавить ссылку на CloseHandle
Остин Салонен
6
К сожалению, я не смог запустить это в 64-битном режиме. Здесь я разместил рабочий пример, который основан на этом.
Александр Езутов
2
@Matt Howells - откуда Win32.CloseHandleпроисходит? Это импортируется из kernel32.dll? Там есть соответствующая подпись, но вы не импортируете ее явно, как другие функции API.
Эзотерическое
6
Для приложений в 64-битном режиме -> stackoverflow.com/a/5976162 Для проблемы Vista / Win7 -> social.msdn.microsoft.com/forums/en-US/windowssecurity/thread/…
hB0
3
Хорошо, MSDN говорит: флаг JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE требует использования структуры JOBOBJECT_EXTENDED_LIMIT_INFORMATION.
SerG
54

Этот ответ начался с отличного ответа @Matt Howells плюс другие (см. Ссылки в коде ниже). Улучшения:

  • Поддерживает 32-битные и 64-битные.
  • Исправляет некоторые проблемы в ответе @Matt Howells:
    1. Небольшая утечка памяти extendedInfoPtr
    2. Ошибка компиляции Win32 и
    3. Неуравновешенное по стеку исключение, которое я получил в вызове CreateJobObject(с использованием Windows 10, Visual Studio 2015, 32-разрядная версия).
  • Называет задание, поэтому, если вы, например, используете SysInternals, вы можете легко его найти.
  • Имеет несколько более простой API и меньше кода.

Вот как использовать этот код:

// Get a Process object somehow.
Process process = Process.Start(exePath, args);
// Add the Process to ChildProcessTracker.
ChildProcessTracker.AddProcess(process);

Для поддержки Windows 7 требуется:

В моем случае мне не нужно было поддерживать Windows 7, поэтому у меня есть простая проверка вверху статического конструктора ниже.

/// <summary>
/// Allows processes to be automatically killed if this parent process unexpectedly quits.
/// This feature requires Windows 8 or greater. On Windows 7, nothing is done.</summary>
/// <remarks>References:
///  https://stackoverflow.com/a/4657392/386091
///  https://stackoverflow.com/a/9164742/386091 </remarks>
public static class ChildProcessTracker
{
    /// <summary>
    /// Add the process to be tracked. If our current process is killed, the child processes
    /// that we are tracking will be automatically killed, too. If the child process terminates
    /// first, that's fine, too.</summary>
    /// <param name="process"></param>
    public static void AddProcess(Process process)
    {
        if (s_jobHandle != IntPtr.Zero)
        {
            bool success = AssignProcessToJobObject(s_jobHandle, process.Handle);
            if (!success && !process.HasExited)
                throw new Win32Exception();
        }
    }

    static ChildProcessTracker()
    {
        // This feature requires Windows 8 or later. To support Windows 7 requires
        //  registry settings to be added if you are using Visual Studio plus an
        //  app.manifest change.
        //  https://stackoverflow.com/a/4232259/386091
        //  https://stackoverflow.com/a/9507862/386091
        if (Environment.OSVersion.Version < new Version(6, 2))
            return;

        // The job name is optional (and can be null) but it helps with diagnostics.
        //  If it's not null, it has to be unique. Use SysInternals' Handle command-line
        //  utility: handle -a ChildProcessTracker
        string jobName = "ChildProcessTracker" + Process.GetCurrentProcess().Id;
        s_jobHandle = CreateJobObject(IntPtr.Zero, jobName);

        var info = new JOBOBJECT_BASIC_LIMIT_INFORMATION();

        // This is the key flag. When our process is killed, Windows will automatically
        //  close the job handle, and when that happens, we want the child processes to
        //  be killed, too.
        info.LimitFlags = JOBOBJECTLIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;

        var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION();
        extendedInfo.BasicLimitInformation = info;

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

            if (!SetInformationJobObject(s_jobHandle, JobObjectInfoType.ExtendedLimitInformation,
                extendedInfoPtr, (uint)length))
            {
                throw new Win32Exception();
            }
        }
        finally
        {
            Marshal.FreeHGlobal(extendedInfoPtr);
        }
    }

    [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
    static extern IntPtr CreateJobObject(IntPtr lpJobAttributes, string name);

    [DllImport("kernel32.dll")]
    static extern bool SetInformationJobObject(IntPtr job, JobObjectInfoType infoType,
        IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);

    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);

    // Windows will automatically close any open job handles when our process terminates.
    //  This can be verified by using SysInternals' Handle utility. When the job handle
    //  is closed, the child processes will be killed.
    private static readonly IntPtr s_jobHandle;
}

public enum JobObjectInfoType
{
    AssociateCompletionPortInformation = 7,
    BasicLimitInformation = 2,
    BasicUIRestrictions = 4,
    EndOfJobTimeInformation = 6,
    ExtendedLimitInformation = 9,
    SecurityLimitInformation = 5,
    GroupInformation = 11
}

[StructLayout(LayoutKind.Sequential)]
public struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
    public Int64 PerProcessUserTimeLimit;
    public Int64 PerJobUserTimeLimit;
    public JOBOBJECTLIMIT LimitFlags;
    public UIntPtr MinimumWorkingSetSize;
    public UIntPtr MaximumWorkingSetSize;
    public UInt32 ActiveProcessLimit;
    public Int64 Affinity;
    public UInt32 PriorityClass;
    public UInt32 SchedulingClass;
}

[Flags]
public enum JOBOBJECTLIMIT : uint
{
    JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x2000
}

[StructLayout(LayoutKind.Sequential)]
public struct IO_COUNTERS
{
    public UInt64 ReadOperationCount;
    public UInt64 WriteOperationCount;
    public UInt64 OtherOperationCount;
    public UInt64 ReadTransferCount;
    public UInt64 WriteTransferCount;
    public UInt64 OtherTransferCount;
}

[StructLayout(LayoutKind.Sequential)]
public 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;
}

Я тщательно протестировал как 32-битные, так и 64-битные версии структур, программно сравнивая управляемые и собственные версии друг с другом (общий размер, а также смещения для каждого элемента).

Я тестировал этот код на Windows 7, 8 и 10.

Рон
источник
как насчет закрытия ручки работы?
Фрэнк В.
@FrankQ. Важно, чтобы Windows закрыла для нас s_jobHandle, когда наш процесс завершается, потому что наш процесс может неожиданно завершиться (например, в результате сбоя или если пользователь использует диспетчер задач). Смотрите мой комментарий на s_jobHandle's.
Рон
Это работает для меня. Могу ли я спросить, что вы думаете об использовании флага JOB_OBJECT_LIMIT_BREAKAWAY_OK? docs.microsoft.com/en-us/windows/desktop/api/winnt/...
Yiping
1
@yiping Похоже, CREATE_BREAKAWAY_FROM_JOB позволяет вашему дочернему процессу порождать процесс, который может пережить ваш первоначальный процесс. Это другое требование, чем то, о чем просил ОП. Вместо этого, если вы можете заставить свой оригинальный процесс порождать этот долгоживущий процесс (и не использовать ChildProcessTracker), это будет проще.
Рон
@Ron Можете ли вы добавить перегрузку, которая принимает только дескриптор процесса, а не весь процесс?
Jannik
47

Этот пост предназначен для расширения ответа @Matt Howells, особенно для тех, кто сталкивается с проблемами при использовании объектов заданий в Vista или Win7 , особенно если вы получаете ошибку отказа в доступе ('5') при вызове AssignProcessToJobObject.

ТЛ; др

Чтобы обеспечить совместимость с Vista и Win7, добавьте следующий манифест в родительский процесс .NET:

<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <v3:trustInfo xmlns:v3="urn:schemas-microsoft-com:asm.v3">
    <v3:security>
      <v3:requestedPrivileges>
        <v3:requestedExecutionLevel level="asInvoker" uiAccess="false" />
      </v3:requestedPrivileges>
    </v3:security>
  </v3:trustInfo>
  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
    <!-- We specify these, in addition to the UAC above, so we avoid Program Compatibility Assistant in Vista and Win7 -->
    <!-- We try to avoid PCA so we can use Windows Job Objects -->
    <!-- See https://stackoverflow.com/questions/3342941/kill-child-process-when-parent-process-is-killed -->

    <application>
      <!--The ID below indicates application support for Windows Vista -->
      <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
      <!--The ID below indicates application support for Windows 7 -->
      <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>
    </application>
  </compatibility>
</assembly>

Обратите внимание, что при добавлении нового манифеста в Visual Studio 2012 он уже будет содержать приведенный выше фрагмент кода, поэтому вам не нужно копировать его из списка прослушивания. Он также будет включать узел для Windows 8.

полное объяснение

Ваша ассоциация заданий потерпит неудачу с ошибкой доступа, если запускаемый вами процесс уже связан с другим заданием. Введите Ассистент совместимости программ, который, начиная с Windows Vista, будет назначать все виды процессов для своих собственных заданий.

В Vista вы можете пометить ваше приложение как исключенное из PCA, просто включив манифест приложения. Visual Studio, кажется, делает это для приложений .NET автоматически, так что все в порядке.

Простой манифест больше не режет его в Win7. [1] Там вы должны указать, что вы совместимы с Win7 с тегом в манифесте. [2]

Это заставило меня задуматься о Windows 8. Должен ли я изменить свой манифест еще раз? Очевидно, в облаках есть перерыв, поскольку Windows 8 теперь позволяет процессу принадлежать нескольким заданиям. [3] Так что я еще не проверял это, но я думаю, что это безумие закончится, если вы просто включите манифест с информацией о поддерживаемых ОС.

Совет 1 : Если вы разрабатываете приложение .NET с Visual Studio, как я, здесь [4] приведены несколько хороших инструкций по настройке манифеста вашего приложения.

Совет 2 : будьте осторожны при запуске приложения из Visual Studio. Я обнаружил, что после добавления соответствующего манифеста у меня все еще были проблемы с PCA при запуске из Visual Studio, даже если я использовал Пуск без отладки. Однако запуск приложения из Проводника сработал. После добавления devenv вручную для исключения из PCA с использованием реестра, также начали работать приложения, использующие Job Objects из VS. [5]

Совет 3 : Если вы когда-нибудь захотите узнать, является ли PCA вашей проблемой, попробуйте запустить приложение из командной строки или скопировать программу на сетевой диск и запустить его оттуда. PCA автоматически отключается в этих условиях.

[1] http://blogs.msdn.com/b/cjacks/archive/2009/06/18/pca-changes-for-windows-7-how-to-tell-us-you-are-not-an -installer-дубль-2-потому-мы-изменил-на-правила-на-you.aspx

[2] http://ayende.com/blog/4360/how-to-opt-out-of-program-compatibility-assistant

[3] http://msdn.microsoft.com/en-us/library/windows/desktop/ms681949(v=vs.85).aspx : «Процесс может быть связан с несколькими заданиями в Windows 8»

[4] Как я могу встроить манифест приложения в приложение с использованием VS2008?

[5] Как остановить отладчик Visual Studio, запускающий мой процесс в объекте задания?

Адам Смит
источник
2
Это отличные дополнения к теме, спасибо! Я использовал все аспекты этого ответа, включая ссылки.
Джонни Кауфман
16

Вот альтернатива, которая может работать для некоторых, когда у вас есть контроль над кодом, который запускает дочерний процесс. Преимущество этого подхода заключается в том, что он не требует никаких вызовов Windows.

Основная идея состоит в том, чтобы перенаправить стандартный ввод дочернего элемента в поток, другой конец которого подключен к родительскому элементу, и использовать этот поток, чтобы определить, когда родительский элемент исчез. Когда вы используете System.Diagnostics.Processдочерний элемент, легко убедиться, что его стандартный ввод перенаправлен:

Process childProcess = new Process();
childProcess.StartInfo = new ProcessStartInfo("pathToConsoleModeApp.exe");
childProcess.StartInfo.RedirectStandardInput = true;

childProcess.StartInfo.CreateNoWindow = true; // no sense showing an empty black console window which the user can't input into

И затем, на дочернем процессе, воспользуйтесь тем фактом, что Reads из стандартного входного потока всегда будет возвращаться по крайней мере с 1 байтом, пока поток не будет закрыт, когда они начнут возвращать 0 байтов. Схема того, как я это сделал, приведена ниже; Мой путь также использует рассылку сообщений, чтобы держать основной поток доступным для вещей, отличных от просмотра стандарта, но этот общий подход можно использовать и без рассылок сообщений.

using System;
using System.IO;
using System.Threading;
using System.Windows.Forms;

static int Main()
{
    Application.Run(new MyApplicationContext());
    return 0;
}

public class MyApplicationContext : ApplicationContext
{
    private SynchronizationContext _mainThreadMessageQueue = null;
    private Stream _stdInput;

    public MyApplicationContext()
    {
        _stdInput = Console.OpenStandardInput();

        // feel free to use a better way to post to the message loop from here if you know one ;)    
        System.Windows.Forms.Timer handoffToMessageLoopTimer = new System.Windows.Forms.Timer();
        handoffToMessageLoopTimer.Interval = 1;
        handoffToMessageLoopTimer.Tick += new EventHandler((obj, eArgs) => { PostMessageLoopInitialization(handoffToMessageLoopTimer); });
        handoffToMessageLoopTimer.Start();
    }

    private void PostMessageLoopInitialization(System.Windows.Forms.Timer t)
    {
        if (_mainThreadMessageQueue == null)
        {
            t.Stop();
            _mainThreadMessageQueue = SynchronizationContext.Current;
        }

        // constantly monitor standard input on a background thread that will
        // signal the main thread when stuff happens.
        BeginMonitoringStdIn(null);

        // start up your application's real work here
    }

    private void BeginMonitoringStdIn(object state)
    {
        if (SynchronizationContext.Current == _mainThreadMessageQueue)
        {
            // we're already running on the main thread - proceed.
            var buffer = new byte[128];

            _stdInput.BeginRead(buffer, 0, buffer.Length, (asyncResult) =>
                {
                    int amtRead = _stdInput.EndRead(asyncResult);

                    if (amtRead == 0)
                    {
                        _mainThreadMessageQueue.Post(new SendOrPostCallback(ApplicationTeardown), null);
                    }
                    else
                    {
                        BeginMonitoringStdIn(null);
                    }
                }, null);
        }
        else
        {
            // not invoked from the main thread - dispatch another call to this method on the main thread and return
            _mainThreadMessageQueue.Post(new SendOrPostCallback(BeginMonitoringStdIn), null);
        }
    }

    private void ApplicationTeardown(object state)
    {
        // tear down your application gracefully here
        _stdInput.Close();

        this.ExitThread();
    }
}

Предостережения к этому подходу:

  1. фактический дочерний файл .exe, который запускается, должен быть консольным приложением, поэтому он остается подключенным к stdin / out / err. Как и в приведенном выше примере, я легко адаптировал свое существующее приложение, которое использовало насос сообщений (но не показывал графический интерфейс), просто создав крошечный консольный проект, который ссылался на существующий проект, создавал экземпляр моего контекста приложения и вызывал Application.Run()внутри Mainметода консоль .exe.

  2. Технически это просто сигнализирует о дочернем процессе при выходе из родительского процесса, поэтому он будет работать независимо от того, завершился ли родительский процесс нормально или произошел сбой, но его дочерние процессы по-прежнему должны самостоятельно завершать работу. Это может или не может быть то, что вы хотите ...

mbaynton
источник
11

Одним из способов является передача PID родительского процесса ребенку. Ребенок будет периодически опрашивать, существует ли процесс с указанным pid или нет. Если нет, то просто выйдет.

Вы также можете использовать метод Process.WaitForExit в дочернем методе, чтобы получать уведомления о завершении родительского процесса, но он может не работать в случае диспетчера задач.

Гиорги
источник
Как я могу передать родительский PID ребенку? Есть ли системное решение? Я не могу изменить двоичные файлы дочерних процессов.
SiberianGuy
1
Что ж, если вы не можете изменить дочерний процесс, вы не сможете использовать мое решение, даже если передадите ему PID.
Георгий
@Idsa Вы можете передать его через командную строку:Process.Start(string fileName, string arguments)
Distortum
2
Вместо опроса вы можете подключиться к событию Exit в классе процесса.
RichardOD
Пробовал, но родительский процесс не всегда завершается, если у него есть живые потомки (по крайней мере, в моем случае Cobol-> NET). Легко проверить, наблюдая за иерархией процессов в Sysinternals ProcessExplorer.
Иван Феррер Вилла
8

Существует другой соответствующий метод, простой и эффективный, для завершения дочерних процессов после завершения программы. Вы можете реализовать и прикрепить к ним отладчик от родителя; Когда родительский процесс завершается, дочерние процессы будут уничтожены ОС. Прикрепить отладчик к родительскому элементу от дочернего процесса можно двумя способами (обратите внимание, что одновременно можно подключить только один отладчик). Вы можете найти больше информации по этому вопросу здесь .

Здесь у вас есть служебный класс, который запускает новый процесс и присоединяет к нему отладчик. Это было адаптировано из этого поста Роджером Кнаппом. Единственное требование состоит в том, что оба процесса должны иметь одинаковую битность. Вы не можете отлаживать 32-битный процесс из 64-битного процесса или наоборот.

public class ProcessRunner
{
    #region "API imports"

    private const int DBG_CONTINUE = 0x00010002;
    private const int DBG_EXCEPTION_NOT_HANDLED = unchecked((int) 0x80010001);

    private enum DebugEventType : int
    {
        CREATE_PROCESS_DEBUG_EVENT = 3,
        //Reports a create-process debugging event. The value of u.CreateProcessInfo specifies a CREATE_PROCESS_DEBUG_INFO structure.
        CREATE_THREAD_DEBUG_EVENT = 2,
        //Reports a create-thread debugging event. The value of u.CreateThread specifies a CREATE_THREAD_DEBUG_INFO structure.
        EXCEPTION_DEBUG_EVENT = 1,
        //Reports an exception debugging event. The value of u.Exception specifies an EXCEPTION_DEBUG_INFO structure.
        EXIT_PROCESS_DEBUG_EVENT = 5,
        //Reports an exit-process debugging event. The value of u.ExitProcess specifies an EXIT_PROCESS_DEBUG_INFO structure.
        EXIT_THREAD_DEBUG_EVENT = 4,
        //Reports an exit-thread debugging event. The value of u.ExitThread specifies an EXIT_THREAD_DEBUG_INFO structure.
        LOAD_DLL_DEBUG_EVENT = 6,
        //Reports a load-dynamic-link-library (DLL) debugging event. The value of u.LoadDll specifies a LOAD_DLL_DEBUG_INFO structure.
        OUTPUT_DEBUG_STRING_EVENT = 8,
        //Reports an output-debugging-string debugging event. The value of u.DebugString specifies an OUTPUT_DEBUG_STRING_INFO structure.
        RIP_EVENT = 9,
        //Reports a RIP-debugging event (system debugging error). The value of u.RipInfo specifies a RIP_INFO structure.
        UNLOAD_DLL_DEBUG_EVENT = 7,
        //Reports an unload-DLL debugging event. The value of u.UnloadDll specifies an UNLOAD_DLL_DEBUG_INFO structure.
    }

    [StructLayout(LayoutKind.Sequential)]
    private struct DEBUG_EVENT
    {
        [MarshalAs(UnmanagedType.I4)] public DebugEventType dwDebugEventCode;
        public int dwProcessId;
        public int dwThreadId;
        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 1024)] public byte[] bytes;
    }

    [DllImport("Kernel32.dll", SetLastError = true)]
    private static extern bool DebugActiveProcess(int dwProcessId);

    [DllImport("Kernel32.dll", SetLastError = true)]
    private static extern bool WaitForDebugEvent([Out] out DEBUG_EVENT lpDebugEvent, int dwMilliseconds);

    [DllImport("Kernel32.dll", SetLastError = true)]
    private static extern bool ContinueDebugEvent(int dwProcessId, int dwThreadId, int dwContinueStatus);

    [DllImport("Kernel32.dll", SetLastError = true)]
    public static extern bool IsDebuggerPresent();

    #endregion

    public Process ChildProcess { get; set; }

    public bool StartProcess(string fileName)
    {
        var processStartInfo = new ProcessStartInfo(fileName)
        {
            UseShellExecute = false,
            WindowStyle = ProcessWindowStyle.Normal,
            ErrorDialog = false
        };

        this.ChildProcess = Process.Start(processStartInfo);
        if (ChildProcess == null)
            return false;

        new Thread(NullDebugger) {IsBackground = true}.Start(ChildProcess.Id);
        return true;
    }

    private void NullDebugger(object arg)
    {
        // Attach to the process we provided the thread as an argument
        if (DebugActiveProcess((int) arg))
        {
            var debugEvent = new DEBUG_EVENT {bytes = new byte[1024]};
            while (!this.ChildProcess.HasExited)
            {
                if (WaitForDebugEvent(out debugEvent, 1000))
                {
                    // return DBG_CONTINUE for all events but the exception type
                    var continueFlag = DBG_CONTINUE;
                    if (debugEvent.dwDebugEventCode == DebugEventType.EXCEPTION_DEBUG_EVENT)
                        continueFlag = DBG_EXCEPTION_NOT_HANDLED;
                    ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, continueFlag);
                }
            }
        }
        else
        {
            //we were not able to attach the debugger
            //do the processes have the same bitness?
            //throw ApplicationException("Unable to attach debugger") // Kill child? // Send Event? // Ignore?
        }
    }
}

Использование:

    new ProcessRunner().StartProcess("c:\\Windows\\system32\\calc.exe");
Марко Регейра
источник
8

Я искал решение этой проблемы, которое не требовало бы неуправляемого кода. Я также не смог использовать стандартное перенаправление ввода / вывода, потому что это было приложение Windows Forms.

Моим решением было создать именованный канал в родительском процессе, а затем подключить дочерний процесс к тому же каналу. Если родительский процесс завершается, то канал разрывается, и дочерний процесс может обнаружить это.

Ниже приведен пример использования двух консольных приложений:

родитель

private const string PipeName = "471450d6-70db-49dc-94af-09d3f3eba529";

public static void Main(string[] args)
{
    Console.WriteLine("Main program running");

    using (NamedPipeServerStream pipe = new NamedPipeServerStream(PipeName, PipeDirection.Out))
    {
        Process.Start("child.exe");

        Console.WriteLine("Press any key to exit");
        Console.ReadKey();
    }
}

ребенок

private const string PipeName = "471450d6-70db-49dc-94af-09d3f3eba529"; // same as parent

public static void Main(string[] args)
{
    Console.WriteLine("Child process running");

    using (NamedPipeClientStream pipe = new NamedPipeClientStream(".", PipeName, PipeDirection.In))
    {
        pipe.Connect();
        pipe.BeginRead(new byte[1], 0, 1, PipeBrokenCallback, pipe);

        Console.WriteLine("Press any key to exit");
        Console.ReadKey();
    }
}

private static void PipeBrokenCallback(IAsyncResult ar)
{
    // the pipe was closed (parent process died), so exit the child process too

    try
    {
        NamedPipeClientStream pipe = (NamedPipeClientStream)ar.AsyncState;
        pipe.EndRead(ar);
    }
    catch (IOException) { }

    Environment.Exit(1);
}
Alsty
источник
3

Используйте обработчики событий для создания ловушек в нескольких сценариях выхода:

var process = Process.Start("program.exe");
AppDomain.CurrentDomain.DomainUnload += (s, e) => { process.Kill(); process.WaitForExit(); };
AppDomain.CurrentDomain.ProcessExit += (s, e) => { process.Kill(); process.WaitForExit(); };
AppDomain.CurrentDomain.UnhandledException += (s, e) => { process.Kill(); process.WaitForExit(); };
Джастин харрис
источник
Так просто, но так эффективно.
uncommon_name
2

Просто моя версия 2018 года. Используйте его в стороне от вашего метода Main ().

    using System.Management;
    using System.Diagnostics;

    ...

    // Called when the Main Window is closed
    protected override void OnClosed(EventArgs EventArgs)
    {
        string query = "Select * From Win32_Process Where ParentProcessId = " + Process.GetCurrentProcess().Id;
        ManagementObjectSearcher searcher = new ManagementObjectSearcher(query);
        ManagementObjectCollection processList = searcher.Get();
        foreach (var obj in processList)
        {
            object data = obj.Properties["processid"].Value;
            if (data != null)
            {
                // retrieve the process
                var childId = Convert.ToInt32(data);
                var childProcess = Process.GetProcessById(childId);

                // ensure the current process is still live
                if (childProcess != null) childProcess.Kill();
            }
        }
        Environment.Exit(0);
    }
Lenor
источник
4
Я сомневаюсь, что это выполняется, когда родительский процесс убивается .
springy76
1

Я вижу два варианта:

  1. Если вы точно знаете, какой дочерний процесс может быть запущен, и уверены, что они запускаются только из вашего основного процесса, то вы можете просто искать их по имени и убивать.
  2. Выполните итерацию всех процессов и уничтожьте каждый процесс, у которого ваш процесс является родительским (я полагаю, вам сначала нужно уничтожить дочерние процессы). Здесь объясняется, как вы можете получить идентификатор родительского процесса.
Стефан Эгли
источник
1

Я создал дочернюю библиотеку управления процессами, где родительский процесс и дочерний процесс отслеживаются с помощью двунаправленного канала WCF. Если либо дочерний процесс завершается, либо родительский процесс завершает работу друг друга, это уведомляется. Также доступен помощник отладчика, который автоматически присоединяет отладчик VS к запущенному дочернему процессу.

Сайт проекта:

http://www.crawler-lib.net/child-processes

Пакеты NuGet:

https://www.nuget.org/packages/ChildProcesses https://www.nuget.org/packages/ChildProcesses.VisualStudioDebug/

Томас Майерхофер
источник
0

Вызовите job.AddProcess лучше сделать после запуска процесса:

prc.Start();
job.AddProcess(prc.Handle);

При вызове AddProcess до завершения дочерние процессы не уничтожаются. (Windows 7 SP1)

private void KillProcess(Process proc)
{
    var job = new Job();
    job.AddProcess(proc.Handle);
    job.Close();
}
Алексей
источник
Вызов job.AddProcess после запуска процесса убил бы цель использования этого объекта.
SerG
0

Еще одно дополнение к изобилию предлагаемых решений ....

Проблема многих из них заключается в том, что они полагаются на то, что родительский и дочерний процессы завершают свою работу упорядоченным образом, что не всегда верно в процессе разработки. Я обнаружил, что мой дочерний процесс часто становился осиротевшим всякий раз, когда я завершал родительский процесс в отладчике, что требовало от меня уничтожить потерянный процесс (-ы) с помощью диспетчера задач, чтобы перестроить мое решение.

Решение: передать идентификатор родительского процесса в командной строке (или даже менее инвазивно, в переменных среды) дочернего процесса.

В родительском процессе идентификатор процесса доступен как:

 Process.CurrentProcess.Id;

В дочернем процессе:

Process parentProcess = Process.GetProcessById(parentProcessId);
parentProcess.Exited += (s, e) =>
{
    // clean up what you can.
    this.Dispose();
    // maybe log an error
    // ...

    // And terminate with prejudice! 
    //(since something has already gone terribly wrong)
    Process.GetCurrentProcess().Kill();
};

У меня два мнения о том, является ли это приемлемой практикой в ​​производственном коде. С одной стороны, этого никогда не должно случиться. Но с другой стороны, это может означать разницу между перезапуском процесса и перезагрузкой рабочего сервера. И то, что никогда не должно случаться, часто происходит.

И это, безусловно, полезно при отладке проблем с завершением работы.

Робин Дэвис
источник