[sandbox] Add C# for sandbox

This commit is contained in:
orosmatthew 2024-01-15 18:39:47 -05:00
parent 2060d079c8
commit f5e8990c0a
10 changed files with 273 additions and 181 deletions

View File

@ -5,41 +5,25 @@ WORKDIR /app
RUN apt-get update
RUN apt-get install curl -y
RUN apt-get install -y ca-certificates curl gnupg
RUN mkdir -p /etc/apt/keyrings
RUN curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg
ENV NODE_MAJOR=18
RUN echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_$NODE_MAJOR.x nodistro main" | tee /etc/apt/sources.list.d/nodesource.list
RUN apt-get update
RUN apt-get install nodejs -y
RUN apt-get install git -y
RUN apt-get install nodejs git openjdk-17-jdk-headless dotnet-sdk-7.0 -y
ENV DOTNET_NOLOGO=true
ENV DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true
RUN git config --global user.name "Admin"
RUN git config --global user.email noemail@example.com
WORKDIR /opt
RUN apt-get install wget -y
RUN wget -O java.tar.gz https://github.com/adoptium/temurin17-binaries/releases/download/jdk-17.0.7%2B7/OpenJDK17U-jdk_x64_linux_hotspot_17.0.7_7.tar.gz
RUN tar -xzvf java.tar.gz
RUN rm java.tar.gz
ENV JAVA_PATH=/opt/jdk-17.0.7+7/bin
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
CMD ["node", "build"]

View File

@ -12,6 +12,7 @@
"dotenv": "^16.3.1",
"fs-extra": "^11.2.0",
"simple-git": "^3.22.0",
"tree-kill": "^1.2.2",
"url-join": "^5.0.0",
"zod": "^3.22.4"
},
@ -153,6 +154,14 @@
"url": "https://github.com/steveukx/git-js?sponsor=1"
}
},
"node_modules/tree-kill": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
"integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
"bin": {
"tree-kill": "cli.js"
}
},
"node_modules/typescript": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz",

View File

@ -21,6 +21,7 @@
"dotenv": "^16.3.1",
"fs-extra": "^11.2.0",
"simple-git": "^3.22.0",
"tree-kill": "^1.2.2",
"url-join": "^5.0.0",
"zod": "^3.22.4"
}

View File

