Skip to content

Instantly share code, notes, and snippets.

@AArnott
Last active June 4, 2022 19:49
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save AArnott/2609636d2f2369495abe76e8a01446a4 to your computer and use it in GitHub Desktop.
Save AArnott/2609636d2f2369495abe76e8a01446a4 to your computer and use it in GitHub Desktop.
How to kill child processes when the parent process dies
CreateJobObject
JOBOBJECT_EXTENDED_LIMIT_INFORMATION
SetInformationJobObject
AssignProcessToJobObject
// Copyright (c) Andrew Arnott. All rights reserved.
// Licensed under the MIT license.
/* This is a derivative from multiple answers on https://stackoverflow.com/questions/3342941/kill-child-process-when-parent-process-is-killed */
using System;
using System.ComponentModel;
using System.Diagnostics;
using Microsoft.Win32.SafeHandles;
using Windows.Win32.System.JobObjects;
using static Windows.Win32.PInvoke;
/// <summary>
/// Allows processes to be automatically killed if this parent process unexpectedly quits
/// (or when an instance of this class is disposed).
/// </summary>
/// <remarks>
/// This "just works" on Windows 8.
/// To support Windows Vista or Windows 7 requires an app.manifest with specific content
/// <see href="https://stackoverflow.com/a/9507862/46926">as described here</see>.
/// </remarks>
public class ProcessJobTracker : IDisposable
{
private readonly object disposeLock = new object();
private bool disposed;
/// <summary>
/// The job handle.
/// </summary>
/// <remarks>
/// Closing this handle would close all tracked processes. So we don't do it in this process
/// so that it happens automatically when our process exits.
/// </remarks>
private readonly SafeFileHandle jobHandle;
/// <summary>
/// Initializes a new instance of the <see cref="ProcessJobTracker"/> class.
/// </summary>
public unsafe ProcessJobTracker()
{
#if NET5_0_OR_GREATER
if (OperatingSystem.IsWindowsVersionAtLeast(5, 1, 2600))
#else
if (Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version > new Version(5, 1, 2600))
#endif
{
// 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 = nameof(ProcessJobTracker) + Process.GetCurrentProcess().Id;
this.jobHandle = CreateJobObject(null, jobName);
var extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION
{
LimitFlags = JOB_OBJECT_LIMIT.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
},
};
if (!SetInformationJobObject(this.jobHandle, JOBOBJECTINFOCLASS.JobObjectExtendedLimitInformation, &extendedInfo, (uint)sizeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)))
{
throw new Win32Exception();
}
}
}
/// <summary>
/// Ensures a given process is killed when the current process exits.
/// </summary>
/// <param name="process">The process whose lifetime should never exceed the lifetime of the current process.</param>
public void AddProcess(Process process)
{
if (process is null)
{
throw new ArgumentNullException(nameof(process));
}
#if NET5_0_OR_GREATER
if (OperatingSystem.IsWindowsVersionAtLeast(5, 1, 2600))
#else
if (Environment.OSVersion.Platform == PlatformID.Win32NT && Environment.OSVersion.Version > new Version(5, 1, 2600))
#endif
{
bool success = AssignProcessToJobObject(this.jobHandle, new SafeFileHandle(process.Handle, ownsHandle: false));
if (!success && !process.HasExited)
{
throw new Win32Exception();
}
}
}
/// <summary>
/// Kills all processes previously tracked with <see cref="AddProcess(Process)"/> by closing the Windows Job.
/// </summary>
public void Dispose()
{
this.Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing)
{
lock (this.disposeLock)
{
if (!this.disposed)
{
this.jobHandle?.Dispose();
}
this.disposed = true;
}
}
}
}
<ItemGroup>
<PackageReference Include="Microsoft.Windows.CsWin32" Version="0.1.619-beta" PrivateAssets="all" />
</ItemGroup>
@GieltjE
Copy link

GieltjE commented Apr 8, 2021

Are we missing the PInvoke declarations?

@AArnott
Copy link
Author

AArnott commented Apr 8, 2021

@GieltjE Yes, we were missing the NativeMethods.txt file. I've updated the gist. The .txt file works with the Microsoft.Windows.CsWin32 package to declare the p/invoke methods and supporting types as part of the build.

@Julius-Rapp
Copy link

There is a reference to a LICENSE.txt file in the project root, but I cannot find it. Could you please check copyright and license information?

https://gist.github.com/AArnott/2609636d2f2369495abe76e8a01446a4#file-processjobtracker-cs-L1-L2

@AArnott
Copy link
Author

AArnott commented May 29, 2021

I removed the reference to LICENSE.txt. It's simply the MIT license.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment