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}