@ -6,6 +6,7 @@ import os, { EOL } from 'os';
import { join } from 'path';
import { simpleGit, SimpleGit } from 'simple-git';
import { runJava } from './run/java.js';
import { runCSharp } from './run/csharp.js';
export const timeoutSeconds = 30;
@ -39,6 +40,7 @@ const submissionGetData = z
contestName: z.string(),
teamId: z.number(),
teamName: z.string(),
teamLanguage: z.enum(['Java', 'CSharp']),
problem: z.object({
id: z.number(),
pascalName: z.string(),
@ -81,7 +83,7 @@ async function cloneAndRun(submissionData: SubmissionGetData) {
return;
}
const tmpDir = os.tmpdir();
const buildDir = join(tmpDir, 'bwcontest_java');
const buildDir = join(tmpDir, 'bwcontest-build');
if (fs.existsSync(buildDir)) {
fs.removeSync(buildDir);
}
@ -100,16 +102,32 @@ async function cloneAndRun(submissionData: SubmissionGetData) {
await git.clone(teamRepoUrl, '.');
await git.checkout(submissionData.submission.commitHash);
const problemName = submissionData.submission.problem.pascalName;
let runResult: RunResult;
let runResult: RunResult | undefined;
try {
runResult = await runJava(
javaBinPath,
buildDir,
join(repoDir, problemName, problemName + '.java'),
problemName,
submissionData.submission.problem.realInput
);
if (submissionData.submission.teamLanguage === 'Java') {
let res = await runJava({
srcDir: buildDir,
mainFile: join(repoDir, problemName, problemName + '.java'),
mainClass: problemName,
input: submissionData.submission.problem.realInput
});
if (res.success === true) {
runResult = await res.runResult;
} else {
runResult = res.runResult;
}
} else if (submissionData.submission.teamLanguage === 'CSharp') {
let res = await runCSharp({
srcDir: join(repoDir, problemName),
input: submissionData.submission.problem.realInput
});
if (res.success === true) {
runResult = await res.runResult;
} else {
runResult = res.runResult;
}
}
} catch (error) {
runResult = {
kind: 'SandboxError',
@ -117,29 +135,33 @@ async function cloneAndRun(submissionData: SubmissionGetData) {
};
}
printRunResult(runResult);
if (runResult !== undefined) {
printRunResult(runResult);
const postBodyObject: SubmissionPostData = {
submissionId: submissionData.submission.id,
result: runResult
};
const res = await fetch(urlJoin(adminUrl, 'api/submission'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(postBodyObject)
});
if (res.status !== 200) {
console.error('- POST: Failed with error code: ' + res.status + ' ' + res.statusText);
return;
const postBodyObject: SubmissionPostData = {
submissionId: submissionData.submission.id,
result: runResult
};
const res = await fetch(urlJoin(adminUrl, 'api/submission'), {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(postBodyObject)
});
if (res.status !== 200) {
console.error('- POST: Failed with error code: ' + res.status + ' ' + res.statusText);
return;
}
const data = (await res.json()) as { success: boolean };
if (!data.success) {
console.error('- POST: Failed with response: ' + JSON.stringify(data));
return;
}
console.log(`- POST: Succeeded`);
} else {
console.warn(`runResult is undefined`);
}
const data = (await res.json()) as { success: boolean };
if (!data.success) {
console.error('- POST: Failed with response: ' + JSON.stringify(data));
return;
}
console.log(`- POST: Succeeded`);
}
function printRunResult(runResult: RunResult) {
@ -163,11 +185,7 @@ function printRunResult(runResult: RunResult) {
}
function validateEnv(): boolean {
return (
process.env.ADMIN_URL !== undefined &&
process.env.REPO_URL !== undefined &&
process.env.JAVA_PATH !== undefined
);
return process.env.ADMIN_URL !== undefined && process.env.REPO_URL !== undefined;
}
dotenv.config();

89
sandbox/src/run/csharp.ts Normal file
View File

@ -0,0 +1,89 @@
import * as fs from 'fs-extra';
import { join } from 'path';
import os = require('os');
import { spawn } from 'child_process';
import kill from 'tree-kill';
import { RunResult, timeoutSeconds } from '../index.js';
import { IRunner, IRunnerReturn } from './types.js';
export const runCSharp: IRunner = async function (params: {
srcDir: string;
input: string;
outputCallback?: (data: string) => void;
}): IRunnerReturn {
console.log(`- RUN: ${params.srcDir}`);
const child = spawn('dotnet run', { shell: true, cwd: params.srcDir });
let outputBuffer = '';
child.stdout.setEncoding('utf8');
child.stdout.on('data', (data) => {
outputBuffer += data.toString();
params.outputCallback?.(data.toString());
});
child.stderr.setEncoding('utf8');
child.stderr.on('data', (data) => {
outputBuffer += data.toString();
params.outputCallback?.(data.toString());
});
let runStartTime = performance.now();
child.stdin.write(params.input);
child.stdin.end();
let timeLimitExceeded = false;
let completedNormally = false;
return {
success: true,
runResult: new Promise<RunResult>((resolve) => {
child.on('close', () => {
completedNormally = !timeLimitExceeded;
let runEndTime = performance.now();
const runtimeMilliseconds = Math.floor(runEndTime - runStartTime);
if (completedNormally) {
clearTimeout(timeoutHandle);
resolve({
kind: 'Completed',
output: outputBuffer,
exitCode: child.exitCode!,
runtimeMilliseconds
});
} else {
console.log(`Process terminated, total sandbox time: ${runtimeMilliseconds}ms`);
resolve({
kind: 'TimeLimitExceeded',
output: outputBuffer,
resultKindReason: `Timeout after ${timeoutSeconds} seconds`
});
}
});
let timeoutHandle = setTimeout(() => {
if (completedNormally) {
return;
}
console.log(`Run timed out after ${timeoutSeconds} seconds, killing process...`);
timeLimitExceeded = true;
child.stdin.end();
child.stdin.destroy();
child.stdout.destroy();
child.stderr.destroy();
if (child.pid !== undefined) {
kill(child.pid);
}
}, timeoutSeconds * 1000);
}),
killFunc() {
if (child.pid !== undefined) {
if (!completedNormally && !timeLimitExceeded) {
kill(child.pid);
params.outputCallback?.('\n[Manually stopped]');
}
}
}
};
};

View File

@ -1,89 +1,107 @@
import { join } from 'path';
import { exec, spawn } from 'child_process';
import util from 'util';
import { RunResult, RunResultKind, timeoutSeconds } from '../index.js';
import { RunResult, timeoutSeconds } from '../index.js';
import { IRunner, IRunnerParams, IRunnerReturn } from './types.js';
import kill from 'tree-kill';
const execPromise = util.promisify(exec);
export async function runJava(
javaBinPath: string,
buildDir: string,
mainFile: string,
mainClass: string,
input: string
): Promise<RunResult> {
console.log(`- BUILD: ${mainFile}`);
const compileCommand = `${join(javaBinPath, 'javac')} -cp ${join(
buildDir,
'src'
)} ${mainFile} -d ${join(buildDir, 'build')}`;
interface IRunnerParamsJava extends IRunnerParams {
srcDir: string;
mainFile: string;
mainClass: string;
input: string;
outputCallback?: (data: string) => void;
}
export const runJava: IRunner<IRunnerParamsJava> = async function (
params: IRunnerParamsJava
): IRunnerReturn {
console.log(`- BUILD: ${params.mainFile}`);
const compileCommand = `javac -cp ${join(params.srcDir, 'src')} ${params.mainFile} -d ${join(params.srcDir, 'build')}`;
try {
await execPromise(compileCommand);
} catch (e) {
const buildErrorText = e?.toString() ?? 'Unknown build errors.';
console.log('Build errors: ' + buildErrorText);
return { kind: 'CompileFailed', resultKindReason: buildErrorText };
return {
success: false,
runResult: { kind: 'CompileFailed', resultKindReason: buildErrorText }
};
}
console.log(`- RUN: ${mainClass}`);
const runCommand = `${join(javaBinPath, 'java')} -cp "${join(buildDir, 'build')}" ${mainClass}`;
return new Promise((resolve) => {
let outputBuffer = '';
const child = spawn(runCommand, { shell: true });
child.stdout.setEncoding('utf8');
child.stdout.on('data', (data) => {
outputBuffer += data.toString();
});
child.stderr.setEncoding('utf8');
child.stderr.on('data', (data) => {
outputBuffer += data.toString();
});
console.log(`- RUN: ${params.mainClass}`);
const runCommand = `java -cp "${join(params.srcDir, 'build')}" ${params.mainClass}`;
let runStartTime = performance.now();
child.stdin.write(input);
child.stdin.end();
let timeLimitExceeded = false;
let completedNormally = false;
child.on('close', () => {
completedNormally = !timeLimitExceeded;
let runEndTime = performance.now();
const runtimeMilliseconds = Math.floor(runEndTime - runStartTime);
if (completedNormally) {
clearTimeout(timeoutHandle);
resolve({
kind: 'Completed',
output: outputBuffer,
exitCode: child.exitCode!,
runtimeMilliseconds
});
} else {
console.log(`Process terminated, total sandbox time: ${runtimeMilliseconds}ms`);
resolve({
kind: 'TimeLimitExceeded',
output: outputBuffer,
resultKindReason: `Timeout after ${timeoutSeconds} seconds`
});
}
});
let timeoutHandle = setTimeout(() => {
if (completedNormally) {
return;
}
console.log(`Run timed out after ${timeoutSeconds} seconds, killing process...`);
timeLimitExceeded = true;
child.stdin.end();
child.stdin.destroy();
child.stdout.destroy();
child.stderr.destroy();
child.kill('SIGKILL');
}, timeoutSeconds * 1000);
let outputBuffer = '';
const child = spawn(runCommand, { shell: true });
child.stdout.setEncoding('utf8');
child.stdout.on('data', (data) => {
outputBuffer += data.toString();
});
}
child.stderr.setEncoding('utf8');
child.stderr.on('data', (data) => {
outputBuffer += data.toString();
});
let runStartTime = performance.now();
child.stdin.write(params.input);
child.stdin.end();
let timeLimitExceeded = false;
let completedNormally = false;
return {
success: true,
runResult: new Promise<RunResult>(async (resolve) => {
child.on('close', () => {
completedNormally = !timeLimitExceeded;
let runEndTime = performance.now();
const runtimeMilliseconds = Math.floor(runEndTime - runStartTime);
if (completedNormally) {
clearTimeout(timeoutHandle);
resolve({
kind: 'Completed',
output: outputBuffer,
exitCode: child.exitCode!,
runtimeMilliseconds
});
} else {
console.log(`Process terminated, total sandbox time: ${runtimeMilliseconds}ms`);
resolve({
kind: 'TimeLimitExceeded',
output: outputBuffer,
resultKindReason: `Timeout after ${timeoutSeconds} seconds`
});
}
});
let timeoutHandle = setTimeout(() => {
if (completedNormally) {
return;
}
console.log(`Run timed out after ${timeoutSeconds} seconds, killing process...`);
timeLimitExceeded = true;
child.stdin.end();
child.stdin.destroy();
child.stdout.destroy();
child.stderr.destroy();
child.kill('SIGKILL');
}, timeoutSeconds * 1000);
}),
killFunc() {
if (child.pid !== undefined) {
if (!completedNormally && !timeLimitExceeded) {
kill(child.pid);
params.outputCallback?.('\n[Manually stopped]');
}
}
}
};
};

16
sandbox/src/run/types.d.ts vendored Normal file
View File

@ -0,0 +1,16 @@
import { RunResult } from '../index.ts';
interface IRunnerParams {
srcDir: string;
input: string;
outputCallback?: (data: string) => void;
}
type IRunnerReturn = Promise<
| { success: true; killFunc: () => void; runResult: Promise<RunResult> }
| { success: false; runResult: RunResult }
>;
interface IRunner<T extends IRunnerParams = IRunnerParams> {
(params: T): IRunnerReturn;
}

View File

@ -7,6 +7,7 @@
import { goto } from '$app/navigation';
import { stretchTextarea } from '$lib/util';
import FormAlert from '$lib/FormAlert.svelte';
import { theme } from '../../../stores';
export let data: PageData;
export let form: Actions;
@ -72,7 +73,7 @@
<textarea use:stretchTextarea class="code mb-3 form-control" disabled>{data.output}</textarea>
<h3>Diff</h3>
<div class="mt-3" id="diff" />
<div class="mt-3" id="diff" class:d2h-dark-color-scheme={$theme === 'dark'} class:d2h-light-color-scheme={$theme === 'light'}/>
<form method="POST" action="?/submit" use:enhance>
<h5>Message</h5>

View File

@ -6,6 +6,7 @@
import { enhance } from '$app/forms';
import { stretchTextarea } from '$lib/util';
import ConfirmModal from '$lib/ConfirmModal.svelte';
import { theme } from '../../../stores';
export let data: PageData;
export let form: Actions;
@ -141,56 +142,10 @@
<h3 style="text-align:center">Output</h3>
<textarea use:stretchTextarea class="code mb-3 form-control" disabled>{data.output}</textarea>
<h3 style="text-align:center">Diff</h3>
<div id="diff" class="dark-diff" />
<div
id="diff"
class="dark-diff"
class:d2h-dark-color-scheme={$theme === 'dark'}
class:d2h-light-color-scheme={$theme === 'light'}
/>
{/if}
<style lang="scss">
:global(.dark-diff) {
:global(.d2h-code-side-linenumber),
:global(.d2h-info),
:global(.d2h-emptyplaceholder),
:global(.d2h-code-side-emptyplaceholder),
:global(.d2h-file-header),
:global(.d2h-tag) {
background-color: var(--bs-body-bg);
color: var(--bs-body-color);
}
:global(span) {
color: var(--bs-body-color);
}
:global(.d2h-file-wrapper) {
border-color: var(--bs-border-color);
}
:global(.d2h-file-header) {
border-bottom-color: var(--bs-border-color);
}
:global(.d2h-info) {
border-color: var(--bs-border-color);
}
:global(.d2h-del) {
background-color: var(--bs-danger-border-subtle);
border-color: var(--bs-danger);
}
:global(del) {
background-color: rgba(210, 85, 97, 0.5);
}
:global(.d2h-ins) {
background-color: var(--bs-success-border-subtle);
border-color: var(--bs-success);
}
:global(.d2h-code-side-emptyplaceholder),
:global(.d2h-emptyplaceholder) {
border-color: var(--bs-border-color);
}
:global(ins) {
background-color: rgba(13, 125, 75, 0.5);
}
}
</style>

View File

@ -21,6 +21,7 @@ export const GET = (async () => {
contestName: submissions[0].contest.name,
teamId: submissions[0].team.id,
teamName: submissions[0].team.name,
teamLanguage: submissions[0].team.language,
problem: {
id: submissions[0].problemId,
pascalName: submissions[0].problem.pascalName,