Untergeordneten Prozess beenden, wenn der übergeordnete Prozess beendet wird

156

Ich erstelle neue Prozesse mit der System.Diagnostics.ProcessKlasse aus meiner Anwendung.

Ich möchte, dass diese Prozesse abgebrochen werden, wenn meine Anwendung abgestürzt ist. Wenn ich meine Anwendung jedoch über den Task-Manager beende, werden untergeordnete Prozesse nicht beendet.

Gibt es eine Möglichkeit, untergeordnete Prozesse vom übergeordneten Prozess abhängig zu machen?

SiberianGuy
quelle

Antworten:

176

Von diesem Forum an 'Josh'.

Application.Quit()und Process.Kill()sind mögliche Lösungen, haben sich aber als unzuverlässig erwiesen. Wenn Ihre Hauptanwendung stirbt, werden immer noch untergeordnete Prozesse ausgeführt. Was wir wirklich wollen, ist, dass die untergeordneten Prozesse sterben, sobald der Hauptprozess stirbt.

Die Lösung besteht darin, "Jobobjekte" http://msdn.microsoft.com/en-us/library/ms682409(VS.85).aspx zu verwenden .

Die Idee ist, ein "Jobobjekt" für Ihre Hauptanwendung zu erstellen und Ihre untergeordneten Prozesse beim Jobobjekt zu registrieren. Wenn der Hauptprozess abbricht, kümmert sich das Betriebssystem um das Beenden der untergeordneten Prozesse.

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

}

Blick auf den Konstruktor ...

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

Der Schlüssel hier ist, das Jobobjekt richtig einzurichten. Im Konstruktor setze ich die "Grenzen" auf 0x2000, was der numerische Wert für ist JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE.

MSDN definiert dieses Flag als:

Bewirkt, dass alle mit dem Job verknüpften Prozesse beendet werden, wenn das letzte Handle für den Job geschlossen wird.

Sobald diese Klasse eingerichtet ist, müssen Sie nur noch jeden untergeordneten Prozess beim Job registrieren. Beispielsweise:

