Compare commits

..

1 Commits

Author SHA1 Message Date
prixod
dfbb01757c update test endpoint 2025-11-05 21:43:24 +04:00
16 changed files with 89 additions and 6540 deletions

View File

@@ -7,5 +7,7 @@ public record SubmitForTesterModel(
string LanguageVersion,
string SourceCode,
string PackageUrl,
string CallbackUrl
string CallbackUrl,
int? TimeLimitMs = null,
int? MemoryLimitMb = null
);

View File

@@ -82,7 +82,9 @@ public class TesterController : ControllerBase
LanguageVersion: request.LanguageVersion,
SourceCode: request.SourceCode,
PackageUrl: packagePath, // Use local path instead of URL
CallbackUrl: request.CallbackUrl
CallbackUrl: request.CallbackUrl,
TimeLimitMs: request.TimeLimitMs,
MemoryLimitMb: request.MemoryLimitMb
);
// Send to appropriate worker based on language

View File

@@ -9,4 +9,14 @@ public class LocalSubmitModel
public string SourceCode { get; set; } = string.Empty;
public string CallbackUrl { get; set; } = string.Empty;
public IFormFile? Package { get; set; }
/// <summary>
/// Optional time limit override in milliseconds (for testing purposes)
/// </summary>
public int? TimeLimitMs { get; set; }
/// <summary>
/// Optional memory limit override in megabytes (for testing purposes)
/// </summary>
public int? MemoryLimitMb { get; set; }
}

View File

@@ -38,6 +38,16 @@ public class WorkerClientService : IWorkerClientService
form.Add(new StringContent(submit.SourceCode), "SourceCode");
form.Add(new StringContent(submit.CallbackUrl), "CallbackUrl");
// Add optional limit overrides (for testing purposes)
if (submit.TimeLimitMs.HasValue)
{
form.Add(new StringContent(submit.TimeLimitMs.Value.ToString()), "TimeLimitMs");
}
if (submit.MemoryLimitMb.HasValue)
{
form.Add(new StringContent(submit.MemoryLimitMb.Value.ToString()), "MemoryLimitMb");
}
// Add package file
var fileStream = File.OpenRead(packagePath);
var fileContent = new StreamContent(fileStream);
@@ -78,10 +88,10 @@ public class WorkerClientService : IWorkerClientService
{
var workerUrl = language.ToLowerInvariant() switch
{
"c++" or "cpp" => _configuration["Workers:Cpp"],
"c++" => _configuration["Workers:Cpp"],
"java" => _configuration["Workers:Java"],
"kotlin" => _configuration["Workers:Kotlin"],
"c#" or "csharp" => _configuration["Workers:CSharp"],
"c#" => _configuration["Workers:CSharp"],
"python" => _configuration["Workers:Python"],
_ => throw new NotSupportedException($"Language {language} is not supported")
};

View File

@@ -51,7 +51,9 @@ public class TestController : ControllerBase
SourceCode = request.SourceCode,
CallbackUrl = request.CallbackUrl,
Package = null, // Will use file path instead
PackageFilePath = packageFilePath
PackageFilePath = packageFilePath,
TimeLimitMs = request.TimeLimitMs,
MemoryLimitMb = request.MemoryLimitMb
};
// Start testing in background
@@ -109,4 +111,14 @@ public class TestRequest
public string CallbackUrl { get; set; } = string.Empty;
public IFormFile? Package { get; set; }
public string? PackageFilePath { get; set; } // Internal use - path to saved package file
/// <summary>
/// Optional time limit override in milliseconds (for testing purposes)
/// </summary>
public int? TimeLimitMs { get; set; }
/// <summary>
/// Optional memory limit override in megabytes (for testing purposes)
/// </summary>
public int? MemoryLimitMb { get; set; }
}

View File

@@ -87,10 +87,10 @@ RUN useradd -m -u 1001 -s /bin/bash workeruser && \
chmod 755 /var/local/lib/isolate && \
chown -R workeruser:workeruser /var/local/lib/isolate
# Configure isolate directories and control-group root
RUN printf "box_root = /var/local/lib/isolate\nlock_root = /run/isolate/locks\ncg_root = /sys/fs/cgroup\nfirst_uid = 60000\nfirst_gid = 60000\nnum_boxes = 1000\n" > /usr/local/etc/isolate.conf && \
ln -sf /usr/local/etc/isolate.conf /usr/local/etc/isolate && \
mkdir -p /run/isolate/locks
# Configure isolate
RUN echo "cg_root = /sys/fs/cgroup" > /usr/local/etc/isolate && \
echo "cg_enable = 1" >> /usr/local/etc/isolate && \
echo "box_root = /var/local/lib/isolate" >> /usr/local/etc/isolate
# Copy published app
COPY --from=publish /app/publish .

View File

@@ -61,15 +61,8 @@ public class CSharpExecutionServiceIsolate : IExecutionService
});
chmodProcess?.WaitForExit();
// Prepare input/output files inside the sandbox
// Prepare output file in box
var outputFilePath = Path.Combine(boxDir, "output.txt");
string? sandboxInputPath = null;
if (!string.IsNullOrEmpty(inputFilePath) && File.Exists(inputFilePath))
{
sandboxInputPath = Path.Combine(boxDir, "input.txt");
File.Copy(inputFilePath, sandboxInputPath, overwrite: true);
}
// Run in Isolate
var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions
@@ -82,7 +75,7 @@ public class CSharpExecutionServiceIsolate : IExecutionService
StackLimitKb = 256 * 1024,
ProcessLimit = 1, // Single process for C#
EnableNetwork = false,
StdinFile = sandboxInputPath,
StdinFile = inputFilePath,
StdoutFile = outputFilePath,
WorkingDirectory = "/box"
});

View File

@@ -33,14 +33,6 @@ public class CallbackService : ICallbackService
var content = new StringContent(json, Encoding.UTF8, "application/json");
var httpResponse = await httpClient.PostAsync(callbackUrl, content);
if (!httpResponse.IsSuccessStatusCode)
{
var responseBody = await httpResponse.Content.ReadAsStringAsync();
_logger.LogWarning("Callback returned non-success status {StatusCode} with body: {Body}",
(int)httpResponse.StatusCode, Truncate(responseBody, 2048));
}
httpResponse.EnsureSuccessStatusCode();
_logger.LogInformation("Status update sent successfully");
@@ -52,16 +44,6 @@ public class CallbackService : ICallbackService
}
}
private static string Truncate(string value, int maxLength)
{
if (string.IsNullOrEmpty(value) || value.Length <= maxLength)
{
return value;
}
return value.Substring(0, maxLength) + "…";
}
private bool IsLogCallback(string callbackUrl)
{
if (string.IsNullOrWhiteSpace(callbackUrl))

View File

@@ -1,11 +1,8 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using LiquidCode.Tester.Worker.Services.Isolate;
using LiquidCode.Tester.Worker.Models;
/// <summary>
namespace LiquidCode.Tester.Worker.Services;
/// <summary>
@@ -88,58 +85,19 @@ public class CppCompilationServiceIsolate : ICompilationService
File.Copy(sourceFilePath, boxSourcePath, overwrite: true);
// Copy common headers from the source directory (e.g., testlib.h)
var sourceDirectory = Path.GetDirectoryName(sourceFilePath);
if (!string.IsNullOrEmpty(sourceDirectory) && Directory.Exists(sourceDirectory))
{
foreach (var header in Directory.EnumerateFiles(sourceDirectory))
{
if (string.Equals(header, sourceFilePath, StringComparison.OrdinalIgnoreCase))
{
continue;
}
var extension = Path.GetExtension(header);
if (extension is ".h" or ".hpp" or ".hh" or ".hxx" or ".h++" or ".inl" or ".tcc" )
{
var destination = Path.Combine(boxDir, Path.GetFileName(header));
File.Copy(header, destination, overwrite: true);
}
}
}
// Resolve compiler and flags
var (compiler, compilerFlags) = ResolveVersion(version);
_logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, string.Join(' ', compilerFlags));
// Build compiler arguments
var arguments = new List<string>(compilerFlags);
var includeCounter = 0;
// Add include directories
if (includeDirectories != null)
{
foreach (var includeDir in includeDirectories.Where(d => !string.IsNullOrWhiteSpace(d)))
{
var resolvedIncludeDir = includeDir;
if (!Path.IsPathRooted(resolvedIncludeDir))
{
var baseDir = sourceDirectory ?? Directory.GetCurrentDirectory();
resolvedIncludeDir = Path.GetFullPath(Path.Combine(baseDir, includeDir));
}
if (Directory.Exists(resolvedIncludeDir))
{
includeCounter++;
var targetIncludeDir = Path.Combine(boxDir, $"include_{includeCounter}");
CopyDirectory(resolvedIncludeDir, targetIncludeDir);
arguments.Add($"-I/box/include_{includeCounter}");
}
else
{
arguments.Add($"-I{includeDir}");
}
arguments.Add($"-I{includeDir}");
}
}
@@ -160,25 +118,8 @@ public class CppCompilationServiceIsolate : ICompilationService
var stderrFilePath = Path.Combine(boxDir, "compile_stderr.txt");
// Run compiler in Isolate
// Bind the system toolchain directories read-only so the linker and headers remain reachable
var directoryBindings = new List<DirectoryBinding>
{
new DirectoryBinding { HostPath = "/usr/include", SandboxPath = "/usr/include", ReadOnly = true },
new DirectoryBinding { HostPath = "/usr/bin", SandboxPath = "/usr/bin", ReadOnly = true },
new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true },
new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true }
};
if (Directory.Exists("/bin"))
{
directoryBindings.Add(new DirectoryBinding { HostPath = "/bin", SandboxPath = "/bin", ReadOnly = true });
}
if (Directory.Exists("/usr/local/bin"))
{
directoryBindings.Add(new DirectoryBinding { HostPath = "/usr/local/bin", SandboxPath = "/usr/local/bin", ReadOnly = true });
}
// 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,
@@ -192,10 +133,11 @@ public class CppCompilationServiceIsolate : ICompilationService
EnableNetwork = false,
StderrFile = stderrFilePath,
WorkingDirectory = "/box",
DirectoryBindings = directoryBindings,
EnvironmentVariables = new Dictionary<string, string>
DirectoryBindings = new List<DirectoryBinding>
{
["PATH"] = GetSandboxPath()
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 }
}
});
@@ -289,55 +231,6 @@ public class CppCompilationServiceIsolate : ICompilationService
}
}
private static void CopyDirectory(string sourceDirectory, string destinationDirectory)
{
var sourceRoot = Path.GetFullPath(sourceDirectory);
var destinationRoot = Path.GetFullPath(destinationDirectory);
Directory.CreateDirectory(destinationRoot);
foreach (var directory in Directory.EnumerateDirectories(sourceRoot, "*", SearchOption.AllDirectories))
{
var relativePath = Path.GetRelativePath(sourceRoot, directory);
var targetDir = Path.Combine(destinationRoot, relativePath);
Directory.CreateDirectory(targetDir);
}
foreach (var file in Directory.EnumerateFiles(sourceRoot, "*", SearchOption.AllDirectories))
{
var relativePath = Path.GetRelativePath(sourceRoot, file);
var targetFile = Path.Combine(destinationRoot, relativePath);
Directory.CreateDirectory(Path.GetDirectoryName(targetFile)!);
File.Copy(file, targetFile, overwrite: true);
}
}
private static string GetSandboxPath()
{
var defaultPaths = new[] { "/usr/local/bin", "/usr/bin", "/bin" };
var hostPath = Environment.GetEnvironmentVariable("PATH");
if (string.IsNullOrWhiteSpace(hostPath))
{
return string.Join(':', defaultPaths);
}
var segments = hostPath
.Split(':', StringSplitOptions.RemoveEmptyEntries)
.Where(path => path.StartsWith("/usr", StringComparison.Ordinal) || path.StartsWith("/bin", StringComparison.Ordinal))
.ToList();
foreach (var defaultPath in defaultPaths)
{
if (!segments.Contains(defaultPath))
{
segments.Add(defaultPath);
}
}
return segments.Count == 0 ? string.Join(':', defaultPaths) : string.Join(':', segments);
}
private (string compiler, List<string> compilerFlags) ResolveVersion(string? version)
{
var defaultCompiler = _configuration["Cpp:Compiler"] ?? "g++";

View File

@@ -61,15 +61,8 @@ public class CppExecutionServiceIsolate : IExecutionService
});
chmodProcess?.WaitForExit();
// Prepare input/output files inside the sandbox
// Prepare output file in box
var outputFilePath = Path.Combine(boxDir, "output.txt");
string? sandboxInputPath = null;
if (!string.IsNullOrEmpty(inputFilePath) && File.Exists(inputFilePath))
{
sandboxInputPath = Path.Combine(boxDir, "input.txt");
File.Copy(inputFilePath, sandboxInputPath, overwrite: true);
}
// Run in Isolate
var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions
@@ -82,7 +75,7 @@ public class CppExecutionServiceIsolate : IExecutionService
StackLimitKb = 256 * 1024, // 256 MB stack
ProcessLimit = 1, // Single process only
EnableNetwork = false, // No network access
StdinFile = sandboxInputPath,
StdinFile = inputFilePath,
StdoutFile = outputFilePath,
WorkingDirectory = "/box"
});

