Убить дочерний процесс, когда родительский процесс убит
Я создаю новые процессы, используя System.Diagnostics.Process
класс из моего приложения.
Я хочу, чтобы эти процессы были убиты, когда / если мое приложение потерпело крах. Но если я убью свое приложение из диспетчера задач, дочерние процессы не будут убиты.
Есть ли способ сделать дочерние процессы зависимыми от родительского процесса?
16 ответов
От этого форума, кредит на "Джош".
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);
Этот ответ начался с отличного ответа @Matt Howells плюс другие (см. Ссылки в коде ниже). Улучшения:
- Поддерживает 32-битные и 64-битные.
- Исправляет некоторые проблемы в ответе @Matt Howells:
- Небольшая утечка памяти
extendedInfoPtr
- Ошибка компиляции Win32, и
- Несбалансированное по стеку исключение, которое я получил в вызове
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 требуется:
- Простое изменение app.manifest, как описывает @adam smith.
- Параметры реестра, которые будут добавлены, если вы используете Visual Studio.
В моем случае мне не нужно было поддерживать 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://stackru.com/a/4657392/386091
/// https://stackru.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://stackru.com/a/4232259/386091
// https://stackru.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.
Этот пост предназначен как расширение ответа @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://stackru.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 автоматически отключается в этих условиях.
[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, запускающий мой процесс в объекте задания?
Вот альтернатива, которая может работать для некоторых, когда у вас есть контроль над кодом, который запускает дочерний процесс. Преимущество этого подхода заключается в том, что он не требует никаких собственных вызовов 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
А затем, на дочернем процессе, воспользоваться тем фактом, что Read
s из стандартного входного потока всегда будут возвращаться по крайней мере с 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();
}
}
Предостережения к этому подходу:
фактический дочерний файл.exe, который запускается, должен быть консольным приложением, поэтому он остается подключенным к stdin / out / err. Как и в приведенном выше примере, я легко адаптировал свое существующее приложение, которое использовало насос сообщений (но не показывал графический интерфейс), просто создав крошечный консольный проект, который ссылался на существующий проект, создавал экземпляр контекста моего приложения и вызывал
Application.Run()
внутриMain
метод консоли.exe.Технически, это просто сигнализирует о дочернем процессе при выходе из родительского процесса, поэтому он будет работать независимо от того, завершился ли родительский процесс нормально или произошел сбой, но его дочерние процессы по-прежнему должны самостоятельно завершать работу. Это может или не может быть то, что вы хотите...
Существует другой соответствующий метод, простой и эффективный, для завершения дочерних процессов после завершения программы. Вы можете реализовать и прикрепить к ним отладчик от родителя; Когда родительский процесс завершается, дочерние процессы будут уничтожены ОС. Прикрепить отладчик к родительскому элементу от дочернего процесса можно двумя способами (обратите внимание, что одновременно можно подключить только один отладчик). Вы можете найти больше информации по этому вопросу здесь.
Здесь у вас есть служебный класс, который запускает новый процесс и присоединяет к нему отладчик. Это было адаптировано из этого поста Роджером Кнаппом. Единственное требование состоит в том, что оба процесса должны иметь одинаковую битность. Вы не можете отлаживать 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");
Одним из способов является передача PID родительского процесса ребенку. Ребенок будет периодически опрашивать, существует ли процесс с указанным pid или нет. Если нет, то просто выйдет.
Вы также можете использовать метод Process.WaitForExit в дочернем методе, чтобы получать уведомления о завершении родительского процесса, но он может не работать в случае диспетчера задач.
Я искал решение этой проблемы, которое не требовало бы неуправляемого кода. Я также не смог использовать стандартное перенаправление ввода / вывода, потому что это было приложение 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);
}
Используйте обработчики событий для создания ловушек в нескольких сценариях выхода:
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(); };
Очередное дополнение к изобилию предложенных решений....
Проблема со многими из них заключается в том, что они полагаются на то, что родительский и дочерний процессы завершаются упорядоченным образом, что не всегда верно в процессе разработки. Я обнаружил, что мой дочерний процесс часто становился сиротским всякий раз, когда я завершал родительский процесс в отладчике, что требовало от меня убить потерянные процессы с помощью диспетчера задач, чтобы восстановить мое решение.
Решение: передайте идентификатор родительского процесса в командной строке (или, что менее агрессивно, в переменных среды) дочернего процесса.
В родительском процессе идентификатор процесса доступен как:
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();
};
Я не понимаю, является ли это приемлемой практикой в производственном коде. С одной стороны, этого никогда не должно происходить. Но с другой стороны, это может означать разницу между перезапуском процесса и перезагрузкой рабочего сервера. А то, что никогда не должно происходить, случается часто.
И это, безусловно, полезно при отладке проблем с правильным завершением работы.
Решение, которое сработало для меня:
При создании процесса добавьте тег process.EnableRaisingEvents = true;
:
csc = new Process();
csc.StartInfo.UseShellExecute = false;
csc.StartInfo.CreateNoWindow = true;
csc.StartInfo.FileName = Path.Combine(HLib.path_dataFolder, "csc.exe");
csc.StartInfo.WindowStyle = ProcessWindowStyle.Hidden;
csc.StartInfo.ErrorDialog = false;
csc.StartInfo.RedirectStandardInput = true;
csc.StartInfo.RedirectStandardOutput = true;
csc.EnableRaisingEvents = true;
csc.Start();
Просто моя версия 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);
}
Я вижу два варианта:
- Если вы точно знаете, какой дочерний процесс может быть запущен, и уверены, что они запускаются только из вашего основного процесса, то вы можете просто искать их по имени и убивать.
- Выполните итерацию всех процессов и уничтожьте каждый процесс, в котором ваш процесс является родительским (я полагаю, вам сначала нужно уничтожить дочерние процессы). Здесь объясняется, как вы можете получить идентификатор родительского процесса.
Я создал дочернюю библиотеку управления процессами, в которой родительский процесс и дочерний процесс отслеживаются с помощью двунаправленного канала WCF. Если либо дочерний процесс завершается, либо родительский процесс завершает работу друг друга, это уведомляется. Также доступен помощник отладчика, который автоматически присоединяет отладчик VS к запущенному дочернему процессу.
Сайт проекта:
http://www.crawler-lib.net/child-processes
Пакеты NuGet:
https://www.nuget.org/packages/ChildProcesses https://www.nuget.org/packages/ChildProcesses.VisualStudioDebug/
У меня такая же проблема. Я создавал дочерние процессы, которые никогда не прекращались при сбое моего основного приложения. При отладке пришлось вручную уничтожать дочерние процессы. Я обнаружил, что нет необходимости заставлять детей в какой-то мере зависеть от родителей. В моем основном я добавил попытку выполнить CleanUp() дочерних процессов при выходе.
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
try
{
Application.Run(new frmMonitorSensors());
}
catch(Exception ex)
{
CleanUp();
ErrorLogging.Add(ex.ToString());
}
}
static private void CleanUp()
{
List<string> processesToKill = new List<string>() { "Process1", "Process2" };
foreach (string toKill in processesToKill)
{
Process[] processes = Process.GetProcessesByName(toKill);
foreach (Process p in processes)
{
p.Kill();
}
}
}
Если вы можете поймать ситуацию, когда ваше дерево процессов должно быть уничтожено, начиная с .NET 5.0 (.NET Core 3.0) вы можете использовать Process.Kill(bool allProcessTree)
Вызовите 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();
}