diff --git a/src/LiquidCode.Tester.Worker/Program.cs b/src/LiquidCode.Tester.Worker/Program.cs index 1a65b7d..c1917d0 100644 --- a/src/LiquidCode.Tester.Worker/Program.cs +++ b/src/LiquidCode.Tester.Worker/Program.cs @@ -23,16 +23,28 @@ builder.Services.AddSingleton(sp => builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Register compilation services +// Always register both standard and isolate versions builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + builder.Services.AddSingleton(); +builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // Register execution services diff --git a/src/LiquidCode.Tester.Worker/Services/CSharpCompilationServiceIsolate.cs b/src/LiquidCode.Tester.Worker/Services/CSharpCompilationServiceIsolate.cs new file mode 100644 index 0000000..8f82480 --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/CSharpCompilationServiceIsolate.cs @@ -0,0 +1,218 @@ +using LiquidCode.Tester.Worker.Services.Isolate; +using LiquidCode.Tester.Worker.Models; + +namespace LiquidCode.Tester.Worker.Services; + +/// +/// C# compilation service using Isolate sandbox for security +/// +public class CSharpCompilationServiceIsolate : ICompilationService +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IsolateService _isolateService; + private readonly IsolateBoxPool _boxPool; + + private const int CompilationTimeLimitSeconds = 30; + private const int CompilationMemoryLimitMb = 512; + + public CSharpCompilationServiceIsolate( + ILogger logger, + IConfiguration configuration, + IsolateService isolateService, + IsolateBoxPool boxPool) + { + _logger = logger; + _configuration = configuration; + _isolateService = isolateService; + _boxPool = boxPool; + } + + public async Task CompileAsync(string sourceCode, string workingDirectory, string? version = null) + { + var sourceFilePath = Path.Combine(workingDirectory, "Solution.cs"); + var executablePath = Path.Combine(workingDirectory, "solution.exe"); + + _logger.LogInformation("Compiling C# code with Isolate in {WorkingDirectory} with version {Version}", + workingDirectory, version ?? "latest"); + + try + { + await File.WriteAllTextAsync(sourceFilePath, sourceCode); + return await CompileFileInIsolateAsync(sourceFilePath, executablePath, version); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Isolate C# compilation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation error: {ex.Message}" + }; + } + } + + private async Task CompileFileInIsolateAsync( + string sourceFilePath, + string executablePath, + string? version = null) + { + int boxId = -1; + + try + { + boxId = await _boxPool.AcquireBoxAsync(); + _logger.LogDebug("Acquired isolate box {BoxId} for C# compilation", boxId); + + await _isolateService.InitBoxAsync(boxId); + + var boxDir = $"/var/local/lib/isolate/{boxId}/box"; + var sourceFileName = Path.GetFileName(sourceFilePath); + var boxSourcePath = Path.Combine(boxDir, sourceFileName); + var exeFileName = "solution.exe"; + var boxExePath = Path.Combine(boxDir, exeFileName); + + File.Copy(sourceFilePath, boxSourcePath, overwrite: true); + + var (compiler, compilerFlags) = ResolveVersion(version); + _logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, compilerFlags); + + var arguments = new List(); + if (!string.IsNullOrWhiteSpace(compilerFlags)) + { + arguments.AddRange(compilerFlags.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + } + arguments.Add($"/out:/box/{exeFileName}"); + arguments.Add($"/box/{sourceFileName}"); + + var stderrFilePath = Path.Combine(boxDir, "compile_stderr.txt"); + + var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions + { + BoxId = boxId, + Executable = $"/usr/bin/{compiler}", + Arguments = arguments.ToArray(), + TimeLimitSeconds = CompilationTimeLimitSeconds, + WallTimeLimitSeconds = CompilationTimeLimitSeconds * 2, + MemoryLimitKb = CompilationMemoryLimitMb * 1024, + StackLimitKb = 256 * 1024, + ProcessLimit = 10, + EnableNetwork = false, + StderrFile = stderrFilePath, + WorkingDirectory = "/box", + DirectoryBindings = new List + { + new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true }, + new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true } + } + }); + + var compilerOutput = string.Empty; + if (File.Exists(stderrFilePath)) + { + compilerOutput = await File.ReadAllTextAsync(stderrFilePath); + } + if (!string.IsNullOrEmpty(isolateResult.ErrorOutput)) + { + compilerOutput = $"{compilerOutput}\n{isolateResult.ErrorOutput}".Trim(); + } + + if (isolateResult.TimeLimitExceeded) + { + _logger.LogWarning("C# compilation time limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation timeout: exceeded {CompilationTimeLimitSeconds}s limit", + CompilerOutput = compilerOutput + }; + } + + if (isolateResult.MemoryLimitExceeded) + { + _logger.LogWarning("C# compilation memory limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation memory limit exceeded: {CompilationMemoryLimitMb}MB", + CompilerOutput = compilerOutput + }; + } + + if (isolateResult.ExitCode == 0 && File.Exists(boxExePath)) + { + Directory.CreateDirectory(Path.GetDirectoryName(executablePath)!); + File.Copy(boxExePath, executablePath, overwrite: true); + + _logger.LogInformation("C# compilation successful in Isolate box {BoxId}", boxId); + return new CompilationResult + { + Success = true, + ExecutablePath = executablePath, + CompilerOutput = compilerOutput + }; + } + + _logger.LogWarning("C# compilation failed with exit code {ExitCode} in box {BoxId}", + isolateResult.ExitCode, boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation failed with exit code {isolateResult.ExitCode}", + CompilerOutput = compilerOutput + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Isolate C# compilation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation error: {ex.Message}" + }; + } + finally + { + if (boxId >= 0) + { + try + { + await _isolateService.CleanupBoxAsync(boxId); + _logger.LogDebug("Cleaned up isolate box {BoxId}", boxId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup compilation box {BoxId}", boxId); + } + + _boxPool.ReleaseBox(boxId); + _logger.LogDebug("Released compilation box {BoxId} back to pool", boxId); + } + } + } + + private (string compiler, string compilerFlags) ResolveVersion(string? version) + { + if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase)) + { + var compiler = _configuration["CSharp:Compiler"] ?? "csc"; + var compilerFlags = _configuration["CSharp:CompilerFlags"] ?? "/optimize+"; + return (compiler, compilerFlags); + } + + var versionKey = $"CSharp:Versions:{version}"; + var versionCompiler = _configuration[$"{versionKey}:Compiler"]; + var versionFlags = _configuration[$"{versionKey}:CompilerFlags"]; + + if (!string.IsNullOrEmpty(versionCompiler)) + { + _logger.LogInformation("Using C# version {Version} configuration", version); + return (versionCompiler, versionFlags ?? "/optimize+"); + } + + _logger.LogWarning("C# version {Version} not found in configuration, using default", version); + var defaultCompiler = _configuration["CSharp:Compiler"] ?? "csc"; + var defaultFlags = _configuration["CSharp:CompilerFlags"] ?? "/optimize+"; + return (defaultCompiler, defaultFlags); + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/CheckerServiceIsolate.cs b/src/LiquidCode.Tester.Worker/Services/CheckerServiceIsolate.cs new file mode 100644 index 0000000..b774bb8 --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/CheckerServiceIsolate.cs @@ -0,0 +1,222 @@ +using LiquidCode.Tester.Worker.Services.Isolate; + +namespace LiquidCode.Tester.Worker.Services; + +/// +/// Service for running custom checkers in Isolate sandbox +/// +public class CheckerServiceIsolate +{ + private readonly ILogger _logger; + private readonly IsolateService _isolateService; + private readonly IsolateBoxPool _boxPool; + + private const int CheckerTimeLimitSeconds = 5; + private const int CheckerMemoryLimitMb = 256; + + public CheckerServiceIsolate( + ILogger logger, + IsolateService isolateService, + IsolateBoxPool boxPool) + { + _logger = logger; + _isolateService = isolateService; + _boxPool = boxPool; + } + + /// + /// Check user output using custom checker in Isolate sandbox + /// + /// Path to checker executable + /// Path to input file + /// User program output + /// Path to answer file + /// Checker result + public async Task CheckAsync( + string checkerPath, + string inputPath, + string userOutput, + string answerPath) + { + if (!File.Exists(checkerPath)) + { + _logger.LogError("Checker not found: {CheckerPath}", checkerPath); + return new CheckerResult + { + Accepted = false, + ExitCode = -1, + Message = "Checker executable not found" + }; + } + + int boxId = -1; + + try + { + boxId = await _boxPool.AcquireBoxAsync(); + _logger.LogDebug("Acquired isolate box {BoxId} for checker execution", boxId); + + await _isolateService.InitBoxAsync(boxId); + + var boxDir = $"/var/local/lib/isolate/{boxId}/box"; + + // Copy checker executable to box + var checkerName = Path.GetFileName(checkerPath); + var boxCheckerPath = Path.Combine(boxDir, checkerName); + File.Copy(checkerPath, boxCheckerPath, overwrite: true); + + // Make checker executable + var chmodProcess = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = "chmod", + Arguments = $"+x {boxCheckerPath}", + UseShellExecute = false + }); + chmodProcess?.WaitForExit(); + + // Copy input file + var inputName = "input.txt"; + var boxInputPath = Path.Combine(boxDir, inputName); + File.Copy(inputPath, boxInputPath, overwrite: true); + + // Save user output to file in box + var outputName = "output.txt"; + var boxOutputPath = Path.Combine(boxDir, outputName); + await File.WriteAllTextAsync(boxOutputPath, userOutput); + + // Copy answer file + var answerName = "answer.txt"; + var boxAnswerPath = Path.Combine(boxDir, answerName); + File.Copy(answerPath, boxAnswerPath, overwrite: true); + + // Prepare files for checker stdout/stderr + var stdoutPath = Path.Combine(boxDir, "checker_stdout.txt"); + var stderrPath = Path.Combine(boxDir, "checker_stderr.txt"); + + _logger.LogDebug("Running checker in Isolate: {Checker} {Input} {Output} {Answer}", + checkerName, inputName, outputName, answerName); + + // Run checker in Isolate + // Checker arguments: + var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions + { + BoxId = boxId, + Executable = $"/box/{checkerName}", + Arguments = new[] { $"/box/{inputName}", $"/box/{outputName}", $"/box/{answerName}" }, + TimeLimitSeconds = CheckerTimeLimitSeconds, + WallTimeLimitSeconds = CheckerTimeLimitSeconds * 2, + MemoryLimitKb = CheckerMemoryLimitMb * 1024, + StackLimitKb = 64 * 1024, + ProcessLimit = 1, // Single process for checker + EnableNetwork = false, + StdoutFile = stdoutPath, + StderrFile = stderrPath, + WorkingDirectory = "/box" + }); + + // Read checker output + var stdout = string.Empty; + if (File.Exists(stdoutPath)) + { + stdout = await File.ReadAllTextAsync(stdoutPath); + } + + var stderr = string.Empty; + if (File.Exists(stderrPath)) + { + stderr = await File.ReadAllTextAsync(stderrPath); + } + + if (isolateResult.TimeLimitExceeded) + { + _logger.LogWarning("Checker timeout in box {BoxId}", boxId); + return new CheckerResult + { + Accepted = false, + ExitCode = -1, + Message = "Checker timeout", + Verdict = CheckerVerdict.CheckerFailed + }; + } + + if (isolateResult.MemoryLimitExceeded) + { + _logger.LogWarning("Checker memory limit exceeded in box {BoxId}", boxId); + return new CheckerResult + { + Accepted = false, + ExitCode = -1, + Message = "Checker memory limit exceeded", + Verdict = CheckerVerdict.CheckerFailed + }; + } + + if (isolateResult.RuntimeError) + { + _logger.LogWarning("Checker runtime error in box {BoxId}: {Message}", boxId, isolateResult.Message); + return new CheckerResult + { + Accepted = false, + ExitCode = isolateResult.ExitCode, + Message = $"Checker runtime error: {isolateResult.Message}", + Verdict = CheckerVerdict.CheckerFailed + }; + } + + var exitCode = isolateResult.ExitCode; + var message = string.IsNullOrWhiteSpace(stderr) ? stdout : stderr; + + _logger.LogDebug("Checker exit code: {ExitCode}, message: {Message}", exitCode, message); + + return new CheckerResult + { + Accepted = exitCode == 0, + ExitCode = exitCode, + Message = message, + Verdict = GetVerdictFromExitCode(exitCode) + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error running checker in Isolate"); + return new CheckerResult + { + Accepted = false, + ExitCode = -1, + Message = $"Checker error: {ex.Message}", + Verdict = CheckerVerdict.CheckerFailed + }; + } + finally + { + if (boxId >= 0) + { + try + { + await _isolateService.CleanupBoxAsync(boxId); + _logger.LogDebug("Cleaned up isolate box {BoxId}", boxId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup checker box {BoxId}", boxId); + } + + _boxPool.ReleaseBox(boxId); + _logger.LogDebug("Released checker box {BoxId} back to pool", boxId); + } + } + } + + private CheckerVerdict GetVerdictFromExitCode(int exitCode) + { + return exitCode switch + { + 0 => CheckerVerdict.OK, + 1 => CheckerVerdict.WrongAnswer, + 2 => CheckerVerdict.PresentationError, + 3 => CheckerVerdict.CheckerFailed, + 7 => CheckerVerdict.PartialScore, + _ => CheckerVerdict.Unknown + }; + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/CompilationServiceFactory.cs b/src/LiquidCode.Tester.Worker/Services/CompilationServiceFactory.cs index f502384..d3e31dc 100644 --- a/src/LiquidCode.Tester.Worker/Services/CompilationServiceFactory.cs +++ b/src/LiquidCode.Tester.Worker/Services/CompilationServiceFactory.cs @@ -3,27 +3,54 @@ namespace LiquidCode.Tester.Worker.Services; public class CompilationServiceFactory : ICompilationServiceFactory { private readonly IServiceProvider _serviceProvider; + private readonly IConfiguration _configuration; private readonly ILogger _logger; + private readonly bool _useIsolate; - public CompilationServiceFactory(IServiceProvider serviceProvider, ILogger logger) + public CompilationServiceFactory( + IServiceProvider serviceProvider, + IConfiguration configuration, + ILogger logger) { _serviceProvider = serviceProvider; + _configuration = configuration; _logger = logger; + _useIsolate = configuration.GetValue("Isolate:Enabled", false); + + if (_useIsolate) + { + _logger.LogInformation("Isolate sandbox is ENABLED for compilation"); + } + else + { + _logger.LogWarning("Isolate sandbox is DISABLED for compilation - using standard compilation (NOT SECURE for production!)"); + } } public ICompilationService GetCompilationService(string language) { var normalizedLanguage = language.ToLowerInvariant().Replace(" ", ""); - _logger.LogInformation("Getting compilation service for language: {Language}", normalizedLanguage); + _logger.LogInformation("Getting compilation service for language: {Language} (Isolate: {UseIsolate})", + normalizedLanguage, _useIsolate); return normalizedLanguage switch { - "c++" or "cpp" => _serviceProvider.GetRequiredService(), - "java" => _serviceProvider.GetRequiredService(), - "kotlin" => _serviceProvider.GetRequiredService(), - "c#" or "csharp" => _serviceProvider.GetRequiredService(), - "python" => _serviceProvider.GetRequiredService(), + "c++" or "cpp" => _useIsolate + ? _serviceProvider.GetRequiredService() + : _serviceProvider.GetRequiredService(), + "java" => _useIsolate + ? _serviceProvider.GetRequiredService() + : _serviceProvider.GetRequiredService(), + "kotlin" => _useIsolate + ? _serviceProvider.GetRequiredService() + : _serviceProvider.GetRequiredService(), + "c#" or "csharp" => _useIsolate + ? _serviceProvider.GetRequiredService() + : _serviceProvider.GetRequiredService(), + "python" => _useIsolate + ? _serviceProvider.GetRequiredService() + : _serviceProvider.GetRequiredService(), _ => throw new NotSupportedException($"Language '{language}' is not supported") }; } diff --git a/src/LiquidCode.Tester.Worker/Services/CppCompilationServiceIsolate.cs b/src/LiquidCode.Tester.Worker/Services/CppCompilationServiceIsolate.cs new file mode 100644 index 0000000..654693e --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/CppCompilationServiceIsolate.cs @@ -0,0 +1,280 @@ +using System.Collections.Generic; +using System.Linq; +using LiquidCode.Tester.Worker.Services.Isolate; +using LiquidCode.Tester.Worker.Models; + +namespace LiquidCode.Tester.Worker.Services; + +/// +/// C++ compilation service using Isolate sandbox for security +/// +public class CppCompilationServiceIsolate : ICompilationService +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IsolateService _isolateService; + private readonly IsolateBoxPool _boxPool; + + // Compilation limits (more generous than execution limits) + private const int CompilationTimeLimitSeconds = 30; + private const int CompilationMemoryLimitMb = 512; + + public CppCompilationServiceIsolate( + ILogger logger, + IConfiguration configuration, + IsolateService isolateService, + IsolateBoxPool boxPool) + { + _logger = logger; + _configuration = configuration; + _isolateService = isolateService; + _boxPool = boxPool; + } + + public async Task CompileAsync(string sourceCode, string workingDirectory, string? version = null) + { + var sourceFilePath = Path.Combine(workingDirectory, "solution.cpp"); + var executablePath = Path.Combine(workingDirectory, "solution"); + + _logger.LogInformation("Compiling C++ code with Isolate in {WorkingDirectory} with version {Version}", + workingDirectory, version ?? "latest"); + + try + { + await File.WriteAllTextAsync(sourceFilePath, sourceCode); + return await CompileFileInIsolateAsync(sourceFilePath, executablePath, version); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Isolate compilation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation error: {ex.Message}" + }; + } + } + + private async Task CompileFileInIsolateAsync( + string sourceFilePath, + string outputExecutablePath, + string? version = null) + { + int boxId = -1; + + try + { + // Acquire box from pool + boxId = await _boxPool.AcquireBoxAsync(); + _logger.LogDebug("Acquired isolate box {BoxId} for compilation", boxId); + + // Initialize box + await _isolateService.InitBoxAsync(boxId); + + // Copy source file to box + var boxDir = $"/var/local/lib/isolate/{boxId}/box"; + var sourceFileName = Path.GetFileName(sourceFilePath); + var boxSourcePath = Path.Combine(boxDir, sourceFileName); + var boxOutputName = "solution"; + var boxOutputPath = Path.Combine(boxDir, boxOutputName); + + File.Copy(sourceFilePath, boxSourcePath, overwrite: true); + + // Resolve compiler and flags + var (compiler, compilerFlags) = ResolveVersion(version); + _logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, string.Join(' ', compilerFlags)); + + // Build compiler arguments + var arguments = new List(compilerFlags); + arguments.Add($"/box/{sourceFileName}"); + arguments.Add("-o"); + arguments.Add($"/box/{boxOutputName}"); + + // Prepare stderr output file for compiler messages + var stderrFilePath = Path.Combine(boxDir, "compile_stderr.txt"); + + // Run compiler in Isolate + // Note: Isolate by default provides access to /usr, /lib, etc. via --share-net=no + // For compilation, we need access to system headers and libraries + var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions + { + BoxId = boxId, + Executable = $"/usr/bin/{compiler}", + Arguments = arguments.ToArray(), + TimeLimitSeconds = CompilationTimeLimitSeconds, + WallTimeLimitSeconds = CompilationTimeLimitSeconds * 2, + MemoryLimitKb = CompilationMemoryLimitMb * 1024, + StackLimitKb = 256 * 1024, + ProcessLimit = 10, // g++ spawns multiple processes + EnableNetwork = false, + StderrFile = stderrFilePath, + WorkingDirectory = "/box", + DirectoryBindings = new List + { + new DirectoryBinding { HostPath = "/usr/include", SandboxPath = "/usr/include", ReadOnly = true }, + new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true }, + new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true } + } + }); + + // Read compiler output + var compilerOutput = string.Empty; + if (File.Exists(stderrFilePath)) + { + compilerOutput = await File.ReadAllTextAsync(stderrFilePath); + } + if (!string.IsNullOrEmpty(isolateResult.ErrorOutput)) + { + compilerOutput = $"{compilerOutput}\n{isolateResult.ErrorOutput}".Trim(); + } + + // Check for time/memory limits during compilation + if (isolateResult.TimeLimitExceeded) + { + _logger.LogWarning("Compilation time limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation timeout: exceeded {CompilationTimeLimitSeconds}s limit", + CompilerOutput = compilerOutput + }; + } + + if (isolateResult.MemoryLimitExceeded) + { + _logger.LogWarning("Compilation memory limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation memory limit exceeded: {CompilationMemoryLimitMb}MB", + CompilerOutput = compilerOutput + }; + } + + // Copy compiled executable back if successful + if (isolateResult.ExitCode == 0 && File.Exists(boxOutputPath)) + { + Directory.CreateDirectory(Path.GetDirectoryName(outputExecutablePath)!); + File.Copy(boxOutputPath, outputExecutablePath, overwrite: true); + + _logger.LogInformation("Compilation successful in Isolate box {BoxId}", boxId); + return new CompilationResult + { + Success = true, + ExecutablePath = outputExecutablePath, + CompilerOutput = compilerOutput + }; + } + + // Compilation failed + _logger.LogWarning("Compilation failed with exit code {ExitCode} in box {BoxId}", + isolateResult.ExitCode, boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation failed with exit code {isolateResult.ExitCode}", + CompilerOutput = compilerOutput + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Isolate compilation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation error: {ex.Message}" + }; + } + finally + { + // Cleanup box + if (boxId >= 0) + { + try + { + await _isolateService.CleanupBoxAsync(boxId); + _logger.LogDebug("Cleaned up isolate box {BoxId}", boxId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup compilation box {BoxId}", boxId); + } + + // Release box back to pool + _boxPool.ReleaseBox(boxId); + _logger.LogDebug("Released compilation box {BoxId} back to pool", boxId); + } + } + } + + private (string compiler, List compilerFlags) ResolveVersion(string? version) + { + var defaultCompiler = _configuration["Cpp:Compiler"] ?? "g++"; + var defaultFlags = SplitFlags(_configuration["Cpp:CompilerFlags"] ?? "-O2 -std=c++17 -Wall"); + + if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase)) + { + return (defaultCompiler, defaultFlags); + } + + var versionKey = $"Cpp:Versions:{version}"; + var versionCompiler = _configuration[$"{versionKey}:Compiler"]; + var versionFlagsValue = _configuration[$"{versionKey}:CompilerFlags"]; + + if (!string.IsNullOrEmpty(versionCompiler) || !string.IsNullOrEmpty(versionFlagsValue)) + { + var resolvedFlags = !string.IsNullOrEmpty(versionFlagsValue) + ? SplitFlags(versionFlagsValue) + : defaultFlags; + + _logger.LogInformation("Using C++ version {Version} configuration", version); + return (versionCompiler ?? defaultCompiler, resolvedFlags); + } + + var normalized = NormalizeCppVersion(version); + if (normalized != null) + { + var flagsWithoutStd = defaultFlags + .Where(flag => !flag.StartsWith("-std=", StringComparison.OrdinalIgnoreCase)) + .ToList(); + flagsWithoutStd.Add($"-std=c++{normalized}"); + _logger.LogInformation("Using inferred C++ standard c++{Standard}", normalized); + return (defaultCompiler, flagsWithoutStd); + } + + _logger.LogWarning("C++ version {Version} not found in configuration, using default", version); + return (defaultCompiler, defaultFlags); + } + + private static List SplitFlags(string flags) => + flags.Split(' ', StringSplitOptions.RemoveEmptyEntries) + .ToList(); + + private static string? NormalizeCppVersion(string version) + { + var cleaned = version.Trim().ToLowerInvariant(); + cleaned = cleaned.Replace("c++", string.Empty, StringComparison.OrdinalIgnoreCase) + .Replace("gnu++", string.Empty, StringComparison.OrdinalIgnoreCase) + .Trim('+', ' '); + + cleaned = cleaned switch + { + "2b" => "23", + "2a" => "20", + "1z" => "17", + "0x" => "11", + _ => cleaned + }; + + return cleaned switch + { + "26" => "26", + "23" => "23", + "20" => "20", + "17" => "17", + "14" => "14", + "11" => "11", + _ => null + }; + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/JavaCompilationServiceIsolate.cs b/src/LiquidCode.Tester.Worker/Services/JavaCompilationServiceIsolate.cs new file mode 100644 index 0000000..09be33c --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/JavaCompilationServiceIsolate.cs @@ -0,0 +1,219 @@ +using LiquidCode.Tester.Worker.Services.Isolate; +using LiquidCode.Tester.Worker.Models; + +namespace LiquidCode.Tester.Worker.Services; + +/// +/// Java compilation service using Isolate sandbox for security +/// +public class JavaCompilationServiceIsolate : ICompilationService +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IsolateService _isolateService; + private readonly IsolateBoxPool _boxPool; + + private const int CompilationTimeLimitSeconds = 30; + private const int CompilationMemoryLimitMb = 512; + + public JavaCompilationServiceIsolate( + ILogger logger, + IConfiguration configuration, + IsolateService isolateService, + IsolateBoxPool boxPool) + { + _logger = logger; + _configuration = configuration; + _isolateService = isolateService; + _boxPool = boxPool; + } + + public async Task CompileAsync(string sourceCode, string workingDirectory, string? version = null) + { + var sourceFilePath = Path.Combine(workingDirectory, "Solution.java"); + var classFilePath = Path.Combine(workingDirectory, "Solution.class"); + + _logger.LogInformation("Compiling Java code with Isolate in {WorkingDirectory} with version {Version}", + workingDirectory, version ?? "latest"); + + try + { + await File.WriteAllTextAsync(sourceFilePath, sourceCode); + return await CompileFileInIsolateAsync(sourceFilePath, classFilePath, workingDirectory, version); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Isolate Java compilation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation error: {ex.Message}" + }; + } + } + + private async Task CompileFileInIsolateAsync( + string sourceFilePath, + string classFilePath, + string workingDirectory, + string? version = null) + { + int boxId = -1; + + try + { + boxId = await _boxPool.AcquireBoxAsync(); + _logger.LogDebug("Acquired isolate box {BoxId} for Java compilation", boxId); + + await _isolateService.InitBoxAsync(boxId); + + var boxDir = $"/var/local/lib/isolate/{boxId}/box"; + var sourceFileName = Path.GetFileName(sourceFilePath); + var boxSourcePath = Path.Combine(boxDir, sourceFileName); + + File.Copy(sourceFilePath, boxSourcePath, overwrite: true); + + var (compiler, compilerFlags) = ResolveVersion(version); + _logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, compilerFlags); + + // Build arguments + var arguments = new List(); + if (!string.IsNullOrWhiteSpace(compilerFlags)) + { + arguments.AddRange(compilerFlags.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + } + arguments.Add($"/box/{sourceFileName}"); + + var stderrFilePath = Path.Combine(boxDir, "compile_stderr.txt"); + + var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions + { + BoxId = boxId, + Executable = $"/usr/bin/{compiler}", + Arguments = arguments.ToArray(), + TimeLimitSeconds = CompilationTimeLimitSeconds, + WallTimeLimitSeconds = CompilationTimeLimitSeconds * 2, + MemoryLimitKb = CompilationMemoryLimitMb * 1024, + StackLimitKb = 256 * 1024, + ProcessLimit = 10, // javac may spawn multiple processes + EnableNetwork = false, + StderrFile = stderrFilePath, + WorkingDirectory = "/box", + DirectoryBindings = new List + { + new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true }, + new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true } + } + }); + + var compilerOutput = string.Empty; + if (File.Exists(stderrFilePath)) + { + compilerOutput = await File.ReadAllTextAsync(stderrFilePath); + } + if (!string.IsNullOrEmpty(isolateResult.ErrorOutput)) + { + compilerOutput = $"{compilerOutput}\n{isolateResult.ErrorOutput}".Trim(); + } + + if (isolateResult.TimeLimitExceeded) + { + _logger.LogWarning("Java compilation time limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation timeout: exceeded {CompilationTimeLimitSeconds}s limit", + CompilerOutput = compilerOutput + }; + } + + if (isolateResult.MemoryLimitExceeded) + { + _logger.LogWarning("Java compilation memory limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation memory limit exceeded: {CompilationMemoryLimitMb}MB", + CompilerOutput = compilerOutput + }; + } + + // Copy .class file back + var boxClassPath = Path.Combine(boxDir, "Solution.class"); + if (isolateResult.ExitCode == 0 && File.Exists(boxClassPath)) + { + Directory.CreateDirectory(Path.GetDirectoryName(classFilePath)!); + File.Copy(boxClassPath, classFilePath, overwrite: true); + + _logger.LogInformation("Java compilation successful in Isolate box {BoxId}", boxId); + return new CompilationResult + { + Success = true, + ExecutablePath = classFilePath, + CompilerOutput = compilerOutput + }; + } + + _logger.LogWarning("Java compilation failed with exit code {ExitCode} in box {BoxId}", + isolateResult.ExitCode, boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation failed with exit code {isolateResult.ExitCode}", + CompilerOutput = compilerOutput + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Isolate Java compilation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation error: {ex.Message}" + }; + } + finally + { + if (boxId >= 0) + { + try + { + await _isolateService.CleanupBoxAsync(boxId); + _logger.LogDebug("Cleaned up isolate box {BoxId}", boxId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup compilation box {BoxId}", boxId); + } + + _boxPool.ReleaseBox(boxId); + _logger.LogDebug("Released compilation box {BoxId} back to pool", boxId); + } + } + } + + private (string compiler, string compilerFlags) ResolveVersion(string? version) + { + if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase)) + { + var compiler = _configuration["Java:Compiler"] ?? "javac"; + var compilerFlags = _configuration["Java:CompilerFlags"] ?? ""; + return (compiler, compilerFlags); + } + + var versionKey = $"Java:Versions:{version}"; + var versionCompiler = _configuration[$"{versionKey}:Compiler"]; + var versionFlags = _configuration[$"{versionKey}:CompilerFlags"]; + + if (!string.IsNullOrEmpty(versionCompiler)) + { + _logger.LogInformation("Using Java version {Version} configuration", version); + return (versionCompiler, versionFlags ?? ""); + } + + _logger.LogWarning("Java version {Version} not found in configuration, using default", version); + var defaultCompiler = _configuration["Java:Compiler"] ?? "javac"; + var defaultFlags = _configuration["Java:CompilerFlags"] ?? ""; + return (defaultCompiler, defaultFlags); + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/KotlinCompilationServiceIsolate.cs b/src/LiquidCode.Tester.Worker/Services/KotlinCompilationServiceIsolate.cs new file mode 100644 index 0000000..6eaf5ca --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/KotlinCompilationServiceIsolate.cs @@ -0,0 +1,221 @@ +using LiquidCode.Tester.Worker.Services.Isolate; +using LiquidCode.Tester.Worker.Models; + +namespace LiquidCode.Tester.Worker.Services; + +/// +/// Kotlin compilation service using Isolate sandbox for security +/// +public class KotlinCompilationServiceIsolate : ICompilationService +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IsolateService _isolateService; + private readonly IsolateBoxPool _boxPool; + + private const int CompilationTimeLimitSeconds = 30; + private const int CompilationMemoryLimitMb = 512; + + public KotlinCompilationServiceIsolate( + ILogger logger, + IConfiguration configuration, + IsolateService isolateService, + IsolateBoxPool boxPool) + { + _logger = logger; + _configuration = configuration; + _isolateService = isolateService; + _boxPool = boxPool; + } + + public async Task CompileAsync(string sourceCode, string workingDirectory, string? version = null) + { + var sourceFilePath = Path.Combine(workingDirectory, "Solution.kt"); + var jarFilePath = Path.Combine(workingDirectory, "solution.jar"); + + _logger.LogInformation("Compiling Kotlin code with Isolate in {WorkingDirectory} with version {Version}", + workingDirectory, version ?? "latest"); + + try + { + await File.WriteAllTextAsync(sourceFilePath, sourceCode); + return await CompileFileInIsolateAsync(sourceFilePath, jarFilePath, version); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Isolate Kotlin compilation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation error: {ex.Message}" + }; + } + } + + private async Task CompileFileInIsolateAsync( + string sourceFilePath, + string jarFilePath, + string? version = null) + { + int boxId = -1; + + try + { + boxId = await _boxPool.AcquireBoxAsync(); + _logger.LogDebug("Acquired isolate box {BoxId} for Kotlin compilation", boxId); + + await _isolateService.InitBoxAsync(boxId); + + var boxDir = $"/var/local/lib/isolate/{boxId}/box"; + var sourceFileName = Path.GetFileName(sourceFilePath); + var boxSourcePath = Path.Combine(boxDir, sourceFileName); + var jarFileName = "solution.jar"; + var boxJarPath = Path.Combine(boxDir, jarFileName); + + File.Copy(sourceFilePath, boxSourcePath, overwrite: true); + + var (compiler, compilerFlags) = ResolveVersion(version); + _logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, compilerFlags); + + var arguments = new List(); + if (!string.IsNullOrWhiteSpace(compilerFlags)) + { + arguments.AddRange(compilerFlags.Split(' ', StringSplitOptions.RemoveEmptyEntries)); + } + arguments.Add($"/box/{sourceFileName}"); + arguments.Add("-include-runtime"); + arguments.Add("-d"); + arguments.Add($"/box/{jarFileName}"); + + var stderrFilePath = Path.Combine(boxDir, "compile_stderr.txt"); + + var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions + { + BoxId = boxId, + Executable = $"/usr/local/bin/{compiler}", + Arguments = arguments.ToArray(), + TimeLimitSeconds = CompilationTimeLimitSeconds, + WallTimeLimitSeconds = CompilationTimeLimitSeconds * 2, + MemoryLimitKb = CompilationMemoryLimitMb * 1024, + StackLimitKb = 256 * 1024, + ProcessLimit = 10, + EnableNetwork = false, + StderrFile = stderrFilePath, + WorkingDirectory = "/box", + DirectoryBindings = new List + { + new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true }, + new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true }, + new DirectoryBinding { HostPath = "/opt/kotlinc", SandboxPath = "/opt/kotlinc", ReadOnly = true } + } + }); + + var compilerOutput = string.Empty; + if (File.Exists(stderrFilePath)) + { + compilerOutput = await File.ReadAllTextAsync(stderrFilePath); + } + if (!string.IsNullOrEmpty(isolateResult.ErrorOutput)) + { + compilerOutput = $"{compilerOutput}\n{isolateResult.ErrorOutput}".Trim(); + } + + if (isolateResult.TimeLimitExceeded) + { + _logger.LogWarning("Kotlin compilation time limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation timeout: exceeded {CompilationTimeLimitSeconds}s limit", + CompilerOutput = compilerOutput + }; + } + + if (isolateResult.MemoryLimitExceeded) + { + _logger.LogWarning("Kotlin compilation memory limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation memory limit exceeded: {CompilationMemoryLimitMb}MB", + CompilerOutput = compilerOutput + }; + } + + if (isolateResult.ExitCode == 0 && File.Exists(boxJarPath)) + { + Directory.CreateDirectory(Path.GetDirectoryName(jarFilePath)!); + File.Copy(boxJarPath, jarFilePath, overwrite: true); + + _logger.LogInformation("Kotlin compilation successful in Isolate box {BoxId}", boxId); + return new CompilationResult + { + Success = true, + ExecutablePath = jarFilePath, + CompilerOutput = compilerOutput + }; + } + + _logger.LogWarning("Kotlin compilation failed with exit code {ExitCode} in box {BoxId}", + isolateResult.ExitCode, boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation failed with exit code {isolateResult.ExitCode}", + CompilerOutput = compilerOutput + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Isolate Kotlin compilation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Compilation error: {ex.Message}" + }; + } + finally + { + if (boxId >= 0) + { + try + { + await _isolateService.CleanupBoxAsync(boxId); + _logger.LogDebug("Cleaned up isolate box {BoxId}", boxId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup compilation box {BoxId}", boxId); + } + + _boxPool.ReleaseBox(boxId); + _logger.LogDebug("Released compilation box {BoxId} back to pool", boxId); + } + } + } + + private (string compiler, string compilerFlags) ResolveVersion(string? version) + { + if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase)) + { + var compiler = _configuration["Kotlin:Compiler"] ?? "kotlinc"; + var compilerFlags = _configuration["Kotlin:CompilerFlags"] ?? ""; + return (compiler, compilerFlags); + } + + var versionKey = $"Kotlin:Versions:{version}"; + var versionCompiler = _configuration[$"{versionKey}:Compiler"]; + var versionFlags = _configuration[$"{versionKey}:CompilerFlags"]; + + if (!string.IsNullOrEmpty(versionCompiler)) + { + _logger.LogInformation("Using Kotlin version {Version} configuration", version); + return (versionCompiler, versionFlags ?? ""); + } + + _logger.LogWarning("Kotlin version {Version} not found in configuration, using default", version); + var defaultCompiler = _configuration["Kotlin:Compiler"] ?? "kotlinc"; + var defaultFlags = _configuration["Kotlin:CompilerFlags"] ?? ""; + return (defaultCompiler, defaultFlags); + } +} diff --git a/src/LiquidCode.Tester.Worker/Services/OutputCheckerService.cs b/src/LiquidCode.Tester.Worker/Services/OutputCheckerService.cs index ea10b50..ede72d2 100644 --- a/src/LiquidCode.Tester.Worker/Services/OutputCheckerService.cs +++ b/src/LiquidCode.Tester.Worker/Services/OutputCheckerService.cs @@ -4,11 +4,24 @@ public class OutputCheckerService : IOutputCheckerService { private readonly ILogger _logger; private readonly CheckerService _checkerService; + private readonly CheckerServiceIsolate _checkerServiceIsolate; + private readonly bool _useIsolate; - public OutputCheckerService(ILogger logger, CheckerService checkerService) + public OutputCheckerService( + ILogger logger, + CheckerService checkerService, + CheckerServiceIsolate checkerServiceIsolate, + IConfiguration configuration) { _logger = logger; _checkerService = checkerService; + _checkerServiceIsolate = checkerServiceIsolate; + _useIsolate = configuration.GetValue("Isolate:Enabled", false); + + if (_useIsolate) + { + _logger.LogInformation("Using Isolate sandbox for checker execution"); + } } public async Task CheckOutputAsync(string actualOutput, string expectedOutputPath) @@ -69,13 +82,12 @@ public class OutputCheckerService : IOutputCheckerService // If custom checker is available, use it if (!string.IsNullOrEmpty(checkerPath) && File.Exists(checkerPath)) { - _logger.LogDebug("Using custom checker: {CheckerPath}", checkerPath); + _logger.LogDebug("Using custom checker: {CheckerPath} (Isolate: {UseIsolate})", + checkerPath, _useIsolate); - var checkerResult = await _checkerService.CheckAsync( - checkerPath, - inputFilePath, - actualOutput, - expectedOutputPath); + var checkerResult = _useIsolate + ? await _checkerServiceIsolate.CheckAsync(checkerPath, inputFilePath, actualOutput, expectedOutputPath) + : await _checkerService.CheckAsync(checkerPath, inputFilePath, actualOutput, expectedOutputPath); if (!checkerResult.Accepted) { diff --git a/src/LiquidCode.Tester.Worker/Services/PythonCompilationServiceIsolate.cs b/src/LiquidCode.Tester.Worker/Services/PythonCompilationServiceIsolate.cs new file mode 100644 index 0000000..d498877 --- /dev/null +++ b/src/LiquidCode.Tester.Worker/Services/PythonCompilationServiceIsolate.cs @@ -0,0 +1,221 @@ +using LiquidCode.Tester.Worker.Services.Isolate; +using LiquidCode.Tester.Worker.Models; + +namespace LiquidCode.Tester.Worker.Services; + +/// +/// Python compilation service using Isolate sandbox for security +/// Python doesn't require compilation, but performs syntax validation in sandbox +/// +public class PythonCompilationServiceIsolate : ICompilationService +{ + private readonly ILogger _logger; + private readonly IConfiguration _configuration; + private readonly IsolateService _isolateService; + private readonly IsolateBoxPool _boxPool; + + private const int ValidationTimeLimitSeconds = 10; + private const int ValidationMemoryLimitMb = 256; + + public PythonCompilationServiceIsolate( + ILogger logger, + IConfiguration configuration, + IsolateService isolateService, + IsolateBoxPool boxPool) + { + _logger = logger; + _configuration = configuration; + _isolateService = isolateService; + _boxPool = boxPool; + } + + public async Task CompileAsync(string sourceCode, string workingDirectory, string? version = null) + { + var sourceFilePath = Path.Combine(workingDirectory, "solution.py"); + + _logger.LogInformation("Preparing Python code with Isolate validation in {WorkingDirectory} with version {Version}", + workingDirectory, version ?? "latest"); + + try + { + await File.WriteAllTextAsync(sourceFilePath, sourceCode); + + // Optionally validate syntax in Isolate sandbox + var validateSyntax = _configuration.GetValue("Python:ValidateSyntax", true); + if (validateSyntax) + { + var validationResult = await ValidateSyntaxInIsolateAsync(sourceFilePath, version); + if (!validationResult.Success) + { + return validationResult; + } + } + + var executable = ResolveVersion(version); + _logger.LogInformation("Python code prepared and validated successfully"); + + return new CompilationResult + { + Success = true, + ExecutablePath = sourceFilePath, + CompilerOutput = $"Python code validated successfully (using {executable})" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error preparing Python code"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Error preparing code: {ex.Message}" + }; + } + } + + private async Task ValidateSyntaxInIsolateAsync( + string sourceFilePath, + string? version = null) + { + int boxId = -1; + + try + { + boxId = await _boxPool.AcquireBoxAsync(); + _logger.LogDebug("Acquired isolate box {BoxId} for Python syntax validation", boxId); + + await _isolateService.InitBoxAsync(boxId); + + var boxDir = $"/var/local/lib/isolate/{boxId}/box"; + var sourceFileName = Path.GetFileName(sourceFilePath); + var boxSourcePath = Path.Combine(boxDir, sourceFileName); + + File.Copy(sourceFilePath, boxSourcePath, overwrite: true); + + var executable = ResolveVersion(version); + _logger.LogDebug("Using Python executable: {Executable}", executable); + + var stderrFilePath = Path.Combine(boxDir, "validation_stderr.txt"); + + // Validate syntax using: python3 -m py_compile solution.py + var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions + { + BoxId = boxId, + Executable = $"/usr/bin/{executable}", + Arguments = new[] { "-m", "py_compile", $"/box/{sourceFileName}" }, + TimeLimitSeconds = ValidationTimeLimitSeconds, + WallTimeLimitSeconds = ValidationTimeLimitSeconds * 2, + MemoryLimitKb = ValidationMemoryLimitMb * 1024, + StackLimitKb = 64 * 1024, + ProcessLimit = 1, + EnableNetwork = false, + StderrFile = stderrFilePath, + WorkingDirectory = "/box", + DirectoryBindings = new List + { + new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true }, + new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true } + } + }); + + var validationOutput = string.Empty; + if (File.Exists(stderrFilePath)) + { + validationOutput = await File.ReadAllTextAsync(stderrFilePath); + } + if (!string.IsNullOrEmpty(isolateResult.ErrorOutput)) + { + validationOutput = $"{validationOutput}\n{isolateResult.ErrorOutput}".Trim(); + } + + if (isolateResult.TimeLimitExceeded) + { + _logger.LogWarning("Python validation time limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Validation timeout: exceeded {ValidationTimeLimitSeconds}s limit", + CompilerOutput = validationOutput + }; + } + + if (isolateResult.MemoryLimitExceeded) + { + _logger.LogWarning("Python validation memory limit exceeded for box {BoxId}", boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Validation memory limit exceeded: {ValidationMemoryLimitMb}MB", + CompilerOutput = validationOutput + }; + } + + if (isolateResult.ExitCode != 0) + { + _logger.LogWarning("Python syntax validation failed with exit code {ExitCode} in box {BoxId}", + isolateResult.ExitCode, boxId); + return new CompilationResult + { + Success = false, + ErrorMessage = "Python syntax error", + CompilerOutput = validationOutput + }; + } + + _logger.LogDebug("Python syntax validation successful in Isolate box {BoxId}", boxId); + return new CompilationResult + { + Success = true, + CompilerOutput = "Syntax validation passed" + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error during Python syntax validation"); + return new CompilationResult + { + Success = false, + ErrorMessage = $"Validation error: {ex.Message}" + }; + } + finally + { + if (boxId >= 0) + { + try + { + await _isolateService.CleanupBoxAsync(boxId); + _logger.LogDebug("Cleaned up isolate box {BoxId}", boxId); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to cleanup validation box {BoxId}", boxId); + } + + _boxPool.ReleaseBox(boxId); + _logger.LogDebug("Released validation box {BoxId} back to pool", boxId); + } + } + } + + private string ResolveVersion(string? version) + { + if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase)) + { + var executable = _configuration["Python:Executable"] ?? "python3"; + return executable; + } + + var versionKey = $"Python:Versions:{version}"; + var versionExecutable = _configuration[$"{versionKey}:Executable"]; + + if (!string.IsNullOrEmpty(versionExecutable)) + { + _logger.LogInformation("Using Python version {Version} configuration", version); + return versionExecutable; + } + + _logger.LogWarning("Python version {Version} not found in configuration, using default", version); + var defaultExecutable = _configuration["Python:Executable"] ?? "python3"; + return defaultExecutable; + } +}