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 47s
Build and Push Docker Images / build (src/LiquidCode.Tester.Worker/Dockerfile, git.nullptr.top/liquidcode/liquidcode-tester-worker-roman, worker) (push) Successful in 1m2s
412 lines
16 KiB
C#
412 lines
16 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using LiquidCode.Tester.Worker.Services.Isolate;
|
|
using LiquidCode.Tester.Worker.Models;
|
|
|
|
/// <summary>
|
|
namespace LiquidCode.Tester.Worker.Services;
|
|
|
|
/// <summary>
|
|
/// C++ compilation service using Isolate sandbox for security
|
|
/// </summary>
|
|
public class CppCompilationServiceIsolate : ICompilationService
|
|
{
|
|
private readonly ILogger<CppCompilationServiceIsolate> _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<CppCompilationServiceIsolate> logger,
|
|
IConfiguration configuration,
|
|
IsolateService isolateService,
|
|
IsolateBoxPool boxPool)
|
|
{
|
|
_logger = logger;
|
|
_configuration = configuration;
|
|
_isolateService = isolateService;
|
|
_boxPool = boxPool;
|
|
}
|
|
|
|
public async Task<CompilationResult> 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}"
|
|
};
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compile a C++ source file to an executable using Isolate sandbox
|
|
/// </summary>
|
|
public async Task<CompilationResult> CompileFileAsync(
|
|
string sourceFilePath,
|
|
string outputExecutablePath,
|
|
string? version = null,
|
|
IEnumerable<string>? includeDirectories = null,
|
|
IEnumerable<string>? 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<string>(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<DirectoryBinding>
|
|
{
|
|
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<string, string>
|
|
{
|
|
["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<string> 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<string> 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
|
|
};
|
|
}
|
|
}
|