Zabij proces potomny, gdy proces nadrzędny zostanie zabity

156

Tworzę nowe procesy za pomocą System.Diagnostics.Processklasy z mojej aplikacji.

Chcę, aby te procesy zostały zabite, gdy / jeśli moja aplikacja uległa awarii. Ale jeśli zabiję moją aplikację z Menedżera zadań, procesy potomne nie zostaną zabite.

Czy istnieje sposób, aby uzależnić procesy potomne od procesu nadrzędnego?

SiberianGuy
źródło

Odpowiedzi:

176

Z tego forum , uznanie dla „Josha”.

Application.Quit()i Process.Kill()są możliwymi rozwiązaniami, ale okazały się niewiarygodne. Kiedy główna aplikacja umiera, nadal pozostają uruchomione procesy potomne. To, czego naprawdę chcemy, to aby dziecko umierało, gdy tylko główny proces umrze.

Rozwiązaniem jest użycie „obiektów zadań” http://msdn.microsoft.com/en-us/library/ms682409(VS.85).aspx .

Pomysł polega na utworzeniu „obiektu zadania” dla głównej aplikacji i zarejestrowaniu procesów podrzędnych w obiekcie zadania. Jeśli główny proces umrze, system operacyjny zajmie się zakończeniem procesów potomnych.

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

}

Patrząc na konstruktora ...

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

Kluczem jest tutaj prawidłowe ustawienie obiektu zadania. W konstruktorze ustawiam „limity” na 0x2000, co jest wartością liczbową JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE.

MSDN definiuje tę flagę jako:

Powoduje, że wszystkie procesy związane z zadaniem kończą się, gdy ostatnie dojście do zadania zostaje zamknięte.

Po skonfigurowaniu tej klasy ... wystarczy zarejestrować każdy proces potomny w zadaniu. Na przykład:

