Matar proceso hijo cuando se mata el proceso padre

156

Estoy creando nuevos procesos usando la System.Diagnostics.Processclase desde mi aplicación.

Quiero que se eliminen estos procesos cuando / si mi aplicación se ha bloqueado. Pero si elimino mi aplicación desde el Administrador de tareas, los procesos secundarios no se eliminan.

¿Hay alguna forma de hacer que los procesos secundarios dependan del proceso primario?

SiberianGuy
fuente

Respuestas:

176

De este foro , crédito a 'Josh'.

Application.Quit()y Process.Kill()son posibles soluciones, pero han demostrado ser poco confiables. Cuando su aplicación principal muere, todavía le quedan procesos secundarios en ejecución. Lo que realmente queremos es que los procesos secundarios mueran tan pronto como muera el proceso principal.

La solución es utilizar "objetos de trabajo" http://msdn.microsoft.com/en-us/library/ms682409(VS.85).aspx .

La idea es crear un "objeto de trabajo" para su aplicación principal y registrar sus procesos secundarios con el objeto de trabajo. Si el proceso principal muere, el sistema operativo se encargará de finalizar los procesos secundarios.

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

}

Mirando al constructor ...

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

La clave aquí es configurar el objeto de trabajo correctamente. En el constructor estoy estableciendo los "límites" a 0x2000, que es el valor numérico para JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE.

MSDN define esta bandera como:

Hace que todos los procesos asociados con el trabajo finalicen cuando se cierra el último identificador del trabajo.

Una vez que esta clase está configurada ... solo tiene que registrar cada proceso secundario con el trabajo. Por ejemplo:

