using System.ComponentModel;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Microsoft.Win32.SafeHandles;
namespace ZB.MOM.WW.OtOpcUa.Core.Scripting.OutOfProcess;
///
/// A Windows Job Object holding one ScriptWorker (design §3.3, §5.2): KILL_ON_JOB_CLOSE so the kernel kills
/// the worker when the host dies (its handle closes with it), ACTIVE_PROCESS = 1 so the worker can start no
/// child process, and a process-memory backstop slightly above the managed heap limit for native and JIT memory.
///
[SupportedOSPlatform("windows")]
internal sealed class WindowsJobObject : IDisposable
{
private const int JobObjectExtendedLimitInformation = 9;
private const uint JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 0x00000008;
private const uint JOB_OBJECT_LIMIT_PROCESS_MEMORY = 0x00000100;
private const uint JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION = 0x00000400;
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
private readonly SafeFileHandle _handle;
private WindowsJobObject(SafeFileHandle handle) => _handle = handle;
/// Creates a job with the worker limits and assigns to it.
/// The freshly started worker.
/// The per-process commit limit.
/// The job; dispose it to kill the worker.
public static WindowsJobObject Assign(Process process, long processMemoryLimitBytes)
=> Assign(process.Handle, processMemoryLimitBytes);
///
/// Creates a job with the worker limits and assigns the process behind to it — the
/// restricted launch assigns its worker while it is still suspended ().
///
/// A handle with PROCESS_SET_QUOTA and PROCESS_TERMINATE.
/// The per-process commit limit.
/// The job; dispose it to kill the worker.
public static WindowsJobObject Assign(IntPtr processHandle, long processMemoryLimitBytes)
{
var handle = CreateJobObjectW(IntPtr.Zero, null);
if (handle.IsInvalid) throw new Win32Exception(Marshal.GetLastWin32Error(), "CreateJobObject failed");
var job = new WindowsJobObject(handle);
try
{
var info = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION
{
LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE | JOB_OBJECT_LIMIT_ACTIVE_PROCESS
| JOB_OBJECT_LIMIT_PROCESS_MEMORY | JOB_OBJECT_LIMIT_DIE_ON_UNHANDLED_EXCEPTION,
ActiveProcessLimit = 1,
},
ProcessMemoryLimit = (UIntPtr)(ulong)processMemoryLimitBytes,
};
if (!SetInformationJobObject(handle, JobObjectExtendedLimitInformation, ref info,
(uint)Marshal.SizeOf()))
throw new Win32Exception(Marshal.GetLastWin32Error(), "SetInformationJobObject failed");
if (!AssignProcessToJobObject(handle, processHandle))
throw new Win32Exception(Marshal.GetLastWin32Error(), "AssignProcessToJobObject failed");
return job;
}
catch
{
job.Dispose();
throw;
}
}
///
/// Kills every process in the job at once (lmxopcua#789). The job admits exactly one process, so this is the
/// worker's whole tree without Process.Kill(entireProcessTree: true)'s system-wide process snapshot,
/// which cost about 100–200 ms of the Windows kill latency.
///
/// False when the job could not be terminated (the caller falls back to a process kill).
public bool Terminate() => !_handle.IsClosed && TerminateJobObject(_handle, unchecked((uint)-1));
/// Closes the job handle, which kills every process still in it.
public void Dispose() => _handle.Dispose();
[StructLayout(LayoutKind.Sequential)]
private struct JOBOBJECT_BASIC_LIMIT_INFORMATION
{
public long PerProcessUserTimeLimit;
public long PerJobUserTimeLimit;
public uint LimitFlags;
public UIntPtr MinimumWorkingSetSize;
public UIntPtr MaximumWorkingSetSize;
public uint ActiveProcessLimit;
public UIntPtr Affinity;
public uint PriorityClass;
public uint SchedulingClass;
}
[StructLayout(LayoutKind.Sequential)]
private struct IO_COUNTERS
{
public ulong ReadOperationCount;
public ulong WriteOperationCount;
public ulong OtherOperationCount;
public ulong ReadTransferCount;
public ulong WriteTransferCount;
public ulong OtherTransferCount;
}
[StructLayout(LayoutKind.Sequential)]
private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
{
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
public IO_COUNTERS IoInfo;
public UIntPtr ProcessMemoryLimit;
public UIntPtr JobMemoryLimit;
public UIntPtr PeakProcessMemoryUsed;
public UIntPtr PeakJobMemoryUsed;
}
#pragma warning disable SYSLIB1054 // DllImport kept: LibraryImport would need AllowUnsafeBlocks for four calls.
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern SafeFileHandle CreateJobObjectW(IntPtr securityAttributes, string? name);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool SetInformationJobObject(SafeFileHandle job, int infoClass,
ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION info, uint length);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool AssignProcessToJobObject(SafeFileHandle job, IntPtr process);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool TerminateJobObject(SafeFileHandle job, uint exitCode);
#pragma warning restore SYSLIB1054
}