[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
źródło
4
Dodałbym link do CloseHandle
Austin Salonen
6
Niestety nie mogłem tego uruchomić w trybie 64-bitowym. Tutaj zamieściłem działający przykład, który jest na tym oparty.
Alexander Yezutov
2
@Matt Howells - Skąd się bierze Win32.CloseHandle? Czy to jest importowane z kernel32.dll? Jest tam pasujący podpis, ale nie importujesz go jawnie, tak jak inne funkcje API.
Ezoteryczna nazwa ekranowa
6
W przypadku aplikacji w trybie 64-bitowym -> stackoverflow.com/a/5976162 W przypadku problemów z systemem Vista / Win7 -> social.msdn.microsoft.com/forums/en-US/windowssecurity/thread/…
hB0
3
OK, MSDN mówi: Flaga JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE wymaga użycia struktury JOBOBJECT_EXTENDED_LIMIT_INFORMATION.
SerG
54

Ta odpowiedź zaczęła się od doskonałej odpowiedzi @Matt Howells oraz innych (zobacz linki w poniższym kodzie). Ulepszenia:

  • Obsługuje wersje 32-bitowe i 64-bitowe.
  • Naprawia niektóre problemy w odpowiedzi @Matt Howells:
    1. Mały wyciek pamięci extendedInfoPtr
    2. Błąd kompilacji „Win32” i
    3. Wyjątek niezbalansowanego stosu, który otrzymałem w wywołaniu CreateJobObject(w systemie Windows 10, Visual Studio 2015, 32-bitowy).
  • Nazywa zadanie, więc jeśli korzystasz na przykład z SysInternals, możesz je łatwo znaleźć.
  • Ma nieco prostszy interfejs API i mniej kodu.

Oto jak używać tego kodu:

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

Do obsługi systemu Windows 7 wymagane są:

W moim przypadku nie musiałem obsługiwać systemu Windows 7, więc mam proste sprawdzenie u góry konstruktora statycznego poniżej.

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

Dokładnie przetestowałem zarówno 32-bitowe, jak i 64-bitowe wersje struktur, porównując programowo wersje zarządzane i natywne ze sobą (całkowity rozmiar oraz przesunięcia dla każdego elementu członkowskiego).

Przetestowałem ten kod na Windows 7, 8 i 10.

Ron
źródło
co z zamknięciem stanowiska pracy?
Frank Q.
@FrankQ. Ważne jest, aby system Windows zamknął s_jobHandle za nas, gdy nasz proces się zakończy, ponieważ nasz proces może się nieoczekiwanie zakończyć (na przykład w wyniku awarii lub jeśli użytkownik korzysta z Menedżera zadań). Zobacz mój komentarz na temat s_jobHandle.
Ron
To działa dla mnie. Czy mogę zapytać, co sądzisz o używaniu flagi JOB_OBJECT_LIMIT_BREAKAWAY_OK? docs.microsoft.com/en-us/windows/desktop/api/winnt/…
Yiping
1
@yiping Wygląda na to, że CREATE_BREAKAWAY_FROM_JOB pozwala twojemu dziecku na utworzenie procesu, który może przeżyć twój oryginalny proces. To inny wymóg niż ten, o który prosił OP. Zamiast tego, jeśli możesz sprawić, by twój oryginalny proces odrodził ten długotrwały proces (i nie używałeś ChildProcessTracker), byłoby to prostsze.
Ron
@Ron Czy możesz dodać przeciążenie, które akceptuje tylko uchwyt procesu, a nie cały proces?
Jannik
47

Ten post ma na celu rozszerzenie odpowiedzi @Matt Howells, szczególnie dla tych, którzy mają problemy z używaniem Job Objects pod Vista lub Win7 , zwłaszcza jeśli otrzymujesz błąd odmowy dostępu ('5') podczas wywoływania AssignProcessToJobObject.

tl; dr

Aby zapewnić zgodność z systemami Vista i Win7, dodaj następujący manifest do procesu nadrzędnego .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>

Zwróć uwagę, że po dodaniu nowego manifestu w programie Visual Studio 2012 będzie on już zawierał powyższy fragment, więc nie musisz go kopiować z funkcji Hear. Będzie również zawierał węzeł dla systemu Windows 8.

pełne wyjaśnienie

Twoje powiązanie zadania zakończy się niepowodzeniem i zostanie wyświetlony błąd odmowy dostępu, jeśli uruchamiany proces jest już powiązany z innym zadaniem. Wejdź do Asystenta zgodności programów, który począwszy od systemu Windows Vista przypisze wszystkie rodzaje procesów do własnych zadań.

W systemie Vista możesz oznaczyć swoją aplikację jako wykluczoną z PCA, po prostu dołączając manifest aplikacji. Wydaje się, że Visual Studio robi to automatycznie dla aplikacji .NET, więc wszystko w porządku.

Prosty manifest nie działa już w systemie Win7. [1] Tam musisz konkretnie określić, że jesteś kompatybilny z Win7 za pomocą tagu w swoim manifeście. [2]

To spowodowało, że zacząłem martwić się systemem Windows 8. Czy będę musiał ponownie zmienić manifest? Najwyraźniej w chmurach jest przerwa, ponieważ Windows 8 pozwala teraz na przynależność procesu do wielu zadań. [3] Więc jeszcze tego nie testowałem, ale wyobrażam sobie, że to szaleństwo minie, jeśli po prostu dołączysz manifest z informacjami o obsługiwanym systemie operacyjnym.

Wskazówka 1 : Jeśli tworzysz aplikację .NET w Visual Studio, tak jak ja, tutaj [4] znajdziesz kilka przydatnych instrukcji, jak dostosować manifest aplikacji.

Porada 2 : Zachowaj ostrożność podczas uruchamiania aplikacji z programu Visual Studio. Stwierdziłem, że po dodaniu odpowiedniego manifestu nadal miałem problemy z PCA podczas uruchamiania z Visual Studio, nawet jeśli użyłem Start bez debugowania. Uruchomienie mojej aplikacji z Eksploratora zadziałało jednak. Po ręcznym dodaniu devenv w celu wykluczenia z PCA przy użyciu rejestru, zaczęło działać również uruchamianie aplikacji korzystających z obiektów zadań z VS. [5]

Wskazówka 3 : Jeśli kiedykolwiek chcesz wiedzieć, czy problem dotyczy PCA, spróbuj uruchomić aplikację z wiersza poleceń lub skopiuj program na dysk sieciowy i uruchom go stamtąd. W tych kontekstach PCA jest automatycznie wyłączane.

[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-ponieważ-zmieniliśmy-zasady-na-ciebie.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 : „Proces może być powiązany z więcej niż jednym zadaniem w systemie Windows 8”

[4] Jak mogę osadzić manifest aplikacji w aplikacji przy użyciu VS2008?

[5] Jak zatrzymać debuger programu Visual Studio, uruchamiając mój proces w obiekcie zadania?

Adam Smith
źródło
2
To świetne dodatki do tematu, dziękuję! Wykorzystałem każdy aspekt tej odpowiedzi, w tym linki.
Johnny Kauffman,
16

Oto alternatywa, która może zadziałać dla niektórych, gdy masz kontrolę nad kodem uruchamianym przez proces potomny. Zaletą tego podejścia jest to, że nie wymaga żadnych natywnych wywołań systemu Windows.

Podstawowym pomysłem jest przekierowanie standardowego wejścia dziecka do strumienia, którego drugi koniec jest połączony z rodzicem, i użycie tego strumienia do wykrycia, kiedy rodzic odszedł. Kiedy używasz System.Diagnostics.Processdo uruchomienia dziecka, łatwo jest upewnić się, że jego standardowe wejście zostanie przekierowane:

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

A następnie, w procesie potomnym, wykorzystaj fakt, że Reads ze standardowego strumienia wejściowego zawsze będzie zwracać co najmniej 1 bajt, dopóki strumień nie zostanie zamknięty, kiedy zaczną zwracać 0 bajtów. Poniżej znajduje się zarys sposobu, w jaki to zrobiłem; mój sposób używa również pompy wiadomości, aby główny wątek był dostępny dla rzeczy innych niż oglądanie standardu w, ale to ogólne podejście może być również używane bez pomp wiadomości.

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

Ostrzeżenia dotyczące tego podejścia:

  1. rzeczywisty uruchamiany podrzędny plik .exe musi być aplikacją konsolową, więc pozostaje dołączony do stdin / out / err. Jak w powyższym przykładzie, z łatwością dostosowałem moją istniejącą aplikację, która korzystała z pompy komunikatów (ale nie wyświetlała GUI), po prostu tworząc niewielki projekt konsoli, który odwoływał się do istniejącego projektu, tworząc instancję mojego kontekstu aplikacji i wywołując Application.Run()wewnątrz Mainmetody konsola .exe.

  2. Technicznie rzecz biorąc, to tylko sygnalizuje procesowi podrzędnemu, kiedy wychodzi rodzic, więc będzie działać, niezależnie od tego, czy proces nadrzędny zakończył normalnie, czy się zawiesił, ale nadal to procesy potomne muszą wykonać własne zamknięcie. To może być to, czego chcesz, ale nie musi ...

mbaynton
źródło
11

Jednym ze sposobów jest przekazanie dziecku PID procesu rodzica. Dziecko będzie okresowo odpytywać, czy proces z określonym pid istnieje, czy nie. Jeśli nie, po prostu przestanie.

Możesz także użyć metody Process.WaitForExit w metodzie potomnej, aby otrzymać powiadomienie o zakończeniu procesu nadrzędnego, ale może to nie działać w przypadku Menedżera zadań.

Giorgi
źródło
Jak mogę przekazać dziecku PID rodzica? Czy jest jakieś rozwiązanie systemowe? Nie mogę modyfikować plików binarnych procesów potomnych.
SiberianGuy,
1
Cóż, jeśli nie możesz zmodyfikować procesu potomnego, nie możesz użyć mojego rozwiązania, nawet jeśli przekażesz mu PID.
Giorgi
@Idsa Możesz to przekazać za pomocą wiersza poleceń:Process.Start(string fileName, string arguments)
Distortum
2
Zamiast odpytywania możesz podłączyć się do zdarzenia Exit w klasie procesu.
RichardOD
Próbowałem, ale proces nadrzędny nie zawsze kończy się, jeśli ma żywe dzieci (przynajmniej w moim przypadku Cobol-> NET). Łatwe do sprawdzenia obserwowanie hierarchii procesów w Sysinternals ProcessExplorer.
Ivan Ferrer Villa
8

Jest jeszcze jedna odpowiednia metoda, łatwa i skuteczna, aby zakończyć procesy potomne po zakończeniu programu. Możesz zaimplementować i dołączyć do nich debugger od rodzica; po zakończeniu procesu nadrzędnego procesy potomne zostaną zabite przez system operacyjny. Może to działać w obie strony, dołączając debuger do rodzica od dziecka (pamiętaj, że możesz dołączyć tylko jeden debuger na raz). Więcej informacji na ten temat znajdziesz tutaj .

Tutaj masz klasę narzędziową, która uruchamia nowy proces i dołącza do niego debugger. Został zaadaptowany na podstawie tego postu przez Rogera Knappa. Jedynym wymaganiem jest to, że oba procesy muszą mieć tę samą bitowość. Nie można debugować procesu 32-bitowego z procesu 64-bitowego ani odwrotnie.

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

Stosowanie:

    new ProcessRunner().StartProcess("c:\\Windows\\system32\\calc.exe");
Marco Regueira
źródło
8

Szukałem rozwiązania tego problemu, które nie wymagałoby niezarządzanego kodu. Nie mogłem również użyć standardowego przekierowania wejścia / wyjścia, ponieważ była to aplikacja Windows Forms.

Moim rozwiązaniem było utworzenie nazwanego potoku w procesie nadrzędnym, a następnie połączenie procesu potomnego z tym samym potokiem. Jeśli proces macierzysty zostanie zamknięty, potok zostanie przerwany i dziecko może to wykryć.

Poniżej znajduje się przykład użycia dwóch aplikacji konsolowych:

Rodzic

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

Dziecko

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
źródło
3

Użyj programów obsługi zdarzeń, aby utworzyć punkty zaczepienia w kilku scenariuszach wyjścia:

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
źródło
Tak proste, ale tak skuteczne.
uncommon_name
2

Tylko moja wersja 2018. Użyj go poza swoją metodą 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
źródło
4
Wątpię, żeby to zostało wykonane, gdy proces macierzysty jest zabijany .
springy76
1

Widzę dwie opcje:

  1. Jeśli wiesz dokładnie, jaki proces potomny może zostać uruchomiony i jesteś pewien, że zostały one uruchomione tylko z Twojego procesu głównego, możesz po prostu rozważyć wyszukanie ich po nazwie i zabicie.
  2. Powtarzaj wszystkie procesy i zabij każdy proces, który ma twój proces jako rodzica (myślę, że najpierw musisz zabić procesy potomne). Tutaj wyjaśniono, jak uzyskać identyfikator procesu nadrzędnego.
Stefan Egli
źródło
1

Utworzyłem bibliotekę zarządzania procesami podrzędnymi, w której proces nadrzędny i proces podrzędny są monitorowane z powodu dwukierunkowego potoku WCF. Jeśli albo proces potomny zostanie zakończony, albo proces nadrzędny, zostanie o tym powiadomiony. Dostępny jest również pomocnik debugera, który automatycznie dołącza debuger VS do uruchomionego procesu podrzędnego

Strona projektu:

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

Pakiety NuGet:

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

Thomas Maierhofer
źródło
0

call job.AddProcess lepiej zrobić po rozpoczęciu procesu:

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

Podczas wywoływania AddProcess przed zakończeniem procesy potomne nie są zabijane. (Windows 7 z dodatkiem SP1)

private void KillProcess(Process proc)
{
    var job = new Job();
    job.AddProcess(proc.Handle);
    job.Close();
}
Alexey
źródło
Wywołanie job.AddProcess po uruchomieniu procesu zabiłoby cel używania tego obiektu.
SerG
0

Kolejny dodatek do bogactwa dotychczas proponowanych rozwiązań ...

Problem z wieloma z nich polega na tym, że polegają na procesie rodziców i dziecka, aby zamknęli się w uporządkowany sposób, co nie zawsze jest prawdą, gdy rozwój jest w toku. Zauważyłem, że mój proces podrzędny był często osierocony za każdym razem, gdy kończyłem proces nadrzędny w debugerze, co wymagało ode mnie zabicia osieroconych procesów za pomocą Menedżera zadań w celu odbudowania rozwiązania.

Rozwiązanie: Przekaż identyfikator procesu nadrzędnego w wierszu poleceń (lub nawet mniej inwazyjnie w zmiennych środowiskowych) procesu potomnego.

W procesie nadrzędnym identyfikator procesu jest dostępny jako:

  Process.CurrentProcess.Id;

W procesie dziecięcym:

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

Nie mam zdania, czy jest to dopuszczalna praktyka w kodzie produkcyjnym. Z jednej strony to nigdy nie powinno mieć miejsca. Z drugiej strony może to oznaczać różnicę między ponownym uruchomieniem procesu a ponownym uruchomieniem serwera produkcyjnego. A to, co nigdy nie powinno się zdarzyć, często się dzieje.

I z pewnością jest przydatny podczas debugowania problemów z uporządkowanym zamykaniem.

Robin Davies
źródło