Merge shared code for submission running (extension/sandbox) and team submission info (extension/web) (#16)

* Unify submission execution implementations into submissionRunner

* Unify contestMonitorTypes definitions between extension & web

* Make line separator in entry use LF

* Add entry.sh for sandbox

* Fix web imports

* Sandbox read from .env

---------

Co-authored-by: orosmatthew <orosmatthew@pm.me>
This commit is contained in:
David Poeschl 2024-03-11 10:32:23 -07:00 committed by GitHub
parent 6e95a955a8
commit 22bc7460df
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 1443 additions and 503 deletions

22
.dockerignore Normal file
View File

@ -0,0 +1,22 @@
sandbox/node_modules
sandbox/build
sandbox/.env
web/.DS_Store
web/node_modules
web/build
web/.svelte-kit
web/package
web/.env
web/.env.*
web/!.env.example
web/vite.config.js.timestamp-*
web/vite.config.ts.timestamp-*
web/temp
web/db
shared/submissionRunner/node_modules
shared/submissionRunner/build
shared/extensionWeb/node_modules
shared/extensionWeb/build

View File

@ -7,7 +7,7 @@ import {
ProblemNameForExtension, ProblemNameForExtension,
FullStateForExtension, FullStateForExtension,
SubmissionForExtension SubmissionForExtension
} from './contestMonitorSharedTypes'; } from '@extensionWeb/contestMonitorTypes.cjs';
import { LiteEvent } from '../utilities/LiteEvent'; import { LiteEvent } from '../utilities/LiteEvent';
export type ContestTeamState = { export type ContestTeamState = {

View File

@ -2,11 +2,11 @@ import * as vscode from 'vscode';
import { getNonce } from './getNonce'; import { getNonce } from './getNonce';
import urlJoin from 'url-join'; import urlJoin from 'url-join';
import { extensionSettings } from './extension'; import { extensionSettings } from './extension';
import { runJava } from './run/java'; import { runJava } from '@submissionRunner/java.cjs';
import { join } from 'path'; import { join } from 'path';
import { submitProblem } from './submit'; import { submitProblem } from './submit';
import { runCSharp } from './run/csharp'; import { runCSharp } from '@submissionRunner/csharp.cjs';
import { runCpp } from './run/cpp'; import { runCpp } from '@submissionRunner/cpp.cjs';
import { TeamData } from './sharedTypes'; import { TeamData } from './sharedTypes';
import outputPanelLog from './outputPanelLog'; import outputPanelLog from './outputPanelLog';
import { recordInitialSubmission } from './contestMonitor/contestStateSyncManager'; import { recordInitialSubmission } from './contestMonitor/contestStateSyncManager';

View File

@ -1,146 +0,0 @@
import { join } from 'path';
import { exec, spawn } from 'child_process';
import {
timeoutSeconds,
type IRunner,
type IRunnerParams,
type IRunnerReturn,
type RunResult
} from './types';
import kill = require('tree-kill');
import * as os from 'os';
import * as fs from 'fs-extra';
import * as util from 'util';
const execPromise = util.promisify(exec);
export type CppPlatform = 'VisualStudio' | 'GCC';
interface IRunnerParamsCpp extends IRunnerParams {
srcDir: string;
problemName: string;
input: string;
cppPlatform: CppPlatform;
outputCallback?: (data: string) => void;
}
export const runCpp: IRunner<IRunnerParamsCpp> = async function (
params: IRunnerParamsCpp
): IRunnerReturn {
const tmpDir = os.tmpdir();
const buildDir = join(tmpDir, 'bwcontest-cpp');
if (fs.existsSync(buildDir)) {
fs.removeSync(buildDir);
}
fs.mkdirSync(buildDir);
console.log(`- BUILD: ${params.problemName}`);
const configureCommand = `cmake -S ${params.srcDir} -B ${buildDir}`;
try {
await execPromise(configureCommand);
} catch (e) {
const buildErrorText = e?.toString() ?? 'Unknown build errors.';
console.log('Build errors: ' + buildErrorText);
return {
success: false,
runResult: { kind: 'CompileFailed', resultKindReason: buildErrorText }
};
}
const compileCommand = `cmake --build ${buildDir} --target ${params.problemName}`;
try {
await execPromise(compileCommand);
} catch (e) {
const buildErrorText = e?.toString() ?? 'Unknown build errors.';
console.log('Build errors: ' + buildErrorText);
return {
success: false,
runResult: { kind: 'CompileFailed', resultKindReason: buildErrorText }
};
}
console.log(`- RUN: ${params.problemName}`);
let runCommand = '';
if (params.cppPlatform === 'VisualStudio') {
runCommand = `${join(buildDir, 'Debug', `${params.problemName}.exe`)}`;
} else {
runCommand = `${join(buildDir, params.problemName)}`;
}
try {
let outputBuffer = '';
const child = spawn(runCommand, { shell: true });
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());
});
const 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;
const 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`
});
}
});
const 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]');
}
}
}
};
} catch (error) {
return { success: false, runResult: { kind: 'RunError' } };
}
};

View File

@ -1,89 +0,0 @@
import { spawn } from 'child_process';
import kill = require('tree-kill');
import { timeoutSeconds, type IRunner, type IRunnerReturn, type RunResult } from './types';
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 });
try {
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());
});
const 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;
const 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`
});
}
});
const 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]');
}
}
}
};
} catch (error) {
return { success: false, runResult: { kind: 'RunError' } };
}
};