View File

@@ -1,4 +1,3 @@
using System;
using System.Diagnostics;
using System.Text;
@@ -25,7 +24,7 @@ public class IsolateService
{
_logger.LogDebug("Initializing isolate box {BoxId}", boxId);
var result = await RunIsolateCommandAsync($"--box-id={boxId} --cg --init");
var result = await RunIsolateCommandAsync($"--box-id={boxId} --init");
if (result.ExitCode != 0)
{
@@ -91,7 +90,7 @@ public class IsolateService
{
_logger.LogDebug("Cleaning up isolate box {BoxId}", boxId);
var result = await RunIsolateCommandAsync($"--box-id={boxId} --cg --cleanup");
var result = await RunIsolateCommandAsync($"--box-id={boxId} --cleanup");
if (result.ExitCode != 0)
{
@@ -158,17 +157,17 @@ public class IsolateService
// I/O redirection
if (!string.IsNullOrEmpty(options.StdinFile))
{
args.Add($"--stdin={MapSandboxPath(options.StdinFile, options.BoxId)}");
args.Add($"--stdin={options.StdinFile}");
}
if (!string.IsNullOrEmpty(options.StdoutFile))
{
args.Add($"--stdout={MapSandboxPath(options.StdoutFile, options.BoxId)}");
args.Add($"--stdout={options.StdoutFile}");
}
if (!string.IsNullOrEmpty(options.StderrFile))
{
args.Add($"--stderr={MapSandboxPath(options.StderrFile, options.BoxId)}");
args.Add($"--stderr={options.StderrFile}");
}
// Working directory
@@ -183,7 +182,7 @@ public class IsolateService
foreach (var binding in options.DirectoryBindings)
{
var dirSpec = binding.ReadOnly
? $"--dir={binding.HostPath}={binding.SandboxPath}"
? $"--dir={binding.HostPath}={binding.SandboxPath}:ro"
: $"--dir={binding.HostPath}={binding.SandboxPath}:rw";
args.Add(dirSpec);
}
@@ -211,25 +210,6 @@ public class IsolateService
return string.Join(" ", args);
}
private static string MapSandboxPath(string path, int boxId)
{
if (string.IsNullOrEmpty(path))
{
return path;
}
var normalizedPath = Path.GetFullPath(path);
var boxRoot = Path.GetFullPath($"/var/local/lib/isolate/{boxId}/box");
if (normalizedPath.StartsWith(boxRoot, StringComparison.Ordinal))
{
var relative = normalizedPath.Substring(boxRoot.Length).TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
return relative.Length == 0 ? "/box" : $"/box/{relative.Replace(Path.DirectorySeparatorChar, '/')}";
}
return normalizedPath;
}
/// <summary>
/// Parse isolate metadata file
/// </summary>
@@ -287,6 +267,9 @@ public class IsolateService
case "status":
result.Status = value;
result.TimeLimitExceeded = value == "TO";
result.MemoryLimitExceeded = value == "XX" || value == "MLE";
result.RuntimeError = value == "RE" || value == "SG";
break;
case "message":
@@ -299,10 +282,7 @@ public class IsolateService
case "cg-oom-killed":
result.CgroupOomKilled = value == "1";
if (result.CgroupOomKilled)
{
result.MemoryLimitExceeded = true;
}
result.MemoryLimitExceeded = true;
break;
case "csw-voluntary":
@@ -317,29 +297,6 @@ public class IsolateService
}
}
// Derive status-related flags after parsing all metadata
switch (result.Status)
{
case "TO":
result.TimeLimitExceeded = true;
break;
case "RE":
case "SG":
result.RuntimeError = true;
break;
case "XX":
// Internal error reported by isolate
result.RuntimeError = true;
break;
}
if (!result.MemoryLimitExceeded &&
!string.IsNullOrEmpty(result.Message) &&
result.Message.Contains("memory limit", StringComparison.OrdinalIgnoreCase))
{
result.MemoryLimitExceeded = true;
}
return result;
}

View File

@@ -57,15 +57,8 @@ public class JavaExecutionServiceIsolate : IExecutionService
File.Copy(file, destPath, overwrite: true);
}
// Prepare input/output files inside the sandbox
// Prepare output file in box
var outputFilePath = Path.Combine(boxDir, "output.txt");
string? sandboxInputPath = null;
if (!string.IsNullOrEmpty(inputFilePath) && File.Exists(inputFilePath))
{
sandboxInputPath = Path.Combine(boxDir, "input.txt");
File.Copy(inputFilePath, sandboxInputPath, overwrite: true);
}
// Run in Isolate
// Note: Java needs more memory for JVM overhead
@@ -82,7 +75,7 @@ public class JavaExecutionServiceIsolate : IExecutionService
StackLimitKb = 256 * 1024, // 256 MB stack
ProcessLimit = 64, // Java creates multiple threads
EnableNetwork = false,
StdinFile = sandboxInputPath,
StdinFile = inputFilePath,
StdoutFile = outputFilePath,
WorkingDirectory = "/box"
});

View File

@@ -52,15 +52,8 @@ public class KotlinExecutionServiceIsolate : IExecutionService
File.Copy(executablePath, boxJarPath, overwrite: true);
// Prepare input/output files inside the sandbox
// Prepare output file in box
var outputFilePath = Path.Combine(boxDir, "output.txt");
string? sandboxInputPath = null;
if (!string.IsNullOrEmpty(inputFilePath) && File.Exists(inputFilePath))
{
sandboxInputPath = Path.Combine(boxDir, "input.txt");
File.Copy(inputFilePath, sandboxInputPath, overwrite: true);
}
// Run in Isolate (Kotlin runs via Java)
var kotlinMemoryMb = Math.Max(memoryLimitMb, 128); // Minimum 128MB for JVM
@@ -76,7 +69,7 @@ public class KotlinExecutionServiceIsolate : IExecutionService
StackLimitKb = 256 * 1024,
ProcessLimit = 64, // JVM creates multiple threads
EnableNetwork = false,
StdinFile = sandboxInputPath,
StdinFile = inputFilePath,
StdoutFile = outputFilePath,
WorkingDirectory = "/box"
});

View File

@@ -55,15 +55,8 @@ public class PythonExecutionServiceIsolate : IExecutionService
File.Copy(executablePath, boxScriptPath, overwrite: true);
// Prepare input/output files inside the sandbox
// Prepare output file in box
var outputFilePath = Path.Combine(boxDir, "output.txt");
string? sandboxInputPath = null;
if (!string.IsNullOrEmpty(inputFilePath) && File.Exists(inputFilePath))
{
sandboxInputPath = Path.Combine(boxDir, "input.txt");
File.Copy(inputFilePath, sandboxInputPath, overwrite: true);
}
// Get Python executable from configuration
var pythonExecutable = _configuration["Python:Executable"] ?? "python3";
@@ -80,7 +73,7 @@ public class PythonExecutionServiceIsolate : IExecutionService
StackLimitKb = 256 * 1024,
ProcessLimit = 1, // Single process for Python
EnableNetwork = false,
StdinFile = sandboxInputPath,
StdinFile = inputFilePath,
StdoutFile = outputFilePath,
WorkingDirectory = "/box"
});

View File

@@ -88,6 +88,17 @@ public class TestingService : ITestingService
_logger.LogInformation("Compilation successful");
// Check for limit overrides (for testing purposes)
var timeLimitOverride = request.TimeLimitMs;
var memoryLimitOverride = request.MemoryLimitMb;
if (timeLimitOverride.HasValue || memoryLimitOverride.HasValue)
{
_logger.LogInformation("Using limit overrides - TimeLimit: {TimeLimit}ms, MemoryLimit: {MemoryLimit}MB",
timeLimitOverride?.ToString() ?? "default",
memoryLimitOverride?.ToString() ?? "default");
}
// Send testing status
await SendStatusAsync(request, State.Testing, ErrorCode.None, "Running tests", 0, package.TestCases.Count);
@@ -100,12 +111,16 @@ public class TestingService : ITestingService
await SendStatusAsync(request, State.Testing, ErrorCode.None,
$"Running test {testCase.Number}", testCase.Number, package.TestCases.Count);
// Use override limits if provided, otherwise use test case limits
var timeLimit = timeLimitOverride ?? testCase.TimeLimit;
var memoryLimit = memoryLimitOverride ?? testCase.MemoryLimit;
// Execute solution
var executionResult = await executionService.ExecuteAsync(
compilationResult.ExecutablePath!,
testCase.InputFilePath,
testCase.TimeLimit,
testCase.MemoryLimit);
timeLimit,
memoryLimit);
// Check for execution errors
if (executionResult.TimeLimitExceeded)

6299
testlib.h

File diff suppressed because it is too large Load Diff