release orchestrator v1 draft and build fixes
This commit is contained in:
@@ -0,0 +1,353 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Versioning;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
using SystemProcess = System.Diagnostics.Process;
|
||||
|
||||
namespace StellaOps.Plugin.Sandbox.Resources;
|
||||
|
||||
/// <summary>
|
||||
/// Windows Job Object resource limiter implementation.
|
||||
/// </summary>
|
||||
[SupportedOSPlatform("windows")]
|
||||
public sealed class WindowsResourceLimiter : IResourceLimiter, IDisposable
|
||||
{
|
||||
private readonly ILogger<WindowsResourceLimiter> _logger;
|
||||
private readonly Dictionary<int, SafeFileHandle> _jobHandles = new();
|
||||
private readonly object _lock = new();
|
||||
private bool _disposed;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Windows resource limiter.
|
||||
/// </summary>
|
||||
public WindowsResourceLimiter(ILogger<WindowsResourceLimiter> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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<NativeMethods.JOBOBJECT_EXTENDED_LIMIT_INFORMATION>();
|
||||
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.JOBOBJECT_CPU_RATE_CONTROL_INFORMATION>();
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<ResourceUsage> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<LimitCheckResult> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes resources.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user