release orchestrator v1 draft and build fixes

This commit is contained in:
master
2026-01-12 12:24:17 +02:00
parent f3de858c59
commit 9873f80830
1598 changed files with 240385 additions and 5944 deletions

View File

@@ -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}"
};
}

View File

@@ -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;
}
}
}

View File

@@ -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; }
}

View File

@@ -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);
}
}