//******************************************************************************************************
// ChildProcessManager.cs - Gbtc
//
// Copyright © 2018, Grid Protection Alliance. All Rights Reserved.
//
// Licensed to the Grid Protection Alliance (GPA) under one or more contributor license agreements. See
// the NOTICE file distributed with this work for additional information regarding copyright ownership.
// The GPA licenses this file to you under the MIT License (MIT), the "License"; you may not use this
// file except in compliance with the License. You may obtain a copy of the License at:
//
// http://opensource.org/licenses/MIT
//
// Unless agreed to in writing, the subject software distributed under the License is distributed on an
// "AS-IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. Refer to the
// License for the specific language governing permissions and limitations.
//
// Code Modification History:
// ----------------------------------------------------------------------------------------------------
// 02/05/2018 - J. Ritchie Carroll
// Generated original version of source code.
//
//******************************************************************************************************
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace GSF.Diagnostics
{
///
/// Represents a manager for automatically terminating child processes.
///
public sealed class ChildProcessManager : IDisposable
{
#region [ Members ]
// Nested Types
// ReSharper disable FieldCanBeMadeReadOnly.Local
// ReSharper disable UnusedMember.Local
// ReSharper disable InconsistentNaming
// ReSharper disable MemberCanBePrivate.Local
[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_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 SECURITY_ATTRIBUTES
{
public uint nLength;
public IntPtr lpSecurityDescriptor;
public int bInheritHandle;
}
[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;
}
private enum JobObjectInfoType
{
AssociateCompletionPortInformation = 7,
BasicLimitInformation = 2,
BasicUIRestrictions = 4,
EndOfJobTimeInformation = 6,
ExtendedLimitInformation = 9,
SecurityLimitInformation = 5,
GroupInformation = 11
}
// ReSharper restore FieldCanBeMadeReadOnly.Local
// ReSharper restore UnusedMember.Local
// ReSharper restore InconsistentNaming
// ReSharper restore MemberCanBePrivate.Local
private sealed class SafeJobHandle : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeJobHandle(IntPtr handle) : base(true)
{
SetHandle(handle);
}
protected override bool ReleaseHandle()
{
return CloseHandle(handle);
}
}
// Events
///
/// Raised when there is an exception while attempting to terminate child process.
///
///
/// This is currently only raised on non-Windows operating systems.
///
public event EventHandler> TerminationException;
// Fields
private readonly List> m_childProcesses;
private SafeJobHandle m_jobHandle;
private bool m_disposed;
#endregion
#region [ Constructors ]
///
/// Creates a new .
///
public ChildProcessManager()
{
if (Common.IsPosixEnvironment)
{
// On non-Windows operating systems we just track associated processes
m_childProcesses = new List>();
}
else
{
// Let safe handle manage terminations on Windows
GC.SuppressFinalize(this);
// On Windows we add child processes to a job object such that when the job
// is terminated, so are the child processes. Since safe handle ensures proper
// closing of job handle, child processes will be terminated even if parent
// process is abnormally terminated
m_jobHandle = new SafeJobHandle(CreateJobObject(IntPtr.Zero, null));
JOBOBJECT_BASIC_LIMIT_INFORMATION info = new JOBOBJECT_BASIC_LIMIT_INFORMATION
{
LimitFlags = 0x2000
};
JOBOBJECT_EXTENDED_LIMIT_INFORMATION extendedInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
BasicLimitInformation = info
};
int length = Marshal.SizeOf(typeof(JOBOBJECT_EXTENDED_LIMIT_INFORMATION));
IntPtr extendedInfoPtr = Marshal.AllocHGlobal(length);
Marshal.StructureToPtr(extendedInfo, extendedInfoPtr, false);
if (!SetInformationJobObject(m_jobHandle, JobObjectInfoType.ExtendedLimitInformation, extendedInfoPtr, (uint)length))
throw new InvalidOperationException($"Unable to set information for ChildProcessManager job. Error: {Marshal.GetLastWin32Error()}");
}
}
///
/// Make sure child processes get disposed.
///
~ChildProcessManager()
{
Dispose();
}
#endregion
#region [ Methods ]
///
/// Releases all the resources used by the object.
///
[SuppressMessage("Microsoft.Usage", "CA2213:DisposableFieldsShouldBeDisposed", MessageId = "m_jobHandle", Justification = "It is disposed")]
public void Dispose()
{
if (m_disposed)
return;
try
{
if (Common.IsPosixEnvironment)
{
foreach (WeakReference childProcessReference in m_childProcesses)
{
Process childProcess;
if (!childProcessReference.TryGetTarget(out childProcess))
continue;
try
{
childProcess.Kill();
}
catch (Exception ex)
{
TerminationException?.Invoke(this, new EventArgs(ex));
}
}
}
m_jobHandle?.Dispose();
m_jobHandle = null;
}
finally
{
m_disposed = true;
GC.SuppressFinalize(this);
}
}
///
/// Associates the specified as a child of this instance.
///
/// The to associate.
///
///
/// The will be managed as an associated process of this
/// instance. When this instance is disposed or garbage collected, the children
/// processes will be terminated.
///
///
/// Creating an instance of this class with lifetime scope of the executing application will cause any child processes
/// to be terminated when the parent process shuts down, on Windows environments this will happen even when the parent
/// process termination is abnormal.
///
///
public void AddProcess(Process process)
{
if (m_disposed)
throw new ObjectDisposedException(nameof(ChildProcessManager));
if (Common.IsPosixEnvironment)
{
m_childProcesses.Add(new WeakReference(process));
}
else
{
if (!AssignProcessToJobObject(m_jobHandle, process.SafeHandle))
throw new InvalidOperationException($"Unable to add process to ChildProcessManager job. Error: {Marshal.GetLastWin32Error()}");
}
}
#endregion
#region [ Static ]
// Static Methods
// ReSharper disable InconsistentNaming
[DllImport("kernel32", CharSet = CharSet.Unicode)]
private static extern IntPtr CreateJobObject(IntPtr hObject, string lpName);
[DllImport("kernel32", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetInformationJobObject(SafeJobHandle jobHandle, JobObjectInfoType infoType, IntPtr lpJobObjectInfo, uint cbJobObjectInfoLength);
[DllImport("kernel32", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AssignProcessToJobObject(SafeJobHandle jobHandle, SafeProcessHandle process);
[DllImport("kernel32")]
[ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr hObject);
// ReSharper restore InconsistentNaming
#endregion
}
}