update isolate integrity
This commit is contained in:
@@ -23,16 +23,28 @@ builder.Services.AddSingleton(sp =>
|
|||||||
builder.Services.AddSingleton<PolygonProblemXmlParser>();
|
builder.Services.AddSingleton<PolygonProblemXmlParser>();
|
||||||
builder.Services.AddSingleton<AnswerGenerationService>();
|
builder.Services.AddSingleton<AnswerGenerationService>();
|
||||||
builder.Services.AddSingleton<CheckerService>();
|
builder.Services.AddSingleton<CheckerService>();
|
||||||
|
builder.Services.AddSingleton<CheckerServiceIsolate>();
|
||||||
builder.Services.AddSingleton<IPackageParserService, PackageParserService>();
|
builder.Services.AddSingleton<IPackageParserService, PackageParserService>();
|
||||||
builder.Services.AddSingleton<IOutputCheckerService, OutputCheckerService>();
|
builder.Services.AddSingleton<IOutputCheckerService, OutputCheckerService>();
|
||||||
builder.Services.AddSingleton<ICallbackService, CallbackService>();
|
builder.Services.AddSingleton<ICallbackService, CallbackService>();
|
||||||
|
|
||||||
// Register compilation services
|
// Register compilation services
|
||||||
|
// Always register both standard and isolate versions
|
||||||
builder.Services.AddSingleton<CppCompilationService>();
|
builder.Services.AddSingleton<CppCompilationService>();
|
||||||
|
builder.Services.AddSingleton<CppCompilationServiceIsolate>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<JavaCompilationService>();
|
builder.Services.AddSingleton<JavaCompilationService>();
|
||||||
|
builder.Services.AddSingleton<JavaCompilationServiceIsolate>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<KotlinCompilationService>();
|
builder.Services.AddSingleton<KotlinCompilationService>();
|
||||||
|
builder.Services.AddSingleton<KotlinCompilationServiceIsolate>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<CSharpCompilationService>();
|
builder.Services.AddSingleton<CSharpCompilationService>();
|
||||||
|
builder.Services.AddSingleton<CSharpCompilationServiceIsolate>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<PythonCompilationService>();
|
builder.Services.AddSingleton<PythonCompilationService>();
|
||||||
|
builder.Services.AddSingleton<PythonCompilationServiceIsolate>();
|
||||||
|
|
||||||
builder.Services.AddSingleton<ICompilationServiceFactory, CompilationServiceFactory>();
|
builder.Services.AddSingleton<ICompilationServiceFactory, CompilationServiceFactory>();
|
||||||
|
|
||||||
// Register execution services
|
// Register execution services
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
using LiquidCode.Tester.Worker.Services.Isolate;
|
||||||
|
using LiquidCode.Tester.Worker.Models;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// C# compilation service using Isolate sandbox for security
|
||||||
|
/// </summary>
|
||||||
|
public class CSharpCompilationServiceIsolate : ICompilationService
|
||||||
|
{
|
||||||
|
private readonly ILogger<CSharpCompilationServiceIsolate> _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<CSharpCompilationServiceIsolate> 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.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<CompilationResult> 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<string>();
|
||||||
|
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<DirectoryBinding>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
222
src/LiquidCode.Tester.Worker/Services/CheckerServiceIsolate.cs
Normal file
222
src/LiquidCode.Tester.Worker/Services/CheckerServiceIsolate.cs
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
using LiquidCode.Tester.Worker.Services.Isolate;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service for running custom checkers in Isolate sandbox
|
||||||
|
/// </summary>
|
||||||
|
public class CheckerServiceIsolate
|
||||||
|
{
|
||||||
|
private readonly ILogger<CheckerServiceIsolate> _logger;
|
||||||
|
private readonly IsolateService _isolateService;
|
||||||
|
private readonly IsolateBoxPool _boxPool;
|
||||||
|
|
||||||
|
private const int CheckerTimeLimitSeconds = 5;
|
||||||
|
private const int CheckerMemoryLimitMb = 256;
|
||||||
|
|
||||||
|
public CheckerServiceIsolate(
|
||||||
|
ILogger<CheckerServiceIsolate> logger,
|
||||||
|
IsolateService isolateService,
|
||||||
|
IsolateBoxPool boxPool)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_isolateService = isolateService;
|
||||||
|
_boxPool = boxPool;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check user output using custom checker in Isolate sandbox
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="checkerPath">Path to checker executable</param>
|
||||||
|
/// <param name="inputPath">Path to input file</param>
|
||||||
|
/// <param name="userOutput">User program output</param>
|
||||||
|
/// <param name="answerPath">Path to answer file</param>
|
||||||
|
/// <returns>Checker result</returns>
|
||||||
|
public async Task<CheckerResult> 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: <input> <output> <answer>
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,27 +3,54 @@ namespace LiquidCode.Tester.Worker.Services;
|
|||||||
public class CompilationServiceFactory : ICompilationServiceFactory
|
public class CompilationServiceFactory : ICompilationServiceFactory
|
||||||
{
|
{
|
||||||
private readonly IServiceProvider _serviceProvider;
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
private readonly IConfiguration _configuration;
|
||||||
private readonly ILogger<CompilationServiceFactory> _logger;
|
private readonly ILogger<CompilationServiceFactory> _logger;
|
||||||
|
private readonly bool _useIsolate;
|
||||||
|
|
||||||
public CompilationServiceFactory(IServiceProvider serviceProvider, ILogger<CompilationServiceFactory> logger)
|
public CompilationServiceFactory(
|
||||||
|
IServiceProvider serviceProvider,
|
||||||
|
IConfiguration configuration,
|
||||||
|
ILogger<CompilationServiceFactory> logger)
|
||||||
{
|
{
|
||||||
_serviceProvider = serviceProvider;
|
_serviceProvider = serviceProvider;
|
||||||
|
_configuration = configuration;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
|
_useIsolate = configuration.GetValue<bool>("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)
|
public ICompilationService GetCompilationService(string language)
|
||||||
{
|
{
|
||||||
var normalizedLanguage = language.ToLowerInvariant().Replace(" ", "");
|
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
|
return normalizedLanguage switch
|
||||||
{
|
{
|
||||||
"c++" or "cpp" => _serviceProvider.GetRequiredService<CppCompilationService>(),
|
"c++" or "cpp" => _useIsolate
|
||||||
"java" => _serviceProvider.GetRequiredService<JavaCompilationService>(),
|
? _serviceProvider.GetRequiredService<CppCompilationServiceIsolate>()
|
||||||
"kotlin" => _serviceProvider.GetRequiredService<KotlinCompilationService>(),
|
: _serviceProvider.GetRequiredService<CppCompilationService>(),
|
||||||
"c#" or "csharp" => _serviceProvider.GetRequiredService<CSharpCompilationService>(),
|
"java" => _useIsolate
|
||||||
"python" => _serviceProvider.GetRequiredService<PythonCompilationService>(),
|
? _serviceProvider.GetRequiredService<JavaCompilationServiceIsolate>()
|
||||||
|
: _serviceProvider.GetRequiredService<JavaCompilationService>(),
|
||||||
|
"kotlin" => _useIsolate
|
||||||
|
? _serviceProvider.GetRequiredService<KotlinCompilationServiceIsolate>()
|
||||||
|
: _serviceProvider.GetRequiredService<KotlinCompilationService>(),
|
||||||
|
"c#" or "csharp" => _useIsolate
|
||||||
|
? _serviceProvider.GetRequiredService<CSharpCompilationServiceIsolate>()
|
||||||
|
: _serviceProvider.GetRequiredService<CSharpCompilationService>(),
|
||||||
|
"python" => _useIsolate
|
||||||
|
? _serviceProvider.GetRequiredService<PythonCompilationServiceIsolate>()
|
||||||
|
: _serviceProvider.GetRequiredService<PythonCompilationService>(),
|
||||||
_ => throw new NotSupportedException($"Language '{language}' is not supported")
|
_ => throw new NotSupportedException($"Language '{language}' is not supported")
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
/// <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 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<CompilationResult> 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<string>(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<DirectoryBinding>
|
||||||
|
{
|
||||||
|
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<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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
using LiquidCode.Tester.Worker.Services.Isolate;
|
||||||
|
using LiquidCode.Tester.Worker.Models;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Java compilation service using Isolate sandbox for security
|
||||||
|
/// </summary>
|
||||||
|
public class JavaCompilationServiceIsolate : ICompilationService
|
||||||
|
{
|
||||||
|
private readonly ILogger<JavaCompilationServiceIsolate> _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<JavaCompilationServiceIsolate> 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.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<CompilationResult> 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<string>();
|
||||||
|
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<DirectoryBinding>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
using LiquidCode.Tester.Worker.Services.Isolate;
|
||||||
|
using LiquidCode.Tester.Worker.Models;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Kotlin compilation service using Isolate sandbox for security
|
||||||
|
/// </summary>
|
||||||
|
public class KotlinCompilationServiceIsolate : ICompilationService
|
||||||
|
{
|
||||||
|
private readonly ILogger<KotlinCompilationServiceIsolate> _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<KotlinCompilationServiceIsolate> 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.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<CompilationResult> 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<string>();
|
||||||
|
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<DirectoryBinding>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,11 +4,24 @@ public class OutputCheckerService : IOutputCheckerService
|
|||||||
{
|
{
|
||||||
private readonly ILogger<OutputCheckerService> _logger;
|
private readonly ILogger<OutputCheckerService> _logger;
|
||||||
private readonly CheckerService _checkerService;
|
private readonly CheckerService _checkerService;
|
||||||
|
private readonly CheckerServiceIsolate _checkerServiceIsolate;
|
||||||
|
private readonly bool _useIsolate;
|
||||||
|
|
||||||
public OutputCheckerService(ILogger<OutputCheckerService> logger, CheckerService checkerService)
|
public OutputCheckerService(
|
||||||
|
ILogger<OutputCheckerService> logger,
|
||||||
|
CheckerService checkerService,
|
||||||
|
CheckerServiceIsolate checkerServiceIsolate,
|
||||||
|
IConfiguration configuration)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_checkerService = checkerService;
|
_checkerService = checkerService;
|
||||||
|
_checkerServiceIsolate = checkerServiceIsolate;
|
||||||
|
_useIsolate = configuration.GetValue<bool>("Isolate:Enabled", false);
|
||||||
|
|
||||||
|
if (_useIsolate)
|
||||||
|
{
|
||||||
|
_logger.LogInformation("Using Isolate sandbox for checker execution");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<bool> CheckOutputAsync(string actualOutput, string expectedOutputPath)
|
public async Task<bool> CheckOutputAsync(string actualOutput, string expectedOutputPath)
|
||||||
@@ -69,13 +82,12 @@ public class OutputCheckerService : IOutputCheckerService
|
|||||||
// If custom checker is available, use it
|
// If custom checker is available, use it
|
||||||
if (!string.IsNullOrEmpty(checkerPath) && File.Exists(checkerPath))
|
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(
|
var checkerResult = _useIsolate
|
||||||
checkerPath,
|
? await _checkerServiceIsolate.CheckAsync(checkerPath, inputFilePath, actualOutput, expectedOutputPath)
|
||||||
inputFilePath,
|
: await _checkerService.CheckAsync(checkerPath, inputFilePath, actualOutput, expectedOutputPath);
|
||||||
actualOutput,
|
|
||||||
expectedOutputPath);
|
|
||||||
|
|
||||||
if (!checkerResult.Accepted)
|
if (!checkerResult.Accepted)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
using LiquidCode.Tester.Worker.Services.Isolate;
|
||||||
|
using LiquidCode.Tester.Worker.Models;
|
||||||
|
|
||||||
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Python compilation service using Isolate sandbox for security
|
||||||
|
/// Python doesn't require compilation, but performs syntax validation in sandbox
|
||||||
|
/// </summary>
|
||||||
|
public class PythonCompilationServiceIsolate : ICompilationService
|
||||||
|
{
|
||||||
|
private readonly ILogger<PythonCompilationServiceIsolate> _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<PythonCompilationServiceIsolate> 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.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<bool>("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<CompilationResult> 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<DirectoryBinding>
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user