[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
quelle
4
Ich würde einen Link zu CloseHandle
Austin Salonen
6
Leider konnte ich dies nicht im 64-Bit-Modus ausführen. Hier habe ich ein Arbeitsbeispiel gepostet, das darauf basiert.
Alexander Yezutov
2
@ Matt Howells - Woher kommt der Win32.CloseHandleUrsprung? Wird dies aus kernel32.dll importiert? Dort gibt es eine passende Signatur, aber Sie importieren sie nicht explizit wie die anderen API-Funktionen.
Esoterischer Bildschirmname
6
Für 64-Bit-Modus-Apps -> stackoverflow.com/a/5976162 Für Vista / Win7-Problem -> social.msdn.microsoft.com/forums/en-US/windowssecurity/thread/…
hB0
3
Ok, MSDN sagt: Das Flag JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE erfordert die Verwendung einer JOBOBJECT_EXTENDED_LIMIT_INFORMATION-Struktur.
SerG
54

Diese Antwort begann mit der hervorragenden Antwort von @Matt Howells und anderen (siehe Links im Code unten). Verbesserungen:

  • Unterstützt 32-Bit und 64-Bit.
  • Behebt einige Probleme in der Antwort von @Matt Howells:
    1. Der kleine Speicherverlust von extendedInfoPtr
    2. Der Kompilierungsfehler 'Win32' und
    3. Eine stapelunausgeglichene Ausnahme, die ich beim Aufruf von CreateJobObject(unter Windows 10, Visual Studio 2015, 32-Bit) erhalten habe.
  • Benennt den Job. Wenn Sie beispielsweise SysInternals verwenden, können Sie ihn leicht finden.
  • Hat eine etwas einfachere API und weniger Code.

So verwenden Sie diesen Code:

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

Zur Unterstützung von Windows 7 sind folgende Voraussetzungen erforderlich:

In meinem Fall musste ich Windows 7 nicht unterstützen, daher habe ich oben im statischen Konstruktor unten eine einfache Überprüfung.

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

Ich habe sowohl die 32-Bit- als auch die 64-Bit-Version der Strukturen sorgfältig getestet, indem ich die verwaltete und die native Version programmgesteuert miteinander verglichen habe (die Gesamtgröße sowie die Offsets für jedes Mitglied).

Ich habe diesen Code unter Windows 7, 8 und 10 getestet.

Ron
quelle
Was ist mit dem Schließen des Job-Handles?
Frank Q.
@FrankQ. Es ist wichtig, dass Windows s_jobHandle für uns schließt, wenn unser Prozess beendet wird, da unser Prozess möglicherweise unerwartet beendet wird (z. B. durch einen Absturz oder wenn der Benutzer den Task-Manager verwendet). Siehe meinen Kommentar zu s_jobHandle's.
Ron
Es funktioniert für mich. Darf ich fragen, wie Sie zur Verwendung des Flags JOB_OBJECT_LIMIT_BREAKAWAY_OK stehen? docs.microsoft.com/en-us/windows/desktop/api/winnt/…
Yiping
1
@yiping Es klingt so, als ob CREATE_BREAKAWAY_FROM_JOB es Ihrem untergeordneten Prozess ermöglicht, einen Prozess zu erzeugen, der Ihren ursprünglichen Prozess überleben kann. Das ist eine andere Anforderung als die, die das OP verlangt hat. Wenn Sie stattdessen Ihren ursprünglichen Prozess dazu bringen können, diesen langlebigen Prozess hervorzubringen (und ChildProcessTracker nicht verwenden), wäre dies einfacher.
Ron
@Ron Können Sie eine Überladung hinzufügen, die nur das Prozesshandle und nicht den gesamten Prozess akzeptiert?
Jannik
47

Dieser Beitrag ist als Erweiterung der Antwort von @Matt Howells gedacht, insbesondere für diejenigen, die Probleme mit der Verwendung von Jobobjekten unter Vista oder Win7 haben , insbesondere wenn beim Aufrufen von AssignProcessToJobObject der Fehler "Zugriff verweigert" ('5') angezeigt wird.

tl; dr

Fügen Sie dem übergeordneten .NET-Prozess das folgende Manifest hinzu, um die Kompatibilität mit Vista und Win7 sicherzustellen:

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

Beachten Sie, dass beim Hinzufügen eines neuen Manifests in Visual Studio 2012 das obige Snippet bereits enthalten ist, sodass Sie es nicht aus Hear kopieren müssen. Es wird auch einen Knoten für Windows 8 enthalten.

vollständige Erklärung

Ihre Jobzuordnung schlägt mit einem Fehler "Zugriff verweigert" fehl, wenn der von Ihnen gestartete Prozess bereits einem anderen Job zugeordnet ist. Rufen Sie den Programmkompatibilitäts-Assistenten auf, der ab Windows Vista alle Arten von Prozessen seinen eigenen Jobs zuweist.

In Vista können Sie Ihre Anwendung als von PCA ausgeschlossen markieren, indem Sie einfach ein Anwendungsmanifest einfügen. Visual Studio scheint dies für .NET-Apps automatisch zu tun, sodass Sie dort in Ordnung sind.

Ein einfaches Manifest schneidet es in Win7 nicht mehr. [1] Dort müssen Sie speziell angeben, dass Sie mit Win7 mit dem Tag in Ihrem Manifest kompatibel sind. [2]

Dies führte dazu, dass ich mir Sorgen um Windows 8 machte. Muss ich mein Manifest noch einmal ändern? Anscheinend gibt es eine Unterbrechung in den Clouds, da Windows 8 jetzt zulässt, dass ein Prozess zu mehreren Jobs gehört. [3] Also habe ich es noch nicht getestet, aber ich stelle mir vor, dass dieser Wahnsinn jetzt vorbei sein wird, wenn Sie einfach ein Manifest mit den unterstützten OS-Informationen hinzufügen.

Tipp 1 : Wenn Sie wie ich eine .NET-App mit Visual Studio entwickeln, finden Sie hier [4] einige nützliche Anweisungen zum Anpassen Ihres Anwendungsmanifests.

Tipp 2 : Seien Sie vorsichtig beim Starten Ihrer Anwendung in Visual Studio. Ich stellte fest, dass ich nach dem Hinzufügen des entsprechenden Manifests beim Starten von Visual Studio immer noch Probleme mit PCA hatte, selbst wenn ich Start ohne Debugging verwendet habe. Das Starten meiner Anwendung über den Explorer hat jedoch funktioniert. Nach dem manuellen Hinzufügen von devenv zum Ausschluss von PCA mithilfe der Registrierung funktionierten auch das Starten von Anwendungen, die Jobobjekte von VS verwendeten. [5]

Tipp 3 : Wenn Sie jemals wissen möchten, ob PCA Ihr Problem ist, starten Sie Ihre Anwendung über die Befehlszeile oder kopieren Sie das Programm auf ein Netzwerklaufwerk und führen Sie es von dort aus aus. PCA wird in diesen Kontexten automatisch deaktiviert.

[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-weil-wir-die-Regeln-für-dich-geändert haben.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 : "Ein Prozess kann in Windows 8 mehreren Jobs zugeordnet werden."

[4] Wie kann ich mit VS2008 ein Anwendungsmanifest in eine Anwendung einbetten?

[5] Wie kann ich verhindern, dass der Visual Studio-Debugger meinen Prozess in einem Jobobjekt startet?

Adam Smith
quelle
2
Dies sind großartige Ergänzungen zum Thema, danke! Ich habe jeden Aspekt dieser Antwort genutzt, einschließlich der Links.
Johnny Kauffman
16

Hier ist eine Alternative, die für einige möglicherweise funktioniert, wenn Sie die Kontrolle über den Code haben, den der untergeordnete Prozess ausführt. Der Vorteil dieses Ansatzes besteht darin, dass keine nativen Windows-Aufrufe erforderlich sind.

Die Grundidee besteht darin, die Standardeingabe des Kindes in einen Stream umzuleiten, dessen anderes Ende mit dem übergeordneten Element verbunden ist, und diesen Stream zu verwenden, um zu erkennen, wann das übergeordnete Element verschwunden ist. Wenn Sie das untergeordnete System.Diagnostics.ProcessElement starten, können Sie leicht sicherstellen, dass die Standardeingabe umgeleitet wird:

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

Nutzen Sie dann beim untergeordneten Prozess die Tatsache, dass Reads aus dem Standardeingabestream immer mit mindestens 1 Byte zurückgegeben wird, bis der Stream geschlossen wird und dann 0 Byte zurückgegeben werden. Ein Überblick darüber, wie ich dies getan habe, ist unten aufgeführt. Mein Weg verwendet auch eine Nachrichtenpumpe, um den Haupt-Thread für andere Dinge als das Beobachten von Standard in verfügbar zu halten, aber dieser allgemeine Ansatz könnte auch ohne Nachrichtenpumpen verwendet werden.

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

Vorsichtsmaßnahmen für diesen Ansatz:

  1. Die tatsächlich untergeordnete .exe, die gestartet wird, muss eine Konsolenanwendung sein, damit sie an stdin / out / err angehängt bleibt. Wie im obigen Beispiel habe ich meine vorhandene Anwendung, die eine Nachrichtenpumpe verwendete (aber keine GUI zeigte), einfach angepasst, indem ich nur ein kleines Konsolenprojekt erstellt habe, das auf das vorhandene Projekt verweist, meinen Anwendungskontext instanziiert und Application.Run()die MainMethode von aufgerufen hat Konsole .exe.

  2. Technisch gesehen signalisiert dies lediglich den untergeordneten Prozess, wenn der übergeordnete Prozess beendet wird. Es funktioniert also unabhängig davon, ob der übergeordnete Prozess normal beendet wurde oder abgestürzt ist. Es liegt jedoch weiterhin an den untergeordneten Prozessen, das eigene Herunterfahren durchzuführen. Dies kann oder kann nicht sein, was Sie wollen ...

mbaynton
quelle
11

Eine Möglichkeit besteht darin, die PID des übergeordneten Prozesses an das untergeordnete Element zu übergeben. Das Kind fragt regelmäßig ab, ob der Prozess mit der angegebenen PID vorhanden ist oder nicht. Wenn nicht, wird es einfach beendet.

Sie können die Process.WaitForExit- Methode auch in der untergeordneten Methode verwenden, um benachrichtigt zu werden, wenn der übergeordnete Prozess endet. Im Task-Manager funktioniert dies jedoch möglicherweise nicht.

Giorgi
quelle
Wie kann ich die PID des Elternteils an das Kind weitergeben? Gibt es eine Systemlösung? Ich kann untergeordnete Prozess-Binärdateien nicht ändern.
SiberianGuy
1
Wenn Sie den untergeordneten Prozess nicht ändern können, können Sie meine Lösung auch dann nicht verwenden, wenn Sie die PID an sie übergeben.
Giorgi
@ Idsa Sie können es über die Befehlszeile übergeben:Process.Start(string fileName, string arguments)
Distortum
2
Anstatt abzufragen, können Sie sich in das Exit-Ereignis der Prozessklasse einbinden.
RichardOD
Versuchte es, aber der übergeordnete Prozess wird nicht immer beendet, wenn Kinder am Leben sind (zumindest in meinem Fall Cobol-> NET). Einfache Überprüfung der Prozesshierarchie in Sysinternals ProcessExplorer.
Ivan Ferrer Villa
8

Es gibt eine andere relevante Methode, einfach und effektiv, um untergeordnete Prozesse beim Beenden des Programms abzuschließen. Sie können einen Debugger vom übergeordneten Element implementieren und an ihn anhängen . Wenn der übergeordnete Prozess endet, werden untergeordnete Prozesse vom Betriebssystem beendet. Es kann in beide Richtungen gehen, indem ein Debugger vom Kind an das übergeordnete Element angehängt wird (beachten Sie, dass Sie jeweils nur einen Debugger anhängen können). Weitere Informationen zu diesem Thema finden Sie hier .

Hier haben Sie eine Dienstprogrammklasse, die einen neuen Prozess startet und einen Debugger an ihn anfügt. Es wurde aus diesem Beitrag von Roger Knapp angepasst . Die einzige Voraussetzung ist, dass beide Prozesse die gleiche Bitness haben müssen. Sie können einen 32-Bit-Prozess nicht von einem 64-Bit-Prozess debuggen oder umgekehrt.

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

Verwendung:

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

Ich suchte nach einer Lösung für dieses Problem, für die kein nicht verwalteter Code erforderlich war. Ich konnte auch keine Standard-Eingabe- / Ausgabeumleitung verwenden, da es sich um eine Windows Forms-Anwendung handelte.

Meine Lösung bestand darin, im übergeordneten Prozess eine Named Pipe zu erstellen und dann den untergeordneten Prozess mit derselben Pipe zu verbinden. Wenn der übergeordnete Prozess beendet wird, wird die Pipe unterbrochen und das untergeordnete Element kann dies erkennen.

Unten sehen Sie ein Beispiel mit zwei Konsolenanwendungen:

Elternteil

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

Kind

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

Verwenden Sie Ereignishandler , um Hooks für einige Exit-Szenarien zu erstellen:

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
quelle
So einfach und doch so effektiv.
ungewöhnlicher_name
2

Nur meine Version 2018. Verwenden Sie es neben Ihrer Main () -Methode.

    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
quelle
4
Ich bezweifle , dass dies ausgeführt wird , wenn der übergeordnete Prozess wird getötet .
springy76
1

Ich sehe zwei Möglichkeiten:

  1. Wenn Sie genau wissen, welcher untergeordnete Prozess gestartet werden könnte, und Sie sicher sind, dass sie nur von Ihrem Hauptprozess aus gestartet werden, können Sie sie einfach nach Namen suchen und töten.
  2. Durchlaufen Sie alle Prozesse und beenden Sie jeden Prozess, der Ihren Prozess als übergeordnetes Element hat (ich denke, Sie müssen zuerst die untergeordneten Prozesse beenden). Hier wird erklärt, wie Sie die übergeordnete Prozess-ID erhalten können.
Stefan Egli
quelle
1

Ich habe eine untergeordnete Prozessverwaltungsbibliothek erstellt, in der der übergeordnete Prozess und der untergeordnete Prozess aufgrund einer bidirektionalen WCF-Pipe überwacht werden. Wenn entweder der untergeordnete Prozess oder der übergeordnete Prozess sich gegenseitig beendet, wird dies benachrichtigt. Es ist auch ein Debugger-Helfer verfügbar, der den VS-Debugger automatisch an den gestarteten untergeordneten Prozess anfügt

Projektseite:

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

NuGet-Pakete:

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

Thomas Maierhofer
quelle
0

Rufen Sie job.AddProcess nach dem Start des Prozesses besser auf:

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

Wenn Sie AddProcess vor dem Beenden aufrufen, werden untergeordnete Prozesse nicht beendet. (Windows 7 SP1)

private void KillProcess(Process proc)
{
    var job = new Job();
    job.AddProcess(proc.Handle);
    job.Close();
}
Alexey
quelle
Das Aufrufen von job.AddProcess nach dem Start des Prozesses würde den Zweck der Verwendung dieses Objekts zunichte machen.
SerG
0

Noch eine Ergänzung zu der Fülle der bisher vorgeschlagenen Lösungen ....

Das Problem bei vielen von ihnen ist, dass sie sich darauf verlassen, dass der Eltern- und Kinderprozess ordnungsgemäß heruntergefahren wird, was bei der Entwicklung nicht immer der Fall ist. Ich stellte fest, dass mein untergeordneter Prozess häufig verwaist war, wenn ich den übergeordneten Prozess im Debugger beendete. Daher musste ich die verwaisten Prozesse mit dem Task-Manager beenden, um meine Lösung neu zu erstellen.

Die Lösung: Übergeben Sie die übergeordnete Prozess-ID in der Befehlszeile (oder noch weniger invasiv in den Umgebungsvariablen) des untergeordneten Prozesses.

Im übergeordneten Prozess ist die Prozess-ID verfügbar als:

  Process.CurrentProcess.Id;

Im untergeordneten Prozess:

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

Ich bin mir nicht sicher, ob dies im Produktionscode akzeptabel ist. Einerseits sollte dies niemals passieren. Andererseits kann dies den Unterschied zwischen dem Neustart eines Prozesses und dem Neustart eines Produktionsservers bedeuten. Und was niemals passieren sollte, tut es oft.

Und es ist sicher nützlich, wenn Sie Probleme beim ordnungsgemäßen Herunterfahren beheben.

Robin Davies
quelle