-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathWindowsKillOnCloseJob.cs
More file actions
144 lines (126 loc) · 5.26 KB
/
Copy pathWindowsKillOnCloseJob.cs
File metadata and controls
144 lines (126 loc) · 5.26 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using Microsoft.Extensions.Hosting;
namespace Jellyfin.Plugin.JavOrganizer;
/// <summary>
/// Binds a child process to a Win32 Job Object configured with
/// <c>JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE</c>, so the operating system
/// terminates the child — and every process it spawned — the instant this
/// process (the Jellyfin server) dies, no matter how abruptly: graceful
/// shutdown, crash, stack overflow, <c>taskkill /F</c>, End Task or a
/// closed console window. Windows closes the job handle when the owning
/// process is terminated, and the kernel then kills everything that is
/// still inside the job.
/// </summary>
/// <remarks>
/// This closes the gap that <see cref="IHostedService.StopAsync"/> alone
/// cannot: hosted services only run during a *graceful* shutdown. When the
/// server is force-killed the plugin gets no callback at all, and without
/// the job object the child would be orphaned and keep running.
/// </remarks>
internal sealed class WindowsKillOnCloseJob : IDisposable
{
private IntPtr _handle;
private WindowsKillOnCloseJob(IntPtr handle)
{
_handle = handle;
}
/// <summary>
/// Creates a kill-on-close job object and puts the just-started
/// process into it. Returns <c>null</c> on non-Windows platforms,
/// where job objects do not exist and the caller falls back to
/// managing the child purely in software.
/// </summary>
/// <param name="process">The process to bind, straight from <see cref="Process.Start(ProcessStartInfo)"/>.</param>
/// <returns>The bound job, or <c>null</c> when not on Windows.</returns>
/// <exception cref="Win32Exception">The job could not be created or the process could not be assigned to it.</exception>
public static WindowsKillOnCloseJob? TryCreate(Process process)
{
if (!OperatingSystem.IsWindows())
{
return null;
}
var handle = CreateJobObjectW(IntPtr.Zero, IntPtr.Zero);
if (handle == IntPtr.Zero)
{
throw new Win32Exception();
}
var info = default(JOBOBJECT_EXTENDED_LIMIT_INFORMATION);
info.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
if (!SetInformationJobObject(handle, JobObjectExtendedLimitInformation, ref info, (uint)Marshal.SizeOf<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>()))
{
var error = Marshal.GetLastWin32Error();
CloseHandle(handle);
throw new Win32Exception(error);
}
if (!AssignProcessToJobObject(handle, process.Handle))
{
var error = Marshal.GetLastWin32Error();
CloseHandle(handle);
throw new Win32Exception(error);
}
return new WindowsKillOnCloseJob(handle);
}
/// <inheritdoc />
public void Dispose()
{
if (_handle != IntPtr.Zero)
{
// Merely closing the last handle kills every process still in
// the job (that is the whole point of the job object).
CloseHandle(_handle);
_handle = IntPtr.Zero;
}
}
private const int JobObjectExtendedLimitInformation = 9;
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
[DllImport("kernel32.dll", EntryPoint = "CreateJobObjectW", SetLastError = true)]
private static extern IntPtr CreateJobObjectW(IntPtr lpJobAttributes, IntPtr lpName);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetInformationJobObject(
IntPtr hJob,
int jobObjectInfoClass,
ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION lpJobObjectInformation,
uint cbJobObjectInformationLength);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AssignProcessToJobObject(IntPtr hJob, IntPtr hProcess);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr hObject);
[StructLayout(LayoutKind.Sequential)]
private struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
public long PerProcessUserTimeLimit;
public long PerJobUserTimeLimit;
public uint LimitFlags;
public UIntPtr MinimumWorkingSetSize;
public UIntPtr MaximumWorkingSetSize;
public uint ActiveProcessLimit;
public UIntPtr Affinity;
public uint PriorityClass;
public uint SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
private struct IO_COUNTERS
{
public ulong ReadOperationCount;
public ulong WriteOperationCount;
public ulong OtherOperationCount;
public ulong ReadTransferCount;
public ulong WriteTransferCount;
public ulong OtherTransferCount;
}
[StructLayout(LayoutKind.Sequential)]
private 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;
}
}