All checks were successful
Build and Push Docker Images / build (src/LiquidCode.Tester.Gateway/Dockerfile, git.nullptr.top/liquidcode/liquidcode-tester-gateway-roman, gateway) (push) Successful in 56s
Build and Push Docker Images / build (src/LiquidCode.Tester.Worker/Dockerfile, git.nullptr.top/liquidcode/liquidcode-tester-worker-roman, worker) (push) Successful in 1m7s
Configures isolate to use cgroups for improved resource management. Adds the `--cg` flag to the isolate init and cleanup commands, leveraging cgroups for better resource isolation during code execution testing. Makes isolate configuration file explicit.
350 lines
10 KiB
C#
350 lines
10 KiB
C#
using System.Diagnostics;
|
|
using System.Text;
|
|
|
|
namespace LiquidCode.Tester.Worker.Services.Isolate;
|
|
|
|
/// <summary>
|
|
/// Service for running programs in Isolate sandbox
|
|
/// </summary>
|
|
public class IsolateService
|
|
{
|
|
private readonly ILogger<IsolateService> _logger;
|
|
private readonly string _isolatePath;
|
|
|
|
public IsolateService(ILogger<IsolateService> logger)
|
|
{
|
|
_logger = logger;
|
|
_isolatePath = FindIsolatePath();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initialize a sandbox box
|
|
/// </summary>
|
|
public async Task InitBoxAsync(int boxId)
|
|
{
|
|
_logger.LogDebug("Initializing isolate box {BoxId}", boxId);
|
|
|
|
var result = await RunIsolateCommandAsync($"--box-id={boxId} --cg --init");
|
|
|
|
if (result.ExitCode != 0)
|
|
{
|
|
throw new InvalidOperationException(
|
|
$"Failed to initialize isolate box {boxId}: {result.Error}");
|
|
}
|
|
|
|
_logger.LogDebug("Box {BoxId} initialized at {Path}", boxId, result.Output.Trim());
|
|
}
|
|
|
|
/// <summary>
|
|
/// Execute program in sandbox
|
|
/// </summary>
|
|
public async Task<IsolateExecutionResult> RunAsync(IsolateRunOptions options)
|
|
{
|
|
_logger.LogInformation("Running in isolate box {BoxId}: {Executable}",
|
|
options.BoxId, options.Executable);
|
|
|
|
// Create metadata file path
|
|
var metaFile = Path.Combine(Path.GetTempPath(), $"isolate_meta_{options.BoxId}_{Guid.NewGuid()}.txt");
|
|
|
|
try
|
|
{
|
|
// Build isolate command
|
|
var command = BuildRunCommand(options, metaFile);
|
|
|
|
_logger.LogDebug("Isolate command: {Command}", command);
|
|
|
|
// Execute
|
|
var cmdResult = await RunIsolateCommandAsync(command);
|
|
|
|
// Parse metadata
|
|
var result = await ParseMetadataAsync(metaFile);
|
|
result.Output = cmdResult.Output;
|
|
result.ErrorOutput = cmdResult.Error;
|
|
|
|
_logger.LogInformation("Execution completed: time={Time}s, memory={Memory}KB, exitcode={ExitCode}",
|
|
result.CpuTimeSeconds, result.MemoryUsedKb, result.ExitCode);
|
|
|
|
return result;
|
|
}
|
|
finally
|
|
{
|
|
// Cleanup metadata file
|
|
try
|
|
{
|
|
if (File.Exists(metaFile))
|
|
{
|
|
File.Delete(metaFile);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "Failed to delete metadata file {MetaFile}", metaFile);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Cleanup sandbox box
|
|
/// </summary>
|
|
public async Task CleanupBoxAsync(int boxId)
|
|
{
|
|
_logger.LogDebug("Cleaning up isolate box {BoxId}", boxId);
|
|
|
|
var result = await RunIsolateCommandAsync($"--box-id={boxId} --cg --cleanup");
|
|
|
|
if (result.ExitCode != 0)
|
|
{
|
|
_logger.LogWarning("Failed to cleanup isolate box {BoxId}: {Error}", boxId, result.Error);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Build isolate run command from options
|
|
/// </summary>
|
|
private string BuildRunCommand(IsolateRunOptions options, string metaFile)
|
|
{
|
|
var args = new List<string>
|
|
{
|
|
$"--box-id={options.BoxId}",
|
|
$"--meta={metaFile}",
|
|
"--cg", // Enable cgroups
|
|
"--silent" // Suppress status messages
|
|
};
|
|
|
|
// Time limits
|
|
if (options.TimeLimitSeconds > 0)
|
|
{
|
|
args.Add($"--time={options.TimeLimitSeconds:F3}");
|
|
}
|
|
|
|
if (options.WallTimeLimitSeconds > 0)
|
|
{
|
|
args.Add($"--wall-time={options.WallTimeLimitSeconds:F3}");
|
|
}
|
|
|
|
// Memory limit
|
|
if (options.MemoryLimitKb > 0)
|
|
{
|
|
args.Add($"--cg-mem={options.MemoryLimitKb}");
|
|
}
|
|
|
|
// Process limit
|
|
if (options.ProcessLimit > 0)
|
|
{
|
|
args.Add($"--processes={options.ProcessLimit}");
|
|
}
|
|
else
|
|
{
|
|
args.Add("--processes=1"); // Default: single process
|
|
}
|
|
|
|
// Stack size
|
|
if (options.StackLimitKb > 0)
|
|
{
|
|
args.Add($"--stack={options.StackLimitKb}");
|
|
}
|
|
|
|
// Network isolation (default: disabled)
|
|
if (!options.EnableNetwork)
|
|
{
|
|
// Network is isolated by default in isolate
|
|
}
|
|
else
|
|
{
|
|
args.Add("--share-net");
|
|
}
|
|
|
|
// I/O redirection
|
|
if (!string.IsNullOrEmpty(options.StdinFile))
|
|
{
|
|
args.Add($"--stdin={options.StdinFile}");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(options.StdoutFile))
|
|
{
|
|
args.Add($"--stdout={options.StdoutFile}");
|
|
}
|
|
|
|
if (!string.IsNullOrEmpty(options.StderrFile))
|
|
{
|
|
args.Add($"--stderr={options.StderrFile}");
|
|
}
|
|
|
|
// Working directory
|
|
if (!string.IsNullOrEmpty(options.WorkingDirectory))
|
|
{
|
|
args.Add($"--chdir={options.WorkingDirectory}");
|
|
}
|
|
|
|
// Directory bindings
|
|
if (options.DirectoryBindings != null)
|
|
{
|
|
foreach (var binding in options.DirectoryBindings)
|
|
{
|
|
var dirSpec = binding.ReadOnly
|
|
? $"--dir={binding.HostPath}={binding.SandboxPath}"
|
|
: $"--dir={binding.HostPath}={binding.SandboxPath}:rw";
|
|
args.Add(dirSpec);
|
|
}
|
|
}
|
|
|
|
// Environment variables
|
|
if (options.EnvironmentVariables != null)
|
|
{
|
|
foreach (var env in options.EnvironmentVariables)
|
|
{
|
|
args.Add($"--env={env.Key}={env.Value}");
|
|
}
|
|
}
|
|
|
|
// Run command
|
|
args.Add("--run");
|
|
args.Add("--");
|
|
args.Add(options.Executable);
|
|
|
|
if (options.Arguments != null)
|
|
{
|
|
args.AddRange(options.Arguments);
|
|
}
|
|
|
|
return string.Join(" ", args);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parse isolate metadata file
|
|
/// </summary>
|
|
private async Task<IsolateExecutionResult> ParseMetadataAsync(string metaFile)
|
|
{
|
|
var result = new IsolateExecutionResult();
|
|
|
|
if (!File.Exists(metaFile))
|
|
{
|
|
_logger.LogWarning("Metadata file not found: {MetaFile}", metaFile);
|
|
return result;
|
|
}
|
|
|
|
var lines = await File.ReadAllLinesAsync(metaFile);
|
|
|
|
foreach (var line in lines)
|
|
{
|
|
var parts = line.Split(':', 2);
|
|
if (parts.Length != 2) continue;
|
|
|
|
var key = parts[0].Trim();
|
|
var value = parts[1].Trim();
|
|
|
|
switch (key)
|
|
{
|
|
case "time":
|
|
if (double.TryParse(value, out var time))
|
|
result.CpuTimeSeconds = time;
|
|
break;
|
|
|
|
case "time-wall":
|
|
if (double.TryParse(value, out var wallTime))
|
|
result.WallTimeSeconds = wallTime;
|
|
break;
|
|
|
|
case "max-rss":
|
|
if (long.TryParse(value, out var rss))
|
|
result.MaxRssKb = rss;
|
|
break;
|
|
|
|
case "cg-mem":
|
|
if (long.TryParse(value, out var cgMem))
|
|
result.MemoryUsedKb = cgMem;
|
|
break;
|
|
|
|
case "exitcode":
|
|
if (int.TryParse(value, out var exitCode))
|
|
result.ExitCode = exitCode;
|
|
break;
|
|
|
|
case "exitsig":
|
|
if (int.TryParse(value, out var exitSig))
|
|
result.ExitSignal = exitSig;
|
|
break;
|
|
|
|
case "status":
|
|
result.Status = value;
|
|
result.TimeLimitExceeded = value == "TO";
|
|
result.MemoryLimitExceeded = value == "XX" || value == "MLE";
|
|
result.RuntimeError = value == "RE" || value == "SG";
|
|
break;
|
|
|
|
case "message":
|
|
result.Message = value;
|
|
break;
|
|
|
|
case "killed":
|
|
result.WasKilled = value == "1";
|
|
break;
|
|
|
|
case "cg-oom-killed":
|
|
result.CgroupOomKilled = value == "1";
|
|
result.MemoryLimitExceeded = true;
|
|
break;
|
|
|
|
case "csw-voluntary":
|
|
if (int.TryParse(value, out var csvVoluntary))
|
|
result.VoluntaryContextSwitches = csvVoluntary;
|
|
break;
|
|
|
|
case "csw-forced":
|
|
if (int.TryParse(value, out var csvForced))
|
|
result.ForcedContextSwitches = csvForced;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Execute isolate command
|
|
/// </summary>
|
|
private async Task<(int ExitCode, string Output, string Error)> RunIsolateCommandAsync(string arguments)
|
|
{
|
|
var process = new Process
|
|
{
|
|
StartInfo = new ProcessStartInfo
|
|
{
|
|
FileName = _isolatePath,
|
|
Arguments = arguments,
|
|
RedirectStandardOutput = true,
|
|
RedirectStandardError = true,
|
|
UseShellExecute = false,
|
|
CreateNoWindow = true
|
|
}
|
|
};
|
|
|
|
process.Start();
|
|
|
|
var outputTask = process.StandardOutput.ReadToEndAsync();
|
|
var errorTask = process.StandardError.ReadToEndAsync();
|
|
|
|
await process.WaitForExitAsync();
|
|
|
|
return (process.ExitCode, await outputTask, await errorTask);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Find isolate binary path
|
|
/// </summary>
|
|
private string FindIsolatePath()
|
|
{
|
|
var paths = new[] { "/usr/local/bin/isolate", "/usr/bin/isolate" };
|
|
|
|
foreach (var path in paths)
|
|
{
|
|
if (File.Exists(path))
|
|
{
|
|
_logger.LogInformation("Found isolate at {Path}", path);
|
|
return path;
|
|
}
|
|
}
|
|
|
|
throw new FileNotFoundException("Isolate binary not found. Make sure isolate is installed.");
|
|
}
|
|
}
|