Files
LiquidCode.Tester/src/LiquidCode.Tester.Worker/Services/CppCompilationServiceIsolate.cs
Roman Pytkov f4d855c958
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
Попытка добавить ld
2025-11-06 12:21:27 +03:00

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
};
}
}