using System.Diagnostics; using System.Text; namespace LiquidCode.Tester.Worker.Services.Isolate; /// /// Service for running programs in Isolate sandbox /// public class IsolateService { private readonly ILogger _logger; private readonly string _isolatePath; public IsolateService(ILogger logger) { _logger = logger; _isolatePath = FindIsolatePath(); } /// /// Initialize a sandbox box /// 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()); } /// /// Execute program in sandbox /// public async Task 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); } } } /// /// Cleanup sandbox box /// 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); } } /// /// Build isolate run command from options /// private string BuildRunCommand(IsolateRunOptions options, string metaFile) { var args = new List { $"--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); } /// /// Parse isolate metadata file /// private async Task 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; } /// /// Execute isolate command /// 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); } /// /// Find isolate binary path /// 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."); } }