remove k8s configs, update worker for multi-languages support, add local-submit option
This commit is contained in:
@@ -23,16 +23,63 @@ public class TestController : ControllerBase
|
||||
|
||||
try
|
||||
{
|
||||
// Save package to temporary file before starting background task
|
||||
// This is necessary because IFormFile becomes unavailable after request completes
|
||||
string? packageFilePath = null;
|
||||
if (request.Package != null)
|
||||
{
|
||||
var tempDirectory = Path.Combine(Path.GetTempPath(), "worker-packages");
|
||||
Directory.CreateDirectory(tempDirectory);
|
||||
|
||||
packageFilePath = Path.Combine(tempDirectory, $"package_{Guid.NewGuid()}.zip");
|
||||
|
||||
await using (var fileStream = new FileStream(packageFilePath, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
{
|
||||
await request.Package.CopyToAsync(fileStream);
|
||||
}
|
||||
|
||||
_logger.LogInformation("Package saved to temporary file: {FilePath}", packageFilePath);
|
||||
}
|
||||
|
||||
// Create a copy of request data for background task
|
||||
var backgroundRequest = new TestRequest
|
||||
{
|
||||
Id = request.Id,
|
||||
MissionId = request.MissionId,
|
||||
Language = request.Language,
|
||||
LanguageVersion = request.LanguageVersion,
|
||||
SourceCode = request.SourceCode,
|
||||
CallbackUrl = request.CallbackUrl,
|
||||
Package = null, // Will use file path instead
|
||||
PackageFilePath = packageFilePath
|
||||
};
|
||||
|
||||
// Start testing in background
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _testingService.ProcessSubmitAsync(request);
|
||||
await _testingService.ProcessSubmitAsync(backgroundRequest);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error processing submit {SubmitId}", request.Id);
|
||||
_logger.LogError(ex, "Error processing submit {SubmitId}", backgroundRequest.Id);
|
||||
}
|
||||
finally
|
||||
{
|
||||
// Cleanup temporary package file
|
||||
if (packageFilePath != null && System.IO.File.Exists(packageFilePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
System.IO.File.Delete(packageFilePath);
|
||||
_logger.LogInformation("Deleted temporary package file: {FilePath}", packageFilePath);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Failed to delete temporary package file: {FilePath}", packageFilePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -48,7 +95,7 @@ public class TestController : ControllerBase
|
||||
[HttpGet("health")]
|
||||
public IActionResult Health()
|
||||
{
|
||||
return Ok(new { status = "healthy", service = "cpp-worker", timestamp = DateTime.UtcNow });
|
||||
return Ok(new { status = "healthy", service = "universal-worker", timestamp = DateTime.UtcNow });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,4 +108,5 @@ public class TestRequest
|
||||
public string SourceCode { get; set; } = string.Empty;
|
||||
public string CallbackUrl { get; set; } = string.Empty;
|
||||
public IFormFile? Package { get; set; }
|
||||
public string? PackageFilePath { get; set; } // Internal use - path to saved package file
|
||||
}
|
||||
|
||||
@@ -24,16 +24,44 @@ FROM build AS publish
|
||||
ARG BUILD_CONFIGURATION=Release
|
||||
RUN dotnet publish "./LiquidCode.Tester.Worker.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false
|
||||
|
||||
# Final stage - use aspnet runtime with C++ compiler
|
||||
# Final stage - use aspnet runtime with all compilers
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS final
|
||||
WORKDIR /app
|
||||
|
||||
# Install C++ compiler and build tools
|
||||
# Install compilers and runtimes for all supported languages
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
# C++ compiler and build tools
|
||||
g++ \
|
||||
gcc \
|
||||
make \
|
||||
# Java Development Kit and Runtime
|
||||
openjdk-17-jdk \
|
||||
# Python
|
||||
python3 \
|
||||
python3-pip \
|
||||
# Kotlin compiler
|
||||
wget \
|
||||
unzip \
|
||||
&& wget -q https://github.com/JetBrains/kotlin/releases/download/v1.9.20/kotlin-compiler-1.9.20.zip -O /tmp/kotlin.zip \
|
||||
&& unzip -q /tmp/kotlin.zip -d /opt \
|
||||
&& rm /tmp/kotlin.zip \
|
||||
&& ln -s /opt/kotlinc/bin/kotlinc /usr/local/bin/kotlinc \
|
||||
&& ln -s /opt/kotlinc/bin/kotlin /usr/local/bin/kotlin \
|
||||
# Cleanup
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Mono for C# compilation (csc)
|
||||
RUN apt-get update && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
ca-certificates \
|
||||
gnupg \
|
||||
&& gpg --homedir /tmp --no-default-keyring --keyring /usr/share/keyrings/mono-official-archive-keyring.gpg --keyserver hkp://keyserver.ubuntu.com:80 --recv-keys 3FA7E0328081BFF6A14DA29AA6A19B38D3D831EF \
|
||||
&& echo "deb [signed-by=/usr/share/keyrings/mono-official-archive-keyring.gpg] https://download.mono-project.com/repo/debian stable-buster main" | tee /etc/apt/sources.list.d/mono-official-stable.list \
|
||||
&& apt-get update \
|
||||
&& apt-get install -y --no-install-recommends mono-devel \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Copy published app
|
||||
|
||||
15
src/LiquidCode.Tester.Worker/Models/LanguageConfig.cs
Normal file
15
src/LiquidCode.Tester.Worker/Models/LanguageConfig.cs
Normal file
@@ -0,0 +1,15 @@
|
||||
namespace LiquidCode.Tester.Worker.Models;
|
||||
|
||||
public class LanguageConfig
|
||||
{
|
||||
public string DefaultVersion { get; set; } = "latest";
|
||||
public Dictionary<string, VersionConfig> Versions { get; set; } = new();
|
||||
}
|
||||
|
||||
public class VersionConfig
|
||||
{
|
||||
public string? Compiler { get; set; }
|
||||
public string? CompilerFlags { get; set; }
|
||||
public string? Executable { get; set; }
|
||||
public string? Runtime { get; set; }
|
||||
}
|
||||
@@ -11,10 +11,26 @@ builder.Services.AddHttpClient();
|
||||
|
||||
// Register application services
|
||||
builder.Services.AddSingleton<IPackageParserService, PackageParserService>();
|
||||
builder.Services.AddSingleton<ICompilationService, CppCompilationService>();
|
||||
builder.Services.AddSingleton<IExecutionService, CppExecutionService>();
|
||||
builder.Services.AddSingleton<IOutputCheckerService, OutputCheckerService>();
|
||||
builder.Services.AddSingleton<ICallbackService, CallbackService>();
|
||||
|
||||
// Register compilation services
|
||||
builder.Services.AddSingleton<CppCompilationService>();
|
||||
builder.Services.AddSingleton<JavaCompilationService>();
|
||||
builder.Services.AddSingleton<KotlinCompilationService>();
|
||||
builder.Services.AddSingleton<CSharpCompilationService>();
|
||||
builder.Services.AddSingleton<PythonCompilationService>();
|
||||
builder.Services.AddSingleton<ICompilationServiceFactory, CompilationServiceFactory>();
|
||||
|
||||
// Register execution services
|
||||
builder.Services.AddSingleton<CppExecutionService>();
|
||||
builder.Services.AddSingleton<JavaExecutionService>();
|
||||
builder.Services.AddSingleton<KotlinExecutionService>();
|
||||
builder.Services.AddSingleton<CSharpExecutionService>();
|
||||
builder.Services.AddSingleton<PythonExecutionService>();
|
||||
builder.Services.AddSingleton<IExecutionServiceFactory, ExecutionServiceFactory>();
|
||||
|
||||
// Register testing service
|
||||
builder.Services.AddSingleton<ITestingService, TestingService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
using System.Diagnostics;
|
||||
using LiquidCode.Tester.Worker.Models;
|
||||
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
|
||||
public class CSharpCompilationService : ICompilationService
|
||||
{
|
||||
private readonly ILogger<CSharpCompilationService> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public CSharpCompilationService(ILogger<CSharpCompilationService> logger, IConfiguration configuration)
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
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 in {WorkingDirectory} with version {Version}", workingDirectory, version ?? "latest");
|
||||
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(sourceFilePath, sourceCode);
|
||||
|
||||
// Resolve version-specific configuration
|
||||
var (compiler, compilerFlags) = ResolveVersion(version);
|
||||
|
||||
_logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, compilerFlags);
|
||||
|
||||
var arguments = $"{compilerFlags} /out:\"{executablePath}\" \"{sourceFilePath}\"";
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = compiler,
|
||||
Arguments = arguments,
|
||||
WorkingDirectory = workingDirectory,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
var output = await process.StandardOutput.ReadToEndAsync();
|
||||
var error = await process.StandardError.ReadToEndAsync();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
var compilerOutput = $"{output}\n{error}".Trim();
|
||||
|
||||
if (process.ExitCode == 0 && File.Exists(executablePath))
|
||||
{
|
||||
_logger.LogInformation("C# compilation successful");
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = true,
|
||||
ExecutablePath = executablePath,
|
||||
CompilerOutput = compilerOutput
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("C# compilation failed with exit code {ExitCode}", process.ExitCode);
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Compilation failed",
|
||||
CompilerOutput = compilerOutput
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during C# compilation");
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = $"Compilation error: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private (string compiler, string compilerFlags) ResolveVersion(string? version)
|
||||
{
|
||||
// If version is null or "latest", use default configuration
|
||||
if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var compiler = _configuration["CSharp:Compiler"] ?? "csc";
|
||||
var compilerFlags = _configuration["CSharp:CompilerFlags"] ?? "/optimize+";
|
||||
return (compiler, compilerFlags);
|
||||
}
|
||||
|
||||
// Try to find version-specific configuration
|
||||
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+");
|
||||
}
|
||||
|
||||
// Version not found, use default and log warning
|
||||
_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);
|
||||
}
|
||||
}
|
||||
108
src/LiquidCode.Tester.Worker/Services/CSharpExecutionService.cs
Normal file
108
src/LiquidCode.Tester.Worker/Services/CSharpExecutionService.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
|
||||
public class CSharpExecutionService : IExecutionService
|
||||
{
|
||||
private readonly ILogger<CSharpExecutionService> _logger;
|
||||
|
||||
public CSharpExecutionService(ILogger<CSharpExecutionService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ExecutionResult> ExecuteAsync(string executablePath, string inputFilePath, int timeLimitMs, int memoryLimitMb)
|
||||
{
|
||||
_logger.LogInformation("Executing C# executable {Executable} with input {Input}, time limit {TimeLimit}ms, memory limit {MemoryLimit}MB",
|
||||
executablePath, inputFilePath, timeLimitMs, memoryLimitMb);
|
||||
|
||||
var result = new ExecutionResult();
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
using var inputStream = File.OpenRead(inputFilePath);
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = executablePath,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
await inputStream.CopyToAsync(process.StandardInput.BaseStream);
|
||||
process.StandardInput.Close();
|
||||
|
||||
var outputTask = process.StandardOutput.ReadToEndAsync();
|
||||
var errorTask = process.StandardError.ReadToEndAsync();
|
||||
|
||||
var completedInTime = await Task.Run(() => process.WaitForExit(timeLimitMs));
|
||||
|
||||
stopwatch.Stop();
|
||||
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||
|
||||
if (!completedInTime)
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch { }
|
||||
|
||||
result.TimeLimitExceeded = true;
|
||||
result.ErrorMessage = "Time limit exceeded";
|
||||
_logger.LogWarning("Execution exceeded time limit");
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Output = await outputTask;
|
||||
result.ErrorOutput = await errorTask;
|
||||
result.ExitCode = process.ExitCode;
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
result.RuntimeError = true;
|
||||
result.ErrorMessage = $"Runtime error (exit code {process.ExitCode})";
|
||||
_logger.LogWarning("Runtime error with exit code {ExitCode}", process.ExitCode);
|
||||
return result;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result.MemoryUsedMb = process.PeakWorkingSet64 / (1024 * 1024);
|
||||
if (result.MemoryUsedMb > memoryLimitMb)
|
||||
{
|
||||
result.MemoryLimitExceeded = true;
|
||||
result.ErrorMessage = "Memory limit exceeded";
|
||||
_logger.LogWarning("Memory limit exceeded: {Used}MB > {Limit}MB", result.MemoryUsedMb, memoryLimitMb);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not measure memory usage");
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
_logger.LogInformation("Execution completed successfully in {Time}ms", result.ExecutionTimeMs);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||
result.RuntimeError = true;
|
||||
result.ErrorMessage = $"Execution error: {ex.Message}";
|
||||
_logger.LogError(ex, "Error during execution");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,13 @@ public class CallbackService : ICallbackService
|
||||
|
||||
public async Task SendStatusAsync(string callbackUrl, TesterResponseModel response)
|
||||
{
|
||||
// Check if callback should be logged instead of sent via HTTP
|
||||
if (IsLogCallback(callbackUrl))
|
||||
{
|
||||
LogCallback(response);
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Sending status update to {CallbackUrl} for submit {SubmitId}", callbackUrl, response.SubmitId);
|
||||
|
||||
try
|
||||
@@ -36,4 +43,37 @@ public class CallbackService : ICallbackService
|
||||
// Don't throw - callback failures shouldn't stop testing
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsLogCallback(string callbackUrl)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(callbackUrl))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var normalized = callbackUrl.Trim().ToLowerInvariant();
|
||||
return normalized == "log" ||
|
||||
normalized == "console" ||
|
||||
normalized.StartsWith("log://");
|
||||
}
|
||||
|
||||
private void LogCallback(TesterResponseModel response)
|
||||
{
|
||||
var options = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase
|
||||
};
|
||||
|
||||
var json = JsonSerializer.Serialize(response, options);
|
||||
|
||||
_logger.LogInformation(
|
||||
"\n" +
|
||||
"╔═══════════════════════════════════════════════════════════════╗\n" +
|
||||
"║ CALLBACK RESULT ║\n" +
|
||||
"╠═══════════════════════════════════════════════════════════════╣\n" +
|
||||
"{Json}\n" +
|
||||
"╚═══════════════════════════════════════════════════════════════╝",
|
||||
json);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
|
||||
public class CompilationServiceFactory : ICompilationServiceFactory
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<CompilationServiceFactory> _logger;
|
||||
|
||||
public CompilationServiceFactory(IServiceProvider serviceProvider, ILogger<CompilationServiceFactory> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public ICompilationService GetCompilationService(string language)
|
||||
{
|
||||
var normalizedLanguage = language.ToLowerInvariant().Replace(" ", "");
|
||||
|
||||
_logger.LogInformation("Getting compilation service for language: {Language}", normalizedLanguage);
|
||||
|
||||
return normalizedLanguage switch
|
||||
{
|
||||
"c++" or "cpp" => _serviceProvider.GetRequiredService<CppCompilationService>(),
|
||||
"java" => _serviceProvider.GetRequiredService<JavaCompilationService>(),
|
||||
"kotlin" => _serviceProvider.GetRequiredService<KotlinCompilationService>(),
|
||||
"c#" or "csharp" => _serviceProvider.GetRequiredService<CSharpCompilationService>(),
|
||||
"python" => _serviceProvider.GetRequiredService<PythonCompilationService>(),
|
||||
_ => throw new NotSupportedException($"Language '{language}' is not supported")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using System.Diagnostics;
|
||||
using LiquidCode.Tester.Worker.Models;
|
||||
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
|
||||
@@ -13,21 +14,22 @@ public class CppCompilationService : ICompilationService
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public async Task<CompilationResult> CompileAsync(string sourceCode, string workingDirectory)
|
||||
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 in {WorkingDirectory}", workingDirectory);
|
||||
_logger.LogInformation("Compiling C++ code in {WorkingDirectory} with version {Version}", workingDirectory, version ?? "latest");
|
||||
|
||||
try
|
||||
{
|
||||
// Write source code to file
|
||||
await File.WriteAllTextAsync(sourceFilePath, sourceCode);
|
||||
|
||||
// Compile using g++
|
||||
var compiler = _configuration["Cpp:Compiler"] ?? "g++";
|
||||
var compilerFlags = _configuration["Cpp:CompilerFlags"] ?? "-O2 -std=c++17 -Wall";
|
||||
// Resolve version-specific configuration
|
||||
var (compiler, compilerFlags) = ResolveVersion(version);
|
||||
|
||||
_logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, compilerFlags);
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
@@ -83,4 +85,32 @@ public class CppCompilationService : ICompilationService
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private (string compiler, string compilerFlags) ResolveVersion(string? version)
|
||||
{
|
||||
// If version is null or "latest", use default configuration
|
||||
if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var compiler = _configuration["Cpp:Compiler"] ?? "g++";
|
||||
var compilerFlags = _configuration["Cpp:CompilerFlags"] ?? "-O2 -std=c++17 -Wall";
|
||||
return (compiler, compilerFlags);
|
||||
}
|
||||
|
||||
// Try to find version-specific configuration
|
||||
var versionKey = $"Cpp: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 ?? "-O2 -Wall");
|
||||
}
|
||||
|
||||
// Version not found, use default and log warning
|
||||
_logger.LogWarning("C++ version {Version} not found in configuration, using default", version);
|
||||
var defaultCompiler = _configuration["Cpp:Compiler"] ?? "g++";
|
||||
var defaultFlags = _configuration["Cpp:CompilerFlags"] ?? "-O2 -std=c++17 -Wall";
|
||||
return (defaultCompiler, defaultFlags);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
|
||||
public class ExecutionServiceFactory : IExecutionServiceFactory
|
||||
{
|
||||
private readonly IServiceProvider _serviceProvider;
|
||||
private readonly ILogger<ExecutionServiceFactory> _logger;
|
||||
|
||||
public ExecutionServiceFactory(IServiceProvider serviceProvider, ILogger<ExecutionServiceFactory> logger)
|
||||
{
|
||||
_serviceProvider = serviceProvider;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public IExecutionService GetExecutionService(string language)
|
||||
{
|
||||
var normalizedLanguage = language.ToLowerInvariant().Replace(" ", "");
|
||||
|
||||
_logger.LogInformation("Getting execution service for language: {Language}", normalizedLanguage);
|
||||
|
||||
return normalizedLanguage switch
|
||||
{
|
||||
"c++" or "cpp" => _serviceProvider.GetRequiredService<CppExecutionService>(),
|
||||
"java" => _serviceProvider.GetRequiredService<JavaExecutionService>(),
|
||||
"kotlin" => _serviceProvider.GetRequiredService<KotlinExecutionService>(),
|
||||
"c#" or "csharp" => _serviceProvider.GetRequiredService<CSharpExecutionService>(),
|
||||
"python" => _serviceProvider.GetRequiredService<PythonExecutionService>(),
|
||||
_ => throw new NotSupportedException($"Language '{language}' is not supported")
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,9 @@ public interface ICompilationService
|
||||
/// </summary>
|
||||
/// <param name="sourceCode">Source code to compile</param>
|
||||
/// <param name="workingDirectory">Directory to compile in</param>
|
||||
/// <param name="version">Language version (e.g., "17", "20", "latest"). If null or "latest", uses default version.</param>
|
||||
/// <returns>Result containing success status, executable path, and error messages</returns>
|
||||
Task<CompilationResult> CompileAsync(string sourceCode, string workingDirectory);
|
||||
Task<CompilationResult> CompileAsync(string sourceCode, string workingDirectory, string? version = null);
|
||||
}
|
||||
|
||||
public class CompilationResult
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
|
||||
public interface ICompilationServiceFactory
|
||||
{
|
||||
ICompilationService GetCompilationService(string language);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
|
||||
public interface IExecutionServiceFactory
|
||||
{
|
||||
IExecutionService GetExecutionService(string language);
|
||||
}
|
||||
119
src/LiquidCode.Tester.Worker/Services/JavaCompilationService.cs
Normal file
119
src/LiquidCode.Tester.Worker/Services/JavaCompilationService.cs
Normal file
@@ -0,0 +1,119 @@
|
||||
using System.Diagnostics;
|
||||
using LiquidCode.Tester.Worker.Models;
|
||||
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
|
||||
public class JavaCompilationService : ICompilationService
|
||||
{
|
||||
private readonly ILogger<JavaCompilationService> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public JavaCompilationService(ILogger<JavaCompilationService> logger, IConfiguration configuration)
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
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 in {WorkingDirectory} with version {Version}", workingDirectory, version ?? "latest");
|
||||
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(sourceFilePath, sourceCode);
|
||||
|
||||
// Resolve version-specific configuration
|
||||
var (compiler, compilerFlags) = ResolveVersion(version);
|
||||
|
||||
_logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, compilerFlags);
|
||||
|
||||
var arguments = string.IsNullOrWhiteSpace(compilerFlags)
|
||||
? $"\"{sourceFilePath}\""
|
||||
: $"{compilerFlags} \"{sourceFilePath}\"";
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = compiler,
|
||||
Arguments = arguments,
|
||||
WorkingDirectory = workingDirectory,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
var output = await process.StandardOutput.ReadToEndAsync();
|
||||
var error = await process.StandardError.ReadToEndAsync();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
var compilerOutput = $"{output}\n{error}".Trim();
|
||||
|
||||
if (process.ExitCode == 0 && File.Exists(classFilePath))
|
||||
{
|
||||
_logger.LogInformation("Java compilation successful");
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = true,
|
||||
ExecutablePath = classFilePath,
|
||||
CompilerOutput = compilerOutput
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Java compilation failed with exit code {ExitCode}", process.ExitCode);
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Compilation failed",
|
||||
CompilerOutput = compilerOutput
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during Java compilation");
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = $"Compilation error: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private (string compiler, string compilerFlags) ResolveVersion(string? version)
|
||||
{
|
||||
// If version is null or "latest", use default configuration
|
||||
if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var compiler = _configuration["Java:Compiler"] ?? "javac";
|
||||
var compilerFlags = _configuration["Java:CompilerFlags"] ?? "";
|
||||
return (compiler, compilerFlags);
|
||||
}
|
||||
|
||||
// Try to find version-specific configuration
|
||||
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 ?? "");
|
||||
}
|
||||
|
||||
// Version not found, use default and log warning
|
||||
_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);
|
||||
}
|
||||
}
|
||||
111
src/LiquidCode.Tester.Worker/Services/JavaExecutionService.cs
Normal file
111
src/LiquidCode.Tester.Worker/Services/JavaExecutionService.cs
Normal file
@@ -0,0 +1,111 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
|
||||
public class JavaExecutionService : IExecutionService
|
||||
{
|
||||
private readonly ILogger<JavaExecutionService> _logger;
|
||||
|
||||
public JavaExecutionService(ILogger<JavaExecutionService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ExecutionResult> ExecuteAsync(string executablePath, string inputFilePath, int timeLimitMs, int memoryLimitMb)
|
||||
{
|
||||
var workingDirectory = Path.GetDirectoryName(executablePath)!;
|
||||
_logger.LogInformation("Executing Java class in {WorkingDirectory} with input {Input}, time limit {TimeLimit}ms, memory limit {MemoryLimit}MB",
|
||||
workingDirectory, inputFilePath, timeLimitMs, memoryLimitMb);
|
||||
|
||||
var result = new ExecutionResult();
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
using var inputStream = File.OpenRead(inputFilePath);
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "java",
|
||||
Arguments = "-cp . Solution",
|
||||
WorkingDirectory = workingDirectory,
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
await inputStream.CopyToAsync(process.StandardInput.BaseStream);
|
||||
process.StandardInput.Close();
|
||||
|
||||
var outputTask = process.StandardOutput.ReadToEndAsync();
|
||||
var errorTask = process.StandardError.ReadToEndAsync();
|
||||
|
||||
var completedInTime = await Task.Run(() => process.WaitForExit(timeLimitMs));
|
||||
|
||||
stopwatch.Stop();
|
||||
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||
|
||||
if (!completedInTime)
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch { }
|
||||
|
||||
result.TimeLimitExceeded = true;
|
||||
result.ErrorMessage = "Time limit exceeded";
|
||||
_logger.LogWarning("Execution exceeded time limit");
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Output = await outputTask;
|
||||
result.ErrorOutput = await errorTask;
|
||||
result.ExitCode = process.ExitCode;
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
result.RuntimeError = true;
|
||||
result.ErrorMessage = $"Runtime error (exit code {process.ExitCode})";
|
||||
_logger.LogWarning("Runtime error with exit code {ExitCode}", process.ExitCode);
|
||||
return result;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result.MemoryUsedMb = process.PeakWorkingSet64 / (1024 * 1024);
|
||||
if (result.MemoryUsedMb > memoryLimitMb)
|
||||
{
|
||||
result.MemoryLimitExceeded = true;
|
||||
result.ErrorMessage = "Memory limit exceeded";
|
||||
_logger.LogWarning("Memory limit exceeded: {Used}MB > {Limit}MB", result.MemoryUsedMb, memoryLimitMb);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not measure memory usage");
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
_logger.LogInformation("Execution completed successfully in {Time}ms", result.ExecutionTimeMs);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||
result.RuntimeError = true;
|
||||
result.ErrorMessage = $"Execution error: {ex.Message}";
|
||||
_logger.LogError(ex, "Error during execution");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
using System.Diagnostics;
|
||||
using LiquidCode.Tester.Worker.Models;
|
||||
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
|
||||
public class KotlinCompilationService : ICompilationService
|
||||
{
|
||||
private readonly ILogger<KotlinCompilationService> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public KotlinCompilationService(ILogger<KotlinCompilationService> logger, IConfiguration configuration)
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
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 in {WorkingDirectory} with version {Version}", workingDirectory, version ?? "latest");
|
||||
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(sourceFilePath, sourceCode);
|
||||
|
||||
// Resolve version-specific configuration
|
||||
var (compiler, compilerFlags) = ResolveVersion(version);
|
||||
|
||||
_logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, compilerFlags);
|
||||
|
||||
var arguments = string.IsNullOrWhiteSpace(compilerFlags)
|
||||
? $"\"{sourceFilePath}\" -include-runtime -d \"{jarFilePath}\""
|
||||
: $"{compilerFlags} \"{sourceFilePath}\" -include-runtime -d \"{jarFilePath}\"";
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = compiler,
|
||||
Arguments = arguments,
|
||||
WorkingDirectory = workingDirectory,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
var output = await process.StandardOutput.ReadToEndAsync();
|
||||
var error = await process.StandardError.ReadToEndAsync();
|
||||
|
||||
await process.WaitForExitAsync();
|
||||
|
||||
var compilerOutput = $"{output}\n{error}".Trim();
|
||||
|
||||
if (process.ExitCode == 0 && File.Exists(jarFilePath))
|
||||
{
|
||||
_logger.LogInformation("Kotlin compilation successful");
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = true,
|
||||
ExecutablePath = jarFilePath,
|
||||
CompilerOutput = compilerOutput
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Kotlin compilation failed with exit code {ExitCode}", process.ExitCode);
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = "Compilation failed",
|
||||
CompilerOutput = compilerOutput
|
||||
};
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error during Kotlin compilation");
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = $"Compilation error: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private (string compiler, string compilerFlags) ResolveVersion(string? version)
|
||||
{
|
||||
// If version is null or "latest", use default configuration
|
||||
if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var compiler = _configuration["Kotlin:Compiler"] ?? "kotlinc";
|
||||
var compilerFlags = _configuration["Kotlin:CompilerFlags"] ?? "";
|
||||
return (compiler, compilerFlags);
|
||||
}
|
||||
|
||||
// Try to find version-specific configuration
|
||||
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 ?? "");
|
||||
}
|
||||
|
||||
// Version not found, use default and log warning
|
||||
_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);
|
||||
}
|
||||
}
|
||||
110
src/LiquidCode.Tester.Worker/Services/KotlinExecutionService.cs
Normal file
110
src/LiquidCode.Tester.Worker/Services/KotlinExecutionService.cs
Normal file
@@ -0,0 +1,110 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
|
||||
public class KotlinExecutionService : IExecutionService
|
||||
{
|
||||
private readonly ILogger<KotlinExecutionService> _logger;
|
||||
|
||||
public KotlinExecutionService(ILogger<KotlinExecutionService> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task<ExecutionResult> ExecuteAsync(string executablePath, string inputFilePath, int timeLimitMs, int memoryLimitMb)
|
||||
{
|
||||
_logger.LogInformation("Executing Kotlin JAR {Executable} with input {Input}, time limit {TimeLimit}ms, memory limit {MemoryLimit}MB",
|
||||
executablePath, inputFilePath, timeLimitMs, memoryLimitMb);
|
||||
|
||||
var result = new ExecutionResult();
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
using var inputStream = File.OpenRead(inputFilePath);
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "java",
|
||||
Arguments = $"-jar \"{executablePath}\"",
|
||||
WorkingDirectory = Path.GetDirectoryName(executablePath),
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
await inputStream.CopyToAsync(process.StandardInput.BaseStream);
|
||||
process.StandardInput.Close();
|
||||
|
||||
var outputTask = process.StandardOutput.ReadToEndAsync();
|
||||
var errorTask = process.StandardError.ReadToEndAsync();
|
||||
|
||||
var completedInTime = await Task.Run(() => process.WaitForExit(timeLimitMs));
|
||||
|
||||
stopwatch.Stop();
|
||||
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||
|
||||
if (!completedInTime)
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch { }
|
||||
|
||||
result.TimeLimitExceeded = true;
|
||||
result.ErrorMessage = "Time limit exceeded";
|
||||
_logger.LogWarning("Execution exceeded time limit");
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Output = await outputTask;
|
||||
result.ErrorOutput = await errorTask;
|
||||
result.ExitCode = process.ExitCode;
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
result.RuntimeError = true;
|
||||
result.ErrorMessage = $"Runtime error (exit code {process.ExitCode})";
|
||||
_logger.LogWarning("Runtime error with exit code {ExitCode}", process.ExitCode);
|
||||
return result;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result.MemoryUsedMb = process.PeakWorkingSet64 / (1024 * 1024);
|
||||
if (result.MemoryUsedMb > memoryLimitMb)
|
||||
{
|
||||
result.MemoryLimitExceeded = true;
|
||||
result.ErrorMessage = "Memory limit exceeded";
|
||||
_logger.LogWarning("Memory limit exceeded: {Used}MB > {Limit}MB", result.MemoryUsedMb, memoryLimitMb);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not measure memory usage");
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
_logger.LogInformation("Execution completed successfully in {Time}ms", result.ExecutionTimeMs);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||
result.RuntimeError = true;
|
||||
result.ErrorMessage = $"Execution error: {ex.Message}";
|
||||
_logger.LogError(ex, "Error during execution");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using LiquidCode.Tester.Worker.Models;
|
||||
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
|
||||
public class PythonCompilationService : ICompilationService
|
||||
{
|
||||
private readonly ILogger<PythonCompilationService> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public PythonCompilationService(ILogger<PythonCompilationService> logger, IConfiguration configuration)
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public async Task<CompilationResult> CompileAsync(string sourceCode, string workingDirectory, string? version = null)
|
||||
{
|
||||
var sourceFilePath = Path.Combine(workingDirectory, "solution.py");
|
||||
|
||||
_logger.LogInformation("Preparing Python code in {WorkingDirectory} with version {Version}", workingDirectory, version ?? "latest");
|
||||
|
||||
try
|
||||
{
|
||||
await File.WriteAllTextAsync(sourceFilePath, sourceCode);
|
||||
|
||||
// Resolve version-specific executable (for execution service)
|
||||
var executable = ResolveVersion(version);
|
||||
|
||||
_logger.LogDebug("Using Python executable: {Executable}", executable);
|
||||
_logger.LogInformation("Python code prepared successfully (no compilation needed)");
|
||||
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = true,
|
||||
ExecutablePath = sourceFilePath,
|
||||
CompilerOutput = $"Python is an interpreted language - no compilation needed (using {executable})"
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error preparing Python code");
|
||||
return new CompilationResult
|
||||
{
|
||||
Success = false,
|
||||
ErrorMessage = $"Error preparing code: {ex.Message}"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private string ResolveVersion(string? version)
|
||||
{
|
||||
// If version is null or "latest", use default configuration
|
||||
if (string.IsNullOrEmpty(version) || version.Equals("latest", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var executable = _configuration["Python:Executable"] ?? "python3";
|
||||
return executable;
|
||||
}
|
||||
|
||||
// Try to find version-specific configuration
|
||||
var versionKey = $"Python:Versions:{version}";
|
||||
var versionExecutable = _configuration[$"{versionKey}:Executable"];
|
||||
|
||||
if (!string.IsNullOrEmpty(versionExecutable))
|
||||
{
|
||||
_logger.LogInformation("Using Python version {Version} configuration", version);
|
||||
return versionExecutable;
|
||||
}
|
||||
|
||||
// Version not found, use default and log warning
|
||||
_logger.LogWarning("Python version {Version} not found in configuration, using default", version);
|
||||
var defaultExecutable = _configuration["Python:Executable"] ?? "python3";
|
||||
return defaultExecutable;
|
||||
}
|
||||
}
|
||||
114
src/LiquidCode.Tester.Worker/Services/PythonExecutionService.cs
Normal file
114
src/LiquidCode.Tester.Worker/Services/PythonExecutionService.cs
Normal file
@@ -0,0 +1,114 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace LiquidCode.Tester.Worker.Services;
|
||||
|
||||
public class PythonExecutionService : IExecutionService
|
||||
{
|
||||
private readonly ILogger<PythonExecutionService> _logger;
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
public PythonExecutionService(ILogger<PythonExecutionService> logger, IConfiguration configuration)
|
||||
{
|
||||
_logger = logger;
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
public async Task<ExecutionResult> ExecuteAsync(string executablePath, string inputFilePath, int timeLimitMs, int memoryLimitMb)
|
||||
{
|
||||
_logger.LogInformation("Executing Python script {Executable} with input {Input}, time limit {TimeLimit}ms, memory limit {MemoryLimit}MB",
|
||||
executablePath, inputFilePath, timeLimitMs, memoryLimitMb);
|
||||
|
||||
var result = new ExecutionResult();
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
try
|
||||
{
|
||||
using var inputStream = File.OpenRead(inputFilePath);
|
||||
|
||||
var pythonExecutable = _configuration["Python:Executable"] ?? "python3";
|
||||
|
||||
var process = new Process
|
||||
{
|
||||
StartInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = pythonExecutable,
|
||||
Arguments = $"\"{executablePath}\"",
|
||||
WorkingDirectory = Path.GetDirectoryName(executablePath),
|
||||
RedirectStandardInput = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
}
|
||||
};
|
||||
|
||||
process.Start();
|
||||
|
||||
await inputStream.CopyToAsync(process.StandardInput.BaseStream);
|
||||
process.StandardInput.Close();
|
||||
|
||||
var outputTask = process.StandardOutput.ReadToEndAsync();
|
||||
var errorTask = process.StandardError.ReadToEndAsync();
|
||||
|
||||
var completedInTime = await Task.Run(() => process.WaitForExit(timeLimitMs));
|
||||
|
||||
stopwatch.Stop();
|
||||
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||
|
||||
if (!completedInTime)
|
||||
{
|
||||
try
|
||||
{
|
||||
process.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch { }
|
||||
|
||||
result.TimeLimitExceeded = true;
|
||||
result.ErrorMessage = "Time limit exceeded";
|
||||
_logger.LogWarning("Execution exceeded time limit");
|
||||
return result;
|
||||
}
|
||||
|
||||
result.Output = await outputTask;
|
||||
result.ErrorOutput = await errorTask;
|
||||
result.ExitCode = process.ExitCode;
|
||||
|
||||
if (process.ExitCode != 0)
|
||||
{
|
||||
result.RuntimeError = true;
|
||||
result.ErrorMessage = $"Runtime error (exit code {process.ExitCode})";
|
||||
_logger.LogWarning("Runtime error with exit code {ExitCode}", process.ExitCode);
|
||||
return result;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
result.MemoryUsedMb = process.PeakWorkingSet64 / (1024 * 1024);
|
||||
if (result.MemoryUsedMb > memoryLimitMb)
|
||||
{
|
||||
result.MemoryLimitExceeded = true;
|
||||
result.ErrorMessage = "Memory limit exceeded";
|
||||
_logger.LogWarning("Memory limit exceeded: {Used}MB > {Limit}MB", result.MemoryUsedMb, memoryLimitMb);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not measure memory usage");
|
||||
}
|
||||
|
||||
result.Success = true;
|
||||
_logger.LogInformation("Execution completed successfully in {Time}ms", result.ExecutionTimeMs);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
result.ExecutionTimeMs = stopwatch.ElapsedMilliseconds;
|
||||
result.RuntimeError = true;
|
||||
result.ErrorMessage = $"Execution error: {ex.Message}";
|
||||
_logger.LogError(ex, "Error during execution");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -6,23 +6,23 @@ namespace LiquidCode.Tester.Worker.Services;
|
||||
public class TestingService : ITestingService
|
||||
{
|
||||
private readonly IPackageParserService _packageParser;
|
||||
private readonly ICompilationService _compilationService;
|
||||
private readonly IExecutionService _executionService;
|
||||
private readonly ICompilationServiceFactory _compilationServiceFactory;
|
||||
private readonly IExecutionServiceFactory _executionServiceFactory;
|
||||
private readonly IOutputCheckerService _outputChecker;
|
||||
private readonly ICallbackService _callbackService;
|
||||
private readonly ILogger<TestingService> _logger;
|
||||
|
||||
public TestingService(
|
||||
IPackageParserService packageParser,
|
||||
ICompilationService compilationService,
|
||||
IExecutionService executionService,
|
||||
ICompilationServiceFactory compilationServiceFactory,
|
||||
IExecutionServiceFactory executionServiceFactory,
|
||||
IOutputCheckerService outputChecker,
|
||||
ICallbackService callbackService,
|
||||
ILogger<TestingService> logger)
|
||||
{
|
||||
_packageParser = packageParser;
|
||||
_compilationService = compilationService;
|
||||
_executionService = executionService;
|
||||
_compilationServiceFactory = compilationServiceFactory;
|
||||
_executionServiceFactory = executionServiceFactory;
|
||||
_outputChecker = outputChecker;
|
||||
_callbackService = callbackService;
|
||||
_logger = logger;
|
||||
@@ -39,8 +39,15 @@ public class TestingService : ITestingService
|
||||
|
||||
// Parse package
|
||||
ProblemPackage package;
|
||||
if (request.Package != null)
|
||||
if (!string.IsNullOrEmpty(request.PackageFilePath))
|
||||
{
|
||||
// Use saved file path (from background task)
|
||||
await using var fileStream = File.OpenRead(request.PackageFilePath);
|
||||
package = await _packageParser.ParsePackageAsync(fileStream);
|
||||
}
|
||||
else if (request.Package != null)
|
||||
{
|
||||
// Use IFormFile directly (should not happen in background tasks)
|
||||
using var packageStream = request.Package.OpenReadStream();
|
||||
package = await _packageParser.ParsePackageAsync(packageStream);
|
||||
}
|
||||
@@ -54,8 +61,12 @@ public class TestingService : ITestingService
|
||||
// Send compiling status
|
||||
await SendStatusAsync(request, State.Compiling, ErrorCode.None, "Compiling solution", 0, package.TestCases.Count);
|
||||
|
||||
// Get language-specific services
|
||||
var compilationService = _compilationServiceFactory.GetCompilationService(request.Language);
|
||||
var executionService = _executionServiceFactory.GetExecutionService(request.Language);
|
||||
|
||||
// Compile user solution
|
||||
var compilationResult = await _compilationService.CompileAsync(request.SourceCode, package.WorkingDirectory);
|
||||
var compilationResult = await compilationService.CompileAsync(request.SourceCode, package.WorkingDirectory, request.LanguageVersion);
|
||||
|
||||
if (!compilationResult.Success)
|
||||
{
|
||||
@@ -80,7 +91,7 @@ public class TestingService : ITestingService
|
||||
$"Running test {testCase.Number}", testCase.Number, package.TestCases.Count);
|
||||
|
||||
// Execute solution
|
||||
var executionResult = await _executionService.ExecuteAsync(
|
||||
var executionResult = await executionService.ExecuteAsync(
|
||||
compilationResult.ExecutablePath!,
|
||||
testCase.InputFilePath,
|
||||
testCase.TimeLimit,
|
||||
|
||||
@@ -8,6 +8,83 @@
|
||||
"AllowedHosts": "*",
|
||||
"Cpp": {
|
||||
"Compiler": "g++",
|
||||
"CompilerFlags": "-O2 -std=c++17 -Wall"
|
||||
"CompilerFlags": "-O2 -std=c++17 -Wall",
|
||||
"Versions": {
|
||||
"14": {
|
||||
"Compiler": "g++",
|
||||
"CompilerFlags": "-O2 -std=c++14 -Wall"
|
||||
},
|
||||
"17": {
|
||||
"Compiler": "g++",
|
||||
"CompilerFlags": "-O2 -std=c++17 -Wall"
|
||||
},
|
||||
"20": {
|
||||
"Compiler": "g++",
|
||||
"CompilerFlags": "-O2 -std=c++20 -Wall"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Java": {
|
||||
"Compiler": "javac",
|
||||
"CompilerFlags": "",
|
||||
"Versions": {
|
||||
"8": {
|
||||
"Compiler": "javac",
|
||||
"CompilerFlags": "-source 8 -target 8"
|
||||
},
|
||||
"11": {
|
||||
"Compiler": "javac",
|
||||
"CompilerFlags": "-source 11 -target 11"
|
||||
},
|
||||
"17": {
|
||||
"Compiler": "javac",
|
||||
"CompilerFlags": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"Kotlin": {
|
||||
"Compiler": "kotlinc",
|
||||
"CompilerFlags": "",
|
||||
"Versions": {
|
||||
"1.9": {
|
||||
"Compiler": "kotlinc",
|
||||
"CompilerFlags": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"CSharp": {
|
||||
"Compiler": "csc",
|
||||
"CompilerFlags": "/optimize+",
|
||||
"Versions": {
|
||||
"7": {
|
||||
"Compiler": "csc",
|
||||
"CompilerFlags": "/optimize+ /langversion:7"
|
||||
},
|
||||
"8": {
|
||||
"Compiler": "csc",
|
||||
"CompilerFlags": "/optimize+ /langversion:8"
|
||||
},
|
||||
"9": {
|
||||
"Compiler": "csc",
|
||||
"CompilerFlags": "/optimize+ /langversion:9"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Python": {
|
||||
"Executable": "python3",
|
||||
"Versions": {
|
||||
"3.8": {
|
||||
"Executable": "python3.8"
|
||||
},
|
||||
"3.9": {
|
||||
"Executable": "python3.9"
|
||||
},
|
||||
"3.10": {
|
||||
"Executable": "python3.10"
|
||||
},
|
||||
"3.11": {
|
||||
"Executable": "python3.11"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user