using System; using System.Collections.Generic; using System.IO; 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 CompileFileAsync(sourceFilePath, executablePath, version); } catch (Exception ex) { _logger.LogError(ex, "Error during Isolate compilation"); return new CompilationResult { Success = false, ErrorMessage = $"Compilation error: {ex.Message}" }; } } /// /// Compile a C++ source file to an executable using Isolate sandbox /// public async Task CompileFileAsync( string sourceFilePath, string outputExecutablePath, string? version = null, IEnumerable? includeDirectories = null, IEnumerable? additionalFlags = 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 outputFileName = Path.GetFileName(outputExecutablePath); var boxOutputPath = Path.Combine(boxDir, outputFileName); File.Copy(sourceFilePath, boxSourcePath, overwrite: true); // Copy common headers from the source directory (e.g., testlib.h) var sourceDirectory = Path.GetDirectoryName(sourceFilePath); if (!string.IsNullOrEmpty(sourceDirectory) && Directory.Exists(sourceDirectory)) { foreach (var header in Directory.EnumerateFiles(sourceDirectory)) { if (string.Equals(header, sourceFilePath, StringComparison.OrdinalIgnoreCase)) { continue; } var extension = Path.GetExtension(header); if (extension is ".h" or ".hpp" or ".hh" or ".hxx" or ".h++" or ".inl" or ".tcc" ) { var destination = Path.Combine(boxDir, Path.GetFileName(header)); File.Copy(header, destination, 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); var includeCounter = 0; // Add include directories if (includeDirectories != null) { foreach (var includeDir in includeDirectories.Where(d => !string.IsNullOrWhiteSpace(d))) { var resolvedIncludeDir = includeDir; if (!Path.IsPathRooted(resolvedIncludeDir)) { var baseDir = sourceDirectory ?? Directory.GetCurrentDirectory(); resolvedIncludeDir = Path.GetFullPath(Path.Combine(baseDir, includeDir)); } if (Directory.Exists(resolvedIncludeDir)) { includeCounter++; var targetIncludeDir = Path.Combine(boxDir, $"include_{includeCounter}"); CopyDirectory(resolvedIncludeDir, targetIncludeDir); arguments.Add($"-I/box/include_{includeCounter}"); } else { arguments.Add($"-I{includeDir}"); } } } // Add additional flags if (additionalFlags != null) { foreach (var flag in additionalFlags.Where(f => !string.IsNullOrWhiteSpace(f))) { arguments.Add(flag); } } arguments.Add($"/box/{sourceFileName}"); arguments.Add("-o"); arguments.Add($"/box/{outputFileName}"); // Prepare stderr output file for compiler messages var stderrFilePath = Path.Combine(boxDir, "compile_stderr.txt"); // Run compiler in Isolate // Bind the system toolchain directories read-only so the linker and headers remain reachable var directoryBindings = new List { new DirectoryBinding { HostPath = "/usr/include", SandboxPath = "/usr/include", ReadOnly = true }, new DirectoryBinding { HostPath = "/usr/bin", SandboxPath = "/usr/bin", ReadOnly = true }, new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true }, new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true } }; if (Directory.Exists("/bin")) { directoryBindings.Add(new DirectoryBinding { HostPath = "/bin", SandboxPath = "/bin", ReadOnly = true }); } if (Directory.Exists("/usr/local/bin")) { directoryBindings.Add(new DirectoryBinding { HostPath = "/usr/local/bin", SandboxPath = "/usr/local/bin", ReadOnly = true }); } 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 = directoryBindings, EnvironmentVariables = new Dictionary { ["PATH"] = GetSandboxPath() } }); // 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 static void CopyDirectory(string sourceDirectory, string destinationDirectory) { var sourceRoot = Path.GetFullPath(sourceDirectory); var destinationRoot = Path.GetFullPath(destinationDirectory); Directory.CreateDirectory(destinationRoot); foreach (var directory in Directory.EnumerateDirectories(sourceRoot, "*", SearchOption.AllDirectories)) { var relativePath = Path.GetRelativePath(sourceRoot, directory); var targetDir = Path.Combine(destinationRoot, relativePath); Directory.CreateDirectory(targetDir); } foreach (var file in Directory.EnumerateFiles(sourceRoot, "*", SearchOption.AllDirectories)) { var relativePath = Path.GetRelativePath(sourceRoot, file); var targetFile = Path.Combine(destinationRoot, relativePath); Directory.CreateDirectory(Path.GetDirectoryName(targetFile)!); File.Copy(file, targetFile, overwrite: true); } } private static string GetSandboxPath() { var defaultPaths = new[] { "/usr/local/bin", "/usr/bin", "/bin" }; var hostPath = Environment.GetEnvironmentVariable("PATH"); if (string.IsNullOrWhiteSpace(hostPath)) { return string.Join(':', defaultPaths); } var segments = hostPath .Split(':', StringSplitOptions.RemoveEmptyEntries) .Where(path => path.StartsWith("/usr", StringComparison.Ordinal) || path.StartsWith("/bin", StringComparison.Ordinal)) .ToList(); foreach (var defaultPath in defaultPaths) { if (!segments.Contains(defaultPath)) { segments.Add(defaultPath); } } return segments.Count == 0 ? string.Join(':', defaultPaths) : string.Join(':', segments); } 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 }; } }