diff --git a/System/Diagnostics/IoCompletionPort.cs b/System/Diagnostics/IoCompletionPort.cs new file mode 100644 index 00000000..805d2cf1 --- /dev/null +++ b/System/Diagnostics/IoCompletionPort.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using Vanara.PInvoke; +using static Vanara.PInvoke.Kernel32; + +namespace Vanara.Diagnostics +{ + /// Represents a system I/O completion port. + /// To use this class, create an instance with the method and then add one or more handlers. + /// + public class IoCompletionPort : IDisposable + { + private readonly ConcurrentDictionary> handlers = new ConcurrentDictionary>(); + private bool disposedValue = false; + private HANDLE hComplPort; + + private IoCompletionPort(HANDLE hValidPort) + { + hComplPort = hValidPort; + Task.Factory.StartNew(PollCompletionPortThread); + } + + /// Finalizes an instance of the class. + ~IoCompletionPort() => Dispose(false); + + /// Gets the handle for the I/O completion port. + /// The handle. + public IntPtr Handle => (IntPtr)hComplPort; + + /// + /// Creates an input/output (I/O) completion port that is not yet associated with a file handle, allowing association at a later time. + /// + /// An instance. + public static IoCompletionPort Create() + { + var hComplPort = CreateIoCompletionPort((IntPtr)HFILE.INVALID_HANDLE_VALUE, HANDLE.NULL, default, 0); + if (hComplPort.IsNull) + Win32Error.ThrowLastError(); + + return new IoCompletionPort(hComplPort); + } + + /// Adds key and handler to the I/O completion port. + /// A unique completion key to be passed to the handler when called. + /// An action to perform when an I/O operation is complete. + /// The value for cannot be UIntPtr.Zero. + /// Key already exists. + public void AddKeyHandler(UIntPtr key, Action handler) + { + if (key == UIntPtr.Zero) + throw new ArgumentOutOfRangeException(nameof(key), "Key value cannot be 0."); + + if (!handlers.TryAdd(key, handler)) + throw new InvalidOperationException("Key already exists."); + } + + /// Adds an overlapped handle, key and handler to the I/O completion port. + /// + /// An open handle to an object that supports overlapped I/O. + /// + /// The provided handle has to have been opened for overlapped I/O completion. For example, you must specify the FILE_FLAG_OVERLAPPED + /// flag when using the CreateFile function to obtain the handle. + /// + /// + /// A unique completion key to be passed to the handler when called. + /// An action to perform when an I/O operation is complete. + /// The value for cannot be UIntPtr.Zero. + /// Key already exists. + public void AddKeyHandler(IntPtr overlappedHandle, UIntPtr key, Action handler) + { + AddKeyHandler(key, handler); + + if (CreateIoCompletionPort(overlappedHandle, hComplPort, key, 0).IsNull) + Win32Error.ThrowLastError(); + } + + // Do not change this code. Put cleanup code in Dispose(bool disposing) above. + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// Posts an I/O completion packet to an I/O completion port. + /// + /// The value to be returned through the lpCompletionKey parameter of the GetQueuedCompletionStatus function. + /// + /// + /// The value to be returned through the lpNumberOfBytesTransferred parameter of the GetQueuedCompletionStatus function. + /// + /// + /// The value to be returned through the lpOverlapped parameter of the GetQueuedCompletionStatus function. + /// + public void PostQueuedStatus(UIntPtr completionKey, uint numberOfBytesTransferred = 0, IntPtr lpOverlapped = default) + { + if (completionKey == UIntPtr.Zero) + throw new ArgumentOutOfRangeException(nameof(completionKey), "Key value cannot be 0."); + + if (!PostQueuedCompletionStatus(hComplPort, numberOfBytesTransferred, completionKey, lpOverlapped)) + Win32Error.ThrowLastError(); + } + + /// Posts an I/O completion packet to an I/O completion port. + /// + /// The value to be returned through the lpCompletionKey parameter of the GetQueuedCompletionStatus function. + /// + /// + /// The value to be returned through the lpNumberOfBytesTransferred parameter of the GetQueuedCompletionStatus function. + /// + /// + /// The value to be returned through the lpOverlapped parameter of the GetQueuedCompletionStatus function. + /// + public unsafe void PostQueuedStatus(UIntPtr completionKey, uint numberOfBytesTransferred, NativeOverlapped* lpOverlapped) + { + if (completionKey == UIntPtr.Zero) + throw new ArgumentOutOfRangeException(nameof(completionKey), "Key value cannot be 0."); + + if (!PostQueuedCompletionStatus(hComplPort, numberOfBytesTransferred, completionKey, lpOverlapped)) + Win32Error.ThrowLastError(); + } + + /// Removes the handler associated with . + /// The key of the handler to remove. + /// Key does not exist. + public void RemoveKeyHandler(UIntPtr key) + { + if (!handlers.TryRemove(key, out _)) + throw new InvalidOperationException("Key does not exist."); + } + + /// Releases unmanaged and - optionally - managed resources. + /// + /// to release both managed and unmanaged resources; to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + // TODO: dispose managed state (managed objects). + } + + if (!hComplPort.IsNull) + { + // Shut down background thread processing completion port messages. + PostQueuedCompletionStatus(hComplPort, 0); + + // Close the completion port handle + CloseHandle((IntPtr)hComplPort); + hComplPort = HANDLE.NULL; + } + + disposedValue = true; + } + } + + private void PollCompletionPortThread() + { + while (true) + { + // Wait forever to get the next completion status + if (!GetQueuedCompletionStatus(hComplPort, out var byteCount, out var completionKey, out var overlapped, INFINITE) && overlapped == IntPtr.Zero) + { + var err = Win32Error.GetLastError(); + if (err == Win32Error.ERROR_ABANDONED_WAIT_0) + break; + throw err.GetException(); + } + + // End the thread if terminating completion key signals + if (byteCount == 0 && completionKey == UIntPtr.Zero && overlapped == IntPtr.Zero) + break; + + // Spin this off so we don't hang the completion port. + if (handlers.TryGetValue(completionKey, out var action)) + Task.Factory.StartNew(o => { if (o is Tuple t) action(t.Item1, t.Item2, t.Item3); }, new Tuple(byteCount, completionKey, overlapped)); + } + } + } +} \ No newline at end of file diff --git a/System/Diagnostics/Job.cs b/System/Diagnostics/Job.cs new file mode 100644 index 00000000..4206a40a --- /dev/null +++ b/System/Diagnostics/Job.cs @@ -0,0 +1,1617 @@ +using Microsoft.Win32.SafeHandles; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Runtime.InteropServices; +using System.Security.AccessControl; +using Vanara.Extensions; +using Vanara.InteropServices; +using Vanara.PInvoke; +using static Vanara.PInvoke.Kernel32; + +#if NET20 + +namespace System.IO +{ + /// Specifies whether the underlying handle is inheritable by child processes. + public enum HandleInheritability + { + /// Specifies that the handle is not inheritable by child processes. + None = 0, + + /// Specifies that the handle is inheritable by child processes. + Inheritable = 1, + } +} + +#endif + +namespace Vanara.Diagnostics +{ + /// The job limit type exceeded as communicated by a . + public enum JobLimit + { + /// The or value was exceeded. + IoRateControlTolerance = JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_IO_RATE_CONTROL, + + /// The value was exceeded. + IoReadBytes = JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_READ_BYTES, + + /// The value was exceeded. + IoWriteBytes = JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_WRITE_BYTES, + + /// The value was exceeded. + JobLowMemory = JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_MEMORY_LOW, + + /// The value was exceeded. + JobMemory = JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_MEMORY, + + /// The or value was exceeded. + NetRateControlTolerance = JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_NET_RATE_CONTROL, + + /// The value was exceeded. + PerJobUserTime = JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_TIME, + + /// The or value was exceeded. + RateControlTolerance = JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_RATE_CONTROL, + } + + /// + /// Represents a system Job Object that allows groups of processes to be managed as a unit. Job objects are nameable, securable, sharable + /// objects that control attributes of the processes associated with them. Operations performed on a job object affect all processes + /// associated with the job object. Examples include enforcing limits such as working set size and process priority or terminating all + /// processes associated with a job. For more information see + /// Job Objects. + /// + /// + public class Job : IDisposable + { + internal SafeHJOB hJob; + private readonly IoCompletionPort complPort; + private bool disposedValue = false; + private JobLimits limits; + private JobNotifications notifications; + private JobProcessCollection processes; + private JobSettings settings; + private JobStatistics stats; + + private Job(SafeHJOB jobHandle) + { + if (jobHandle is null || jobHandle.IsNull) + throw new ArgumentNullException(nameof(jobHandle)); + hJob = jobHandle; + + // Create completion port + complPort = IoCompletionPort.Create(); + complPort.AddKeyHandler(hJob.DangerousGetHandle().ToUIntPtr(), FireEventThread); + AssociateCompletionPort(complPort.Handle, hJob.DangerousGetHandle().ToUIntPtr()); + } + + /// Finalizes an instance of the class. + ~Job() => Dispose(false); + + /// Action with a parameter passed by reference. + /// The parameter type. + /// The first parameter value. + internal delegate void RefAction(ref T t1); + + /// Function with parameter passed by reference. + /// The type of the parameter. + /// The type of the return value. + /// The type instance on which to act. + /// The return value. + internal delegate T2 RefFunc(ref T1 t1); + + /// + /// Indicates that a process associated with the job exited with an exit code that indicates an abnormal exit (see the list following + /// this table). + /// + public event EventHandler AbnormalProcessExit; + + /// + /// Indicates that the active process count has been decremented to 0. For example, if the job currently has two active processes, + /// the system sends this message after they both terminate. + /// + public event EventHandler ActiveProcessCountZero; + + /// Indicates that the active process limit has been exceeded. + public event EventHandler ActiveProcessLimitExceeded; + + /// + /// Indicates that the Settings property is set to + /// and the end-of-job time limit has been reached. Upon posting this message, the time limit is canceled and + /// the job's processes can continue to run. + /// + public event EventHandler EndOfJobTime; + + /// + /// Indicates that a process has exceeded a per-process time limit. The system sends this message after the process termination has + /// been requested. + /// + public event EventHandler EndofProcessTime; + + /// + /// Indicates that a process associated with the job caused the job to exceed the job-wide memory limit (if one is in effect). The + /// system does not send this message if the process has not yet reported its process identifier. + /// + public event EventHandler JobMemoryLimitExceeded; + + /// + /// Indicates that a process associated with a job that has registered for resource limit notifications has exceeded one or more + /// limits. The system does not send this message if the process has not yet reported its process identifier. + /// + public event EventHandler JobNotificationLimitExceeded; + + /// + /// Indicates that a process has been added to the job. Processes added to a job at the time a completion port is associated are also reported. + /// + public event EventHandler NewProcess; + + /// Indicates that a process associated with the job has exited. + public event EventHandler ProcessExited; + + /// + /// Indicates that a process associated with the job has exceeded its memory limit (if one is in effect). The system does not send + /// this message if the process has not yet reported its process identifier. + /// + public event EventHandler ProcessMemoryLimitExceeded; + + /// Exposes the handle (HJOB) of the job. + /// The handle. + public IntPtr Handle => hJob.DangerousGetHandle(); + + /// Notification limits that can be set for various properties. + /// The notifications. + public JobNotifications Notifications => notifications ?? (notifications = new JobNotifications(this)); + + /// Gets the processes assigned to this job. + /// The process list for the job. + public IReadOnlyCollection Processes => processes ?? (processes = new JobProcessCollection(this)); + + /// Gets or sets the list of processor groups to which the job is currently assigned. + public IEnumerable ProcessorGroups + { + get + { + using var mem = new SafeHGlobalHandle(4); + uint req; + while (!QueryInformationJobObject(hJob, JOBOBJECTINFOCLASS.JobObjectGroupInformation, mem, mem.Size, out req)) + { + Win32Error.ThrowLastErrorUnless(Win32Error.ERROR_MORE_DATA); + mem.Size = req; + } + return mem.ToEnumerable((int)req / 2).TakeWhile(id => id > 0); + } + set + { + using var mem = SafeHGlobalHandle.CreateFromList(value); + if (!SetInformationJobObject(hJob, JOBOBJECTINFOCLASS.JobObjectGroupInformation, mem, mem.Size)) + Win32Error.ThrowLastError(); + } + } + + /// Gets the hard limits for different runtime values of the job object. + /// The runtime limits. + public JobLimits RuntimeLimits => limits ?? (limits = new JobLimits(this)); + + /// Gets the job settings. + /// The job settings. + public JobSettings Settings => settings ?? (settings = new JobSettings(this)); + + /// Usage statistics for the job. + /// Usage statistics. + public JobStatistics Statistics => stats ?? (stats = new JobStatistics(this)); + + /// Creates or opens a job object. + /// + /// The name of the job. The name is limited to MAX_PATH characters. Name comparison is case-sensitive. + /// If lpName is , the job is created without a name. + /// + /// If lpName matches the name of an existing event, semaphore, mutex, waitable timer, or file-mapping object, the function fails and + /// an exception corresponding to ERROR_INVALID_HANDLE is thrown. This occurs because these objects share the same namespace. + /// + /// The object can be created in a private namespace. For more information, see Object Namespaces. + /// + /// Terminal Services: The name can have a "Global" or "Local" prefix to explicitly create the object in the global or session + /// namespace. The remainder of the name can contain any character except the backslash character (). For more information, see + /// Kernel Object Namespaces. + /// + /// + /// + /// An optional instance that specifies the security descriptor for the job object and determines whether + /// child processes can inherit the returned handle. If , the job object gets a default security descriptor and + /// the handle cannot be inherited. The ACLs in the default security descriptor for a job object come from the primary or + /// impersonation token of the creator. + /// + /// + /// If this value is , processes created by this process will inherit the handle. + /// Otherwise, the processes do not inherit this handle. + /// + /// + /// If the function succeeds, the return value is a object. The handle has the JOB_OBJECT_ALL_ACCESS access right. + /// + /// + /// + /// When a job is created, its accounting information is initialized to zero, all limits are inactive, and there are no associated + /// processes. To assign a process to a job object, use the AssignProcessToJobObject function. To get or set limits for a job, use + /// the object's properties. + /// + /// + /// All processes associated with a job must run in the same session. A job is associated with the session of the first process to be + /// assigned to the job. + /// + /// Windows Server 2003 and Windows XP: A job is associated with the session of the process that created it. + /// + /// If the job has the KillOnJobClose property set to , closing the last job object handle terminates all + /// associated processes and then destroys the job object itself. + /// + /// + public static Job Create(string jobName = null, JobSecurity jobSecurity = null, HandleInheritability inheritable = HandleInheritability.None) + { + var sa = GetSecAttr(jobSecurity, inheritable == HandleInheritability.Inheritable, out var hMem); + var job = new Job(CreateJobObject(sa, jobName)); + hMem?.Dispose(); + return job; + } + + /// Performs an implicit conversion from to . + /// The job. + /// The result of the conversion. + public static implicit operator HJOB(Job job) => job.hJob; + + /// Performs an implicit conversion from to . + /// The Job instance. + /// The result of the conversion. + public static implicit operator SafeWaitHandle(Job job) => new SafeWaitHandle(job.hJob.DangerousGetHandle(), false); + + /// Opens an existing job object. + /// + /// The name of the job to be opened. Name comparisons are case sensitive. + /// This function can open objects in a private namespace. For more information, see Object Namespaces. + /// + /// Terminal Services: The name can have a "Global\" or "Local\" prefix to explicitly open the object in the global or session + /// namespace. The remainder of the name can contain any character except the backslash character (\). For more information, see + /// Kernel Object Namespaces. + /// + /// + /// + /// The access to the job object. This parameter can be one or more of the job object access rights. This access right is checked + /// against any security descriptor for the object. + /// + /// + /// If this value is , processes created by this process will inherit the handle. + /// Otherwise, the processes do not inherit this handle. + /// + /// If the function succeeds, the return value is a object. + public static Job Open(string jobName, JobAccessRight desiredAccess = JobAccessRight.JOB_OBJECT_ALL_ACCESS, HandleInheritability inheritable = HandleInheritability.None) => + new Job(OpenJobObject((uint)desiredAccess, inheritable == HandleInheritability.Inheritable, jobName)); + + /// Assigns a process to an existing job object. + /// + /// + /// The process to associate with the job object. The process must have the PROCESS_SET_QUOTA and PROCESS_TERMINATE access rights. + /// + /// + /// If the process is already associated with a job, this job must be empty or it must be in the hierarchy of nested jobs to which + /// the process already belongs, and it cannot have UI limits set. + /// + /// + /// Windows 7, Windows Server 2008 R2, Windows XP with SP3, Windows Server 2008, Windows Vista and Windows Server 2003: The + /// process must not already be assigned to a job; if it is, the function fails with . This + /// behavior changed starting in Windows 8 and Windows Server 2012. + /// + /// Terminal Services: All processes within a job must run within the same session as the job. + /// + public void AssignProcess(Process process) + { + if (process is null) throw new ArgumentNullException(nameof(process)); + CheckState(); + if (!AssignProcessToJobObject(hJob, process)) + Win32Error.ThrowLastError(); + } + + /// Associates a completion port with this job. You can associate one completion port with a job. + /// + /// The completion port to use in the CompletionPort parameter of the PostQueuedCompletionStatus function when messages are sent on + /// behalf of the job. + /// + /// Windows 8, Windows Server 2012, Windows 8.1, Windows Server 2012 R2, Windows 10 and Windows Server 2016: Specify + /// to remove the association between the current completion port and the job. + /// + /// + /// + /// The value to use in the dwCompletionKey parameter of PostQueuedCompletionStatus when messages are sent on behalf of the job. + /// + public void AssociateCompletionPort(HANDLE completionPort, UIntPtr key = default) => + CheckThenSet((ref JOBOBJECT_ASSOCIATE_COMPLETION_PORT i) => { i.CompletionKey = key; i.CompletionPort = completionPort; }); + + /// Determines whether the process is running in this job. + /// + /// The process to be tested. The handle must have the PROCESS_QUERY_INFORMATION or PROCESS_QUERY_LIMITED_INFORMATION access right. + /// + /// if the job contains the specified process; otherwise, . + public bool ContainsProcess(Process process) + { + if (process is null) throw new ArgumentNullException(nameof(process)); + CheckState(); + if (!IsProcessInJob(process, hJob, out var isIn)) + Win32Error.ThrowLastError(); + return isIn; + } + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + /// Gets information about the control of the I/O rate for a job object. + /// + /// The name of the volume to query. If this value is , the function gets the information about I/O rate + /// control for the job for all of the volumes for the system. + /// + /// + /// An array of JOBOBJECT_IO_RATE_CONTROL_INFORMATION structures that contain the information about I/O rate control for the job. + /// + public JOBOBJECT_IO_RATE_CONTROL_INFORMATION[] GetIoRateControlInformation(string VolumeName = null) => + QueryIoRateControlInformationJobObject(hJob, VolumeName); + + /// + /// Grants or denies access to a handle to a User object to a job that has a user-interface restriction. When access is granted, all + /// processes associated with the job can subsequently recognize and use the handle. When access is denied, the processes can no + /// longer use the handle. For more information see User Objects. + /// + /// A handle to the User object. + /// + /// If this parameter is , all processes associated with the job can recognize and use the handle. If the + /// parameter is , the processes cannot use the handle. + /// + /// + /// + /// + /// The GrantUserAccess function can be called only from a process not associated with the job specified by the hJob + /// parameter. The User handle must not be owned by a process or thread associated with the job. + /// + /// To create user-interface restrictions, user the property. + /// + public void GrantUserAccess(IUserHandle hUserObj, bool grant) + { + if (!User32.UserHandleGrantAccess(hUserObj.DangerousGetHandle(), hJob, grant)) + Win32Error.ThrowLastError(); + } + + /// + /// Starts a process resource by specifying the name of an application and a set of command-line arguments, and associates the + /// resource with a new Process component which is assigned to this job. + /// + /// The name of an application file to run in the process. + /// Command-line arguments to pass when starting the process. + /// + /// A new that is associated with the process resource, or if no process resource is + /// started. Note that a new process that's started alongside already running instances of the same process will be independent from + /// the others. In addition, Start may return a non-null Process with its property already set to + /// . In this case, the started process may have activated an existing instance of itself and then exited. + /// + public Process StartProcess(string filename, string arguments = null) => !string.IsNullOrEmpty(filename) ? StartProcess(new ProcessStartInfo(filename, arguments)) : throw new ArgumentNullException(nameof(filename)); + + /// + /// Starts the process resource that is specified by the parameter containing process start information (for example, the file name + /// of the process to start) and associates the resource with a new Process component which is assigned to this job. + /// + /// + /// The that contains the information that is used to start the process, including the file name and + /// any command-line arguments. + /// + /// + /// A new that is associated with the process resource, or if no process resource is + /// started. Note that a new process that's started alongside already running instances of the same process will be independent from + /// the others. In addition, Start may return a non-null Process with its property already set to + /// . In this case, the started process may have activated an existing instance of itself and then exited. + /// + public Process StartProcess(ProcessStartInfo startInfo) + { + startInfo.UseShellExecute = false; + var proc = new Process { StartInfo = startInfo }; + proc.StartEx(CREATE_PROCESS.CREATE_SUSPENDED); + try + { + AssignProcess(proc); + } + catch + { + proc.Kill(); + throw; + } + proc.ResumePrimaryThread(); + return proc; + } + + /// + /// Terminates all processes currently associated with the job. If the job is nested, this function terminates all processes + /// currently associated with the job and all of its child jobs in the hierarchy. + /// + /// The exit code to be used by all processes and threads in the job object. + public void TerminateAllProcesses(uint exitCode = 0) + { + CheckState(); + if (!TerminateJobObject(hJob, exitCode)) + Win32Error.ThrowLastError(); + } + + internal static SECURITY_ATTRIBUTES GetSecAttr(JobSecurity sec, bool inheritable, out ISafeMemoryHandle hMem) + { + hMem = null; + if (sec is null && !inheritable) return null; + hMem = new SafeHGlobalHandle(sec.GetSecurityDescriptorBinaryForm()); + return new SECURITY_ATTRIBUTES + { + bInheritHandle = inheritable, + lpSecurityDescriptor = hMem.DangerousGetHandle() + }; + } + + internal void CheckState() + { + if (disposedValue) + throw new InvalidOperationException("Object has been disposed."); + } + + internal T CheckThenGet(JOBOBJECTINFOCLASS iClass = 0) where T : struct => CheckThenGet(n => n, iClass); + + internal T CheckThenGet(Func func, JOBOBJECTINFOCLASS iClass = 0) where T2 : struct + { + CheckState(); + if (iClass == 0 && !CorrespondingTypeAttribute.CanGet(out iClass)) + throw new InvalidOperationException("Invalid property retrieval."); + var n = QueryInformationJobObject(hJob, iClass); + return func(n); + } + + internal void CheckThenSet(RefAction action, JOBOBJECTINFOCLASS iClass = 0) where T : struct + { + CheckState(); + if (iClass == 0 && !CorrespondingTypeAttribute.CanSet(out iClass)) + throw new InvalidOperationException("Invalid property retrieval."); + var info = CorrespondingTypeAttribute.CanGet(iClass, typeof(T)) ? QueryInformationJobObject(hJob, iClass) : default; + action?.Invoke(ref info); + SetInformationJobObject(hJob, iClass, info); + } + + /// Releases unmanaged and - optionally - managed resources. + /// + /// to release both managed and unmanaged resources; to release only unmanaged resources. + /// + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + limits?.Dispose(); + notifications?.Dispose(); + processes?.Dispose(); + settings?.Dispose(); + stats?.Dispose(); + } + + // Close the completion port handle + complPort.Dispose(); + + // Close the job. + hJob.Dispose(); + disposedValue = true; + } + } + + private void FireEventThread(uint msg, UIntPtr key, IntPtr ppid) + { + if (disposedValue) return; + var t = new JobEventArgs((JOB_OBJECT_MSG)msg, ppid.ToInt32()); + switch (t.JobMessage) + { + case JOB_OBJECT_MSG.JOB_OBJECT_MSG_END_OF_JOB_TIME: + EndOfJobTime?.Invoke(this, t); + break; + + case JOB_OBJECT_MSG.JOB_OBJECT_MSG_END_OF_PROCESS_TIME: + EndofProcessTime?.Invoke(this, t); + break; + + case JOB_OBJECT_MSG.JOB_OBJECT_MSG_ACTIVE_PROCESS_LIMIT: + ActiveProcessLimitExceeded?.Invoke(this, t); + break; + + case JOB_OBJECT_MSG.JOB_OBJECT_MSG_ACTIVE_PROCESS_ZERO: + ActiveProcessCountZero?.Invoke(this, t); + break; + + case JOB_OBJECT_MSG.JOB_OBJECT_MSG_NEW_PROCESS: + NewProcess?.Invoke(this, t); + break; + + case JOB_OBJECT_MSG.JOB_OBJECT_MSG_EXIT_PROCESS: + ProcessExited?.Invoke(this, t); + break; + + case JOB_OBJECT_MSG.JOB_OBJECT_MSG_ABNORMAL_EXIT_PROCESS: + AbnormalProcessExit?.Invoke(this, t); + break; + + case JOB_OBJECT_MSG.JOB_OBJECT_MSG_PROCESS_MEMORY_LIMIT: + ProcessMemoryLimitExceeded?.Invoke(this, t); + break; + + case JOB_OBJECT_MSG.JOB_OBJECT_MSG_JOB_MEMORY_LIMIT: + JobMemoryLimitExceeded?.Invoke(this, t); + break; + + case JOB_OBJECT_MSG.JOB_OBJECT_MSG_NOTIFICATION_LIMIT: + var vi = GetViolation(); + Debug.WriteLine($"Notification: {vi.ViolationLimitFlags}"); + foreach (var l in vi.ViolationLimitFlags.GetFlags().Cast()) + { + object v = null, n = null; + switch (l) + { + case JobLimit.JobMemory: + v = vi.JobMemory; + n = vi.JobMemoryLimit; + break; + + case JobLimit.PerJobUserTime: + v = vi.PerJobUserTime; + n = vi.PerJobUserTimeLimit; + break; + + case JobLimit.IoReadBytes: + v = vi.IoReadBytes; + n = vi.IoReadBytesLimit; + break; + + case JobLimit.IoWriteBytes: + v = vi.IoWriteBytes; + n = vi.IoWriteBytesLimit; + break; + + case JobLimit.RateControlTolerance: + v = vi.RateControlTolerance; + n = vi.RateControlToleranceLimit; + break; + + case JobLimit.IoRateControlTolerance: + v = vi.IoRateControlTolerance; + n = vi.IoRateControlToleranceLimit; + break; + + case JobLimit.JobLowMemory: + v = vi.JobMemory; + n = vi.JobLowMemoryLimit; + break; + + case JobLimit.NetRateControlTolerance: + v = vi.NetRateControlTolerance; + n = vi.NetRateControlToleranceLimit; + break; + + default: + Debug.WriteLine($"Unable to process notification: {vi.ViolationLimitFlags}, {vi.LimitFlags}"); + continue; + } + JobNotificationLimitExceeded?.Invoke(this, new JobNotificationEventArgs(t.JobMessage, t.ProcessId, l, v, n)); + } + break; + + default: + break; + } + + JOBOBJECT_LIMIT_VIOLATION_INFORMATION_2 GetViolation() + { + using var mem = SafeHeapBlock.CreateFromStructure(); + if (!QueryInformationJobObject(hJob, JOBOBJECTINFOCLASS.JobObjectLimitViolationInformation2, mem, mem.Size, out _)) + { + Debug.WriteLine($"Failed to get JOBOBJECT_LIMIT_VIOLATION_INFORMATION_2: {Win32Error.GetLastError()}"); + if (!QueryInformationJobObject(hJob, JOBOBJECTINFOCLASS.JobObjectLimitViolationInformation, mem, (uint)Marshal.SizeOf(typeof(JOBOBJECT_LIMIT_VIOLATION_INFORMATION)), out _)) + Debug.WriteLine($"Failed to get JOBOBJECT_LIMIT_VIOLATION_INFORMATION: {Win32Error.GetLastError()}"); + } + return mem.ToStructure(); + } + } + + internal class JobProcessCollection : JobHelper, IReadOnlyCollection + { + public JobProcessCollection(Job j) : base(j) + { + } + + /// + /// The total number of processes currently associated with the job. When a process is associated with a job, but the association + /// fails because of a limit violation, this value is temporarily incremented. When the terminated process exits and all + /// references to the process are released, this value is decremented. + /// + public int Count => (int)job.CheckThenGet().ActiveProcesses; + + public IEnumerator GetEnumerator() => new Enumerator(job); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private class Enumerator : IEnumerator + { + private int i; + private Job job; + private Process[] procs; + + public Enumerator(Job j) + { + job = j; + Reset(); + } + + public Process Current => procs[i]; + + object IEnumerator.Current => Current; + + public void Dispose() => job = null; + + public bool MoveNext() => ++i < procs.Length; + + public void Reset() + { + i = -1; + using var mem = SafeHGlobalHandle.CreateFromStructure(); + while (!Kernel32.QueryInformationJobObject(job, JOBOBJECTINFOCLASS.JobObjectBasicProcessIdList, mem.DangerousGetHandle(), mem.Size, out _)) + { + Win32Error.ThrowLastErrorUnless(Win32Error.ERROR_MORE_DATA); + mem.Size *= 2; + } + var l = mem.ToStructure(); + procs = mem.ToEnumerable((int)l, 8).Select(p => { try { return Process.GetProcessById((int)p.ToUInt32()); } catch { return null; } }).Where(p => p != null).ToArray(); + } + } + } + } + + /// Contains information about a job object message. + /// + public class JobEventArgs : EventArgs + { + internal JobEventArgs(JOB_OBJECT_MSG msg, int id = 0) + { + JobMessage = msg; + ProcessId = id; + } + + /// Gets the type of job message posted. + /// The job message. + public JOB_OBJECT_MSG JobMessage { get; } + + /// Gets the process identifier of the process referred to by the message. + /// The process identifier. This value can be 0. + public int ProcessId { get; } + } + + /// Base class for other classes that support the object. + /// + public abstract class JobHelper : IDisposable + { + /// The job object. + protected Job job; + + /// Initializes a new instance of the class. + /// Name of the job. + protected JobHelper(string jobName) : this(Job.Open(jobName, JobAccessRight.JOB_OBJECT_QUERY | JobAccessRight.JOB_OBJECT_SET_ATTRIBUTES)) { } + + /// Initializes a new instance of the class. + /// The job. + protected JobHelper(Job job) => this.job = job; + + /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. + public virtual void Dispose() => job = null; + + /// Gets field values from JOBOBJECT_BASIC_LIMIT_INFORMATION. + /// The return type. + /// The limit flag. + /// The method to get the field. + /// The value. + internal T? GetBasic(JOBOBJECT_LIMIT_FLAGS flag, Func getter) where T : struct => + job.CheckThenGet(n => n.LimitFlags.IsFlagSet(flag) ? (T?)getter(n) : null); + + /// Sets a field value in JOBOBJECT_BASIC_LIMIT_INFORMATION. + /// The field type. + /// The limit flag. + /// The value. + /// The method to set the field. + internal void SetBasic(JOBOBJECT_LIMIT_FLAGS flag, T? value, Job.RefAction setter) where T : struct => + job.CheckThenSet((ref JOBOBJECT_BASIC_LIMIT_INFORMATION i) => { i.LimitFlags = i.LimitFlags.SetFlags(flag, value.HasValue); setter(ref i); }); + } + + /// Settings for that set limits for different runtime values. + /// + public class JobLimits : JobHelper + { + /// Initializes a new instance of the class. + /// Name of the job. + public JobLimits(string jobName) : base(jobName) { } + + /// Initializes a new instance of the class. + /// The job. + public JobLimits(Job job) : base(job) { } + + /// + /// Gets or sets the active process limit for the job. + /// + /// If you try to associate a process with a job, and this causes the active process count to exceed this limit, the process is + /// terminated and the association fails. + /// + /// + public uint? ActiveProcessLimit + { + get => GetBasic(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_ACTIVE_PROCESS, n => n.ActiveProcessLimit); + set => SetBasic(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_ACTIVE_PROCESS, value, (ref JOBOBJECT_BASIC_LIMIT_INFORMATION i) => i.ActiveProcessLimit = value.GetValueOrDefault()); + } + + /// + /// Gets or sets the job's CPU rate as a hard limit. If set, after the job reaches its CPU cycle limit for the current scheduling + /// interval, no threads associated with the job will run until the next interval. + /// + /// + /// Specifies the portion of processor cycles that the threads in a job object can use during each scheduling interval, as a + /// percentage of cycles. This value is greater than 0.0 and less than equal to 100.0. If this value is , then + /// this setting is disabled. + /// + /// Value must be greater than 0.0 and less than equal to 100.0. + public double? CpuRateLimit + { + get => job.CheckThenGet((JOBOBJECT_CPU_RATE_CONTROL_INFORMATION n) => + n.ControlFlags.IsFlagSet(JOB_OBJECT_CPU_RATE_CONTROL_FLAGS.JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP | JOB_OBJECT_CPU_RATE_CONTROL_FLAGS.JOB_OBJECT_CPU_RATE_CONTROL_ENABLE) + ? n.Union.CpuRate / 100.0 : (double?)null); + set + { + if (value.HasValue && (value.Value <= 0.0 || value.Value > 100.0)) + throw new ArgumentOutOfRangeException(nameof(CpuRateLimit)); + job.CheckThenSet((ref JOBOBJECT_CPU_RATE_CONTROL_INFORMATION i) => + { + i.ControlFlags = i.ControlFlags.SetFlags(JOB_OBJECT_CPU_RATE_CONTROL_FLAGS.JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP | JOB_OBJECT_CPU_RATE_CONTROL_FLAGS.JOB_OBJECT_CPU_RATE_CONTROL_ENABLE, value.HasValue); + i.Union.CpuRate = value.HasValue ? (uint)(value.Value * 100) : 0; + }); + } + } + + /// Gets or sets the CPU rate for the job as limited by minimum and maximum rates. + /// + /// + /// Specifies the minimum and maximum portions of the processor cycles that the threads in a job object can reserve during each + /// scheduling interval. Specify these rates as a percentage from 0.0 to 100.0. + /// + /// + /// For the minimum rates to work correctly, the sum of the minimum rates for all of the job objects in the system cannot exceed + /// 100%. After the job reaches the maximum limit for a scheduling interval, no threads associated with the job can run until the + /// next scheduling interval. + /// + /// If this value is , then this setting is disabled. + /// + /// + /// Values must be greater than or equal to 0.0 and less than equal to 100.0 and the minimum value must be less than the maximum value. + /// + public (double minPortion, double maxPortion)? CpuRatePortion + { + get => job.CheckThenGet((JOBOBJECT_CPU_RATE_CONTROL_INFORMATION n) => + n.ControlFlags.IsFlagSet(JOB_OBJECT_CPU_RATE_CONTROL_FLAGS.JOB_OBJECT_CPU_RATE_CONTROL_MIN_MAX_RATE | JOB_OBJECT_CPU_RATE_CONTROL_FLAGS.JOB_OBJECT_CPU_RATE_CONTROL_ENABLE) + ? (n.Union.MinRate / 100.0, n.Union.MaxRate / 100.0) : ((double, double)?)null); + set + { + if (value.HasValue && (value.Value.minPortion < 0.0 || value.Value.minPortion > 100.0 || value.Value.maxPortion < 0.0 || value.Value.maxPortion > 100.0 || value.Value.minPortion > value.Value.maxPortion)) + throw new ArgumentOutOfRangeException(nameof(CpuRatePortion)); + job.CheckThenSet((ref JOBOBJECT_CPU_RATE_CONTROL_INFORMATION i) => + { + i.ControlFlags = i.ControlFlags.SetFlags(JOB_OBJECT_CPU_RATE_CONTROL_FLAGS.JOB_OBJECT_CPU_RATE_CONTROL_MIN_MAX_RATE | JOB_OBJECT_CPU_RATE_CONTROL_FLAGS.JOB_OBJECT_CPU_RATE_CONTROL_ENABLE, value.HasValue); + i.Union.MinRate = value.HasValue ? (ushort)(value.Value.minPortion * 100) : (ushort)0; + i.Union.MaxRate = value.HasValue ? (ushort)(value.Value.maxPortion * 100) : (ushort)0; + }); + } + } + + /// Gets or sets the job's CPU rate when calculated based on its relative weight to the weight of other jobs. + /// + /// + /// Specifies the scheduling weight of the job object, which determines the share of processor time given to the job relative to + /// other workloads on the processor. + /// + /// + /// This member can be a value from 1 through 9, where 1 is the smallest share and 9 is the largest share. The default is 5, which + /// should be used for most workloads. + /// + /// If this value is , then this setting is disabled. + /// + /// weight1to9 + public int? CpuRateRelativeWeight + { + get => job.CheckThenGet((JOBOBJECT_CPU_RATE_CONTROL_INFORMATION n) => + n.ControlFlags.IsFlagSet(JOB_OBJECT_CPU_RATE_CONTROL_FLAGS.JOB_OBJECT_CPU_RATE_CONTROL_WEIGHT_BASED | JOB_OBJECT_CPU_RATE_CONTROL_FLAGS.JOB_OBJECT_CPU_RATE_CONTROL_ENABLE) + ? (int)n.Union.Weight : (int?)null); + set + { + if (value.HasValue && (value.Value < 1 || value.Value > 9)) + throw new ArgumentOutOfRangeException(nameof(CpuRateRelativeWeight)); + job.CheckThenSet((ref JOBOBJECT_CPU_RATE_CONTROL_INFORMATION i) => + { + i.ControlFlags = i.ControlFlags.SetFlags(JOB_OBJECT_CPU_RATE_CONTROL_FLAGS.JOB_OBJECT_CPU_RATE_CONTROL_WEIGHT_BASED | JOB_OBJECT_CPU_RATE_CONTROL_FLAGS.JOB_OBJECT_CPU_RATE_CONTROL_ENABLE, value.HasValue); + i.Union.Weight = value.HasValue ? (uint)value.Value : 0; + }); + } + } + + /// + /// Gets or sets the limit for the virtual memory that can be committed for the job. + /// If this value is , then this setting is disabled. + /// + public ulong? JobMemoryLimit + { + get => job.CheckThenGet((JOBOBJECT_EXTENDED_LIMIT_INFORMATION n) => + n.BasicLimitInformation.LimitFlags.IsFlagSet(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_MEMORY) ? (ulong?)n.JobMemoryLimit : null); + set => job.CheckThenSet((ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION i) => { i.BasicLimitInformation.LimitFlags = i.BasicLimitInformation.LimitFlags.SetFlags(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_MEMORY, value.HasValue); i.JobMemoryLimit = value.GetValueOrDefault(); }); + } + + /// + /// Gets or sets the maximum bandwidth for outgoing network traffic for the job, in bytes. + /// If this value is , then this setting is disabled. + /// + public ulong? MaxBandwidth + { + get + { + var info = job.CheckThenGet(); + return info.ControlFlags.IsFlagSet(JOB_OBJECT_NET_RATE_CONTROL_FLAGS.JOB_OBJECT_NET_RATE_CONTROL_MAX_BANDWIDTH) ? (ulong?)info.MaxBandwidth : null; + } + set + { + job.CheckThenSet((ref JOBOBJECT_NET_RATE_CONTROL_INFORMATION i) => + { + i.ControlFlags = i.ControlFlags.SetFlags(JOB_OBJECT_NET_RATE_CONTROL_FLAGS.JOB_OBJECT_NET_RATE_CONTROL_MAX_BANDWIDTH | JOB_OBJECT_NET_RATE_CONTROL_FLAGS.JOB_OBJECT_NET_RATE_CONTROL_ENABLE, value.HasValue); + i.MaxBandwidth = value.GetValueOrDefault(); + }); + } + } + + /// + /// Gets or sets the per-job user-mode execution time limit. + /// + /// The system adds the current time of the processes associated with the job to this limit. For example, if you set this limit to 1 + /// minute, and the job has a process that has accumulated 5 minutes of user-mode time, the limit actually enforced is 6 minutes. + /// + /// + /// The system periodically checks to determine whether the sum of the user-mode execution time for all processes is greater than + /// this end-of-job limit. If it is, the action specified in the EndOfJobTimeAction property is carried out. By default, all + /// processes are terminated and the status code is set to ERROR_NOT_ENOUGH_QUOTA. + /// + /// If this value is , then this setting is disabled. + /// + public TimeSpan? PerJobUserTimeLimit + { + get => GetBasic(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_TIME, n => n.PerJobUserTimeLimit); + set => SetBasic(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_TIME, value, (ref JOBOBJECT_BASIC_LIMIT_INFORMATION i) => i.PerJobUserTimeLimit = value.GetValueOrDefault()); + } + + /// + /// Gets or sets the per-process user-mode execution time limit. + /// + /// The system periodically checks to determine whether each process associated with the job has accumulated more user-mode time than + /// the set limit. If it has, the process is terminated. + /// + /// If the job is nested, the effective limit is the most restrictive limit in the job chain. + /// If this value is , then this setting is disabled. + /// + public TimeSpan? PerProcessUserTimeLimit + { + get => GetBasic(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_PROCESS_TIME, n => n.PerProcessUserTimeLimit); + set => SetBasic(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_PROCESS_TIME, value, (ref JOBOBJECT_BASIC_LIMIT_INFORMATION i) => i.PerProcessUserTimeLimit = value.GetValueOrDefault()); + } + + /// + /// Gets or sets the limit for the virtual memory that can be committed by a process. + /// If this value is , then this setting is disabled. + /// + public ulong? ProcessMemoryLimit + { + get => job.CheckThenGet((JOBOBJECT_EXTENDED_LIMIT_INFORMATION n) => + n.BasicLimitInformation.LimitFlags.IsFlagSet(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_PROCESS_MEMORY) ? (ulong?)n.ProcessMemoryLimit : null); + set => job.CheckThenSet((ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION i) => { i.BasicLimitInformation.LimitFlags = i.BasicLimitInformation.LimitFlags.SetFlags(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_PROCESS_MEMORY, value.HasValue); i.ProcessMemoryLimit = value.GetValueOrDefault(); }); + } + + /// + /// Gets or sets the working set size in bytes for each process associated with the job. + /// Both min and max must be zero or non-zero. + /// If this value is , then this setting is disabled. + /// + public (SizeT min, SizeT max)? WorkingSetSize + { + get => job.CheckThenGet((JOBOBJECT_BASIC_LIMIT_INFORMATION n) => + n.LimitFlags.IsFlagSet(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_WORKINGSET) ? (n.MinimumWorkingSetSize, n.MaximumWorkingSetSize) : ((SizeT, SizeT)?)null); + set + { + if (value.HasValue && ((value.Value.min == SizeT.Zero && value.Value.max != SizeT.Zero) || (value.Value.max == SizeT.Zero && value.Value.min != SizeT.Zero) || value.Value.min > value.Value.max)) + throw new ArgumentOutOfRangeException(nameof(WorkingSetSize)); + job.CheckThenSet((ref JOBOBJECT_BASIC_LIMIT_INFORMATION i) => + { + i.LimitFlags = i.LimitFlags.SetFlags(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_WORKINGSET, value.HasValue); + i.MinimumWorkingSetSize = value.HasValue ? value.Value.min : SizeT.Zero; + i.MaximumWorkingSetSize = value.HasValue ? value.Value.max : SizeT.Zero; + }); + } + } + } + + /// Contains information about a job object limit notification message. + /// + public class JobNotificationEventArgs : JobEventArgs + { + internal JobNotificationEventArgs(JOB_OBJECT_MSG msg, int id, JobLimit k, object v, object n) : base(msg, id) + { + Limit = k; + ReportedValue = v; + NotificationLimit = n; + } + + /// Gets the limit which was exceeded. + /// The limit. + public JobLimit Limit { get; } + + /// Gets the value of the notification limit. + /// The notification limit value. + public object NotificationLimit { get; } + + /// Gets the value of the limited item at the time of the notification. + /// The reported value at the time of notification. + public object ReportedValue { get; } + } + + /// Settings for that set notification limits for different properties. + /// + public class JobNotifications : JobHelper + { + /// Initializes a new instance of the class. + /// Name of the job. + public JobNotifications(string jobName) : base(jobName) { } + + /// Initializes a new instance of the class. + /// The job. + public JobNotifications(Job job) : base(job) { } + + /// + /// + /// Gets or sets the extent to which a job can exceed its I/O rate control limits during the interval specified by the + /// IoRateControlToleranceInterval member. + /// + /// If this value is , then this setting is disabled. + /// This member can be one of the following values. If no value is specified, ToleranceHigh is used. + /// + /// + /// + /// Value + /// Meaning + /// + /// + /// ToleranceLow + /// The job can exceed its I/O rate control limits for 20% of the tolerance interval. + /// + /// + /// ToleranceMedium + /// The job can exceed its I/O rate control limits for 40% of the tolerance interval. + /// + /// + /// ToleranceHigh + /// The job can exceed its I/O rate control limits for 60% of the tolerance interval. + /// + /// + /// + /// + public JOBOBJECT_RATE_CONTROL_TOLERANCE? IoRateControlTolerance + { + get => Get2(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_IO_RATE_CONTROL, i => i.IoRateControlTolerance); + set => Set2(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_IO_RATE_CONTROL, value, (ref JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION_2 i) => + { + i.IoRateControlTolerance = value.GetValueOrDefault(); + if (value.HasValue && i.IoRateControlToleranceInterval == 0) + i.IoRateControlToleranceInterval = JOBOBJECT_RATE_CONTROL_TOLERANCE_INTERVAL.ToleranceIntervalShort; + }); + } + + /// + /// + /// Gets or sets the interval during which a job's I/O usage is monitored to determine whether the job has exceeded its I/O rate + /// control limits. + /// + /// If this value is , then this setting is disabled. + /// This member can be one of the following values. If no value is specified, ToleranceIntervalShort is used. + /// + /// + /// + /// Value + /// Meaning + /// + /// + /// ToleranceIntervalShort + /// The tolerance interval is 10 seconds. + /// + /// + /// ToleranceIntervalMedium + /// The tolerance interval is one minute. + /// + /// + /// ToleranceIntervalLong + /// The tolerance interval is 10 minutes. + /// + /// + /// + /// + public JOBOBJECT_RATE_CONTROL_TOLERANCE_INTERVAL? IoRateControlToleranceInterval + { + get => Get2(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_IO_RATE_CONTROL, i => i.IoRateControlToleranceInterval); + set => Set2(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_IO_RATE_CONTROL, value, (ref JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION_2 i) => + { + i.IoRateControlToleranceInterval = value.GetValueOrDefault(); + if (value.HasValue && i.IoRateControlTolerance == 0) + i.IoRateControlTolerance = JOBOBJECT_RATE_CONTROL_TOLERANCE.ToleranceHigh; + }); + } + + /// + /// Gets or sets the notification limit for total I/O bytes read by all processes in the job. + /// If this value is , then this setting is disabled. + /// + public ulong? IoReadBytesLimit + { + get => Get1(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_READ_BYTES, i => i.IoReadBytesLimit); + set => Set1(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_READ_BYTES, value, (ref JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION i) => i.IoReadBytesLimit = value.GetValueOrDefault()); + } + + /// + /// Gets or sets the notification limit for total I/O bytes written by all processes in the job. + /// If this value is , then this setting is disabled. + /// + public ulong? IoWriteBytesLimit + { + get => Get1(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_WRITE_BYTES, i => i.IoWriteBytesLimit); + set => Set1(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_WRITE_BYTES, value, (ref JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION i) => i.IoWriteBytesLimit = value.GetValueOrDefault()); + } + + /// + /// The notification limit minimum for the total virtual memory that can be committed by all processes in the job, in bytes. The + /// minimum value is 4096. + /// If this value is , then this setting is disabled. + /// + public ulong? JobLowMemoryLimit + { + get => Get2(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_MEMORY_LOW, i => i.JobLowMemoryLimit); + set => Set2(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_MEMORY_LOW, value, (ref JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION_2 i) => i.JobLowMemoryLimit = value.GetValueOrDefault()); + } + + /// + /// The notification limit for total virtual memory that can be committed by all processes in the job, in bytes. The minimum value is 4096. + /// If this value is , then this setting is disabled. + /// + public ulong? JobMemoryLimit + { + get => Get1(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_MEMORY, i => i.JobMemoryLimit); + set => Set1(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_MEMORY, value, (ref JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION i) => i.JobMemoryLimit = value.GetValueOrDefault()); + } + + /// + /// + /// Gets or sets the extent to which a job can exceed its network rate control limits during the interval specified by the + /// NetRateControlToleranceInterval member. + /// + /// If this value is , then this setting is disabled. + /// This member can be one of the following values. If no value is specified, ToleranceHigh is used. + /// + /// + /// + /// Value + /// Meaning + /// + /// + /// ToleranceLow + /// The job can exceed its network rate control limits for 20% of the tolerance interval. + /// + /// + /// ToleranceMedium + /// The job can exceed its network rate control limits for 40% of the tolerance interval. + /// + /// + /// ToleranceHigh + /// The job can exceed its network rate control limits for 60% of the tolerance interval. + /// + /// + /// + /// + public JOBOBJECT_RATE_CONTROL_TOLERANCE? NetRateControlTolerance + { + get => Get2(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_NET_RATE_CONTROL, i => i.NetRateControlTolerance); + set => Set2(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_NET_RATE_CONTROL, value, (ref JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION_2 i) => + { + i.NetRateControlTolerance = value.GetValueOrDefault(); + if (value.HasValue && i.NetRateControlToleranceInterval == 0) + i.NetRateControlToleranceInterval = JOBOBJECT_RATE_CONTROL_TOLERANCE_INTERVAL.ToleranceIntervalShort; + }); + } + + /// + /// + /// Gets or sets the interval during which a job's network usage is monitored to determine whether the job has exceeded its network + /// rate control limits. + /// + /// If this value is , then this setting is disabled. + /// This member can be one of the following values. If no value is specified, ToleranceIntervalShort is used. + /// + /// + /// + /// Value + /// Meaning + /// + /// + /// ToleranceIntervalShort + /// The tolerance interval is 10 seconds. + /// + /// + /// ToleranceIntervalMedium + /// The tolerance interval is one minute. + /// + /// + /// ToleranceIntervalLong + /// The tolerance interval is 10 minutes. + /// + /// + /// + /// + public JOBOBJECT_RATE_CONTROL_TOLERANCE_INTERVAL? NetRateControlToleranceInterval + { + get => Get2(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_NET_RATE_CONTROL, i => i.NetRateControlToleranceInterval); + set => Set2(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_NET_RATE_CONTROL, value, (ref JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION_2 i) => + { + i.NetRateControlToleranceInterval = value.GetValueOrDefault(); + if (value.HasValue && i.NetRateControlTolerance == 0) + i.NetRateControlTolerance = JOBOBJECT_RATE_CONTROL_TOLERANCE.ToleranceHigh; + }); + } + + /// + /// Gets or sets the notification limit for per-job user-mode execution time, in 100-nanosecond ticks. + /// If this value is , then this setting is disabled. + /// + /// The system adds the accumulated execution time of processes associated with the job to this limit when the limit is set. For + /// example, if a process associated with the job has already accumulated 5 minutes of user-mode execution time and the limit is set + /// to 1 minute, the limit actually enforced is 6 minutes. + /// + /// + /// To specify PerJobUserTimeLimit as an enforceable limit and terminate processes in jobs that exceed the limit, see the + /// JOBOBJECT_BASIC_LIMIT_INFORMATION structure. + /// + /// + public TimeSpan? PerJobUserTimeLimit + { + get => Get1(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_TIME, i => i.PerJobUserTimeLimit); + set => Set1(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_TIME, value, (ref JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION i) => i.PerJobUserTimeLimit = value.GetValueOrDefault()); + } + + /// + /// + /// Gets or sets the extent to which a job can exceed its CPU rate control limits during the interval specified by the + /// RateControlToleranceInterval member. + /// + /// If this value is , then this setting is disabled. + /// This member can be one of the following values. If no value is specified, ToleranceHigh is used. + /// + /// + /// + /// Value + /// Meaning + /// + /// + /// ToleranceLow + /// The job can exceed its CPU rate control limits for 20% of the tolerance interval. + /// + /// + /// ToleranceMedium + /// The job can exceed its CPU rate control limits for 40% of the tolerance interval. + /// + /// + /// ToleranceHigh + /// The job can exceed its CPU rate control limits for 60% of the tolerance interval. + /// + /// + /// + /// + public JOBOBJECT_RATE_CONTROL_TOLERANCE? RateControlTolerance + { + get => Get1(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_RATE_CONTROL, i => i.RateControlTolerance); + set => Set1(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_RATE_CONTROL, value, (ref JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION i) => + { + i.RateControlTolerance = value.GetValueOrDefault(); + if (value.HasValue && i.RateControlToleranceInterval == 0) + i.RateControlToleranceInterval = JOBOBJECT_RATE_CONTROL_TOLERANCE_INTERVAL.ToleranceIntervalShort; + }); + } + + /// + /// + /// Gets or sets the interval during which a job's CPU usage is monitored to determine whether the job has exceeded its CPU rate + /// control limits. + /// + /// If this value is , then this setting is disabled. + /// This member can be one of the following values. If no value is specified, ToleranceIntervalShort is used. + /// + /// + /// + /// Value + /// Meaning + /// + /// + /// ToleranceIntervalShort + /// The tolerance interval is 10 seconds. + /// + /// + /// ToleranceIntervalMedium + /// The tolerance interval is one minute. + /// + /// + /// ToleranceIntervalLong + /// The tolerance interval is 10 minutes. + /// + /// + /// + /// + public JOBOBJECT_RATE_CONTROL_TOLERANCE_INTERVAL? RateControlToleranceInterval + { + get => Get1(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_RATE_CONTROL, i => i.RateControlToleranceInterval); + set => Set1(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_RATE_CONTROL, value, (ref JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION i) => + { + i.RateControlToleranceInterval = value.GetValueOrDefault(); + if (value.HasValue && i.RateControlTolerance == 0) + i.RateControlTolerance = JOBOBJECT_RATE_CONTROL_TOLERANCE.ToleranceHigh; + }); + } + + /// Gets field values from JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION. + /// The return type. + /// The limit flag. + /// The method to get the field. + /// The value. + private T? Get1(JOBOBJECT_LIMIT_FLAGS flag, Func getter) where T : struct => + job.CheckThenGet(n => n.LimitFlags.IsFlagSet(flag) ? (T?)getter(n) : null); + + /// Gets field values from JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION_2. + /// The return type. + /// The limit flag. + /// The method to get the field. + /// The value. + private T? Get2(JOBOBJECT_LIMIT_FLAGS flag, Func getter) where T : struct => + job.CheckThenGet(n => n.LimitFlags.IsFlagSet(flag) ? (T?)getter(n) : null); + + /// Sets a field value in JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION. + /// The field type. + /// The limit flag. + /// The value. + /// The method to set the field. + private void Set1(JOBOBJECT_LIMIT_FLAGS flag, T? value, Job.RefAction setter) where T : struct => + job.CheckThenSet((ref JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION i) => { i.LimitFlags = i.LimitFlags.SetFlags(flag, value.HasValue); setter(ref i); }); + + /// Sets a field value in JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION_2. + /// The field type. + /// The limit flag. + /// The value. + /// The method to set the field. + private void Set2(JOBOBJECT_LIMIT_FLAGS flag, T? value, Job.RefAction setter) where T : struct => + job.CheckThenSet((ref JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION_2 i) => { i.LimitFlags = i.LimitFlags.SetFlags(flag, value.HasValue); setter(ref i); }); + } + + /// Represents the security access rights of a job object. + public class JobSecurity : ObjectSecurity + { + /// + public JobSecurity() : base(true, System.Security.AccessControl.ResourceType.KernelObject) { } + } + + /// Settings related to job objects. + public class JobSettings : JobHelper + { + /// Initializes a new instance of the class. + /// Name of the job. + public JobSettings(string jobName) : base(jobName) { } + + /// Initializes a new instance of the class. + /// The job. + public JobSettings(Job job) : base(job) { } + + /// + /// Gets or sets the processor affinity for all processes associated with the job. + /// + /// The affinity must be a subset of the system affinity mask obtained by calling the GetProcessAffinityMask function. The + /// affinity of each thread is set to this value, but threads are free to subsequently set their affinity, as long as it is a subset + /// of the specified affinity mask. Processes cannot set their own affinity mask. + /// + /// If this value is , then this setting is disabled. + /// + public UIntPtr? Affinity + { + get => GetBasic(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_AFFINITY, n => n.Affinity); + set => SetBasic(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_AFFINITY, value, (ref JOBOBJECT_BASIC_LIMIT_INFORMATION i) => i.Affinity = value.GetValueOrDefault()); + } + + /// + /// If any process associated with the job creates a child process using the CREATE_BREAKAWAY_FROM_JOB flag while this value is + /// , the child process is not associated with the job. + /// + public bool ChildProcessBreakawayAllowed + { + get => job.CheckThenGet().BasicLimitInformation.LimitFlags.IsFlagSet(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_BREAKAWAY_OK); + set => job.CheckThenSet((ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION i) => { i.BasicLimitInformation.LimitFlags = i.BasicLimitInformation.LimitFlags.SetFlags(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_BREAKAWAY_OK, value); }); + } + + /// + /// If , allows any process associated with the job to create child processes that are not associated with the + /// job. If the job is nested and its immediate job object allows breakaway, the child process breaks away from the immediate job + /// object and from each job in the parent job chain, moving up the hierarchy until it reaches a job that does not permit breakaway. + /// If the immediate job object does not allow breakaway, the child process does not break away even if jobs in its parent job chain + /// allow it. + /// + public bool ChildProcessSilentBreakawayAllowed + { + get => job.CheckThenGet().BasicLimitInformation.LimitFlags.IsFlagSet(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK); + set => job.CheckThenSet((ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION i) => { i.BasicLimitInformation.LimitFlags = i.BasicLimitInformation.LimitFlags.SetFlags(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK, value); }); + } + + /// + /// If , Forces a call to the SetErrorMode function with the SEM_NOGPFAULTERRORBOX flag for each process + /// associated with the job. If an exception occurs and the system calls the UnhandledExceptionFilter function, the debugger will be + /// given a chance to act. If there is no debugger, the functions returns EXCEPTION_EXECUTE_HANDLER. Normally, this will cause + /// termination of the process with the exception code as the exit status. + /// + public bool DieOnUnhandledException + { + get => job.CheckThenGet().BasicLimitInformation.LimitFlags.IsFlagSet(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION); + set => job.CheckThenSet((ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION i) => { i.BasicLimitInformation.LimitFlags = i.BasicLimitInformation.LimitFlags.SetFlags(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION, value); }); + } + + /// + /// The value to use for the Differentiated Service code point (DSCP) field to turn on network quality of service (QoS) for all + /// outgoing network traffic generated by the processes of the job object. The valid range is from 0x00 through 0x3F. For information + /// about DSCP, see Differentiated Services. + /// If this value is , then this setting is disabled. + /// + public byte? DscpTag + { + get + { + var info = job.CheckThenGet(); + return info.ControlFlags.IsFlagSet(JOB_OBJECT_NET_RATE_CONTROL_FLAGS.JOB_OBJECT_NET_RATE_CONTROL_DSCP_TAG | JOB_OBJECT_NET_RATE_CONTROL_FLAGS.JOB_OBJECT_NET_RATE_CONTROL_ENABLE) ? (byte?)info.DscpTag : null; + } + set + { + if (value > 0x3F) + throw new ArgumentOutOfRangeException(nameof(DscpTag)); + var flag = JOB_OBJECT_NET_RATE_CONTROL_FLAGS.JOB_OBJECT_NET_RATE_CONTROL_DSCP_TAG; + if (value.HasValue) flag |= JOB_OBJECT_NET_RATE_CONTROL_FLAGS.JOB_OBJECT_NET_RATE_CONTROL_ENABLE; + job.CheckThenSet((ref JOBOBJECT_NET_RATE_CONTROL_INFORMATION i) => { i.ControlFlags = flag; i.DscpTag = value.GetValueOrDefault(); }); + } + } + + /// Gets or sets the list of processor groups to which the job is currently assigned. + public IEnumerable GroupAffinity + { + get + { + var gaSz = Marshal.SizeOf(typeof(GROUP_AFFINITY)); + using var mem = new SafeHGlobalHandle(gaSz); + uint req; + while (!QueryInformationJobObject(job.hJob, JOBOBJECTINFOCLASS.JobObjectGroupInformationEx, mem, mem.Size, out req)) + { + Win32Error.ThrowLastErrorUnless(Win32Error.ERROR_MORE_DATA); + mem.Size = req; + } + return mem.ToArray((int)req / gaSz); + } + set + { + using var mem = SafeHGlobalHandle.CreateFromList(value); + if (!SetInformationJobObject(job.hJob, JOBOBJECTINFOCLASS.JobObjectGroupInformationEx, mem, mem.Size)) + Win32Error.ThrowLastError(); + } + } + + /// Causes all processes associated with the job to terminate when the last handle to the job is closed. + /// to kill all processes when the job is closed; otherwise, . + public bool KillOnJobClose + { + get => job.CheckThenGet().BasicLimitInformation.LimitFlags.IsFlagSet(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE); + set => job.CheckThenSet((ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION i) => { i.BasicLimitInformation.LimitFlags = i.BasicLimitInformation.LimitFlags.SetFlags(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, value); }); + } + + /// + /// Gets or sets the priority class for all processes associated with the job. + /// + /// Processes and threads cannot modify their priority class. The calling process must enable the SE_INC_BASE_PRIORITY_NAME privilege. + /// + /// If this value is , then this setting is disabled. + /// + public ProcessPriorityClass? PriorityClass + { + get => (ProcessPriorityClass?)GetBasic(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_PRIORITY_CLASS, n => n.PriorityClass); + set => SetBasic(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_PRIORITY_CLASS, value, (ref JOBOBJECT_BASIC_LIMIT_INFORMATION i) => i.PriorityClass = (uint)value.GetValueOrDefault()); + } + + /// + /// Gets or sets the scheduling class for all processes associated with the job. + /// + /// The valid values are 0 to 9. Use 0 for the least favorable scheduling class relative to other threads, and 9 for the most + /// favorable scheduling class relative to other threads. By default, this value is 5. To use a scheduling class greater than 5, the + /// calling process must enable the SE_INC_BASE_PRIORITY_NAME privilege. + /// + /// If this value is , then this setting is disabled. + /// + public uint? SchedulingClass + { + get => GetBasic(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_SCHEDULING_CLASS, n => n.SchedulingClass); + set => SetBasic(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_SCHEDULING_CLASS, value, (ref JOBOBJECT_BASIC_LIMIT_INFORMATION i) => i.SchedulingClass = value.GetValueOrDefault()); + } + + /// + /// Determines if all processes will be terminated when the end-of-job time limit has been exceeded or if only an event will be sent. + /// The default is to terminate all processes. + /// + /// If , the system terminates all processes and sets the exit status to ERROR_NOT_ENOUGH_QUOTA. The processes + /// cannot prevent or delay their own termination. The job object is set to the signaled state and remains signaled until this limit + /// is reset. No additional processes can be assigned to the job until the limit is reset. This is the default termination action. + /// + /// + /// If , the system generates the event. After the completion packet is posted, + /// the system clears the end-of-job time limit, and processes in the job can continue their execution. If no completion port is + /// associated with the job when the time limit has been exceeded, the action taken is the same as for JOB_OBJECT_TERMINATE_AT_END_OF_JOB. + /// + /// + public bool TerminateProcessesAtEndOfJobTimeLimit + { + get => job.CheckThenGet().EndOfJobTimeAction == JOBOBJECT_END_OF_JOB_TIME_ACTION.JOB_OBJECT_TERMINATE_AT_END_OF_JOB; + set => job.CheckThenSet((ref JOBOBJECT_END_OF_JOB_TIME_INFORMATION i) => i.EndOfJobTimeAction = (JOBOBJECT_END_OF_JOB_TIME_ACTION)(value ? 0 : 1)); + } + + /// + /// The restriction class for the user interface. This member can be one or more of the following values. + /// + /// + /// + /// Value + /// Meaning + /// + /// + /// JOB_OBJECT_UILIMIT_DESKTOP + /// + /// Prevents processes associated with the job from creating desktops and switching desktops using the CreateDesktop and + /// SwitchDesktop functions. + /// + /// + /// + /// JOB_OBJECT_UILIMIT_DISPLAYSETTINGS + /// Prevents processes associated with the job from calling the ChangeDisplaySettings function. + /// + /// + /// JOB_OBJECT_UILIMIT_EXITWINDOWS + /// Prevents processes associated with the job from calling the ExitWindows or ExitWindowsEx function. + /// + /// + /// JOB_OBJECT_UILIMIT_GLOBALATOMS + /// + /// Prevents processes associated with the job from accessing global atoms. When this flag is used, each job has its own atom table. + /// + /// + /// + /// JOB_OBJECT_UILIMIT_HANDLES + /// Prevents processes associated with the job from using USER handles owned by processes not associated with the same job. + /// + /// + /// JOB_OBJECT_UILIMIT_READCLIPBOARD + /// Prevents processes associated with the job from reading data from the clipboard. + /// + /// + /// JOB_OBJECT_UILIMIT_SYSTEMPARAMETERS + /// Prevents processes associated with the job from changing system parameters by using the SystemParametersInfo function. + /// + /// + /// JOB_OBJECT_UILIMIT_WRITECLIPBOARD + /// Prevents processes associated with the job from writing data to the clipboard. + /// + /// + /// + /// + public JOBOBJECT_UILIMIT_FLAGS UIRestrictionsClass + { + get => job.CheckThenGet().UIRestrictionsClass; + set => job.CheckThenSet((ref JOBOBJECT_BASIC_UI_RESTRICTIONS i) => i.UIRestrictionsClass = value); + } + } + + /// Gets statistics for a job object. + /// + public class JobStatistics : JobHelper + { + /// Initializes a new instance of the class with a job name. + /// Name of the job. + public JobStatistics(string jobName) : base(jobName) { } + + /// Initializes a new instance of the class with a job instance. + /// The job. + public JobStatistics(Job job) : base(job) { } + + /// The number of I/O operations performed, other than read and write operations. + public ulong OtherOperationCount => job.CheckThenGet().IoInfo.OtherOperationCount; + + /// The number of bytes transferred during operations other than read and write operations. + public ulong OtherTransferCount => job.CheckThenGet().IoInfo.OtherTransferCount; + + /// The peak memory usage of all processes currently associated with the job. + public ulong PeakJobMemoryUsed => job.CheckThenGet().PeakJobMemoryUsed; + + /// The peak memory used by any process ever associated with the job. + public ulong PeakProcessMemoryUsed => job.CheckThenGet().PeakProcessMemoryUsed; + + /// The number of read operations performed. + public ulong ReadOperationCount => job.CheckThenGet().IoInfo.ReadOperationCount; + + /// The number of bytes read. + public ulong ReadTransferCount => job.CheckThenGet().IoInfo.ReadTransferCount; + + /// + /// + /// The total amount of kernel-mode execution time for all active processes associated with the job (as well as all terminated + /// processes no longer associated with the job) since the last call that set a per-job kernel-mode time limit, in 100-nanosecond ticks. + /// + /// This member is set to zero on creation of the job, and each time a per-job kernel-mode time limit is established. + /// + public TimeSpan ThisPeriodTotalKernelTime => job.CheckThenGet().ThisPeriodTotalKernelTime; + + /// + /// + /// The total amount of user-mode execution time for all active processes associated with the job (as well as all terminated + /// processes no longer associated with the job) since the last call that set a per-job user-mode time limit, in 100-nanosecond ticks. + /// + /// This member is set to 0 on creation of the job, and each time a per-job user-mode time limit is established. + /// + public TimeSpan ThisPeriodTotalUserTime => job.CheckThenGet().ThisPeriodTotalUserTime; + + /// + /// The total amount of kernel-mode execution time for all active processes associated with the job, as well as all terminated + /// processes no longer associated with the job, in 100-nanosecond ticks. + /// + public TimeSpan TotalKernelTime => job.CheckThenGet().TotalKernelTime; + + /// + /// The total number of page faults encountered by all active processes associated with the job, as well as all terminated processes + /// no longer associated with the job. + /// + public uint TotalPageFaultCount => job.CheckThenGet().TotalPageFaultCount; + + /// + /// The total number of processes associated with the job during its lifetime, including those that have terminated. For example, + /// when a process is associated with a job, but the association fails because of a limit violation, this value is incremented. + /// + public uint TotalProcesses => job.CheckThenGet().TotalProcesses; + + /// The total number of processes terminated because of a limit violation. + public uint TotalTerminatedProcesses => job.CheckThenGet().TotalTerminatedProcesses; + + /// + /// The total amount of user-mode execution time for all active processes associated with the job, as well as all terminated + /// processes no longer associated with the job, in 100-nanosecond ticks. + /// + public TimeSpan TotalUserTime => job.CheckThenGet().TotalUserTime; + + /// The number of write operations performed. + public ulong WriteOperationCount => job.CheckThenGet().IoInfo.WriteOperationCount; + + /// The number of bytes written. + public ulong WriteTransferCount => job.CheckThenGet().IoInfo.WriteTransferCount; + } +} \ No newline at end of file diff --git a/System/Vanara.SystemServices.csproj b/System/Vanara.SystemServices.csproj index a75aa8ed..d5bf2129 100644 --- a/System/Vanara.SystemServices.csproj +++ b/System/Vanara.SystemServices.csproj @@ -36,6 +36,7 @@ BackgroundCopyACLFlags, BackgroundCopyCost, BackgroundCopyErrorContext, Backgrou true ..\Vanara.snk bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml + true true diff --git a/UnitTests/PInvoke/Kernel32/JobApiTests.cs b/UnitTests/PInvoke/Kernel32/JobApiTests.cs index c4b8fee0..113638a5 100644 --- a/UnitTests/PInvoke/Kernel32/JobApiTests.cs +++ b/UnitTests/PInvoke/Kernel32/JobApiTests.cs @@ -12,6 +12,37 @@ namespace Vanara.PInvoke.Tests [TestFixture] public class JobApiTests { + [Test] + public void ClearSetValueTest() + { + using var job = CreateJobObject(); + + var bi = QueryInformationJobObject(job, JOBOBJECTINFOCLASS.JobObjectBasicLimitInformation); + Assert.That(bi.LimitFlags, Is.EqualTo((JOBOBJECT_LIMIT_FLAGS)0)); + Assert.That(bi.ActiveProcessLimit, Is.Zero); + bi.WriteValues(); + + bi.ActiveProcessLimit = 2U; + bi.Affinity = (UIntPtr)0xfU; + bi.LimitFlags |= JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_ACTIVE_PROCESS | JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_AFFINITY; + Assert.That(() => SetInformationJobObject(job, JOBOBJECTINFOCLASS.JobObjectBasicLimitInformation, bi), Throws.Nothing); + + var bi2 = QueryInformationJobObject(job, JOBOBJECTINFOCLASS.JobObjectBasicLimitInformation); + Assert.That(bi2.LimitFlags, Is.EqualTo(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_ACTIVE_PROCESS | JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_AFFINITY)); + Assert.That(bi2.ActiveProcessLimit, Is.Not.Zero); + Assert.That(bi2.Affinity, Is.EqualTo((UIntPtr)0xfU)); + bi2.WriteValues(); + + bi2.ActiveProcessLimit = 0; + bi2.LimitFlags &= ~JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_ACTIVE_PROCESS; + Assert.That(() => SetInformationJobObject(job, JOBOBJECTINFOCLASS.JobObjectBasicLimitInformation, bi2), Throws.Nothing); + + var bi3 = QueryInformationJobObject(job, JOBOBJECTINFOCLASS.JobObjectBasicLimitInformation); + Assert.That(bi3.LimitFlags.IsFlagSet(JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_ACTIVE_PROCESS), Is.False); + Assert.That(bi3.ActiveProcessLimit, Is.Zero); + bi3.WriteValues(); + } + [Test] public void CreateAssignChcekJobObjectTest() { diff --git a/UnitTests/System/JobTests.cs b/UnitTests/System/JobTests.cs new file mode 100644 index 00000000..55df0245 --- /dev/null +++ b/UnitTests/System/JobTests.cs @@ -0,0 +1,298 @@ +using NUnit.Framework; +using NUnit.Framework.Internal; +using System; +using System.Diagnostics; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading; +using Vanara.Extensions; +using Vanara.InteropServices; +using Vanara.PInvoke; +using Vanara.PInvoke.Tests; +using static Vanara.PInvoke.Kernel32; + +namespace Vanara.Diagnostics.Tests +{ + [TestFixture] + public class JobTests + { + [Test] + public void ActiveProcessLimitTest() + { + using var job = Job.Create(); + + job.RuntimeLimits.ActiveProcessLimit = 2; + Assert.That(job.RuntimeLimits.ActiveProcessLimit.Value, Is.EqualTo(2U)); + job.RuntimeLimits.ActiveProcessLimit = 0; + Assert.That(job.RuntimeLimits.ActiveProcessLimit.Value, Is.EqualTo(0U)); + Assert.That(() => job.StartProcess("notepad.exe"), Throws.Exception); + job.RuntimeLimits.ActiveProcessLimit = null; + Assert.That(job.RuntimeLimits.ActiveProcessLimit.HasValue, Is.False); + } + + [Test] + public void EventsTest() + { + using var job = Job.Create(Environment.UserName); + try + { + job.NewProcess += msgHndlr; + job.ProcessExited += msgHndlr; + job.JobMemoryLimitExceeded += msgHndlr; + job.JobNotificationLimitExceeded += notHndlr; + + var startInfo = new ProcessStartInfo("notepad.exe") { UseShellExecute = false }; + var p1 = new Process { StartInfo = startInfo }; + p1.StartEx(CREATE_PROCESS.CREATE_SUSPENDED); + job.AssignProcess(p1); + p1.ResumePrimaryThread(); + + Thread.Sleep(200); + + job.Notifications.JobMemoryLimit = job.Statistics.PeakJobMemoryUsed; + job.RuntimeLimits.JobMemoryLimit = job.Notifications.JobMemoryLimit * 3; + //TestContext.WriteLine($"JobMemory: NotLim: {job.JobMemory.NotificationLimit}, Lim: {job.JobMemory.Limit}"); + + //job.PerJobUserTime.NotificationLimit = job.Statistics.TotalUserTime; + //job.PerJobUserTime.Limit = TimeSpan.FromTicks(job.PerJobUserTime.NotificationLimit.Value.Ticks * 3); + //TestContext.WriteLine($"PerJobUserTime: NotLim: {job.PerJobUserTime.NotificationLimit}, Lim: {job.PerJobUserTime.Limit}"); + + var p2 = new Process { StartInfo = startInfo }; + p2.StartEx(CREATE_PROCESS.CREATE_SUSPENDED); + job.AssignProcess(p2); + p2.ResumePrimaryThread(); + + Thread.Sleep(2000); + Thread.Yield(); + + p1.Kill(); + Thread.Sleep(200); + Thread.Yield(); + } + finally + { + job.TerminateAllProcesses(0); + } + + static void msgHndlr(object s, JobEventArgs e) => TestContext.WriteLine($"{DateTime.Now:u}: {e.JobMessage}, {e.ProcessId}"); + static void notHndlr(object s, JobNotificationEventArgs e) => TestContext.WriteLine($"{DateTime.Now:u}: {e.JobMessage}, {e.Limit}, Limit: {e.NotificationLimit}, Val: {e.ReportedValue}"); + } + + [Test] + public void LimitsTest() + { + using var job = Job.Create(); + try + { + Assert.That(job.Processes.Count, Is.EqualTo(0)); + var p1 = TestHelper.RunThrottleApp(); + job.AssignProcess(p1); + Thread.Sleep(200); + + job.RuntimeLimits.ActiveProcessLimit = 1; + Assert.That(() => job.StartProcess("notepad.exe"), Throws.Exception); + job.RuntimeLimits.ActiveProcessLimit = null; + + Test(job.RuntimeLimits, 1.0, (s, v) => s.CpuRateLimit = v, s => s.CpuRateLimit, true); + Test(job.RuntimeLimits, (25.0, 75.0), (s, v) => s.CpuRatePortion = v, s => s.CpuRatePortion, true); + Test(job.RuntimeLimits, 3, (s, v) => s.CpuRateRelativeWeight = v, s => s.CpuRateRelativeWeight, true); + Test(job.RuntimeLimits, 4096UL, (s, v) => s.JobMemoryLimit = v, s => s.JobMemoryLimit, true); + Test(job.RuntimeLimits, 4096UL, (s, v) => s.MaxBandwidth = v, s => s.MaxBandwidth); + TestGT(job.RuntimeLimits, TimeSpan.FromTicks(30000), (s, v) => s.PerJobUserTimeLimit = v, s => s.PerJobUserTimeLimit); + TestGT(job.RuntimeLimits, TimeSpan.FromTicks(30000), (s, v) => s.PerProcessUserTimeLimit = v, s => s.PerProcessUserTimeLimit); + Test(job.RuntimeLimits, 4096UL, (s, v) => s.ProcessMemoryLimit = v, s => s.ProcessMemoryLimit, true); + var pmc = PROCESS_MEMORY_COUNTERS.Default; + GetProcessMemoryInfo(p1, out pmc, pmc.cb); + Test(job.RuntimeLimits, (pmc.WorkingSetSize, pmc.PeakWorkingSetSize), (s, v) => s.WorkingSetSize = v, s => s.WorkingSetSize, true); + } + finally + { + job.TerminateAllProcesses(0); + Thread.Sleep(200); + } + + void TestGT(TS js, T? value, Action set, Func get) where T : struct + { + Assert.That(() => set(js, value), Throws.Nothing); + Assert.That(get(js).Value, Is.GreaterThanOrEqualTo(value)); + + Assert.That(() => set(js, null), Throws.Nothing); + Assert.That(get(js), Is.Null); + } + } + + [Test] + public void NotificationTest() + { + using var job = Job.Create(); + try + { + job.NewProcess += (s, e) => TestContext.WriteLine($"{DateTime.Now:u}: {e.JobMessage}, {e.ProcessId}"); + job.JobNotificationLimitExceeded += notHndlr; + + job.Notifications.IoRateControlTolerance = JOBOBJECT_RATE_CONTROL_TOLERANCE.ToleranceLow; + job.Notifications.IoRateControlToleranceInterval = JOBOBJECT_RATE_CONTROL_TOLERANCE_INTERVAL.ToleranceIntervalShort; + //job.Notifications.IoReadBytesLimit = 1; + //job.Notifications.IoWriteBytesLimit = 1; + job.Notifications.JobMemoryLimit = 8092; + job.Notifications.JobLowMemoryLimit = 4096; + job.Notifications.NetRateControlTolerance = JOBOBJECT_RATE_CONTROL_TOLERANCE.ToleranceLow; + job.Notifications.NetRateControlToleranceInterval = JOBOBJECT_RATE_CONTROL_TOLERANCE_INTERVAL.ToleranceIntervalShort; + //job.Notifications.PerJobUserTimeLimit = TimeSpan.FromMilliseconds(1); + //job.Notifications.RateControlTolerance = JOBOBJECT_RATE_CONTROL_TOLERANCE.ToleranceLow; + //job.Notifications.RateControlToleranceInterval = JOBOBJECT_RATE_CONTROL_TOLERANCE_INTERVAL.ToleranceIntervalShort; + + job.StartProcess("notepad.exe"); + + for (int i = 0; i < 10; i++) + { + Thread.Sleep(1000); + Thread.Yield(); + } + } + finally + { + job.TerminateAllProcesses(0); + } + + static void notHndlr(object s, JobNotificationEventArgs e) => TestContext.WriteLine($"{DateTime.Now:u}: {e.JobMessage}, {e.Limit}, Limit: {e.NotificationLimit}, Val: {e.ReportedValue}"); + } + + [Test] + public void OpenJobTest() + { + using var job = Job.Create(Environment.UserName); + Assert.That(job.Handle, ResultIs.ValidHandle); + //using var job1 = Job.Open(Environment.UserName, JobAccessRight.JOB_OBJECT_QUERY); + //Assert.That(job1.Handle, ResultIs.ValidHandle); + + Assert.That(() => job.Settings.GroupAffinity, Throws.Nothing); + } + + [Test] + public void ProcessesTest() + { + Process p2 = null; + using (var job = Job.Create()) + { + job.Settings.KillOnJobClose = true; + + var curProc = Process.GetCurrentProcess(); + Assert.That(job.ContainsProcess(curProc), Is.False); + + var p1 = Process.Start("notepad.exe"); + job.AssignProcess(p1); + job.AssignProcess(p2 = Process.Start("notepad.exe")); + + Assert.That(job.Processes.Count, Is.EqualTo(2)); + Assert.That(job.Processes.Count(), Is.EqualTo(2)); + Assert.That(job.Processes.First().Id, Is.EqualTo(p1.Id)); + + p1.Kill(); + Assert.That(p1.WaitForExit(500), Is.True); + Assert.That(job.Processes.Count, Is.EqualTo(1)); + Assert.That(job.Processes.Count(), Is.EqualTo(1)); + } + Assert.That(p2.WaitForExit(500), Is.True); + } + + [Test] + public void SettingsTest() + { + using var job = Job.Create(); + + Test(job.Settings, new UIntPtr(0xf), (s, v) => s.Affinity = v, s => s.Affinity, true); + TestBool(job.Settings, (s, v) => s.ChildProcessBreakawayAllowed = v, s => s.ChildProcessBreakawayAllowed); + TestBool(job.Settings, (s, v) => s.ChildProcessSilentBreakawayAllowed = v, s => s.ChildProcessSilentBreakawayAllowed); + TestBool(job.Settings, (s, v) => s.DieOnUnhandledException = v, s => s.DieOnUnhandledException); + Test(job.Settings, 0xf, (s, v) => s.DscpTag = v, s => s.DscpTag); + TestBool(job.Settings, (s, v) => s.KillOnJobClose = v, s => s.KillOnJobClose); + Test(job.Settings, ProcessPriorityClass.BelowNormal, (s, v) => s.PriorityClass = v, s => s.PriorityClass, true); + Test(job.Settings, 3, (s, v) => s.SchedulingClass = v, s => s.SchedulingClass); + TestBool(job.Settings, (s, v) => s.TerminateProcessesAtEndOfJobTimeLimit = v, s => s.TerminateProcessesAtEndOfJobTimeLimit); + Assert.That(() => job.Settings.UIRestrictionsClass = JOBOBJECT_UILIMIT_FLAGS.JOB_OBJECT_UILIMIT_ALL, Throws.Nothing); + Assert.That(job.Settings.UIRestrictionsClass, Is.EqualTo(JOBOBJECT_UILIMIT_FLAGS.JOB_OBJECT_UILIMIT_ALL)); + + Assert.That(() => job.Settings.GroupAffinity.First(), Throws.Nothing); + Assert.That(job.Settings.GroupAffinity.First().Mask.ToUInt32(), Is.Not.Zero); + Assert.That(() => job.Settings.GroupAffinity = job.Settings.GroupAffinity, Throws.Nothing); + } + + private void Test(TS js, T? value, Action set, Func get, bool ignDef = false) where T : struct + { + Assert.That(() => set(js, value), Throws.Nothing); + Assert.That(get(js).Value, Is.EqualTo(value)); + + if (!ignDef) + { + Assert.That(() => set(js, default(T)), Throws.Nothing); + Assert.That(get(js).Value, Is.EqualTo(default(T))); + } + + Assert.That(() => set(js, null), Throws.Nothing); + Assert.That(get(js), Is.Null); + } + + private void TestBool(TS js, Action set, Func get) + { + var orig = get(js); + + Assert.That(() => set(js, !orig), Throws.Nothing); + Assert.That(get(js), Is.EqualTo(!orig)); + + Assert.That(() => set(js, orig), Throws.Nothing); + Assert.That(get(js), Is.EqualTo(orig)); + } + + [Test] + public void StatsTest() + { + using var job = Job.Create(); + try + { + job.AssignProcess(Process.Start("notepad.exe")); + Thread.Sleep(200); + } + finally + { + job.TerminateAllProcesses(0); + Thread.Sleep(200); + } + var stats = job.Statistics; + stats.WriteValues(); + Assert.That(stats.PeakJobMemoryUsed, Is.GreaterThan(0UL)); + Assert.That(stats.PeakProcessMemoryUsed, Is.GreaterThan(0UL)); + Assert.That(stats.ThisPeriodTotalKernelTime.Ticks, Is.GreaterThan(0UL)); + Assert.That(stats.ThisPeriodTotalUserTime.Ticks, Is.GreaterThanOrEqualTo(0UL)); + Assert.That(stats.TotalKernelTime.Ticks, Is.GreaterThan(0UL)); + Assert.That(stats.TotalPageFaultCount, Is.GreaterThanOrEqualTo(0UL)); + Assert.That(stats.TotalProcesses, Is.GreaterThan(0UL)); + Assert.That(stats.TotalTerminatedProcesses, Is.GreaterThanOrEqualTo(0UL)); + Assert.That(stats.TotalUserTime.Ticks, Is.GreaterThanOrEqualTo(0UL)); + } + + [Test] + public void TempTest() + { + using var job = Job.Create(); + var str = new JOBOBJECT_NOTIFICATION_LIMIT_INFORMATION_2 { LimitFlags = JOBOBJECT_LIMIT_FLAGS.JOB_OBJECT_LIMIT_JOB_MEMORY_LOW, JobLowMemoryLimit = 4096 }; + using var mem = SafeHGlobalHandle.CreateFromStructure(str); + if (!SetInformationJobObject(job, JOBOBJECTINFOCLASS.JobObjectNotificationLimitInformation2, mem, mem.Size)) + TestContext.WriteLine($"{Win32Error.GetLastError()}"); + + mem.Zero(); + if (QueryInformationJobObject(job, JOBOBJECTINFOCLASS.JobObjectNotificationLimitInformation2, mem, mem.Size, out _)) + mem.ToStructure().WriteValues(); + + str.LimitFlags = 0; + str.JobLowMemoryLimit = 0; + mem.Write(str); + if (!SetInformationJobObject(job, JOBOBJECTINFOCLASS.JobObjectNotificationLimitInformation2, mem, mem.Size)) + TestContext.WriteLine($"{Win32Error.GetLastError()}"); + + mem.Zero(); + if (QueryInformationJobObject(job, JOBOBJECTINFOCLASS.JobObjectNotificationLimitInformation2, mem, mem.Size, out _)) + mem.ToStructure().WriteValues(); + } + } +} \ No newline at end of file diff --git a/UnitTests/System/System.csproj b/UnitTests/System/System.csproj index 7a7c9ace..f99feb81 100644 --- a/UnitTests/System/System.csproj +++ b/UnitTests/System/System.csproj @@ -22,6 +22,7 @@ DEBUG;TRACE prompt 4 + true pdbonly @@ -30,17 +31,17 @@ TRACE prompt 4 + true - + - diff --git a/Vanara.sln b/Vanara.sln index d9a23609..8968c42c 100644 --- a/Vanara.sln +++ b/Vanara.sln @@ -173,6 +173,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Vanara.BITS", "BITS\Vanara. EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Vanara.VirtualDisk", "VirtualDisk\Vanara.VirtualDisk.csproj", "{D4E36942-7492-46B9-985B-F99D8F5A35AB}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BITS", "UnitTests\BITS\BITS.csproj", "{5558B8E3-FF1C-401F-978E-E91D1E78B898}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "VirtualDisk", "UnitTests\VirtualDisk\VirtualDisk.csproj", "{687F9162-8CA0-4277-B868-4E7F2EC614F8}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug (no Unit Tests)|Any CPU = Debug (no Unit Tests)|Any CPU @@ -565,6 +569,18 @@ Global {D4E36942-7492-46B9-985B-F99D8F5A35AB}.Debug|Any CPU.Build.0 = Debug|Any CPU {D4E36942-7492-46B9-985B-F99D8F5A35AB}.Release|Any CPU.ActiveCfg = Release|Any CPU {D4E36942-7492-46B9-985B-F99D8F5A35AB}.Release|Any CPU.Build.0 = Release|Any CPU + {5558B8E3-FF1C-401F-978E-E91D1E78B898}.Debug (no Unit Tests)|Any CPU.ActiveCfg = Debug|Any CPU + {5558B8E3-FF1C-401F-978E-E91D1E78B898}.Debug (no Unit Tests)|Any CPU.Build.0 = Debug|Any CPU + {5558B8E3-FF1C-401F-978E-E91D1E78B898}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5558B8E3-FF1C-401F-978E-E91D1E78B898}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5558B8E3-FF1C-401F-978E-E91D1E78B898}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5558B8E3-FF1C-401F-978E-E91D1E78B898}.Release|Any CPU.Build.0 = Release|Any CPU + {687F9162-8CA0-4277-B868-4E7F2EC614F8}.Debug (no Unit Tests)|Any CPU.ActiveCfg = Debug|Any CPU + {687F9162-8CA0-4277-B868-4E7F2EC614F8}.Debug (no Unit Tests)|Any CPU.Build.0 = Debug|Any CPU + {687F9162-8CA0-4277-B868-4E7F2EC614F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {687F9162-8CA0-4277-B868-4E7F2EC614F8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {687F9162-8CA0-4277-B868-4E7F2EC614F8}.Release|Any CPU.ActiveCfg = Release|Any CPU + {687F9162-8CA0-4277-B868-4E7F2EC614F8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -639,6 +655,8 @@ Global {8BC51B6B-77FA-4571-8E1C-EA75ED4DCD56} = {385CAD2D-0A5E-4F80-927B-D5499D126B90} {C1D9429F-A8BD-4094-ABE3-32581FC4614F} = {212ABBD0-B724-4CFA-9D6D-E3891547FA90} {15189584-3BD8-47DB-8B65-F58482063585} = {385CAD2D-0A5E-4F80-927B-D5499D126B90} + {5558B8E3-FF1C-401F-978E-E91D1E78B898} = {3EC6B40D-71D3-4E59-A0E0-544EC605FE11} + {687F9162-8CA0-4277-B868-4E7F2EC614F8} = {3EC6B40D-71D3-4E59-A0E0-544EC605FE11} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {543FAC75-2AF1-4EF1-9609-B242B63FEED4}