Compare commits
15 Commits
dev
...
a6c56ecb22
| Author | SHA1 | Date | |
|---|---|---|---|
| a6c56ecb22 | |||
|
|
6d010ea9ad | ||
| bc9c162de5 | |||
| f4d855c958 | |||
| 27581c4385 | |||
| 0b29ce168e | |||
| 7f8eb875f9 | |||
| 6ed26ae29b | |||
| a8c0ec9ed3 | |||
| 24943a7c86 | |||
| 619b93b042 | |||
| ca4b6925ac | |||
| 6a23cc4c72 | |||
| 55af8257a7 | |||
| e385d6b52e |
@@ -78,10 +78,10 @@ public class WorkerClientService : IWorkerClientService
|
||||
{
|
||||
var workerUrl = language.ToLowerInvariant() switch
|
||||
{
|
||||
"c++" => _configuration["Workers:Cpp"],
|
||||
"c++" or "cpp" => _configuration["Workers:Cpp"],
|
||||
"java" => _configuration["Workers:Java"],
|
||||
"kotlin" => _configuration["Workers:Kotlin"],
|
||||
"c#" => _configuration["Workers:CSharp"],
|
||||
"c#" or "csharp" => _configuration["Workers:CSharp"],
|
||||
"python" => _configuration["Workers:Python"],
|
||||
_ => throw new NotSupportedException($"Language {language} is not supported")
|
||||
};
|
||||
|
||||
@@ -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
|
||||
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
|
||||
# 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
|
||||
|
||||
# Copy published app
|
||||
COPY --from=publish /app/publish .
|
||||
|
||||
@@ -61,8 +61,15 @@ public class CSharpExecutionServiceIsolate : IExecutionService
|
||||
});
|
||||
chmodProcess?.WaitForExit();
|
||||
|
||||
// Prepare output file in box
|
||||
// Prepare input/output files inside the sandbox
|
||||
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
|
||||
@@ -75,7 +82,7 @@ public class CSharpExecutionServiceIsolate : IExecutionService
|
||||
StackLimitKb = 256 * 1024,
|
||||
ProcessLimit = 1, // Single process for C#
|
||||
EnableNetwork = false,
|
||||
StdinFile = inputFilePath,
|
||||
StdinFile = sandboxInputPath,
|
||||
StdoutFile = outputFilePath,
|
||||
WorkingDirectory = "/box"
|
||||
});
|
||||
|
||||
@@ -33,6 +33,14 @@ 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");
|
||||
@@ -44,6 +52,16 @@ 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))
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
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>
|
||||
@@ -85,19 +88,58 @@ 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)))
|
||||
{
|
||||
arguments.Add($"-I{includeDir}");
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,8 +160,25 @@ public class CppCompilationServiceIsolate : ICompilationService
|
||||
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
|
||||
// Bind the system toolchain directories read-only so the linker and headers remain reachable
|
||||
var directoryBindings = new List<DirectoryBinding>
|
||||
{
|
||||
new DirectoryBinding { HostPath = "/usr/include", SandboxPath = "/usr/include", ReadOnly = true },
|
||||
new DirectoryBinding { HostPath = "/usr/bin", SandboxPath = "/usr/bin", ReadOnly = true },
|
||||
new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true },
|
||||
new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true }
|
||||
};
|
||||
|
||||
if (Directory.Exists("/bin"))
|
||||
{
|
||||
directoryBindings.Add(new DirectoryBinding { HostPath = "/bin", SandboxPath = "/bin", ReadOnly = true });
|
||||
}
|
||||
|
||||
if (Directory.Exists("/usr/local/bin"))
|
||||
{
|
||||
directoryBindings.Add(new DirectoryBinding { HostPath = "/usr/local/bin", SandboxPath = "/usr/local/bin", ReadOnly = true });
|
||||
}
|
||||
|
||||
var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions
|
||||
{
|
||||
BoxId = boxId,
|
||||
@@ -133,11 +192,10 @@ public class CppCompilationServiceIsolate : ICompilationService
|
||||
EnableNetwork = false,
|
||||
StderrFile = stderrFilePath,
|
||||
WorkingDirectory = "/box",
|
||||
DirectoryBindings = new List<DirectoryBinding>
|
||||
DirectoryBindings = directoryBindings,
|
||||
EnvironmentVariables = new Dictionary<string, string>
|
||||
{
|
||||
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 }
|
||||
["PATH"] = GetSandboxPath()
|
||||
}
|
||||
});
|
||||
|
||||
@@ -231,6 +289,55 @@ 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++";
|
||||
|
||||
@@ -61,8 +61,15 @@ public class CppExecutionServiceIsolate : IExecutionService
|
||||
});
|
||||
chmodProcess?.WaitForExit();
|
||||
|
||||
// Prepare output file in box
|
||||
// Prepare input/output files inside the sandbox
|
||||
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
|
||||
@@ -75,7 +82,7 @@ public class CppExecutionServiceIsolate : IExecutionService
|
||||
StackLimitKb = 256 * 1024, // 256 MB stack
|
||||
ProcessLimit = 1, // Single process only
|
||||
EnableNetwork = false, // No network access
|
||||
StdinFile = inputFilePath,
|
||||
StdinFile = sandboxInputPath,
|
||||
StdoutFile = outputFilePath,
|
||||
WorkingDirectory = "/box"
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Text;
|
||||
|
||||
@@ -24,7 +25,7 @@ public class IsolateService
|
||||
{
|
||||
_logger.LogDebug("Initializing isolate box {BoxId}", boxId);
|
||||
|
||||
var result = await RunIsolateCommandAsync($"--box-id={boxId} --init");
|
||||
var result = await RunIsolateCommandAsync($"--box-id={boxId} --cg --init");
|
||||
|
||||
if (result.ExitCode != 0)
|
||||
{
|
||||
@@ -90,7 +91,7 @@ public class IsolateService
|
||||
{
|
||||
_logger.LogDebug("Cleaning up isolate box {BoxId}", boxId);
|
||||
|
||||
var result = await RunIsolateCommandAsync($"--box-id={boxId} --cleanup");
|
||||
var result = await RunIsolateCommandAsync($"--box-id={boxId} --cg --cleanup");
|
||||
|
||||
if (result.ExitCode != 0)
|
||||
{
|
||||
@@ -157,17 +158,17 @@ public class IsolateService
|
||||
// I/O redirection
|
||||
if (!string.IsNullOrEmpty(options.StdinFile))
|
||||
{
|
||||
args.Add($"--stdin={options.StdinFile}");
|
||||
args.Add($"--stdin={MapSandboxPath(options.StdinFile, options.BoxId)}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(options.StdoutFile))
|
||||
{
|
||||
args.Add($"--stdout={options.StdoutFile}");
|
||||
args.Add($"--stdout={MapSandboxPath(options.StdoutFile, options.BoxId)}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(options.StderrFile))
|
||||
{
|
||||
args.Add($"--stderr={options.StderrFile}");
|
||||
args.Add($"--stderr={MapSandboxPath(options.StderrFile, options.BoxId)}");
|
||||
}
|
||||
|
||||
// Working directory
|
||||
@@ -182,7 +183,7 @@ public class IsolateService
|
||||
foreach (var binding in options.DirectoryBindings)
|
||||
{
|
||||
var dirSpec = binding.ReadOnly
|
||||
? $"--dir={binding.HostPath}={binding.SandboxPath}:ro"
|
||||
? $"--dir={binding.HostPath}={binding.SandboxPath}"
|
||||
: $"--dir={binding.HostPath}={binding.SandboxPath}:rw";
|
||||
args.Add(dirSpec);
|
||||
}
|
||||
@@ -210,6 +211,25 @@ 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>
|
||||
@@ -267,9 +287,6 @@ 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":
|
||||
@@ -282,7 +299,10 @@ public class IsolateService
|
||||
|
||||
case "cg-oom-killed":
|
||||
result.CgroupOomKilled = value == "1";
|
||||
result.MemoryLimitExceeded = true;
|
||||
if (result.CgroupOomKilled)
|
||||
{
|
||||
result.MemoryLimitExceeded = true;
|
||||
}
|
||||
break;
|
||||
|
||||
case "csw-voluntary":
|
||||
@@ -297,6 +317,29 @@ 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,8 +57,15 @@ public class JavaExecutionServiceIsolate : IExecutionService
|
||||
File.Copy(file, destPath, overwrite: true);
|
||||
}
|
||||
|
||||
// Prepare output file in box
|
||||
// Prepare input/output files inside the sandbox
|
||||
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
|
||||
@@ -75,7 +82,7 @@ public class JavaExecutionServiceIsolate : IExecutionService
|
||||
StackLimitKb = 256 * 1024, // 256 MB stack
|
||||
ProcessLimit = 64, // Java creates multiple threads
|
||||
EnableNetwork = false,
|
||||
StdinFile = inputFilePath,
|
||||
StdinFile = sandboxInputPath,
|
||||
StdoutFile = outputFilePath,
|
||||
WorkingDirectory = "/box"
|
||||
});
|
||||
|
||||
@@ -52,8 +52,15 @@ public class KotlinExecutionServiceIsolate : IExecutionService
|
||||
|
||||
File.Copy(executablePath, boxJarPath, overwrite: true);
|
||||
|
||||
// Prepare output file in box
|
||||
// Prepare input/output files inside the sandbox
|
||||
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
|
||||
@@ -69,7 +76,7 @@ public class KotlinExecutionServiceIsolate : IExecutionService
|
||||
StackLimitKb = 256 * 1024,
|
||||
ProcessLimit = 64, // JVM creates multiple threads
|
||||
EnableNetwork = false,
|
||||
StdinFile = inputFilePath,
|
||||
StdinFile = sandboxInputPath,
|
||||
StdoutFile = outputFilePath,
|
||||
WorkingDirectory = "/box"
|
||||
});
|
||||
|
||||
@@ -55,8 +55,15 @@ public class PythonExecutionServiceIsolate : IExecutionService
|
||||
|
||||
File.Copy(executablePath, boxScriptPath, overwrite: true);
|
||||
|
||||
// Prepare output file in box
|
||||
// Prepare input/output files inside the sandbox
|
||||
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";
|
||||
@@ -73,7 +80,7 @@ public class PythonExecutionServiceIsolate : IExecutionService
|
||||
StackLimitKb = 256 * 1024,
|
||||
ProcessLimit = 1, // Single process for Python
|
||||
EnableNetwork = false,
|
||||
StdinFile = inputFilePath,
|
||||
StdinFile = sandboxInputPath,
|
||||
StdoutFile = outputFilePath,
|
||||
WorkingDirectory = "/box"
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user