[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
fuente
44
Añadiría un enlace a CloseHandle
Austin Salonen
66
Desafortunadamente, no pude ejecutar esto en modo de 64 bits. Aquí publiqué un ejemplo de trabajo, que se basa en esto.
Alexander Yezutov
2
@ Matt Howells: ¿dónde se Win32.CloseHandleorigina? ¿Se importa esto desde kernel32.dll? Hay una firma coincidente allí, pero no la está importando explícitamente como las otras funciones API.
Nombre de pantalla esotérico el
66
Para aplicaciones en modo de 64 bits -> stackoverflow.com/a/5976162 Para el problema de Vista / Win7 -> social.msdn.microsoft.com/forums/en-US/windowssecurity/thread/…
hB0
3
Ok, MSDN dice: El indicador JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE requiere el uso de una estructura JOBOBJECT_EXTENDED_LIMIT_INFORMATION.
SerG
54

Esta respuesta comenzó con la excelente respuesta de @Matt Howells más otras (ver enlaces en el código a continuación). Mejoras:

  • Admite 32 bits y 64 bits.
  • Soluciona algunos problemas en la respuesta de @Matt Howells:
    1. La pequeña pérdida de memoria de extendedInfoPtr
    2. El error de compilación 'Win32' y
    3. Una excepción de desequilibrio de pila que recibí en la llamada CreateJobObject(usando Windows 10, Visual Studio 2015, 32 bits).
  • Nombra el trabajo, por lo que si usa SysInternals, por ejemplo, puede encontrarlo fácilmente.
  • Tiene una API algo más simple y menos código.

Aquí se explica cómo usar este código:

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

Para admitir Windows 7 requiere:

En mi caso, no necesitaba admitir Windows 7, así que tengo una simple verificación en la parte superior del constructor estático a continuación.

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

Probé cuidadosamente las versiones de 32 bits y 64 bits de las estructuras comparando mediante programación las versiones administradas y nativas entre sí (el tamaño general y las compensaciones para cada miembro).

He probado este código en Windows 7, 8 y 10.

Ron
fuente
¿Qué pasa con el cierre de manejar el trabajo?
Frank Q.
@FrankQ. Es importante dejar que Windows cierre s_jobHandle para nosotros cuando nuestro proceso finalice, porque nuestro proceso puede finalizar inesperadamente (como un bloqueo o si el usuario usa el Administrador de tareas). Vea mi comentario en s_jobHandle's.
Ron
Me esta funcionando. ¿Puedo preguntar cuál es su opinión sobre el uso de la bandera JOB_OBJECT_LIMIT_BREAKAWAY_OK? docs.microsoft.com/en-us/windows/desktop/api/winnt/…
Yiping
1
@yiping Parece que CREATE_BREAKAWAY_FROM_JOB le permite a su proceso hijo generar un proceso que puede sobrevivir a su proceso original. Ese es un requisito diferente de lo que solicitó el OP. En cambio, si puede hacer que su proceso original genere ese proceso de larga duración (y no usar ChildProcessTracker), eso sería más simple.
Ron
@Ron ¿Puede agregar una sobrecarga que acepte solo el identificador del proceso y no todo el proceso?
Jannik
47

Esta publicación pretende ser una extensión de la respuesta de @Matt Howells, específicamente para aquellos que tienen problemas al usar Job Objects en Vista o Win7 , especialmente si obtiene un error de acceso denegado ('5') al llamar a AssignProcessToJobObject.

tl; dr

Para garantizar la compatibilidad con Vista y Win7, agregue el siguiente manifiesto al proceso padre .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>

Tenga en cuenta que cuando agregue un nuevo manifiesto en Visual Studio 2012, ya contendrá el fragmento anterior, por lo que no necesita copiarlo desde escuchar. También incluirá un nodo para Windows 8.

explicación completa

Su asociación de trabajo fallará con un error de acceso denegado si el proceso que está iniciando ya está asociado con otro trabajo. Ingrese al Asistente de compatibilidad de programas, que, a partir de Windows Vista, asignará todo tipo de procesos a sus propios trabajos.

En Vista, puede marcar su aplicación para que sea excluida de PCA simplemente incluyendo un manifiesto de aplicación. Visual Studio parece hacer esto para las aplicaciones .NET automáticamente, por lo que está bien allí.

Un manifiesto simple ya no lo corta en Win7. [1] Allí, debes especificar específicamente que eres compatible con Win7 con la etiqueta en tu manifiesto. [2]

Esto me llevó a preocuparme por Windows 8. ¿Tendré que cambiar mi manifiesto una vez más? Aparentemente hay una ruptura en las nubes, ya que Windows 8 ahora permite que un proceso pertenezca a múltiples trabajos. [3] Por lo tanto, aún no lo he probado, pero imagino que esta locura terminará ahora si simplemente incluye un manifiesto con la información del sistema operativo compatible.

Consejo 1 : Si está desarrollando una aplicación .NET con Visual Studio, como lo hice yo, aquí [4] hay algunas buenas instrucciones sobre cómo personalizar el manifiesto de su aplicación.

Consejo 2 : Tenga cuidado al iniciar su aplicación desde Visual Studio. Descubrí que, después de agregar el manifiesto apropiado, todavía tenía problemas con PCA al iniciar desde Visual Studio, incluso si usaba Iniciar sin depurar. Sin embargo, el lanzamiento de mi aplicación desde Explorer funcionó. Después de agregar manualmente devenv para la exclusión de PCA usando el registro, las aplicaciones que usaban Job Objects de VS también comenzaron a funcionar. [5]

Consejo 3 : Si alguna vez desea saber si PCA es su problema, intente iniciar su aplicación desde la línea de comandos o copie el programa en una unidad de red y ejecútelo desde allí. PCA se deshabilita automáticamente en esos contextos.

[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-take-2-because-we-changed-the-rules-on-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 : "Un proceso puede estar asociado con más de un trabajo en Windows 8"

[4] ¿Cómo puedo incrustar un manifiesto de aplicación en una aplicación usando VS2008?

[5] ¿Cómo detener el depurador de Visual Studio que comienza mi proceso en un objeto de trabajo?

Adam Smith
fuente
2
Estas son excelentes adiciones al tema, ¡gracias! Hice uso de todos los aspectos de esta respuesta, incluidos los enlaces.
Johnny Kauffman
16

Aquí hay una alternativa que puede funcionar para algunos cuando tiene control del código que ejecuta el proceso secundario. El beneficio de este enfoque es que no requiere ninguna llamada nativa de Windows.

La idea básica es redirigir la entrada estándar del niño a un flujo cuyo otro extremo está conectado al padre y usar ese flujo para detectar cuándo el padre se ha ido. Cuando utiliza System.Diagnostics.Processpara iniciar el elemento secundario, es fácil asegurarse de que su entrada estándar se redirija:

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

Y luego, en el proceso secundario, aproveche el hecho de que Reads del flujo de entrada estándar siempre regresará con al menos 1 byte hasta que se cierre el flujo, cuando comenzarán a devolver 0 bytes. A continuación se muestra un resumen de la forma en que terminé haciendo esto; my way también usa una bomba de mensajes para mantener el hilo principal disponible para otras cosas que no sean mirar estándar, pero este enfoque general podría usarse sin bombas de mensajes también.

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

Advertencias a este enfoque:

  1. el .exe secundario real que se inicia debe ser una aplicación de consola para que permanezca adjunto a stdin / out / err. Como en el ejemplo anterior, adapté fácilmente mi aplicación existente que usaba una bomba de mensajes (pero no mostraba una GUI) simplemente creando un pequeño proyecto de consola que hacía referencia al proyecto existente, instanciando el contexto de mi aplicación y llamando Application.Run()dentro del Mainmétodo del consola .exe.

  2. Técnicamente, esto simplemente indica el proceso hijo cuando el padre sale, por lo que funcionará si el proceso padre salió normalmente o se bloqueó, pero aún depende de los procesos hijos realizar su propio apagado. Esto puede o no ser lo que quieres ...

mbaynton
fuente
11

Una forma es pasar el PID del proceso padre al niño. El hijo sondeará periódicamente si el proceso con el pid especificado existe o no. Si no, simplemente se cerrará.

También puede usar el método Process.WaitForExit en el método secundario para recibir una notificación cuando finalice el proceso principal, pero podría no funcionar en el caso del Administrador de tareas.

Giorgi
fuente
¿Cómo puedo pasar el PID de los padres al niño? ¿Hay alguna solución del sistema? No puedo modificar los binarios de proceso hijo.
SiberianGuy
1
Bueno, si no puede modificar el proceso secundario, no puede usar mi solución incluso si le pasa el PID.
Giorgi
@Idsa Puede pasarlo a través de la línea de comando:Process.Start(string fileName, string arguments)
Distortum
2
En lugar de sondear, puede conectarse al evento Exit en la clase de proceso.
RichardOD
Lo probé pero el proceso padre no siempre sale si tiene hijos vivos (al menos en mi caso Cobol-> NET). Fácil de verificar observando la jerarquía de procesos en Sysinternals ProcessExplorer.
Ivan Ferrer Villa
8

Existe otro método relevante, fácil y efectivo, para finalizar los procesos secundarios al finalizar el programa. Puede implementar y adjuntar un depurador desde el padre; cuando finaliza el proceso principal, el sistema operativo eliminará los procesos secundarios. Puede ir en ambos sentidos adjuntando un depurador al padre desde el hijo (tenga en cuenta que solo puede adjuntar un depurador a la vez). Puede encontrar más información sobre el tema aquí .

Aquí tiene una clase de utilidad que inicia un nuevo proceso y le agrega un depurador. Ha sido adaptado de esta publicación por Roger Knapp. El único requisito es que ambos procesos deben compartir el mismo bitness. No puede depurar un proceso de 32 bits desde un proceso de 64 bits o viceversa.

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

Uso:

    new ProcessRunner().StartProcess("c:\\Windows\\system32\\calc.exe");
Marco Regueira
fuente
8

Estaba buscando una solución a este problema que no requiriera código no administrado. Tampoco pude usar la redirección de entrada / salida estándar porque era una aplicación de formularios Windows Forms.

Mi solución fue crear una tubería con nombre en el proceso primario y luego conectar el proceso secundario a la misma tubería. Si el proceso padre se cierra, la tubería se rompe y el niño puede detectar esto.

A continuación se muestra un ejemplo con dos aplicaciones de consola:

Padre

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

Niño

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
fuente
3

Use controladores de eventos para hacer enlaces en algunos escenarios de salida:

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(); };
Justin Harris
fuente
Tan simple pero tan efectivo.
uncommon_name
2

Solo mi versión 2018. Úselo a un lado de su método 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
fuente
44
Dudo que este es ejecutado cuando el proceso padre se mató .
springy76
1

Veo dos opciones:

  1. Si sabe exactamente qué proceso secundario podría iniciarse y está seguro de que solo se inicia desde su proceso principal, entonces podría considerar simplemente buscarlos por su nombre y eliminarlos.
  2. Itere a través de todos los procesos y elimine todos los procesos que tienen su proceso como padre (supongo que primero debe eliminar los procesos secundarios). Aquí se explica cómo puede obtener la identificación del proceso principal.
Stefan Egli
fuente
1

He creado una biblioteca de gestión de procesos secundarios donde el proceso principal y el proceso secundario se supervisan debido a una tubería WCF bidireccional. Si el proceso secundario finaliza o el proceso primario finaliza entre sí, se notificará. También hay un asistente de depuración disponible que conecta automáticamente el depurador VS al proceso secundario iniciado

Sitio del proyecto:

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

Paquetes NuGet:

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

Thomas Maierhofer
fuente
0

llame a job.AddProcess mejor que hacer después del inicio del proceso:

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

Al llamar a AddProcess antes de finalizar, los procesos secundarios no se eliminan. (Windows 7 SP1)

private void KillProcess(Process proc)
{
    var job = new Job();
    job.AddProcess(proc.Handle);
    job.Close();
}
Alexey
fuente
Llamar a job.AddProcess después del inicio del proceso mataría el propósito de usar este objeto.
SerG
0

Otra adición a la abundante riqueza de soluciones propuestas hasta ahora ...

El problema con muchos de ellos es que dependen del proceso padre e hijo para cerrar de manera ordenada, lo que no siempre es cierto cuando el desarrollo está en marcha. Descubrí que mi proceso hijo a menudo se quedaba huérfano cada vez que terminaba el proceso padre en el depurador, lo que requería que matara los procesos huérfanos con el Administrador de tareas para reconstruir mi solución.

La solución: Pase la identificación del proceso principal en la línea de comandos (o incluso menos invasiva, en las variables de entorno) del proceso secundario.

En el proceso padre, la ID del proceso está disponible como:

  Process.CurrentProcess.Id;

En el proceso hijo:

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

Tengo dos opiniones sobre si esta es una práctica aceptable en el código de producción. Por un lado, esto nunca debería suceder. Pero, por otro lado, puede significar la diferencia entre reiniciar un proceso y reiniciar un servidor de producción. Y lo que nunca debería suceder a menudo lo hace.

Y seguro que es útil al depurar problemas de apagado ordenado.

Robin Davies
fuente