using Microsoft.Extensions.Logging; using Microsoft.Win32.SafeHandles; using System.Runtime.InteropServices; using System.Runtime.Versioning; using SystemProcess = System.Diagnostics.Process; namespace StellaOps.Plugin.Sandbox.Resources; /// /// Windows Job Object resource limiter implementation. /// [SupportedOSPlatform("windows")] public sealed class WindowsResourceLimiter : IResourceLimiter, IDisposable { private readonly ILogger _logger; private readonly Dictionary _jobHandles = new(); private readonly object _lock = new(); private bool _disposed; /// /// Creates a new Windows resource limiter. /// public WindowsResourceLimiter(ILogger logger) { _logger = logger; } /// public ResourceConfiguration CreateConfiguration(ResourceLimits limits) { var cpuQuotaUs = limits.MaxCpuPercent > 0 ? (long)(limits.MaxCpuPercent / 100.0 * 100_000) : 0; return new ResourceConfiguration { MemoryLimitBytes = limits.MaxMemoryMb * 1024 * 1024, CpuQuotaUs = cpuQuotaUs, CpuPeriodUs = 100_000, MaxProcesses = limits.MaxProcesses, MaxOpenFiles = limits.MaxOpenFiles }; } /// public Task ApplyLimitsAsync( SystemProcess process, ResourceConfiguration config, CancellationToken ct) { if (!OperatingSystem.IsWindows()) { _logger.LogWarning("WindowsResourceLimiter called on non-Windows platform"); return Task.CompletedTask; } var jobName = $"StellaOps_Sandbox_{process.Id}"; try { // Create Job Object var jobHandle = NativeMethods.CreateJobObject(IntPtr.Zero, jobName); if (jobHandle.IsInvalid) { throw new InvalidOperationException( $"Failed to create Job Object: {Marshal.GetLastWin32Error()}"); } // Configure limits var extendedInfo = new NativeMethods.JOBOBJECT_EXTENDED_LIMIT_INFORMATION(); extendedInfo.BasicLimitInformation.LimitFlags = 0; // Memory limit if (config.MemoryLimitBytes > 0) { extendedInfo.ProcessMemoryLimit = (UIntPtr)config.MemoryLimitBytes; extendedInfo.JobMemoryLimit = (UIntPtr)config.MemoryLimitBytes; extendedInfo.BasicLimitInformation.LimitFlags |= NativeMethods.JOB_OBJECT_LIMIT_PROCESS_MEMORY | NativeMethods.JOB_OBJECT_LIMIT_JOB_MEMORY; } // Process limit if (config.MaxProcesses > 0) { extendedInfo.BasicLimitInformation.ActiveProcessLimit = (uint)config.MaxProcesses; extendedInfo.BasicLimitInformation.LimitFlags |= NativeMethods.JOB_OBJECT_LIMIT_ACTIVE_PROCESS; } // Apply extended limits var extendedInfoSize = Marshal.SizeOf(); var success = NativeMethods.SetInformationJobObject( jobHandle, NativeMethods.JobObjectInfoType.ExtendedLimitInformation, ref extendedInfo, extendedInfoSize); if (!success) { jobHandle.Dispose(); throw new InvalidOperationException( $"Failed to set Job Object limits: {Marshal.GetLastWin32Error()}"); } // Configure CPU rate control (Windows 8+) if (config.CpuQuotaUs > 0) { var cpuRate = (uint)(config.CpuQuotaUs / (double)config.CpuPeriodUs * 10000); var cpuInfo = new NativeMethods.JOBOBJECT_CPU_RATE_CONTROL_INFORMATION { ControlFlags = NativeMethods.JOB_OBJECT_CPU_RATE_CONTROL_ENABLE | NativeMethods.JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP, CpuRate = cpuRate }; var cpuInfoSize = Marshal.SizeOf(); NativeMethods.SetInformationJobObject( jobHandle, NativeMethods.JobObjectInfoType.CpuRateControlInformation, ref cpuInfo, cpuInfoSize); // CPU rate control may fail on older Windows versions - non-fatal } // Assign process to Job Object success = NativeMethods.AssignProcessToJobObject(jobHandle, process.Handle); if (!success) { jobHandle.Dispose(); throw new InvalidOperationException( $"Failed to assign process to Job Object: {Marshal.GetLastWin32Error()}"); } lock (_lock) { _jobHandles[process.Id] = jobHandle; } _logger.LogDebug( "Applied Job Object limits to process {ProcessId}: Memory={MemoryBytes}B", process.Id, config.MemoryLimitBytes); } catch (Exception ex) { _logger.LogError(ex, "Failed to apply Job Object limits to process {ProcessId}", process.Id); throw; } return Task.CompletedTask; } /// public Task RemoveLimitsAsync(SystemProcess process, CancellationToken ct) { if (!OperatingSystem.IsWindows()) return Task.CompletedTask; SafeFileHandle? jobHandle; lock (_lock) { if (!_jobHandles.TryGetValue(process.Id, out jobHandle)) return Task.CompletedTask; _jobHandles.Remove(process.Id); } try { // Terminate job object (will terminate all processes in job) NativeMethods.TerminateJobObject(jobHandle, 0); jobHandle.Dispose(); _logger.LogDebug("Removed Job Object for process {ProcessId}", process.Id); } catch (Exception ex) { _logger.LogWarning(ex, "Failed to cleanup Job Object for process {ProcessId}", process.Id); } return Task.CompletedTask; } /// public Task GetUsageAsync(SystemProcess process, CancellationToken ct) { if (!OperatingSystem.IsWindows()) { return Task.FromResult(ResourceUsage.Empty); } try { process.Refresh(); return Task.FromResult(new ResourceUsage { MemoryUsageMb = process.WorkingSet64 / (1024.0 * 1024.0), CpuUsagePercent = GetCpuUsage(process), ProcessCount = process.Threads.Count, OpenFileHandles = process.HandleCount, Timestamp = DateTimeOffset.UtcNow }); } catch { return Task.FromResult(ResourceUsage.Empty); } } /// public async Task CheckLimitsAsync( SystemProcess process, ResourceLimits limits, CancellationToken ct) { var usage = await GetUsageAsync(process, ct); // Check memory if (limits.MaxMemoryMb > 0 && usage.MemoryUsageMb > limits.MaxMemoryMb) { return LimitCheckResult.Exceeded( ResourceType.Memory, usage.MemoryUsageMb, limits.MaxMemoryMb); } // Check CPU if (limits.MaxCpuPercent > 0 && usage.CpuUsagePercent > limits.MaxCpuPercent) { return LimitCheckResult.Exceeded( ResourceType.Cpu, usage.CpuUsagePercent, limits.MaxCpuPercent); } return LimitCheckResult.Ok; } /// /// Disposes resources. /// public void Dispose() { if (_disposed) return; _disposed = true; lock (_lock) { foreach (var handle in _jobHandles.Values) { handle.Dispose(); } _jobHandles.Clear(); } } private static double GetCpuUsage(SystemProcess process) { // Simplified CPU usage - accurate measurement requires time-based sampling try { return process.TotalProcessorTime.TotalMilliseconds / (Environment.ProcessorCount * process.TotalProcessorTime.TotalMilliseconds + 1) * 100; } catch { return 0; } } private static class NativeMethods { public const uint JOB_OBJECT_LIMIT_PROCESS_MEMORY = 0x00000100; public const uint JOB_OBJECT_LIMIT_JOB_MEMORY = 0x00000200; public const uint JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 0x00000008; public const uint JOB_OBJECT_CPU_RATE_CONTROL_ENABLE = 0x00000001; public const uint JOB_OBJECT_CPU_RATE_CONTROL_HARD_CAP = 0x00000004; public enum JobObjectInfoType { BasicLimitInformation = 2, ExtendedLimitInformation = 9, CpuRateControlInformation = 15 } [StructLayout(LayoutKind.Sequential)] public 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)] public struct IO_COUNTERS { public ulong ReadOperationCount; public ulong WriteOperationCount; public ulong OtherOperationCount; public ulong ReadTransferCount; public ulong WriteTransferCount; public ulong OtherTransferCount; } [StructLayout(LayoutKind.Sequential)] public 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; } [StructLayout(LayoutKind.Sequential)] public struct JOBOBJECT_CPU_RATE_CONTROL_INFORMATION { public uint ControlFlags; public uint CpuRate; } [DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)] public static extern SafeFileHandle CreateJobObject(IntPtr lpJobAttributes, string? lpName); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool SetInformationJobObject( SafeFileHandle hJob, JobObjectInfoType infoType, ref JOBOBJECT_EXTENDED_LIMIT_INFORMATION lpJobObjectInfo, int cbJobObjectInfoLength); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool SetInformationJobObject( SafeFileHandle hJob, JobObjectInfoType infoType, ref JOBOBJECT_CPU_RATE_CONTROL_INFORMATION lpJobObjectInfo, int cbJobObjectInfoLength); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool AssignProcessToJobObject(SafeFileHandle hJob, IntPtr hProcess); [DllImport("kernel32.dll", SetLastError = true)] public static extern bool TerminateJobObject(SafeFileHandle hJob, uint uExitCode); } }