実装例
こちらやここのコメント欄などが参考になりそうです。今回はQiitaの記事のコードを使わせて頂きました。
public class JobObject : IDisposable { public static JobObject CreateAsKillOnJobClose() { var job = new JobObject(); var jobInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION() { BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION() { LimitFlags = JobObjectLimit.KillOnJobClose }, }; var size = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION)); if (!Native.SetInformationJobObject(job.SafeHandle, JobObjectInfoClass.ExtendedLimitInformation, ref jobInfo, size)) { throw new Win32Exception(); } return job; } private JobObject(SafeJobHandle safeHandle) => SafeHandle = safeHandle; private JobObject(string name = null) { SafeHandle = Native.CreateJobObject(IntPtr.Zero, name); if (SafeHandle.IsInvalid) throw new Win32Exception(); } public void AssignProcess(int processId) { using (var hProcess = Native.OpenProcess(ProcessAccessFlags.SetQuota | ProcessAccessFlags.Terminate, false, processId)) { AssignProcess(hProcess); } } public void AssignProcess(System.Diagnostics.Process process) => AssignProcess(process.SafeHandle); public void AssignProcess(SafeProcessHandle hProcess) { if (hProcess.IsInvalid) throw new ArgumentException(nameof(hProcess)); if (!Native.AssignProcessToJobObject(SafeHandle, hProcess)) { throw new Win32Exception(); } } public void Terminate(int exitCode = 0) { if (!Native.TerminateJobObject(SafeHandle, exitCode)) { throw new Win32Exception(); } } public SafeJobHandle SafeHandle { get; private set; } #region IDisposable Support private bool _disposedValue = false; protected virtual void Dispose(bool disposing) { if (!_disposedValue) { if (disposing) { SafeHandle.Dispose(); } _disposedValue = true; } } public void Dispose() { Dispose(true); } #endregion } public sealed class SafeJobHandle : SafeHandleZeroOrMinusOneIsInvalid { internal static SafeJobHandle InvalidHandle => new SafeJobHandle(IntPtr.Zero); private SafeJobHandle() : base(true) { } internal SafeJobHandle(IntPtr handle) : base(true) => base.SetHandle(handle); protected override bool ReleaseHandle() => Native.CloseHandle(base.handle); } [Flags] enum ProcessAccessFlags : uint { None = 0, All = 0x001F0FFF, Terminate = 0x00000001, CreateThread = 0x00000002, VirtualMemoryOperation = 0x00000008, VirtualMemoryRead = 0x00000010, VirtualMemoryWrite = 0x00000020, DuplicateHandle = 0x00000040, CreateProcess = 0x000000080, SetQuota = 0x00000100, SetInformation = 0x00000200, QueryInformation = 0x00000400, QueryLimitedInformation = 0x00001000, Synchronize = 0x00100000, } enum JobObjectInfoClass { AssociateCompletionPortInformation = 7, BasicLimitInformation = 2, BasicUIRestrictions = 4, EndOfJobTimeInformation = 6, ExtendedLimitInformation = 9, SecurityLimitInformation = 5, GroupInformation = 11, } [Flags] enum JobObjectLimit : uint { None = 0, // Basic Limits Workingset = 0x00000001, ProcessTime = 0x00000002, JobTime = 0x00000004, ActiveProcess = 0x00000008, Affinity = 0x00000010, PriorityClass = 0x00000020, PreserveJobTime = 0x00000040, SchedulingClass = 0x00000080, // Extended Limits ProcessMemory = 0x00000100, JobMemory = 0x00000200, DieOnUnhandledException = 0x00000400, BreakawayOk = 0x00000800, SilentBreakawayOk = 0x00001000, KillOnJobClose = 0x00002000, SubsetAffinity = 0x00004000, // Notification Limits JobReadBytes = 0x00010000, JobWriteBytes = 0x00020000, RateControl = 0x00040000, } [StructLayout(LayoutKind.Sequential)] 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; } [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 UIntPtr ProcessMemoryLimit; public UIntPtr JobMemoryLimit; public UIntPtr PeakProcessMemoryUsed; public UIntPtr PeakJobMemoryUsed; } static class Native { [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool SetInformationJobObject(SafeJobHandle hJob, JobObjectInfoClass JobObjectInfoClass, ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION lpJobObjectInfo, int cbJobObjectInfoLength); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public static extern bool AssignProcessToJobObject(SafeJobHandle hJob, SafeProcessHandle hProcess); [DllImport("kernel32.dll", SetLastError = true)] public static extern SafeProcessHandle OpenProcess( ProcessAccessFlags processAccess, bool bInheritHandle, int processId); [DllImport("kernel32.dll", SetLastError = true)] public static extern SafeJobHandle CreateJobObject(IntPtr lpJobAttributes, string lpName); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public extern static bool CloseHandle(IntPtr handle); [DllImport("kernel32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] public extern static bool TerminateJobObject(SafeJobHandle handle, int uExitCode); }
使い方は以下の通りで、Disposeが呼ばれるとProcessが終了します。
process.Start();
process.WaitForInputIdle();
using(job = JobObject.CreateAsKillOnJobClose())
{
job.AssignProcess(process);
}
ただしこのままだと子プロセス側は強制終了させられちゃうので、通常時に子プロセスを Kill するような終了方法を好まない状況では Dispose をそのまま呼べません。
通常終了
終了がハンドリングできる状況で後処理を待ちたい場合では、子プロセスに WM_CLOSE メッセージを送って終了させます。終了しない場合に Kill を呼びますが、代わりに JobObject の Dispose を呼んであげればプロセスが終了します。
プロセスを Kill する場合の終了時の実装例。(過去記事より
User32.PostMessage(_childHandleRef.Handle, WM_CLOSE, IntPtr.Zero, IntPtr.Zero); var counter = 30; while (!_childProcess.HasExited) { if (--counter < 0) { Debug.WriteLine("Process not dead yet, killing..."); _childProcess.Kill(); } Thread.Sleep(100); } _childProcess.Dispose();
JobObject を使った実装例。
User32.PostMessage(_childHandleRef.Handle, WM_CLOSE, IntPtr.Zero, IntPtr.Zero);
while (!_childProcess.HasExited)
{
job.Dispose();
}
_childProcess.Dispose();
Unity as a Library で WPFでUnityをホストするという記事を書きましたが、子プロセスがゾンビ化しやすいので注意が必要です。次の画像はWPFを起動しUnityを読み込んだ状態です。
ここで Visual Studio のデバッグを停止したり、タスクマネージャーからWPFのみ終了させるなどの方法を取った場合、Unityのプロセスは残り続けます。しかもたちが悪いことにCPUとGPUをどか食い。。 WPFでUnityをホストする場合には、確実に終了させる方法が必須そうです。