From 2d839e279dda0ed2b6900c1f74db84a56b379317 Mon Sep 17 00:00:00 2001 From: orosmatthew Date: Sat, 17 Feb 2024 15:43:38 -0500 Subject: [PATCH] [extension] Copy runners from sandbox --- extension/bwcontest/src/problemPanel.ts | 105 ++++++++----- extension/bwcontest/src/run/cpp.ts | 187 +++++++++++++++--------- extension/bwcontest/src/run/csharp.ts | 133 ++++++++++------- extension/bwcontest/src/run/java.ts | 162 +++++++++++--------- extension/bwcontest/src/run/types.ts | 31 ++++ 5 files changed, 390 insertions(+), 228 deletions(-) create mode 100644 extension/bwcontest/src/run/types.ts diff --git a/extension/bwcontest/src/problemPanel.ts b/extension/bwcontest/src/problemPanel.ts index 289a9e8..c6a0225 100644 --- a/extension/bwcontest/src/problemPanel.ts +++ b/extension/bwcontest/src/problemPanel.ts @@ -157,15 +157,10 @@ export class BWPanel { let killFunc: (() => void) | undefined; if (teamData.language === 'Java') { - killFunc = await runJava( - join( - repoDir, - 'BWContest', - teamData.contestId.toString(), - teamData.teamId.toString(), - problem.pascalName - ), - join( + const res = await runJava({ + input, + mainClass: problem.pascalName, + mainFile: join( repoDir, 'BWContest', teamData.contestId.toString(), @@ -173,53 +168,87 @@ export class BWPanel { problem.pascalName, `${problem.pascalName}.java` ), - problem.pascalName, - input, - (data: string) => { - outputBuffer.push(data); - this.webviewPostMessage({ msg: 'onRunningOutput', data: outputBuffer.join('') }); - }, - () => { - this.runningProgram = undefined; - this.webviewPostMessage({ msg: 'onRunningDone' }); - } - ); - } else if (teamData.language === 'CSharp') { - killFunc = await runCSharp( - join( + srcDir: join( repoDir, 'BWContest', teamData.contestId.toString(), teamData.teamId.toString(), problem.pascalName ), - input, - (data: string) => { + outputCallback: (data) => { outputBuffer.push(data); this.webviewPostMessage({ msg: 'onRunningOutput', data: outputBuffer.join('') }); - }, - () => { + } + }); + if (res.success === true) { + killFunc = res.killFunc; + res.runResult.then(() => { this.runningProgram = undefined; this.webviewPostMessage({ msg: 'onRunningDone' }); + }) + } else { + this.runningProgram = undefined; + this.webviewPostMessage({ + msg: 'onRunningOutput', + data: `${res.runResult.kind}:\n${res.runResult.output}` + }); + this.webviewPostMessage({ msg: 'onRunningDone' }); + } + } else if (teamData.language === 'CSharp') { + const res = await runCSharp({ + input, + srcDir: join( + repoDir, + 'BWContest', + teamData.contestId.toString(), + teamData.teamId.toString(), + problem.pascalName + ), + outputCallback: (data) => { + outputBuffer.push(data); + this.webviewPostMessage({ msg: 'onRunningOutput', data: outputBuffer.join('') }); } - ); + }) + if (res.success === true) { + killFunc = res.killFunc; + res.runResult.then(() => { + this.runningProgram = undefined; + this.webviewPostMessage({ msg: 'onRunningDone' }); + }) + } else { + this.runningProgram = undefined; + this.webviewPostMessage({ + msg: 'onRunningOutput', + data: `${res.runResult.kind}:\n${res.runResult.output}` + }); + this.webviewPostMessage({ msg: 'onRunningDone' }); + } } else if (teamData.language === 'CPP') { - killFunc = await runCpp( - join(repoDir, 'BWContest', teamData.contestId.toString(), teamData.teamId.toString()), - problem.pascalName, + const res = await runCpp({ input, - process.platform === 'win32' ? 'VisualStudio' : 'GCC', - (data: string) => { + cppPlatform: process.platform === 'win32' ? 'VisualStudio' : 'GCC', + problemName: problem.pascalName, + srcDir: join(repoDir, 'BWContest', teamData.contestId.toString(), teamData.teamId.toString()), + outputCallback: (data) => { outputBuffer.push(data); this.webviewPostMessage({ msg: 'onRunningOutput', data: outputBuffer.join('') }); - }, - () => { + } + }) + if (res.success === true) { + killFunc = res.killFunc; + res.runResult.then(() => { this.runningProgram = undefined; this.webviewPostMessage({ msg: 'onRunningDone' }); - } - ); + }) + } else { + this.runningProgram = undefined; + this.webviewPostMessage({ + msg: 'onRunningOutput', + data: `${res.runResult.kind}:\n${res.runResult.output}` + }); + this.webviewPostMessage({ msg: 'onRunningDone' }); + } } - if (killFunc !== undefined) { this.runningProgram = { problemId: problemId, diff --git a/extension/bwcontest/src/run/cpp.ts b/extension/bwcontest/src/run/cpp.ts index 3a786b4..0b7a4cd 100644 --- a/extension/bwcontest/src/run/cpp.ts +++ b/extension/bwcontest/src/run/cpp.ts @@ -1,93 +1,140 @@ -import * as fs from 'fs-extra'; import { join } from 'path'; -import os = require('os'); import { exec, spawn } from 'child_process'; -import util = require('node:util'); +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'; -export async function runCpp( - srcDir: string, - problemName: string, - input: string, - cppPlatform: CppPlatform, - outputCallback: (data: string) => void, - doneCallback: () => void -): Promise<(() => void) | undefined> { - const tempDir = os.tmpdir(); - const buildDir = join(tempDir, 'bwcontest_cpp'); - if (await fs.exists(buildDir)) { - await fs.remove(buildDir); - } - await fs.mkdir(buildDir); +interface IRunnerParamsCpp extends IRunnerParams { + srcDir: string; + problemName: string; + input: string; + cppPlatform: CppPlatform; + outputCallback?: (data: string) => void; +} - const configureCommand = `cmake -S ${srcDir} -B ${buildDir}`; +export const runCpp: IRunner = 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 (error) { - outputCallback('[Configure Error]\n\n'); - outputCallback(String(error)); - return; + } 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 ${problemName}`; + const compileCommand = `cmake --build ${buildDir} --target ${params.problemName}`; try { await execPromise(compileCommand); - } catch (error) { - outputCallback('[Compile Error]\n\n'); - outputCallback(String(error)); - return; + } catch (e) { + const buildErrorText = e?.toString() ?? 'Unknown build errors.'; + console.log('Build errors: ' + buildErrorText); + return { + success: false, + runResult: { kind: 'CompileFailed', resultKindReason: buildErrorText } + }; } - let runCommand: string = ''; - if (cppPlatform === 'VisualStudio') { - runCommand = `${join(buildDir, 'Debug', `${problemName}.exe`)}`; - } else if (cppPlatform === 'GCC') { - runCommand = `${(join(buildDir), problemName)}`; + 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 child = spawn(runCommand, { shell: true }); - child.stdout.setEncoding('utf8'); - child.stdout.on('data', (data) => { - outputCallback(data.toString()); - }); - child.stderr.setEncoding('utf8'); - child.stderr.on('data', (data) => { - outputCallback(data.toString()); - }); - child.stdin.write(input); - child.stdin.end(); + const runStartTime = performance.now(); + child.stdin.write(params.input); + child.stdin.end(); - let done = false; + let timeLimitExceeded = false; + let completedNormally = false; - child.on('close', () => { - if (done === false) { - done = true; - doneCallback(); - } - }); + return { + success: true, + runResult: new Promise((resolve) => { + child.on('close', () => { + completedNormally = !timeLimitExceeded; - setTimeout(() => { - if (done === false) { - console.log('\n[30 seconds reached, killing process...]'); - done = true; - if (child.pid) { - kill(child.pid); + 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]'); + } + } } - outputCallback('\n[Timeout after 30 seconds]'); - doneCallback(); - } - }, 30000); - - return () => { - if (child.pid) { - done = true; - kill(child.pid); - outputCallback('\n[Manually stopped]'); - doneCallback(); - } - }; -} + }; + } catch (error) { + return { success: false, runResult: { kind: 'RunError' } }; + } +}; diff --git a/extension/bwcontest/src/run/csharp.ts b/extension/bwcontest/src/run/csharp.ts index cb6d982..305f624 100644 --- a/extension/bwcontest/src/run/csharp.ts +++ b/extension/bwcontest/src/run/csharp.ts @@ -1,62 +1,89 @@ -import * as fs from 'fs-extra'; -import { join } from 'path'; -import os = require('os'); import { spawn } from 'child_process'; import kill = require('tree-kill'); +import { timeoutSeconds, type IRunner, type IRunnerReturn, type RunResult } from './types'; -export async function runCSharp( - srcDir: string, - input: string, - outputCallback: (data: string) => void, - doneCallback: () => void -): Promise<(() => void) | undefined> { - const tempDir = os.tmpdir(); - const buildDir = join(tempDir, 'bwcontest_csharp'); - if (await fs.exists(buildDir)) { - await fs.remove(buildDir); - } - await fs.mkdir(buildDir); +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 }); - const child = spawn('dotnet run', { shell: true, cwd: 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()); + }); - child.stdout.setEncoding('utf8'); - child.stdout.on('data', (data) => { - outputCallback(data.toString()); - }); - child.stderr.setEncoding('utf8'); - child.stderr.on('data', (data) => { - outputCallback(data.toString()); - }); - child.stdin.write(input); - child.stdin.end(); + const runStartTime = performance.now(); + child.stdin.write(params.input); + child.stdin.end(); - let done = false; + let timeLimitExceeded = false; + let completedNormally = false; - child.on('close', () => { - if (done === false) { - done = true; - doneCallback(); - } - }); + return { + success: true, + runResult: new Promise((resolve) => { + child.on('close', () => { + completedNormally = !timeLimitExceeded; - setTimeout(() => { - if (done === false) { - console.log('\n[30 seconds reached, killing process...]'); - done = true; - if (child.pid) { - kill(child.pid); + 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]'); + } + } } - outputCallback('\n[Timeout after 30 seconds]'); - doneCallback(); - } - }, 30000); - - return () => { - if (child.pid) { - done = true; - kill(child.pid); - outputCallback('\n[Manually stopped]'); - doneCallback(); - } - }; -} + }; + } catch (error) { + return { success: false, runResult: { kind: 'RunError' } }; + } +}; diff --git a/extension/bwcontest/src/run/java.ts b/extension/bwcontest/src/run/java.ts index 181618b..d0e7798 100644 --- a/extension/bwcontest/src/run/java.ts +++ b/extension/bwcontest/src/run/java.ts @@ -1,84 +1,112 @@ -import * as fs from 'fs-extra'; import { join } from 'path'; -import os = require('os'); import { exec, spawn } from 'child_process'; -import { extensionSettings } from '../extension'; -import { error } from 'console'; -import util = require('node:util'); +import * as util from 'util'; +import { timeoutSeconds, type IRunner, type IRunnerParams, type IRunnerReturn, type RunResult } from './types'; import kill = require('tree-kill'); const execPromise = util.promisify(exec); -export async function runJava( - srcDir: string, - mainFile: string, - mainClass: string, - input: string, - outputCallback: (data: string) => void, - doneCallback: () => void -): Promise<(() => void) | undefined> { - const javaPath = extensionSettings().javaPath; - if (javaPath == '') { - throw error('Java path not set'); - } - const tempDir = os.tmpdir(); - const buildDir = join(tempDir, 'bwcontest_java'); - if (await fs.exists(buildDir)) { - await fs.remove(buildDir); - } - await fs.mkdir(buildDir); +interface IRunnerParamsJava extends IRunnerParams { + srcDir: string; + mainFile: string; + mainClass: string; + input: string; + outputCallback?: (data: string) => void; +} + +export const runJava: IRunner = 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')}`; - const compileCommand = `${join(javaPath, 'javac')} -cp ${srcDir} ${mainFile} -d ${buildDir}`; try { await execPromise(compileCommand); - } catch (error) { - outputCallback('[Compile Error]\n\n'); - outputCallback(String(error)); - return; + } catch (e) { + const buildErrorText = e?.toString() ?? 'Unknown build errors.'; + console.log('Build errors: ' + buildErrorText); + return { + success: false, + runResult: { kind: 'CompileFailed', resultKindReason: buildErrorText } + }; } - const runCommand = `${join(javaPath, 'java')} -cp "${buildDir}" ${mainClass}`; + console.log(`- RUN: ${params.mainClass}`); + const runCommand = `java -cp "${join(params.srcDir, 'build')}" ${params.mainClass}`; - const child = spawn(runCommand, { shell: true }); + 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()); + }); - child.stdout.setEncoding('utf8'); - child.stdout.on('data', (data) => { - outputCallback(data.toString()); - }); - child.stderr.setEncoding('utf8'); - child.stderr.on('data', (data) => { - outputCallback(data.toString()); - }); - child.stdin.write(input); - child.stdin.end(); + const runStartTime = performance.now(); + child.stdin.write(params.input); + child.stdin.end(); - let done = false; + let timeLimitExceeded = false; + let completedNormally = false; - child.on('close', () => { - if (done === false) { - done = true; - doneCallback(); - } - }); + return { + success: true, + runResult: new Promise((resolve) => { + child.on('close', () => { + completedNormally = !timeLimitExceeded; - setTimeout(() => { - if (done === false) { - console.log('\n[30 seconds reached, killing process...]'); - done = true; - if (child.pid) { - kill(child.pid); + 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]'); + } + } } - outputCallback('\n[Timeout after 30 seconds]'); - doneCallback(); - } - }, 30000); - - return () => { - if (child.pid) { - done = true; - kill(child.pid); - outputCallback('\n[Manually stopped]'); - doneCallback(); - } - }; -} + }; + } catch (error) { + return { success: false, runResult: { kind: 'RunError' } }; + } +}; diff --git a/extension/bwcontest/src/run/types.ts b/extension/bwcontest/src/run/types.ts new file mode 100644 index 0000000..9e04924 --- /dev/null +++ b/extension/bwcontest/src/run/types.ts @@ -0,0 +1,31 @@ +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 } + | { success: false; runResult: RunResult } +>; + +export interface IRunner { + (params: T): IRunnerReturn; +}