View File

@ -1,31 +0,0 @@
export const timeoutSeconds = 30;
export type RunResultKind =
| 'CompileFailed'
| 'TimeLimitExceeded'
| 'Completed'
| 'SandboxError'
| 'RunError';
export type RunResult = {
kind: RunResultKind;
output?: string;
exitCode?: number;
runtimeMilliseconds?: number;
resultKindReason?: string;
};
export interface IRunnerParams {
srcDir: string;
input: string;
outputCallback?: (data: string) => void;
}
export type IRunnerReturn = Promise<
| { success: true; killFunc: () => void; runResult: Promise<RunResult> }
| { success: false; runResult: RunResult }
>;
export interface IRunner<T extends IRunnerParams = IRunnerParams> {
(params: T): IRunnerReturn;
}

View File

@ -6,11 +6,19 @@
"lib": ["ES2020"], "lib": ["ES2020"],
"sourceMap": true, "sourceMap": true,
"rootDir": "src", "rootDir": "src",
"strict": true /* enable all strict type-checking options */ "strict": true, /* enable all strict type-checking options */
/* Additional Checks */ /* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */ // "noUnusedParameters": true, /* Report errors on unused parameters. */
"paths": {
"@submissionRunner/*": ["../../shared/submissionRunner/*"],
"@extensionWeb/*": ["../../shared/extensionWeb/*"]
}, },
"exclude": ["webviews"] },
"exclude": ["webviews"],
"references": [
{ "path": "../../shared/submissionRunner" },
{ "path": "../../shared/extensionWeb" }
]
} }

View File

@ -1,6 +1,9 @@
FROM ubuntu:22.04 FROM ubuntu:22.04
WORKDIR /app # Setup
RUN mkdir sandbox
WORKDIR /app/sandbox
RUN apt-get update RUN apt-get update
@ -20,10 +23,28 @@ ENV DOTNET_SKIP_FIRST_TIME_EXPERIENCE=true
RUN git config --global user.name "Admin" RUN git config --global user.name "Admin"
RUN git config --global user.email noemail@example.com RUN git config --global user.email noemail@example.com
WORKDIR /app # Prep Sandbox
COPY package*.json ./ WORKDIR /app/sandbox
COPY ./sandbox/package*.json ./
RUN npm install RUN npm install
COPY . . COPY ./sandbox/ .
# Prep SubmissionRunner
WORKDIR /app
RUN mkdir shared
RUN mkdir shared/submissionRunner
WORKDIR /app/shared/submissionRunner
COPY ./shared/submissionRunner/package*.json .
RUN npm install
COPY ./shared/submissionRunner/ .
# Build/Run
WORKDIR /app/sandbox
RUN npm run build RUN npm run build
CMD ["node", "build"] RUN chmod +x ./docker/entry.sh
CMD ["./docker/entry.sh"]

View File

