release orchestrator v1 draft and build fixes
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
using SystemProcess = System.Diagnostics.Process;
|
||||
|
||||
namespace StellaOps.Plugin.Sandbox.Resources;
|
||||
|
||||
/// <summary>
|
||||
/// Interface for applying and monitoring resource limits on processes.
|
||||
/// </summary>
|
||||
public interface IResourceLimiter
|
||||
{
|
||||
/// <summary>
|
||||
/// Create a resource configuration from the specified limits.
|
||||
/// </summary>
|
||||
/// <param name="limits">Resource limits to configure.</param>
|
||||
/// <returns>Platform-specific resource configuration.</returns>
|
||||
ResourceConfiguration CreateConfiguration(ResourceLimits limits);
|
||||
|
||||
/// <summary>
|
||||
/// Apply resource limits to a process.
|
||||
/// </summary>
|
||||
/// <param name="process">Process to limit.</param>
|
||||
/// <param name="config">Resource configuration.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task ApplyLimitsAsync(SystemProcess process, ResourceConfiguration config, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Get current resource usage for a process.
|
||||
/// </summary>
|
||||
/// <param name="process">Process to monitor.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>Current resource usage.</returns>
|
||||
Task<ResourceUsage> GetUsageAsync(SystemProcess process, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Remove resource limits from a process.
|
||||
/// </summary>
|
||||
/// <param name="process">Process to unlimit.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
Task RemoveLimitsAsync(SystemProcess process, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Check if the process has exceeded any limits.
|
||||
/// </summary>
|
||||
/// <param name="process">Process to check.</param>
|
||||
/// <param name="limits">Limits to check against.</param>
|
||||
/// <param name="ct">Cancellation token.</param>
|
||||
/// <returns>True if any limit is exceeded.</returns>
|
||||
Task<LimitCheckResult> CheckLimitsAsync(SystemProcess process, ResourceLimits limits, CancellationToken ct);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Result of a limit check.
|
||||
/// </summary>
|
||||
public sealed record LimitCheckResult
|
||||
{
|
||||
/// <summary>
|
||||
/// Whether any limit was exceeded.
|
||||
/// </summary>
|
||||
public required bool IsExceeded { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Which resource exceeded its limit, if any.
|
||||
/// </summary>
|
||||
public ResourceType? ExceededResource { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Current value of the exceeded resource.
|
||||
/// </summary>
|
||||
public double? CurrentValue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Limit value that was exceeded.
|
||||
/// </summary>
|
||||
public double? LimitValue { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Message describing the exceeded limit.
|
||||
/// </summary>
|
||||
public string? Message { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Result indicating no limits were exceeded.
|
||||
/// </summary>
|
||||
public static LimitCheckResult Ok => new() { IsExceeded = false };
|
||||
|
||||
/// <summary>
|
||||
/// Create a result for an exceeded limit.
|
||||
/// </summary>
|
||||
public static LimitCheckResult Exceeded(ResourceType resource, double current, double limit) => new()
|
||||
{
|
||||
IsExceeded = true,
|
||||
ExceededResource = resource,
|
||||
CurrentValue = current,
|
||||
LimitValue = limit,
|
||||
Message = $"{resource} limit exceeded: {current:F2} > {limit:F2}"
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
using System.Globalization;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using SystemProcess = System.Diagnostics.Process;
|
||||
|
||||
namespace StellaOps.Plugin.Sandbox.Resources;
|
||||
|
||||
/// <summary>
|
||||
/// Linux cgroups v2 resource limiter implementation.
|
||||
/// </summary>
|
||||
public sealed class LinuxResourceLimiter : IResourceLimiter
|
||||
{
|
||||
private const string CgroupBasePath = "/sys/fs/cgroup";
|
||||
private const string StellaOpsCgroupName = "stellaops-sandbox";
|
||||
|
||||
private readonly ILogger<LinuxResourceLimiter> _logger;
|
||||
private readonly Dictionary<int, string> _processCgroups = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a new Linux resource limiter.
|
||||
/// </summary>
|
||||
public LinuxResourceLimiter(ILogger<LinuxResourceLimiter> 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 async Task ApplyLimitsAsync(
|
||||
SystemProcess process,
|
||||
ResourceConfiguration config,
|
||||
CancellationToken ct)
|
||||
{
|
||||
if (!OperatingSystem.IsLinux())
|
||||
{
|
||||
_logger.LogWarning("LinuxResourceLimiter called on non-Linux platform");
|
||||
return;
|
||||
}
|
||||
|
||||
var cgroupPath = Path.Combine(CgroupBasePath, $"{StellaOpsCgroupName}-{process.Id}");
|
||||
|
||||
try
|
||||
{
|
||||
// Create cgroup directory
|
||||
if (!Directory.Exists(cgroupPath))
|
||||
{
|
||||
Directory.CreateDirectory(cgroupPath);
|
||||
}
|
||||
|
||||
// Configure memory limit
|
||||
if (config.MemoryLimitBytes > 0)
|
||||
{
|
||||
await WriteControlFileAsync(
|
||||
cgroupPath,
|
||||
"memory.max",
|
||||
config.MemoryLimitBytes.ToString(CultureInfo.InvariantCulture),
|
||||
ct);
|
||||
|
||||
// Also set high watermark for throttling before kill
|
||||
var highMark = (long)(config.MemoryLimitBytes * 0.9);
|
||||
await WriteControlFileAsync(
|
||||
cgroupPath,
|
||||
"memory.high",
|
||||
highMark.ToString(CultureInfo.InvariantCulture),
|
||||
ct);
|
||||
}
|
||||
|
||||
// Configure CPU limit
|
||||
if (config.CpuQuotaUs > 0)
|
||||
{
|
||||
await WriteControlFileAsync(
|
||||
cgroupPath,
|
||||
"cpu.max",
|
||||
$"{config.CpuQuotaUs} {config.CpuPeriodUs}",
|
||||
ct);
|
||||
}
|
||||
|
||||
// Configure process limit
|
||||
if (config.MaxProcesses > 0)
|
||||
{
|
||||
await WriteControlFileAsync(
|
||||
cgroupPath,
|
||||
"pids.max",
|
||||
config.MaxProcesses.ToString(CultureInfo.InvariantCulture),
|
||||
ct);
|
||||
}
|
||||
|
||||
// Add process to cgroup
|
||||
await WriteControlFileAsync(
|
||||
cgroupPath,
|
||||
"cgroup.procs",
|
||||
process.Id.ToString(CultureInfo.InvariantCulture),
|
||||
ct);
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_processCgroups[process.Id] = cgroupPath;
|
||||
}
|
||||
|
||||
_logger.LogDebug(
|
||||
"Applied cgroup limits to process {ProcessId}: Memory={MemoryBytes}B, CPU quota={CpuQuotaUs}us",
|
||||
process.Id,
|
||||
config.MemoryLimitBytes,
|
||||
config.CpuQuotaUs);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to apply cgroup limits to process {ProcessId}", process.Id);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task RemoveLimitsAsync(SystemProcess process, CancellationToken ct)
|
||||
{
|
||||
if (!OperatingSystem.IsLinux())
|
||||
return;
|
||||
|
||||
string? cgroupPath;
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_processCgroups.TryGetValue(process.Id, out cgroupPath))
|
||||
return;
|
||||
|
||||
_processCgroups.Remove(process.Id);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Move process to root cgroup first (if still running)
|
||||
if (!process.HasExited)
|
||||
{
|
||||
await WriteControlFileAsync(
|
||||
CgroupBasePath,
|
||||
"cgroup.procs",
|
||||
process.Id.ToString(CultureInfo.InvariantCulture),
|
||||
ct);
|
||||
}
|
||||
|
||||
// Remove cgroup directory
|
||||
if (Directory.Exists(cgroupPath))
|
||||
{
|
||||
Directory.Delete(cgroupPath);
|
||||
}
|
||||
|
||||
_logger.LogDebug("Removed cgroup for process {ProcessId}", process.Id);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to cleanup cgroup for process {ProcessId}", process.Id);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ResourceUsage> GetUsageAsync(SystemProcess process, CancellationToken ct)
|
||||
{
|
||||
if (!OperatingSystem.IsLinux())
|
||||
{
|
||||
return GetFallbackUsage(process);
|
||||
}
|
||||
|
||||
string? cgroupPath;
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_processCgroups.TryGetValue(process.Id, out cgroupPath))
|
||||
{
|
||||
return GetFallbackUsage(process);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Read memory usage
|
||||
var memoryCurrentStr = await ReadControlFileAsync(cgroupPath, "memory.current", ct);
|
||||
var memoryCurrent = long.Parse(memoryCurrentStr.Trim(), CultureInfo.InvariantCulture);
|
||||
|
||||
// Calculate CPU percentage (simplified - would need time delta for accurate calculation)
|
||||
var cpuPercent = 0.0; // Requires tracking over time
|
||||
|
||||
return new ResourceUsage
|
||||
{
|
||||
MemoryUsageMb = memoryCurrent / (1024.0 * 1024.0),
|
||||
CpuUsagePercent = cpuPercent,
|
||||
ProcessCount = process.Threads.Count,
|
||||
OpenFileHandles = GetHandleCount(process),
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to read cgroup stats for process {ProcessId}", process.Id);
|
||||
return GetFallbackUsage(process);
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
|
||||
// Check processes
|
||||
if (limits.MaxProcesses > 0 && usage.ProcessCount > limits.MaxProcesses)
|
||||
{
|
||||
return LimitCheckResult.Exceeded(
|
||||
ResourceType.Cpu, // Use Cpu as proxy for process limits
|
||||
usage.ProcessCount,
|
||||
limits.MaxProcesses);
|
||||
}
|
||||
|
||||
return LimitCheckResult.Ok;
|
||||
}
|
||||
|
||||
private static async Task WriteControlFileAsync(
|
||||
string cgroupPath,
|
||||
string fileName,
|
||||
string value,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var filePath = Path.Combine(cgroupPath, fileName);
|
||||
await File.WriteAllTextAsync(filePath, value, ct);
|
||||
}
|
||||
|
||||
private static async Task<string> ReadControlFileAsync(
|
||||
string cgroupPath,
|
||||
string fileName,
|
||||
CancellationToken ct)
|
||||
{
|
||||
var filePath = Path.Combine(cgroupPath, fileName);
|
||||
return await File.ReadAllTextAsync(filePath, ct);
|
||||
}
|
||||
|
||||
private static ResourceUsage GetFallbackUsage(SystemProcess process)
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Refresh();
|
||||
return new ResourceUsage
|
||||
{
|
||||
MemoryUsageMb = process.WorkingSet64 / (1024.0 * 1024.0),
|
||||
CpuUsagePercent = 0, // Can't easily get without tracking
|
||||
ProcessCount = process.Threads.Count,
|
||||
OpenFileHandles = GetHandleCount(process),
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ResourceUsage.Empty;
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetHandleCount(SystemProcess process)
|
||||
{
|
||||
try
|
||||
{
|
||||
return process.HandleCount;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
namespace StellaOps.Plugin.Sandbox.Resources;
|
||||
|
||||
/// <summary>
|
||||
/// Current resource usage statistics for a sandbox.
|
||||
/// </summary>
|
||||
public sealed record ResourceUsage
|
||||
{
|
||||
/// <summary>
|
||||
/// Memory usage in megabytes.
|
||||
/// </summary>
|
||||
public double MemoryUsageMb { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CPU usage as a percentage.
|
||||
/// </summary>
|
||||
public double CpuUsagePercent { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of active processes/threads.
|
||||
/// </summary>
|
||||
public int ProcessCount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Disk usage in bytes.
|
||||
/// </summary>
|
||||
public long DiskUsageBytes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Network bytes received.
|
||||
/// </summary>
|
||||
public long NetworkBytesIn { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Network bytes sent.
|
||||
/// </summary>
|
||||
public long NetworkBytesOut { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of open file handles.
|
||||
/// </summary>
|
||||
public int OpenFileHandles { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Timestamp when this usage snapshot was taken.
|
||||
/// </summary>
|
||||
public DateTimeOffset Timestamp { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Empty resource usage.
|
||||
/// </summary>
|
||||
public static ResourceUsage Empty => new()
|
||||
{
|
||||
Timestamp = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resource configuration for process limits.
|
||||
/// </summary>
|
||||
public sealed record ResourceConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Memory limit in bytes.
|
||||
/// </summary>
|
||||
public long MemoryLimitBytes { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CPU quota in microseconds.
|
||||
/// </summary>
|
||||
public long CpuQuotaUs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// CPU period in microseconds.
|
||||
/// </summary>
|
||||
public long CpuPeriodUs { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of processes.
|
||||
/// </summary>
|
||||
public int MaxProcesses { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Maximum number of open files.
|
||||
/// </summary>
|
||||
public int MaxOpenFiles { get; init; }
|
||||
}
|
||||
@@ -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