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:
parent
6e95a955a8
commit
22bc7460df
22
.dockerignore
Normal file
22
.dockerignore
Normal 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
|
@ -7,7 +7,7 @@ import {
|
||||
ProblemNameForExtension,
|
||||
FullStateForExtension,
|
||||
SubmissionForExtension
|
||||
} from './contestMonitorSharedTypes';
|
||||
} from '@extensionWeb/contestMonitorTypes.cjs';
|
||||
import { LiteEvent } from '../utilities/LiteEvent';
|
||||
|
||||
export type ContestTeamState = {
|
||||
|
@ -2,11 +2,11 @@ import * as vscode from 'vscode';
|
||||
import { getNonce } from './getNonce';
|
||||
import urlJoin from 'url-join';
|
||||
import { extensionSettings } from './extension';
|
||||
import { runJava } from './run/java';
|
||||
import { runJava } from '@submissionRunner/java.cjs';
|
||||
import { join } from 'path';
|
||||
import { submitProblem } from './submit';
|
||||
import { runCSharp } from './run/csharp';
|
||||
import { runCpp } from './run/cpp';
|
||||
import { runCSharp } from '@submissionRunner/csharp.cjs';
|
||||
import { runCpp } from '@submissionRunner/cpp.cjs';
|
||||
import { TeamData } from './sharedTypes';
|
||||
import outputPanelLog from './outputPanelLog';
|
||||
import { recordInitialSubmission } from './contestMonitor/contestStateSyncManager';
|
||||
|
@ -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' } };
|
||||
}
|
||||
};
|
@ -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' } };
|
||||
}
|
||||
};
|
@ -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;
|
||||
}
|
@ -6,11 +6,19 @@
|
||||
"lib": ["ES2020"],
|
||||
"sourceMap": true,
|
||||
"rootDir": "src",
|
||||
"strict": true /* enable all strict type-checking options */
|
||||
"strict": true, /* enable all strict type-checking options */
|
||||
/* Additional Checks */
|
||||
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
|
||||
// "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" }
|
||||
]
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
FROM ubuntu:22.04
|
||||
|
||||
WORKDIR /app
|
||||
# Setup
|
||||
|
||||
RUN mkdir sandbox
|
||||
WORKDIR /app/sandbox
|
||||
|
||||
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.email noemail@example.com
|
||||
|
||||
WORKDIR /app
|
||||
# Prep Sandbox
|
||||
|
||||
COPY package*.json ./
|
||||
WORKDIR /app/sandbox
|
||||
|
||||
COPY ./sandbox/package*.json ./
|
||||
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
|
||||
CMD ["node", "build"]
|
||||
RUN chmod +x ./docker/entry.sh
|
||||
CMD ["./docker/entry.sh"]
|
@ -1,7 +1,9 @@
|
||||
version: '3'
|
||||
services:
|
||||
sandbox:
|
||||
build: .
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: ./sandbox/Dockerfile
|
||||
environment:
|
||||
- ADMIN_URL=${ADMIN_URL}
|
||||
- REPO_URL=${REPO_URL}
|
||||
|
3
sandbox/docker/entry.sh
Normal file
3
sandbox/docker/entry.sh
Normal file
@ -0,0 +1,3 @@
|
||||
#!/bin/bash
|
||||
# THIS FILE MUST USE LF LINE SEPARATORS!
|
||||
node ./build/sandbox.cjs
|
1068
sandbox/package-lock.json
generated
1068
sandbox/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,18 +5,23 @@
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"build": "esbuild src/index.ts --bundle --outfile=build/sandbox.cjs --format=cjs --platform=node",
|
||||
"format": "prettier --write .",
|
||||
"lint": "prettier --check . && eslint .",
|
||||
"start": "node build"
|
||||
"start": "node ./build/sandbox.cjs"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"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/node": "^20.11.2",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.1",
|
||||
"@typescript-eslint/parser": "^7.0.1",
|
||||
"esbuild": "^0.19.11",
|
||||
"eslint": "^8.56.0",
|
||||
"prettier": "^3.2.2",
|
||||
"typescript": "^5.3.3"
|
||||
@ -24,6 +29,7 @@
|
||||
"dependencies": {
|
||||
"dotenv": "^16.3.1",
|
||||
"fs-extra": "^11.2.0",
|
||||
"rollup": "^4.12.1",
|
||||
"simple-git": "^3.22.0",
|
||||
"tree-kill": "^1.2.2",
|
||||
"url-join": "^5.0.0",
|
||||
|
@ -1,39 +1,19 @@
|
||||
import dotenv from 'dotenv';
|
||||
import 'dotenv/config';
|
||||
import fs from 'fs-extra';
|
||||
import urlJoin from 'url-join';
|
||||
import { z } from 'zod';
|
||||
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';
|
||||
import { runCpp } from './run/cpp.js';
|
||||
|
||||
export const timeoutSeconds = 30;
|
||||
|
||||
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();
|
||||
import { runJava } from '@submissionRunner/java.cjs';
|
||||
import { runCSharp } from '@submissionRunner/csharp.cjs';
|
||||
import { runCpp } from '@submissionRunner/cpp.cjs';
|
||||
import { RunResult, RunResultZod } from '@submissionRunner/types.cjs';
|
||||
import { z } from 'zod';
|
||||
|
||||
const submissionPostData = z
|
||||
.object({
|
||||
submissionId: z.number(),
|
||||
result: RunResult
|
||||
result: RunResultZod
|
||||
})
|
||||
.strict();
|
||||
|
||||
@ -59,7 +39,6 @@ const submissionGetData = z
|
||||
})
|
||||
.strict();
|
||||
|
||||
export type RunResult = z.infer<typeof RunResult>;
|
||||
type SubmissionGetData = z.infer<typeof submissionGetData>;
|
||||
type SubmissionPostData = z.infer<typeof submissionPostData>;
|
||||
|
||||
@ -78,10 +57,12 @@ async function fetchQueuedSubmission(): Promise<SubmissionGetData | undefined> {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const data = submissionGetData.parse(await res.json());
|
||||
const json = await res.json();
|
||||
const data = submissionGetData.parse(json);
|
||||
if (!data.success) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@ -205,9 +186,14 @@ function validateEnv(): boolean {
|
||||
return process.env.ADMIN_URL !== undefined && process.env.REPO_URL !== undefined;
|
||||
}
|
||||
|
||||
dotenv.config();
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
@ -287,15 +273,17 @@ async function run() {
|
||||
break;
|
||||
case SubmissionProcessingResult.NoSubmissions:
|
||||
if (iterationsSinceProcessedSubmission > 0 && iterationsSinceProcessedSubmission % 6 == 0) {
|
||||
const numMinutes = iterationsSinceProcessedSubmission / 6;
|
||||
console.log(
|
||||
`${numMinutes} minute${numMinutes > 1 ? 's' : ''} since ` +
|
||||
`${
|
||||
anySubmissionsProcessed
|
||||
? `last submission processed`
|
||||
: `sandbox startup with no submissions`
|
||||
}`
|
||||
);
|
||||
{
|
||||
const numMinutes = iterationsSinceProcessedSubmission / 6;
|
||||
console.log(
|
||||
`${numMinutes} minute${numMinutes > 1 ? 's' : ''} since ` +
|
||||
`${
|
||||
anySubmissionsProcessed
|
||||
? `last submission processed`
|
||||
: `sandbox startup with no submissions`
|
||||
}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 10000));
|
||||
|
@ -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' } };
|
||||
}
|
||||
};
|
16
sandbox/src/run/types.d.ts
vendored
16
sandbox/src/run/types.d.ts
vendored
@ -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;
|
||||
}
|
@ -4,7 +4,10 @@
|
||||
"sourceMap": true,
|
||||
"outDir": "./build",
|
||||
"esModuleInterop": true,
|
||||
"strict": true
|
||||
"strict": true,
|
||||
"paths": {
|
||||
"@submissionRunner/*": ["../shared/submissionRunner/*"]
|
||||
}
|
||||
},
|
||||
"include": ["./src/**/*"]
|
||||
"references": [{ "path": "../shared/submissionRunner" }]
|
||||
}
|
||||
|
@ -1,3 +1,2 @@
|
||||
node_modules
|
||||
build
|
||||
.env
|
||||
/build
|
10
shared/extensionWeb/package-lock.json
generated
Normal file
10
shared/extensionWeb/package-lock.json
generated
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"name": "extensionWeb",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"devDependencies": {}
|
||||
}
|
||||
}
|
||||
}
|
9
shared/extensionWeb/package.json
Normal file
9
shared/extensionWeb/package.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"scripts": {
|
||||
"build": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
},
|
||||
"dependencies": {
|
||||
}
|
||||
}
|
14
shared/extensionWeb/tsconfig.json
Normal file
14
shared/extensionWeb/tsconfig.json
Normal 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
2
shared/submissionRunner/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
/build
|
@ -1,8 +1,8 @@
|
||||
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 type { IRunner, IRunnerParams, IRunnerReturn, RunResult } from './types.cjs';
|
||||
import { timeoutSeconds } from './settings.cjs';
|
||||
import kill from 'tree-kill';
|
||||
import os from 'os';
|
||||
import fs from 'fs-extra';
|
||||
@ -21,7 +21,7 @@ interface IRunnerParamsCpp extends IRunnerParams {
|
||||
|
||||
export const runCpp: IRunner<IRunnerParamsCpp> = async function (
|
||||
params: IRunnerParamsCpp
|
||||
): IRunnerReturn {
|
||||
): Promise<IRunnerReturn> {
|
||||
const tmpDir = os.tmpdir();
|
||||
const buildDir = join(tmpDir, 'bwcontest-cpp');
|
||||
if (fs.existsSync(buildDir)) {
|
@ -1,13 +1,13 @@
|
||||
import { spawn } from 'child_process';
|
||||
import kill from 'tree-kill';
|
||||
import { RunResult, timeoutSeconds } from '../index.js';
|
||||
import { IRunner, IRunnerReturn } from './types.js';
|
||||
import type { IRunner, IRunnerReturn, RunResult } from './types.cjs';
|
||||
import { timeoutSeconds } from './settings.cjs';
|
||||
|
||||
export const runCSharp: IRunner = async function (params: {
|
||||
srcDir: string;
|
||||
input: string;
|
||||
outputCallback?: (data: string) => void;
|
||||
}): IRunnerReturn {
|
||||
}): Promise<IRunnerReturn> {
|
||||
console.log(`- RUN: ${params.srcDir}`);
|
||||
const child = spawn('dotnet run', { shell: true, cwd: params.srcDir });
|
||||
|
@ -1,13 +1,9 @@
|
||||
import { join } from 'path';
|
||||
import { exec, spawn } from 'child_process';
|
||||
import * as util from 'util';
|
||||
import {
|
||||
timeoutSeconds,
|
||||
type IRunner,
|
||||
type IRunnerParams,
|
||||
type IRunnerReturn,
|
||||
type RunResult
|
||||
} from './types';
|
||||
import type { IRunner, IRunnerParams, IRunnerReturn, RunResult } from './types.cjs';
|
||||
import { timeoutSeconds } from './settings.cjs';
|
||||
|
||||
import kill = require('tree-kill');
|
||||
|
||||
const execPromise = util.promisify(exec);
|
||||
@ -22,7 +18,7 @@ interface IRunnerParamsJava extends IRunnerParams {
|
||||
|
||||
export const runJava: IRunner<IRunnerParamsJava> = async function (
|
||||
params: IRunnerParamsJava
|
||||
): IRunnerReturn {
|
||||
): Promise<IRunnerReturn> {
|
||||
console.log(`- BUILD: ${params.mainFile}`);
|
||||
const compileCommand = `javac -cp ${join(params.srcDir, 'src')} ${params.mainFile} -d ${join(params.srcDir, 'build')}`;
|
||||
|
118
shared/submissionRunner/package-lock.json
generated
Normal file
118
shared/submissionRunner/package-lock.json
generated
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
15
shared/submissionRunner/package.json
Normal file
15
shared/submissionRunner/package.json
Normal 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"
|
||||
}
|
||||
}
|
1
shared/submissionRunner/settings.cts
Normal file
1
shared/submissionRunner/settings.cts
Normal file
@ -0,0 +1 @@
|
||||
export const timeoutSeconds = 30;
|
14
shared/submissionRunner/tsconfig.json
Normal file
14
shared/submissionRunner/tsconfig.json
Normal 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"]
|
||||
}
|
36
shared/submissionRunner/types.cts
Normal file
36
shared/submissionRunner/types.cts
Normal 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>;
|
@ -1,6 +1,9 @@
|
||||
FROM ubuntu:22.04
|
||||
|
||||
WORKDIR /app
|
||||
# Setup
|
||||
|
||||
RUN mkdir web
|
||||
WORKDIR /app/web
|
||||
|
||||
RUN apt-get update
|
||||
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.email noemail@example.com
|
||||
|
||||
COPY package*.json ./
|
||||
# Prep Web
|
||||
|
||||
COPY ./web/package*.json ./
|
||||
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
|
||||
EXPOSE 3000
|
||||
|
@ -10,7 +10,9 @@ services:
|
||||
- POSTGRES_DB=bwcontest
|
||||
restart: unless-stopped
|
||||
web:
|
||||
build: .
|
||||
build:
|
||||
context: ../
|
||||
dockerfile: ./web/Dockerfile
|
||||
ports:
|
||||
- 3000:3000
|
||||
- 7006:7006
|
||||
|
@ -1,4 +1,5 @@
|
||||
#!/bin/bash
|
||||
# THIS FILE MUST USE LF LINE SEPARATORS!
|
||||
npx prisma db push --accept-data-loss
|
||||
npx prisma generate
|
||||
BODY_SIZE_LIMIT=Infinity INIT=true node build
|
@ -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;
|
||||
};
|
@ -1,5 +1,5 @@
|
||||
import type { SubmissionState } from '@prisma/client';
|
||||
import type { SubmissionStateForExtension } from './contestMonitorSharedTypes';
|
||||
import type { SubmissionStateForExtension } from '@extensionWeb/contestMonitorTypes.cjs';
|
||||
|
||||
export function convertSubmissionStateForExtension(
|
||||
state: SubmissionState
|
||||
|
@ -5,7 +5,7 @@ import type {
|
||||
ContestStateForExtension,
|
||||
FullStateForExtension,
|
||||
SubmissionForExtension
|
||||
} from '$lib/contestMonitor/contestMonitorSharedTypes';
|
||||
} from '@extensionWeb/contestMonitorTypes.cjs';
|
||||
import { convertSubmissionStateForExtension } from '$lib/contestMonitor/contestMonitorUtils';
|
||||
|
||||
export const GET = (async ({ params }) => {
|
||||
|
@ -3,7 +3,7 @@ import { error, json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { z } from 'zod';
|
||||
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';
|
||||
|
||||
const submitPostData = z.object({
|
||||
|
@ -6,7 +6,10 @@ const config = {
|
||||
preprocess: vitePreprocess(),
|
||||
|
||||
kit: {
|
||||
adapter: adapter()
|
||||
adapter: adapter(),
|
||||
alias: {
|
||||
'@extensionWeb/*': '../shared/extensionWeb/*'
|
||||
}
|
||||
},
|
||||
vitePlugin: {
|
||||
inspector: true
|
||||
|
@ -10,6 +10,7 @@
|
||||
"sourceMap": true,
|
||||
"strict": true
|
||||
}
|
||||
|
||||
// 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
|
||||
|
Loading…
Reference in New Issue
Block a user