@ -1,7 +1,9 @@
version: '3' version: '3'
services: services:
sandbox: sandbox:
build: . build:
context: ../
dockerfile: ./sandbox/Dockerfile
environment: environment:
- ADMIN_URL=${ADMIN_URL} - ADMIN_URL=${ADMIN_URL}
- REPO_URL=${REPO_URL} - REPO_URL=${REPO_URL}

3
sandbox/docker/entry.sh Normal file
View File

@ -0,0 +1,3 @@
#!/bin/bash
# THIS FILE MUST USE LF LINE SEPARATORS!
node ./build/sandbox.cjs

1068
sandbox/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -5,18 +5,23 @@
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
"scripts": { "scripts": {
"build": "tsc", "build": "esbuild src/index.ts --bundle --outfile=build/sandbox.cjs --format=cjs --platform=node",
"format": "prettier --write .", "format": "prettier --write .",
"lint": "prettier --check . && eslint .", "lint": "prettier --check . && eslint .",
"start": "node build" "start": "node ./build/sandbox.cjs"
}, },
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"devDependencies": { "devDependencies": {
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-node-resolve": "^15.2.3",
"@rollup/plugin-terser": "^0.4.4",
"@rollup/plugin-typescript": "^11.1.6",
"@types/fs-extra": "^11.0.4", "@types/fs-extra": "^11.0.4",
"@types/node": "^20.11.2", "@types/node": "^20.11.2",
"@typescript-eslint/eslint-plugin": "^7.0.1", "@typescript-eslint/eslint-plugin": "^7.0.1",
"@typescript-eslint/parser": "^7.0.1", "@typescript-eslint/parser": "^7.0.1",
"esbuild": "^0.19.11",
"eslint": "^8.56.0", "eslint": "^8.56.0",
"prettier": "^3.2.2", "prettier": "^3.2.2",
"typescript": "^5.3.3" "typescript": "^5.3.3"
@ -24,6 +29,7 @@
"dependencies": { "dependencies": {
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"fs-extra": "^11.2.0", "fs-extra": "^11.2.0",
"rollup": "^4.12.1",
"simple-git": "^3.22.0", "simple-git": "^3.22.0",
"tree-kill": "^1.2.2", "tree-kill": "^1.2.2",
"url-join": "^5.0.0", "url-join": "^5.0.0",

View File

@ -1,39 +1,19 @@
import dotenv from 'dotenv'; import 'dotenv/config';
import fs from 'fs-extra'; import fs from 'fs-extra';
import urlJoin from 'url-join'; import urlJoin from 'url-join';
import { z } from 'zod';
import os, { EOL } from 'os'; import os, { EOL } from 'os';
import { join } from 'path'; import { join } from 'path';
import { simpleGit, SimpleGit } from 'simple-git'; import { simpleGit, SimpleGit } from 'simple-git';
import { runJava } from './run/java.js'; import { runJava } from '@submissionRunner/java.cjs';
import { runCSharp } from './run/csharp.js'; import { runCSharp } from '@submissionRunner/csharp.cjs';
import { runCpp } from './run/cpp.js'; import { runCpp } from '@submissionRunner/cpp.cjs';
import { RunResult, RunResultZod } from '@submissionRunner/types.cjs';
export const timeoutSeconds = 30; import { z } from 'zod';
const RunResultKind = z.enum([
'CompileFailed',
'TimeLimitExceeded',
'Completed',
'SandboxError',
'RunError'
]);
export type RunResultKind = z.infer<typeof RunResultKind>;
const RunResult = z
.object({
kind: RunResultKind,
output: z.string().optional(),
exitCode: z.number().optional(),
runtimeMilliseconds: z.number().optional(),
resultKindReason: z.string().optional()
})
.strict();
const submissionPostData = z const submissionPostData = z
.object({ .object({
submissionId: z.number(), submissionId: z.number(),
result: RunResult result: RunResultZod
}) })
.strict(); .strict();
@ -59,7 +39,6 @@ const submissionGetData = z
}) })
.strict(); .strict();
export type RunResult = z.infer<typeof RunResult>;
type SubmissionGetData = z.infer<typeof submissionGetData>; type SubmissionGetData = z.infer<typeof submissionGetData>;
type SubmissionPostData = z.infer<typeof submissionPostData>; type SubmissionPostData = z.infer<typeof submissionPostData>;
@ -78,10 +57,12 @@ async function fetchQueuedSubmission(): Promise<SubmissionGetData | undefined> {
return undefined; return undefined;
} }
const data = submissionGetData.parse(await res.json()); const json = await res.json();
const data = submissionGetData.parse(json);
if (!data.success) { if (!data.success) {
return undefined; return undefined;
} }
return data; return data;
} }
@ -205,9 +186,14 @@ function validateEnv(): boolean {
return process.env.ADMIN_URL !== undefined && process.env.REPO_URL !== undefined; return process.env.ADMIN_URL !== undefined && process.env.REPO_URL !== undefined;
} }
dotenv.config();
if (!validateEnv()) { if (!validateEnv()) {
console.log(process.env);
console.log(
'process.env.ADMIN_URL is ' +
process.env.ADMIN_URL +
' and process.env.REPO_URL is ' +
process.env.REPO_URL
);
throw Error('Invalid environment'); throw Error('Invalid environment');
} }
@ -287,6 +273,7 @@ async function run() {
break; break;
case SubmissionProcessingResult.NoSubmissions: case SubmissionProcessingResult.NoSubmissions:
if (iterationsSinceProcessedSubmission > 0 && iterationsSinceProcessedSubmission % 6 == 0) { if (iterationsSinceProcessedSubmission > 0 && iterationsSinceProcessedSubmission % 6 == 0) {
{
const numMinutes = iterationsSinceProcessedSubmission / 6; const numMinutes = iterationsSinceProcessedSubmission / 6;
console.log( console.log(
`${numMinutes} minute${numMinutes > 1 ? 's' : ''} since ` + `${numMinutes} minute${numMinutes > 1 ? 's' : ''} since ` +
@ -297,6 +284,7 @@ async function run() {
}` }`
); );
} }
}
await new Promise((resolve) => setTimeout(resolve, 10000)); await new Promise((resolve) => setTimeout(resolve, 10000));
iterationsSinceProcessedSubmission++; iterationsSinceProcessedSubmission++;

View File

@ -1,111 +0,0 @@
import { join } from 'path';
import { exec, spawn } from 'child_process';
import util from 'util';
import { RunResult, timeoutSeconds } from '../index.js';
import { IRunner, IRunnerParams, IRunnerReturn } from './types.js';
import kill from 'tree-kill';
const execPromise = util.promisify(exec);
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 {
success: false,
runResult: { kind: 'CompileFailed', resultKindReason: buildErrorText }
};
}
console.log(`- RUN: ${params.mainClass}`);
const runCommand = `java -cp "${join(params.srcDir, 'build')}" ${params.mainClass}`;
try {
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();
});
const 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;
const 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`
});
}
});
const 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]');
}
}
}
};
} catch (error) {
return { success: false, runResult: { kind: 'RunError' } };
}
};

View File

@ -1,16 +0,0 @@
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

@ -4,7 +4,10 @@
"sourceMap": true, "sourceMap": true,
"outDir": "./build", "outDir": "./build",
"esModuleInterop": true, "esModuleInterop": true,
"strict": true "strict": true,
"paths": {
"@submissionRunner/*": ["../shared/submissionRunner/*"]
}
}, },
"include": ["./src/**/*"] "references": [{ "path": "../shared/submissionRunner" }]
} }

View File

@ -1,3 +1,2 @@
node_modules node_modules
build /build
.env

10
shared/extensionWeb/package-lock.json generated Normal file
View File

@ -0,0 +1,10 @@
{
"name": "extensionWeb",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"devDependencies": {}
}
}
}

View File

@ -0,0 +1,9 @@
{
"scripts": {
"build": "tsc"
},
"devDependencies": {
},
"dependencies": {
}
}

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"composite": true,
"target": "es2016",
"module": "commonjs",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./build",
"esModuleInterop": true,
"strict": true,
},
"include": ["/**/*.cts"]
}

2
shared/submissionRunner/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
node_modules
/build

View File

@ -1,8 +1,8 @@
import { join } from 'path'; import { join } from 'path';
import { exec, spawn } from 'child_process'; import { exec, spawn } from 'child_process';
import util from 'util'; import util from 'util';
import { RunResult, timeoutSeconds } from '../index.js'; import type { IRunner, IRunnerParams, IRunnerReturn, RunResult } from './types.cjs';
import { IRunner, IRunnerParams, IRunnerReturn } from './types.js'; import { timeoutSeconds } from './settings.cjs';
import kill from 'tree-kill'; import kill from 'tree-kill';
import os from 'os'; import os from 'os';
import fs from 'fs-extra'; import fs from 'fs-extra';
@ -21,7 +21,7 @@ interface IRunnerParamsCpp extends IRunnerParams {
export const runCpp: IRunner<IRunnerParamsCpp> = async function ( export const runCpp: IRunner<IRunnerParamsCpp> = async function (
params: IRunnerParamsCpp params: IRunnerParamsCpp
): IRunnerReturn { ): Promise<IRunnerReturn> {
const tmpDir = os.tmpdir(); const tmpDir = os.tmpdir();
const buildDir = join(tmpDir, 'bwcontest-cpp'); const buildDir = join(tmpDir, 'bwcontest-cpp');
if (fs.existsSync(buildDir)) { if (fs.existsSync(buildDir)) {

View File

@ -1,13 +1,13 @@
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import kill from 'tree-kill'; import kill from 'tree-kill';
import { RunResult, timeoutSeconds } from '../index.js'; import type { IRunner, IRunnerReturn, RunResult } from './types.cjs';
import { IRunner, IRunnerReturn } from './types.js'; import { timeoutSeconds } from './settings.cjs';
export const runCSharp: IRunner = async function (params: { export const runCSharp: IRunner = async function (params: {
srcDir: string; srcDir: string;
input: string; input: string;
outputCallback?: (data: string) => void; outputCallback?: (data: string) => void;
}): IRunnerReturn { }): Promise<IRunnerReturn> {
console.log(`- RUN: ${params.srcDir}`); console.log(`- RUN: ${params.srcDir}`);
const child = spawn('dotnet run', { shell: true, cwd: params.srcDir }); const child = spawn('dotnet run', { shell: true, cwd: params.srcDir });

View File

@ -1,13 +1,9 @@
import { join } from 'path'; import { join } from 'path';
import { exec, spawn } from 'child_process'; import { exec, spawn } from 'child_process';
import * as util from 'util'; import * as util from 'util';
import { import type { IRunner, IRunnerParams, IRunnerReturn, RunResult } from './types.cjs';
timeoutSeconds, import { timeoutSeconds } from './settings.cjs';
type IRunner,
type IRunnerParams,
type IRunnerReturn,
type RunResult
} from './types';
import kill = require('tree-kill'); import kill = require('tree-kill');
const execPromise = util.promisify(exec); const execPromise = util.promisify(exec);
@ -22,7 +18,7 @@ interface IRunnerParamsJava extends IRunnerParams {
export const runJava: IRunner<IRunnerParamsJava> = async function ( export const runJava: IRunner<IRunnerParamsJava> = async function (
params: IRunnerParamsJava params: IRunnerParamsJava
): IRunnerReturn { ): Promise<IRunnerReturn> {
console.log(`- BUILD: ${params.mainFile}`); console.log(`- BUILD: ${params.mainFile}`);
const compileCommand = `javac -cp ${join(params.srcDir, 'src')} ${params.mainFile} -d ${join(params.srcDir, 'build')}`; const compileCommand = `javac -cp ${join(params.srcDir, 'src')} ${params.mainFile} -d ${join(params.srcDir, 'build')}`;

118
shared/submissionRunner/package-lock.json generated Normal file
View File

@ -0,0 +1,118 @@
{
"name": "submissionRunner",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"fs-extra": "^11.2.0",
"tree-kill": "^1.2.2",
"typescript": "^5.4.2",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/fs-extra": "^11.0.4",
"@types/node": "20.x"
}
},
"node_modules/@types/fs-extra": {
"version": "11.0.4",
"resolved": "https://registry.npmjs.org/@types/fs-extra/-/fs-extra-11.0.4.tgz",
"integrity": "sha512-yTbItCNreRooED33qjunPthRcSjERP1r4MqCZc7wv0u2sUkzTFp45tgUfS5+r7FrZPdmCCNflLhVSP/o+SemsQ==",
"dev": true,
"dependencies": {
"@types/jsonfile": "*",
"@types/node": "*"
}
},
"node_modules/@types/jsonfile": {
"version": "6.1.4",
"resolved": "https://registry.npmjs.org/@types/jsonfile/-/jsonfile-6.1.4.tgz",
"integrity": "sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/node": {
"version": "20.11.25",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.25.tgz",
"integrity": "sha512-TBHyJxk2b7HceLVGFcpAUjsa5zIdsPWlR6XHfyGzd0SFu+/NFgQgMAl96MSDZgQDvJAvV6BKsFOrt6zIL09JDw==",
"dev": true,
"dependencies": {
"undici-types": "~5.26.4"
}
},
"node_modules/fs-extra": {
"version": "11.2.0",
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz",
"integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==",
"dependencies": {
"graceful-fs": "^4.2.0",
"jsonfile": "^6.0.1",
"universalify": "^2.0.0"
},
"engines": {
"node": ">=14.14"
}
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="
},
"node_modules/jsonfile": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
"integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
"dependencies": {
"universalify": "^2.0.0"
},
"optionalDependencies": {
"graceful-fs": "^4.1.6"
}
},
"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.4.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.2.tgz",
"integrity": "sha512-+2/g0Fds1ERlP6JsakQQDXjZdZMM+rqpamFZJEKh4kwTIn3iDkgKtby0CeNd5ATNZ4Ry1ax15TMx0W2V+miizQ==",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/undici-types": {
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
},
"node_modules/universalify": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
"engines": {
"node": ">= 10.0.0"
}
},
"node_modules/zod": {
"version": "3.22.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

View File

@ -0,0 +1,15 @@
{
"scripts": {
"build": "tsc"
},
"devDependencies": {
"@types/node": "20.x",
"@types/fs-extra": "^11.0.4"
},
"dependencies": {
"typescript": "^5.4.2",
"zod": "^3.22.4",
"tree-kill": "^1.2.2",
"fs-extra": "^11.2.0"
}
}

View File

@ -0,0 +1 @@
export const timeoutSeconds = 30;

View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"composite": true,
"target": "es2016",
"module": "commonjs",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./build",
"esModuleInterop": true,
"strict": true,
},
"include": ["/**/*.cts"]
}

View File

@ -0,0 +1,36 @@
import { z } from 'zod';
const RunResultKind = z.enum([
'CompileFailed',
'TimeLimitExceeded',
'Completed',
'SandboxError',
'RunError'
]);
export type RunResultKind = z.infer<typeof RunResultKind>;
export const RunResultZod = z
.object({
kind: RunResultKind,
output: z.string().optional(),
exitCode: z.number().optional(),
runtimeMilliseconds: z.number().optional(),
resultKindReason: z.string().optional()
})
.strict();
export type RunResult = z.infer<typeof RunResultZod>;
export interface IRunnerParams {
srcDir: string;
input: string;
outputCallback?: (data: string) => void;
}
export type IRunnerReturn =
{ success: true; killFunc: () => void; runResult: Promise<RunResult> } |
{ success: false; runResult: RunResult };
export type IRunner<T extends IRunnerParams = IRunnerParams> =
(params: T) => Promise<IRunnerReturn>;

View File

@ -1,6 +1,9 @@
FROM ubuntu:22.04 FROM ubuntu:22.04
WORKDIR /app # Setup
RUN mkdir web
WORKDIR /app/web
RUN apt-get update RUN apt-get update
RUN apt-get install curl -y RUN apt-get install curl -y
@ -11,9 +14,26 @@ RUN apt-get install nodejs git -y
RUN git config --global user.name "Admin" RUN git config --global user.name "Admin"
RUN git config --global user.email noemail@example.com RUN git config --global user.email noemail@example.com
COPY package*.json ./ # Prep Web
COPY ./web/package*.json ./
RUN npm install RUN npm install
COPY . . COPY ./web/ .
# Prep extensionWeb
WORKDIR /app
RUN mkdir shared
RUN mkdir shared/extensionWeb
WORKDIR /app/shared/extensionWeb
COPY ./shared/extensionWeb/package*.json .
RUN npm install
COPY ./shared/extensionWeb/ .
# Env/Build/Run
WORKDIR /app/web
ENV PORT=3000 ENV PORT=3000
EXPOSE 3000 EXPOSE 3000

View File

@ -10,7 +10,9 @@ services:
- POSTGRES_DB=bwcontest - POSTGRES_DB=bwcontest
restart: unless-stopped restart: unless-stopped
web: web:
build: . build:
context: ../
dockerfile: ./web/Dockerfile
ports: ports:
- 3000:3000 - 3000:3000
- 7006:7006 - 7006:7006

View File

@ -1,4 +1,5 @@
#!/bin/bash #!/bin/bash
# THIS FILE MUST USE LF LINE SEPARATORS!
npx prisma db push --accept-data-loss npx prisma db push --accept-data-loss
npx prisma generate npx prisma generate
BODY_SIZE_LIMIT=Infinity INIT=true node build BODY_SIZE_LIMIT=Infinity INIT=true node build

View File

@ -1,29 +0,0 @@
export type FullStateForExtension = {
contestState: ContestStateForExtension;
submissions: SubmissionForExtension[];
};
export type ProblemNameForExtension = {
id: number;
friendlyName: string;
};
export type ContestStateForExtension = {
startTime: Date | null;
endTime: Date | null;
problems: ProblemNameForExtension[];
isActive: boolean;
isScoreboardFrozen: boolean;
};
export type SubmissionStateForExtension = 'Processing' | 'Correct' | 'Incorrect';
export type SubmissionForExtension = {
id: number;
contestId: number;
teamId: number;
problemId: number;
createdAt: Date;
state: SubmissionStateForExtension;
message: string | null;
};

View File

@ -1,5 +1,5 @@
import type { SubmissionState } from '@prisma/client'; import type { SubmissionState } from '@prisma/client';
import type { SubmissionStateForExtension } from './contestMonitorSharedTypes'; import type { SubmissionStateForExtension } from '@extensionWeb/contestMonitorTypes.cjs';
export function convertSubmissionStateForExtension( export function convertSubmissionStateForExtension(
state: SubmissionState state: SubmissionState

View File

@ -5,7 +5,7 @@ import type {
ContestStateForExtension, ContestStateForExtension,
FullStateForExtension, FullStateForExtension,
SubmissionForExtension SubmissionForExtension
} from '$lib/contestMonitor/contestMonitorSharedTypes'; } from '@extensionWeb/contestMonitorTypes.cjs';
import { convertSubmissionStateForExtension } from '$lib/contestMonitor/contestMonitorUtils'; import { convertSubmissionStateForExtension } from '$lib/contestMonitor/contestMonitorUtils';
export const GET = (async ({ params }) => { export const GET = (async ({ params }) => {

View File

@ -3,7 +3,7 @@ import { error, json } from '@sveltejs/kit';
import type { RequestHandler } from './$types'; import type { RequestHandler } from './$types';
import { z } from 'zod'; import { z } from 'zod';
import { SubmissionState } from '@prisma/client'; import { SubmissionState } from '@prisma/client';
import type { SubmissionForExtension } from '$lib/contestMonitor/contestMonitorSharedTypes'; import type { SubmissionForExtension } from '@extensionWeb/contestMonitorTypes.cjs';
import { convertSubmissionStateForExtension } from '$lib/contestMonitor/contestMonitorUtils'; import { convertSubmissionStateForExtension } from '$lib/contestMonitor/contestMonitorUtils';
const submitPostData = z.object({ const submitPostData = z.object({

View File

@ -6,7 +6,10 @@ const config = {
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
kit: { kit: {
adapter: adapter() adapter: adapter(),
alias: {
'@extensionWeb/*': '../shared/extensionWeb/*'
}
}, },
vitePlugin: { vitePlugin: {
inspector: true inspector: true

View File

@ -10,6 +10,7 @@
"sourceMap": true, "sourceMap": true,
"strict": true "strict": true
} }
// Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
// //
// If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes