Compare commits
1 Commits
7a1f22eb8e
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfbb01757c |
14
compose.yaml
14
compose.yaml
@@ -26,7 +26,6 @@
|
|||||||
|
|
||||||
worker:
|
worker:
|
||||||
image: liquidcode-tester-worker:latest
|
image: liquidcode-tester-worker:latest
|
||||||
privileged: true
|
|
||||||
container_name: liquidcode-tester-worker
|
container_name: liquidcode-tester-worker
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
@@ -37,9 +36,16 @@
|
|||||||
- ASPNETCORE_ENVIRONMENT=Development
|
- ASPNETCORE_ENVIRONMENT=Development
|
||||||
networks:
|
networks:
|
||||||
- liquidcode-network
|
- liquidcode-network
|
||||||
# Mount cgroup for Isolate sandbox
|
# Security hardening for Worker
|
||||||
volumes:
|
security_opt:
|
||||||
- /sys/fs/cgroup:/sys/fs/cgroup:rw
|
- no-new-privileges:true
|
||||||
|
- apparmor=docker-default
|
||||||
|
cap_drop:
|
||||||
|
- ALL
|
||||||
|
cap_add:
|
||||||
|
- SYS_ADMIN # Required for Isolate namespaces
|
||||||
|
- SETUID # Required for Isolate to change user context
|
||||||
|
- SETGID # Required for Isolate to change group context
|
||||||
# Temporary filesystem for compilation and testing
|
# Temporary filesystem for compilation and testing
|
||||||
tmpfs:
|
tmpfs:
|
||||||
- /tmp:exec,size=4G
|
- /tmp:exec,size=4G
|
||||||
|
|||||||
@@ -7,5 +7,7 @@ public record SubmitForTesterModel(
|
|||||||
string LanguageVersion,
|
string LanguageVersion,
|
||||||
string SourceCode,
|
string SourceCode,
|
||||||
string PackageUrl,
|
string PackageUrl,
|
||||||
string CallbackUrl
|
string CallbackUrl,
|
||||||
|
int? TimeLimitMs = null,
|
||||||
|
int? MemoryLimitMb = null
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -82,7 +82,9 @@ public class TesterController : ControllerBase
|
|||||||
LanguageVersion: request.LanguageVersion,
|
LanguageVersion: request.LanguageVersion,
|
||||||
SourceCode: request.SourceCode,
|
SourceCode: request.SourceCode,
|
||||||
PackageUrl: packagePath, // Use local path instead of URL
|
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
|
// Send to appropriate worker based on language
|
||||||
|
|||||||
@@ -9,4 +9,14 @@ public class LocalSubmitModel
|
|||||||
public string SourceCode { get; set; } = string.Empty;
|
public string SourceCode { get; set; } = string.Empty;
|
||||||
public string CallbackUrl { get; set; } = string.Empty;
|
public string CallbackUrl { get; set; } = string.Empty;
|
||||||
public IFormFile? Package { get; set; }
|
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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,9 +35,19 @@ public class WorkerClientService : IWorkerClientService
|
|||||||
form.Add(new StringContent(submit.MissionId.ToString()), "MissionId");
|
form.Add(new StringContent(submit.MissionId.ToString()), "MissionId");
|
||||||
form.Add(new StringContent(submit.Language), "Language");
|
form.Add(new StringContent(submit.Language), "Language");
|
||||||
form.Add(new StringContent(submit.LanguageVersion), "LanguageVersion");
|
form.Add(new StringContent(submit.LanguageVersion), "LanguageVersion");
|
||||||
form.Add(new StringContent(submit.SourceCode, System.Text.Encoding.UTF8, "text/plain"), "SourceCode");
|
form.Add(new StringContent(submit.SourceCode), "SourceCode");
|
||||||
form.Add(new StringContent(submit.CallbackUrl), "CallbackUrl");
|
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
|
// Add package file
|
||||||
var fileStream = File.OpenRead(packagePath);
|
var fileStream = File.OpenRead(packagePath);
|
||||||
var fileContent = new StreamContent(fileStream);
|
var fileContent = new StreamContent(fileStream);
|
||||||
@@ -78,10 +88,10 @@ public class WorkerClientService : IWorkerClientService
|
|||||||
{
|
{
|
||||||
var workerUrl = language.ToLowerInvariant() switch
|
var workerUrl = language.ToLowerInvariant() switch
|
||||||
{
|
{
|
||||||
"c++" or "cpp" => _configuration["Workers:Cpp"],
|
"c++" => _configuration["Workers:Cpp"],
|
||||||
"java" => _configuration["Workers:Java"],
|
"java" => _configuration["Workers:Java"],
|
||||||
"kotlin" => _configuration["Workers:Kotlin"],
|
"kotlin" => _configuration["Workers:Kotlin"],
|
||||||
"c#" or "csharp" => _configuration["Workers:CSharp"],
|
"c#" => _configuration["Workers:CSharp"],
|
||||||
"python" => _configuration["Workers:Python"],
|
"python" => _configuration["Workers:Python"],
|
||||||
_ => throw new NotSupportedException($"Language {language} is not supported")
|
_ => throw new NotSupportedException($"Language {language} is not supported")
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ public class TestController : ControllerBase
|
|||||||
SourceCode = request.SourceCode,
|
SourceCode = request.SourceCode,
|
||||||
CallbackUrl = request.CallbackUrl,
|
CallbackUrl = request.CallbackUrl,
|
||||||
Package = null, // Will use file path instead
|
Package = null, // Will use file path instead
|
||||||
PackageFilePath = packageFilePath
|
PackageFilePath = packageFilePath,
|
||||||
|
TimeLimitMs = request.TimeLimitMs,
|
||||||
|
MemoryLimitMb = request.MemoryLimitMb
|
||||||
};
|
};
|
||||||
|
|
||||||
// Start testing in background
|
// Start testing in background
|
||||||
@@ -109,4 +111,14 @@ public class TestRequest
|
|||||||
public string CallbackUrl { get; set; } = string.Empty;
|
public string CallbackUrl { get; set; } = string.Empty;
|
||||||
public IFormFile? Package { get; set; }
|
public IFormFile? Package { get; set; }
|
||||||
public string? PackageFilePath { get; set; } // Internal use - path to saved package file
|
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; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,23 +82,27 @@ RUN apt-get update && \
|
|||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
# Create unprivileged user for running the worker service
|
# Create unprivileged user for running the worker service
|
||||||
RUN mkdir -p /var/local/lib/isolate && \
|
RUN useradd -m -u 1001 -s /bin/bash workeruser && \
|
||||||
chmod 755 /var/local/lib/isolate
|
mkdir -p /var/local/lib/isolate && \
|
||||||
|
chmod 755 /var/local/lib/isolate && \
|
||||||
|
chown -R workeruser:workeruser /var/local/lib/isolate
|
||||||
|
|
||||||
# Configure isolate directories and control-group root
|
# Configure isolate
|
||||||
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 && \
|
RUN echo "cg_root = /sys/fs/cgroup" > /usr/local/etc/isolate && \
|
||||||
ln -sf /usr/local/etc/isolate.conf /usr/local/etc/isolate && \
|
echo "cg_enable = 1" >> /usr/local/etc/isolate && \
|
||||||
mkdir -p /run/isolate/locks
|
echo "box_root = /var/local/lib/isolate" >> /usr/local/etc/isolate
|
||||||
|
|
||||||
# Copy published app
|
# Copy published app
|
||||||
COPY --from=publish /app/publish .
|
COPY --from=publish /app/publish .
|
||||||
|
|
||||||
# Create temp directory for compilation and testing with proper permissions
|
# Create temp directory for compilation and testing with proper permissions
|
||||||
RUN mkdir -p /tmp/testing
|
RUN mkdir -p /tmp/testing && \
|
||||||
|
chown -R workeruser:workeruser /tmp/testing && \
|
||||||
|
chown -R workeruser:workeruser /app
|
||||||
|
|
||||||
ENV ASPNETCORE_URLS=http://+:8080
|
ENV ASPNETCORE_URLS=http://+:8080
|
||||||
|
|
||||||
# Switch to unprivileged user
|
# Switch to unprivileged user
|
||||||
#USER workeruser
|
USER workeruser
|
||||||
|
|
||||||
ENTRYPOINT ["dotnet", "LiquidCode.Tester.Worker.dll"]
|
ENTRYPOINT ["dotnet", "LiquidCode.Tester.Worker.dll"]
|
||||||
|
|||||||
@@ -103,10 +103,7 @@ public class CSharpCompilationServiceIsolate : ICompilationService
|
|||||||
DirectoryBindings = new List<DirectoryBinding>
|
DirectoryBindings = new List<DirectoryBinding>
|
||||||
{
|
{
|
||||||
new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true },
|
new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true },
|
||||||
new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true },
|
new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true }
|
||||||
new DirectoryBinding { HostPath = "/usr/bin", SandboxPath = "/usr/bin", ReadOnly = true },
|
|
||||||
new DirectoryBinding { HostPath = "/usr/share", SandboxPath = "/usr/share", ReadOnly = true },
|
|
||||||
new DirectoryBinding { HostPath = "/etc/mono", SandboxPath = "/etc/mono", ReadOnly = true }
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using LiquidCode.Tester.Worker.Services.Isolate;
|
using LiquidCode.Tester.Worker.Services.Isolate;
|
||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace LiquidCode.Tester.Worker.Services;
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
@@ -62,41 +61,23 @@ public class CSharpExecutionServiceIsolate : IExecutionService
|
|||||||
});
|
});
|
||||||
chmodProcess?.WaitForExit();
|
chmodProcess?.WaitForExit();
|
||||||
|
|
||||||
// Prepare input/output files inside the sandbox
|
// Prepare output file in box
|
||||||
var outputFilePath = Path.Combine(boxDir, "output.txt");
|
var outputFilePath = Path.Combine(boxDir, "output.txt");
|
||||||
var stderrFilePath = Path.Combine(boxDir, "stderr.txt");
|
|
||||||
string? sandboxInputPath = null;
|
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(inputFilePath) && File.Exists(inputFilePath))
|
// Run in Isolate
|
||||||
{
|
|
||||||
sandboxInputPath = Path.Combine(boxDir, "input.txt");
|
|
||||||
File.Copy(inputFilePath, sandboxInputPath, overwrite: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run in Isolate using mono runtime
|
|
||||||
var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions
|
var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions
|
||||||
{
|
{
|
||||||
BoxId = boxId,
|
BoxId = boxId,
|
||||||
Executable = "/usr/bin/mono",
|
Executable = $"/box/{executableName}",
|
||||||
Arguments = new[] { $"/box/{executableName}" },
|
|
||||||
TimeLimitSeconds = timeLimitMs / 1000.0,
|
TimeLimitSeconds = timeLimitMs / 1000.0,
|
||||||
WallTimeLimitSeconds = (timeLimitMs / 1000.0) * 2,
|
WallTimeLimitSeconds = (timeLimitMs / 1000.0) * 2,
|
||||||
MemoryLimitKb = memoryLimitMb * 1024,
|
MemoryLimitKb = memoryLimitMb * 1024,
|
||||||
StackLimitKb = 256 * 1024,
|
StackLimitKb = 256 * 1024,
|
||||||
ProcessLimit = 64, // Mono may create multiple threads/processes
|
ProcessLimit = 1, // Single process for C#
|
||||||
EnableNetwork = false,
|
EnableNetwork = false,
|
||||||
StdinFile = sandboxInputPath,
|
StdinFile = inputFilePath,
|
||||||
StdoutFile = outputFilePath,
|
StdoutFile = outputFilePath,
|
||||||
StderrFile = stderrFilePath,
|
WorkingDirectory = "/box"
|
||||||
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 = "/usr/share", SandboxPath = "/usr/share", ReadOnly = true },
|
|
||||||
new DirectoryBinding { HostPath = "/etc/mono", SandboxPath = "/etc/mono", ReadOnly = true },
|
|
||||||
new DirectoryBinding { HostPath = "/usr/bin", SandboxPath = "/usr/bin", ReadOnly = true }
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
@@ -107,19 +88,10 @@ public class CSharpExecutionServiceIsolate : IExecutionService
|
|||||||
result.Output = await File.ReadAllTextAsync(outputFilePath);
|
result.Output = await File.ReadAllTextAsync(outputFilePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read stderr
|
|
||||||
var stderrContent = string.Empty;
|
|
||||||
if (File.Exists(stderrFilePath))
|
|
||||||
{
|
|
||||||
stderrContent = await File.ReadAllTextAsync(stderrFilePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map Isolate result to ExecutionResult
|
// Map Isolate result to ExecutionResult
|
||||||
result.ExecutionTimeMs = (long)(isolateResult.CpuTimeSeconds * 1000);
|
result.ExecutionTimeMs = (long)(isolateResult.CpuTimeSeconds * 1000);
|
||||||
result.MemoryUsedMb = isolateResult.MemoryUsedKb / 1024;
|
result.MemoryUsedMb = isolateResult.MemoryUsedKb / 1024;
|
||||||
result.ErrorOutput = string.IsNullOrEmpty(stderrContent)
|
result.ErrorOutput = isolateResult.ErrorOutput;
|
||||||
? isolateResult.ErrorOutput
|
|
||||||
: $"{stderrContent}\n{isolateResult.ErrorOutput}".Trim();
|
|
||||||
result.ExitCode = isolateResult.ExitCode;
|
result.ExitCode = isolateResult.ExitCode;
|
||||||
|
|
||||||
if (isolateResult.TimeLimitExceeded)
|
if (isolateResult.TimeLimitExceeded)
|
||||||
|
|||||||
@@ -33,14 +33,6 @@ public class CallbackService : ICallbackService
|
|||||||
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
var content = new StringContent(json, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
var httpResponse = await httpClient.PostAsync(callbackUrl, content);
|
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();
|
httpResponse.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
_logger.LogInformation("Status update sent successfully");
|
_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)
|
private bool IsLogCallback(string callbackUrl)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(callbackUrl))
|
if (string.IsNullOrWhiteSpace(callbackUrl))
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using LiquidCode.Tester.Worker.Services.Isolate;
|
using LiquidCode.Tester.Worker.Services.Isolate;
|
||||||
using LiquidCode.Tester.Worker.Models;
|
using LiquidCode.Tester.Worker.Models;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
namespace LiquidCode.Tester.Worker.Services;
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -88,58 +85,19 @@ public class CppCompilationServiceIsolate : ICompilationService
|
|||||||
|
|
||||||
File.Copy(sourceFilePath, boxSourcePath, overwrite: true);
|
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
|
// Resolve compiler and flags
|
||||||
var (compiler, compilerFlags) = ResolveVersion(version);
|
var (compiler, compilerFlags) = ResolveVersion(version);
|
||||||
_logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, string.Join(' ', compilerFlags));
|
_logger.LogDebug("Using compiler: {Compiler} with flags: {Flags}", compiler, string.Join(' ', compilerFlags));
|
||||||
|
|
||||||
// Build compiler arguments
|
// Build compiler arguments
|
||||||
var arguments = new List<string>(compilerFlags);
|
var arguments = new List<string>(compilerFlags);
|
||||||
var includeCounter = 0;
|
|
||||||
|
|
||||||
// Add include directories
|
// Add include directories
|
||||||
if (includeDirectories != null)
|
if (includeDirectories != null)
|
||||||
{
|
{
|
||||||
foreach (var includeDir in includeDirectories.Where(d => !string.IsNullOrWhiteSpace(d)))
|
foreach (var includeDir in includeDirectories.Where(d => !string.IsNullOrWhiteSpace(d)))
|
||||||
{
|
{
|
||||||
var resolvedIncludeDir = includeDir;
|
arguments.Add($"-I{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}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,25 +118,8 @@ public class CppCompilationServiceIsolate : ICompilationService
|
|||||||
var stderrFilePath = Path.Combine(boxDir, "compile_stderr.txt");
|
var stderrFilePath = Path.Combine(boxDir, "compile_stderr.txt");
|
||||||
|
|
||||||
// Run compiler in Isolate
|
// Run compiler in Isolate
|
||||||
// Bind the system toolchain directories read-only so the linker and headers remain reachable
|
// Note: Isolate by default provides access to /usr, /lib, etc. via --share-net=no
|
||||||
var directoryBindings = new List<DirectoryBinding>
|
// For compilation, we need access to system headers and libraries
|
||||||
{
|
|
||||||
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
|
var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions
|
||||||
{
|
{
|
||||||
BoxId = boxId,
|
BoxId = boxId,
|
||||||
@@ -192,10 +133,11 @@ public class CppCompilationServiceIsolate : ICompilationService
|
|||||||
EnableNetwork = false,
|
EnableNetwork = false,
|
||||||
StderrFile = stderrFilePath,
|
StderrFile = stderrFilePath,
|
||||||
WorkingDirectory = "/box",
|
WorkingDirectory = "/box",
|
||||||
DirectoryBindings = directoryBindings,
|
DirectoryBindings = new List<DirectoryBinding>
|
||||||
EnvironmentVariables = new Dictionary<string, string>
|
|
||||||
{
|
{
|
||||||
["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)
|
private (string compiler, List<string> compilerFlags) ResolveVersion(string? version)
|
||||||
{
|
{
|
||||||
var defaultCompiler = _configuration["Cpp:Compiler"] ?? "g++";
|
var defaultCompiler = _configuration["Cpp:Compiler"] ?? "g++";
|
||||||
|
|||||||
@@ -61,15 +61,8 @@ public class CppExecutionServiceIsolate : IExecutionService
|
|||||||
});
|
});
|
||||||
chmodProcess?.WaitForExit();
|
chmodProcess?.WaitForExit();
|
||||||
|
|
||||||
// Prepare input/output files inside the sandbox
|
// Prepare output file in box
|
||||||
var outputFilePath = Path.Combine(boxDir, "output.txt");
|
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
|
// Run in Isolate
|
||||||
var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions
|
var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions
|
||||||
@@ -82,7 +75,7 @@ public class CppExecutionServiceIsolate : IExecutionService
|
|||||||
StackLimitKb = 256 * 1024, // 256 MB stack
|
StackLimitKb = 256 * 1024, // 256 MB stack
|
||||||
ProcessLimit = 1, // Single process only
|
ProcessLimit = 1, // Single process only
|
||||||
EnableNetwork = false, // No network access
|
EnableNetwork = false, // No network access
|
||||||
StdinFile = sandboxInputPath,
|
StdinFile = inputFilePath,
|
||||||
StdoutFile = outputFilePath,
|
StdoutFile = outputFilePath,
|
||||||
WorkingDirectory = "/box"
|
WorkingDirectory = "/box"
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using System;
|
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ public class IsolateService
|
|||||||
{
|
{
|
||||||
_logger.LogDebug("Initializing isolate box {BoxId}", boxId);
|
_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)
|
if (result.ExitCode != 0)
|
||||||
{
|
{
|
||||||
@@ -91,7 +90,7 @@ public class IsolateService
|
|||||||
{
|
{
|
||||||
_logger.LogDebug("Cleaning up isolate box {BoxId}", boxId);
|
_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)
|
if (result.ExitCode != 0)
|
||||||
{
|
{
|
||||||
@@ -158,17 +157,17 @@ public class IsolateService
|
|||||||
// I/O redirection
|
// I/O redirection
|
||||||
if (!string.IsNullOrEmpty(options.StdinFile))
|
if (!string.IsNullOrEmpty(options.StdinFile))
|
||||||
{
|
{
|
||||||
args.Add($"--stdin={MapSandboxPath(options.StdinFile, options.BoxId)}");
|
args.Add($"--stdin={options.StdinFile}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(options.StdoutFile))
|
if (!string.IsNullOrEmpty(options.StdoutFile))
|
||||||
{
|
{
|
||||||
args.Add($"--stdout={MapSandboxPath(options.StdoutFile, options.BoxId)}");
|
args.Add($"--stdout={options.StdoutFile}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(options.StderrFile))
|
if (!string.IsNullOrEmpty(options.StderrFile))
|
||||||
{
|
{
|
||||||
args.Add($"--stderr={MapSandboxPath(options.StderrFile, options.BoxId)}");
|
args.Add($"--stderr={options.StderrFile}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Working directory
|
// Working directory
|
||||||
@@ -183,7 +182,7 @@ public class IsolateService
|
|||||||
foreach (var binding in options.DirectoryBindings)
|
foreach (var binding in options.DirectoryBindings)
|
||||||
{
|
{
|
||||||
var dirSpec = binding.ReadOnly
|
var dirSpec = binding.ReadOnly
|
||||||
? $"--dir={binding.HostPath}={binding.SandboxPath}"
|
? $"--dir={binding.HostPath}={binding.SandboxPath}:ro"
|
||||||
: $"--dir={binding.HostPath}={binding.SandboxPath}:rw";
|
: $"--dir={binding.HostPath}={binding.SandboxPath}:rw";
|
||||||
args.Add(dirSpec);
|
args.Add(dirSpec);
|
||||||
}
|
}
|
||||||
@@ -211,25 +210,6 @@ public class IsolateService
|
|||||||
return string.Join(" ", args);
|
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>
|
/// <summary>
|
||||||
/// Parse isolate metadata file
|
/// Parse isolate metadata file
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -287,6 +267,9 @@ public class IsolateService
|
|||||||
|
|
||||||
case "status":
|
case "status":
|
||||||
result.Status = value;
|
result.Status = value;
|
||||||
|
result.TimeLimitExceeded = value == "TO";
|
||||||
|
result.MemoryLimitExceeded = value == "XX" || value == "MLE";
|
||||||
|
result.RuntimeError = value == "RE" || value == "SG";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "message":
|
case "message":
|
||||||
@@ -299,10 +282,7 @@ public class IsolateService
|
|||||||
|
|
||||||
case "cg-oom-killed":
|
case "cg-oom-killed":
|
||||||
result.CgroupOomKilled = value == "1";
|
result.CgroupOomKilled = value == "1";
|
||||||
if (result.CgroupOomKilled)
|
result.MemoryLimitExceeded = true;
|
||||||
{
|
|
||||||
result.MemoryLimitExceeded = true;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "csw-voluntary":
|
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;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ public class JavaCompilationServiceIsolate : ICompilationService
|
|||||||
private readonly IsolateBoxPool _boxPool;
|
private readonly IsolateBoxPool _boxPool;
|
||||||
|
|
||||||
private const int CompilationTimeLimitSeconds = 30;
|
private const int CompilationTimeLimitSeconds = 30;
|
||||||
private const int CompilationMemoryLimitMb = 1024; // JVM needs more memory
|
private const int CompilationMemoryLimitMb = 512;
|
||||||
|
|
||||||
public JavaCompilationServiceIsolate(
|
public JavaCompilationServiceIsolate(
|
||||||
ILogger<JavaCompilationServiceIsolate> logger,
|
ILogger<JavaCompilationServiceIsolate> logger,
|
||||||
@@ -85,7 +85,6 @@ public class JavaCompilationServiceIsolate : ICompilationService
|
|||||||
arguments.Add($"/box/{sourceFileName}");
|
arguments.Add($"/box/{sourceFileName}");
|
||||||
|
|
||||||
var stderrFilePath = Path.Combine(boxDir, "compile_stderr.txt");
|
var stderrFilePath = Path.Combine(boxDir, "compile_stderr.txt");
|
||||||
var stdoutFilePath = Path.Combine(boxDir, "compile_stdout.txt");
|
|
||||||
|
|
||||||
var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions
|
var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions
|
||||||
{
|
{
|
||||||
@@ -95,57 +94,28 @@ public class JavaCompilationServiceIsolate : ICompilationService
|
|||||||
TimeLimitSeconds = CompilationTimeLimitSeconds,
|
TimeLimitSeconds = CompilationTimeLimitSeconds,
|
||||||
WallTimeLimitSeconds = CompilationTimeLimitSeconds * 2,
|
WallTimeLimitSeconds = CompilationTimeLimitSeconds * 2,
|
||||||
MemoryLimitKb = CompilationMemoryLimitMb * 1024,
|
MemoryLimitKb = CompilationMemoryLimitMb * 1024,
|
||||||
StackLimitKb = 128 * 1024, // 128MB stack per process
|
StackLimitKb = 256 * 1024,
|
||||||
ProcessLimit = 64, // JVM needs many threads/processes
|
ProcessLimit = 10, // javac may spawn multiple processes
|
||||||
EnableNetwork = false,
|
EnableNetwork = false,
|
||||||
StdoutFile = stdoutFilePath,
|
|
||||||
StderrFile = stderrFilePath,
|
StderrFile = stderrFilePath,
|
||||||
WorkingDirectory = "/box",
|
WorkingDirectory = "/box",
|
||||||
DirectoryBindings = new List<DirectoryBinding>
|
DirectoryBindings = new List<DirectoryBinding>
|
||||||
{
|
{
|
||||||
new DirectoryBinding { HostPath = "/usr/bin", SandboxPath = "/usr/bin", ReadOnly = true },
|
|
||||||
new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true },
|
new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true },
|
||||||
new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true },
|
new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true }
|
||||||
new DirectoryBinding { HostPath = "/lib64", SandboxPath = "/lib64", ReadOnly = true },
|
|
||||||
new DirectoryBinding { HostPath = "/etc", SandboxPath = "/etc", ReadOnly = true }
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var compilerOutput = string.Empty;
|
var compilerOutput = string.Empty;
|
||||||
|
|
||||||
// Read stdout (javac may output errors there)
|
|
||||||
if (File.Exists(stdoutFilePath))
|
|
||||||
{
|
|
||||||
var stdoutContent = await File.ReadAllTextAsync(stdoutFilePath);
|
|
||||||
if (!string.IsNullOrWhiteSpace(stdoutContent))
|
|
||||||
{
|
|
||||||
compilerOutput = stdoutContent;
|
|
||||||
_logger.LogDebug("Read stdout file: {Length} bytes", stdoutContent.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read stderr
|
|
||||||
if (File.Exists(stderrFilePath))
|
if (File.Exists(stderrFilePath))
|
||||||
{
|
{
|
||||||
var stderrContent = await File.ReadAllTextAsync(stderrFilePath);
|
compilerOutput = await File.ReadAllTextAsync(stderrFilePath);
|
||||||
if (!string.IsNullOrWhiteSpace(stderrContent))
|
|
||||||
{
|
|
||||||
compilerOutput = string.IsNullOrEmpty(compilerOutput)
|
|
||||||
? stderrContent
|
|
||||||
: $"{compilerOutput}\n{stderrContent}";
|
|
||||||
_logger.LogDebug("Read stderr file: {Length} bytes", stderrContent.Length);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(isolateResult.ErrorOutput))
|
if (!string.IsNullOrEmpty(isolateResult.ErrorOutput))
|
||||||
{
|
{
|
||||||
compilerOutput = string.IsNullOrEmpty(compilerOutput)
|
compilerOutput = $"{compilerOutput}\n{isolateResult.ErrorOutput}".Trim();
|
||||||
? isolateResult.ErrorOutput
|
|
||||||
: $"{compilerOutput}\n{isolateResult.ErrorOutput}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_logger.LogDebug("Final compiler output: {Output}", compilerOutput);
|
|
||||||
|
|
||||||
if (isolateResult.TimeLimitExceeded)
|
if (isolateResult.TimeLimitExceeded)
|
||||||
{
|
{
|
||||||
_logger.LogWarning("Java compilation time limit exceeded for box {BoxId}", boxId);
|
_logger.LogWarning("Java compilation time limit exceeded for box {BoxId}", boxId);
|
||||||
|
|||||||
@@ -57,15 +57,8 @@ public class JavaExecutionServiceIsolate : IExecutionService
|
|||||||
File.Copy(file, destPath, overwrite: true);
|
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");
|
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
|
// Run in Isolate
|
||||||
// Note: Java needs more memory for JVM overhead
|
// Note: Java needs more memory for JVM overhead
|
||||||
@@ -82,17 +75,9 @@ public class JavaExecutionServiceIsolate : IExecutionService
|
|||||||
StackLimitKb = 256 * 1024, // 256 MB stack
|
StackLimitKb = 256 * 1024, // 256 MB stack
|
||||||
ProcessLimit = 64, // Java creates multiple threads
|
ProcessLimit = 64, // Java creates multiple threads
|
||||||
EnableNetwork = false,
|
EnableNetwork = false,
|
||||||
StdinFile = sandboxInputPath,
|
StdinFile = inputFilePath,
|
||||||
StdoutFile = outputFilePath,
|
StdoutFile = outputFilePath,
|
||||||
WorkingDirectory = "/box",
|
WorkingDirectory = "/box"
|
||||||
DirectoryBindings = new List<DirectoryBinding>
|
|
||||||
{
|
|
||||||
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 },
|
|
||||||
new DirectoryBinding { HostPath = "/lib64", SandboxPath = "/lib64", ReadOnly = true },
|
|
||||||
new DirectoryBinding { HostPath = "/etc", SandboxPath = "/etc", ReadOnly = true }
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using LiquidCode.Tester.Worker.Services.Isolate;
|
using LiquidCode.Tester.Worker.Services.Isolate;
|
||||||
using LiquidCode.Tester.Worker.Models;
|
using LiquidCode.Tester.Worker.Models;
|
||||||
|
|
||||||
@@ -15,7 +14,7 @@ public class KotlinCompilationServiceIsolate : ICompilationService
|
|||||||
private readonly IsolateBoxPool _boxPool;
|
private readonly IsolateBoxPool _boxPool;
|
||||||
|
|
||||||
private const int CompilationTimeLimitSeconds = 30;
|
private const int CompilationTimeLimitSeconds = 30;
|
||||||
private const int CompilationMemoryLimitMb = 1024; // Kotlin uses JVM, needs more memory
|
private const int CompilationMemoryLimitMb = 512;
|
||||||
|
|
||||||
public KotlinCompilationServiceIsolate(
|
public KotlinCompilationServiceIsolate(
|
||||||
ILogger<KotlinCompilationServiceIsolate> logger,
|
ILogger<KotlinCompilationServiceIsolate> logger,
|
||||||
@@ -39,21 +38,7 @@ public class KotlinCompilationServiceIsolate : ICompilationService
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Normalize line endings
|
await File.WriteAllTextAsync(sourceFilePath, sourceCode);
|
||||||
var normalizedSourceCode = sourceCode
|
|
||||||
.Replace("\\r\\n", "\n") // Handle escaped \r\n (literal text "\\r\\n")
|
|
||||||
.Replace("\\n", "\n") // Handle escaped \n (literal text "\\n")
|
|
||||||
.Replace("\\r", "\n") // Handle escaped \r (literal text "\\r")
|
|
||||||
.Replace("\r\n", "\n") // Handle actual Windows line endings
|
|
||||||
.Replace("\r", "\n"); // Handle actual Mac line endings
|
|
||||||
|
|
||||||
_logger.LogDebug("Source code length: {Length}, normalized length: {NormalizedLength}, line count: {LineCount}",
|
|
||||||
sourceCode.Length, normalizedSourceCode.Length, normalizedSourceCode.Split('\n').Length);
|
|
||||||
|
|
||||||
_logger.LogDebug("First 200 chars of source: {Preview}",
|
|
||||||
normalizedSourceCode.Length > 200 ? normalizedSourceCode.Substring(0, 200) : normalizedSourceCode);
|
|
||||||
|
|
||||||
await File.WriteAllTextAsync(sourceFilePath, normalizedSourceCode);
|
|
||||||
return await CompileFileInIsolateAsync(sourceFilePath, jarFilePath, version);
|
return await CompileFileInIsolateAsync(sourceFilePath, jarFilePath, version);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -103,7 +88,6 @@ public class KotlinCompilationServiceIsolate : ICompilationService
|
|||||||
arguments.Add($"/box/{jarFileName}");
|
arguments.Add($"/box/{jarFileName}");
|
||||||
|
|
||||||
var stderrFilePath = Path.Combine(boxDir, "compile_stderr.txt");
|
var stderrFilePath = Path.Combine(boxDir, "compile_stderr.txt");
|
||||||
var stdoutFilePath = Path.Combine(boxDir, "compile_stdout.txt");
|
|
||||||
|
|
||||||
var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions
|
var isolateResult = await _isolateService.RunAsync(new IsolateRunOptions
|
||||||
{
|
{
|
||||||
@@ -113,57 +97,27 @@ public class KotlinCompilationServiceIsolate : ICompilationService
|
|||||||
TimeLimitSeconds = CompilationTimeLimitSeconds,
|
TimeLimitSeconds = CompilationTimeLimitSeconds,
|
||||||
WallTimeLimitSeconds = CompilationTimeLimitSeconds * 2,
|
WallTimeLimitSeconds = CompilationTimeLimitSeconds * 2,
|
||||||
MemoryLimitKb = CompilationMemoryLimitMb * 1024,
|
MemoryLimitKb = CompilationMemoryLimitMb * 1024,
|
||||||
StackLimitKb = 128 * 1024, // 128MB stack per process
|
StackLimitKb = 256 * 1024,
|
||||||
ProcessLimit = 64, // Kotlin uses JVM, needs many threads/processes
|
ProcessLimit = 10,
|
||||||
EnableNetwork = false,
|
EnableNetwork = false,
|
||||||
StdoutFile = stdoutFilePath,
|
|
||||||
StderrFile = stderrFilePath,
|
StderrFile = stderrFilePath,
|
||||||
WorkingDirectory = "/box",
|
WorkingDirectory = "/box",
|
||||||
DirectoryBindings = new List<DirectoryBinding>
|
DirectoryBindings = new List<DirectoryBinding>
|
||||||
{
|
{
|
||||||
new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true },
|
new DirectoryBinding { HostPath = "/usr/lib", SandboxPath = "/usr/lib", ReadOnly = true },
|
||||||
new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true },
|
new DirectoryBinding { HostPath = "/lib", SandboxPath = "/lib", ReadOnly = true },
|
||||||
new DirectoryBinding { HostPath = "/lib64", SandboxPath = "/lib64", ReadOnly = true },
|
|
||||||
new DirectoryBinding { HostPath = "/usr/bin", SandboxPath = "/usr/bin", ReadOnly = true },
|
|
||||||
new DirectoryBinding { HostPath = "/bin", SandboxPath = "/bin", ReadOnly = true },
|
|
||||||
new DirectoryBinding { HostPath = "/etc", SandboxPath = "/etc", ReadOnly = true },
|
|
||||||
new DirectoryBinding { HostPath = "/opt/kotlinc", SandboxPath = "/opt/kotlinc", ReadOnly = true }
|
new DirectoryBinding { HostPath = "/opt/kotlinc", SandboxPath = "/opt/kotlinc", ReadOnly = true }
|
||||||
},
|
|
||||||
EnvironmentVariables = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
["PATH"] = "/usr/local/bin:/usr/bin:/bin"
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var compilerOutput = string.Empty;
|
var compilerOutput = string.Empty;
|
||||||
|
|
||||||
// Read stdout (kotlinc may output errors there)
|
|
||||||
if (File.Exists(stdoutFilePath))
|
|
||||||
{
|
|
||||||
var stdoutContent = await File.ReadAllTextAsync(stdoutFilePath);
|
|
||||||
if (!string.IsNullOrWhiteSpace(stdoutContent))
|
|
||||||
{
|
|
||||||
compilerOutput = stdoutContent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read stderr
|
|
||||||
if (File.Exists(stderrFilePath))
|
if (File.Exists(stderrFilePath))
|
||||||
{
|
{
|
||||||
var stderrContent = await File.ReadAllTextAsync(stderrFilePath);
|
compilerOutput = await File.ReadAllTextAsync(stderrFilePath);
|
||||||
if (!string.IsNullOrWhiteSpace(stderrContent))
|
|
||||||
{
|
|
||||||
compilerOutput = string.IsNullOrEmpty(compilerOutput)
|
|
||||||
? stderrContent
|
|
||||||
: $"{compilerOutput}\n{stderrContent}";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(isolateResult.ErrorOutput))
|
if (!string.IsNullOrEmpty(isolateResult.ErrorOutput))
|
||||||
{
|
{
|
||||||
compilerOutput = string.IsNullOrEmpty(compilerOutput)
|
compilerOutput = $"{compilerOutput}\n{isolateResult.ErrorOutput}".Trim();
|
||||||
? isolateResult.ErrorOutput
|
|
||||||
: $"{compilerOutput}\n{isolateResult.ErrorOutput}";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isolateResult.TimeLimitExceeded)
|
if (isolateResult.TimeLimitExceeded)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Collections.Generic;
|
|
||||||
using LiquidCode.Tester.Worker.Services.Isolate;
|
using LiquidCode.Tester.Worker.Services.Isolate;
|
||||||
|
|
||||||
namespace LiquidCode.Tester.Worker.Services;
|
namespace LiquidCode.Tester.Worker.Services;
|
||||||
@@ -53,15 +52,8 @@ public class KotlinExecutionServiceIsolate : IExecutionService
|
|||||||
|
|
||||||
File.Copy(executablePath, boxJarPath, overwrite: true);
|
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");
|
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)
|
// Run in Isolate (Kotlin runs via Java)
|
||||||
var kotlinMemoryMb = Math.Max(memoryLimitMb, 128); // Minimum 128MB for JVM
|
var kotlinMemoryMb = Math.Max(memoryLimitMb, 128); // Minimum 128MB for JVM
|
||||||
@@ -77,22 +69,9 @@ public class KotlinExecutionServiceIsolate : IExecutionService
|
|||||||
StackLimitKb = 256 * 1024,
|
StackLimitKb = 256 * 1024,
|
||||||
ProcessLimit = 64, // JVM creates multiple threads
|
ProcessLimit = 64, // JVM creates multiple threads
|
||||||
EnableNetwork = false,
|
EnableNetwork = false,
|
||||||
StdinFile = sandboxInputPath,
|
StdinFile = inputFilePath,
|
||||||
StdoutFile = outputFilePath,
|
StdoutFile = outputFilePath,
|
||||||
WorkingDirectory = "/box",
|
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 = "/lib64", SandboxPath = "/lib64", ReadOnly = true },
|
|
||||||
new DirectoryBinding { HostPath = "/usr/bin", SandboxPath = "/usr/bin", ReadOnly = true },
|
|
||||||
new DirectoryBinding { HostPath = "/bin", SandboxPath = "/bin", ReadOnly = true },
|
|
||||||
new DirectoryBinding { HostPath = "/etc", SandboxPath = "/etc", ReadOnly = true }
|
|
||||||
},
|
|
||||||
EnvironmentVariables = new Dictionary<string, string>
|
|
||||||
{
|
|
||||||
["PATH"] = "/usr/local/bin:/usr/bin:/bin"
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
stopwatch.Stop();
|
stopwatch.Stop();
|
||||||
|
|||||||
@@ -55,15 +55,8 @@ public class PythonExecutionServiceIsolate : IExecutionService
|
|||||||
|
|
||||||
File.Copy(executablePath, boxScriptPath, overwrite: true);
|
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");
|
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
|
// Get Python executable from configuration
|
||||||
var pythonExecutable = _configuration["Python:Executable"] ?? "python3";
|
var pythonExecutable = _configuration["Python:Executable"] ?? "python3";
|
||||||
@@ -80,7 +73,7 @@ public class PythonExecutionServiceIsolate : IExecutionService
|
|||||||
StackLimitKb = 256 * 1024,
|
StackLimitKb = 256 * 1024,
|
||||||
ProcessLimit = 1, // Single process for Python
|
ProcessLimit = 1, // Single process for Python
|
||||||
EnableNetwork = false,
|
EnableNetwork = false,
|
||||||
StdinFile = sandboxInputPath,
|
StdinFile = inputFilePath,
|
||||||
StdoutFile = outputFilePath,
|
StdoutFile = outputFilePath,
|
||||||
WorkingDirectory = "/box"
|
WorkingDirectory = "/box"
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -88,6 +88,17 @@ public class TestingService : ITestingService
|
|||||||
|
|
||||||
_logger.LogInformation("Compilation successful");
|
_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
|
// Send testing status
|
||||||
await SendStatusAsync(request, State.Testing, ErrorCode.None, "Running tests", 0, package.TestCases.Count);
|
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,
|
await SendStatusAsync(request, State.Testing, ErrorCode.None,
|
||||||
$"Running test {testCase.Number}", testCase.Number, package.TestCases.Count);
|
$"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
|
// Execute solution
|
||||||
var executionResult = await executionService.ExecuteAsync(
|
var executionResult = await executionService.ExecuteAsync(
|
||||||
compilationResult.ExecutablePath!,
|
compilationResult.ExecutablePath!,
|
||||||
testCase.InputFilePath,
|
testCase.InputFilePath,
|
||||||
testCase.TimeLimit,
|
timeLimit,
|
||||||
testCase.MemoryLimit);
|
memoryLimit);
|
||||||
|
|
||||||
// Check for execution errors
|
// Check for execution errors
|
||||||
if (executionResult.TimeLimitExceeded)
|
if (executionResult.TimeLimitExceeded)
|
||||||
|
|||||||
Reference in New Issue
Block a user