[extension] Copy runners from sandbox

This commit is contained in:
orosmatthew 2024-02-17 15:43:38 -05:00
parent 084ef0fdc4
commit 2d839e279d
5 changed files with 390 additions and 228 deletions

View File

@ -157,15 +157,10 @@ export class BWPanel {
let killFunc: (() => void) | undefined; let killFunc: (() => void) | undefined;
if (teamData.language === 'Java') { if (teamData.language === 'Java') {
killFunc = await runJava( const res = await runJava({
join( input,
repoDir, mainClass: problem.pascalName,
'BWContest', mainFile: join(
teamData.contestId.toString(),
teamData.teamId.toString(),
problem.pascalName
),
join(
repoDir, repoDir,
'BWContest', 'BWContest',
teamData.contestId.toString(), teamData.contestId.toString(),
@ -173,53 +168,87 @@ export class BWPanel {
problem.pascalName, problem.pascalName,
`${problem.pascalName}.java` `${problem.pascalName}.java`
), ),
problem.pascalName, srcDir: join(
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(
repoDir, repoDir,
'BWContest', 'BWContest',
teamData.contestId.toString(), teamData.contestId.toString(),
teamData.teamId.toString(), teamData.teamId.toString(),
problem.pascalName problem.pascalName
), ),
input, outputCallback: (data) => {
(data: string) => {
outputBuffer.push(data); outputBuffer.push(data);
this.webviewPostMessage({ msg: 'onRunningOutput', data: outputBuffer.join('') }); this.webviewPostMessage({ msg: 'onRunningOutput', data: outputBuffer.join('') });
}, }
() => { });
if (res.success === true) {
killFunc = res.killFunc;
res.runResult.then(() => {
this.runningProgram = undefined; this.runningProgram = undefined;
this.webviewPostMessage({ msg: 'onRunningDone' }); 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') { } else if (teamData.language === 'CPP') {
killFunc = await runCpp( const res = await runCpp({
join(repoDir, 'BWContest', teamData.contestId.toString(), teamData.teamId.toString()),
problem.pascalName,
input, input,
process.platform === 'win32' ? 'VisualStudio' : 'GCC', cppPlatform: process.platform === 'win32' ? 'VisualStudio' : 'GCC',
(data: string) => { problemName: problem.pascalName,
srcDir: join(repoDir, 'BWContest', teamData.contestId.toString(), teamData.teamId.toString()),
outputCallback: (data) => {
outputBuffer.push(data); outputBuffer.push(data);
this.webviewPostMessage({ msg: 'onRunningOutput', data: outputBuffer.join('') }); this.webviewPostMessage({ msg: 'onRunningOutput', data: outputBuffer.join('') });
}, }
() => { })
if (res.success === true) {
killFunc = res.killFunc;
res.runResult.then(() => {
this.runningProgram = undefined; this.runningProgram = undefined;
this.webviewPostMessage({ msg: 'onRunningDone' }); 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) { if (killFunc !== undefined) {
this.runningProgram = { this.runningProgram = {
problemId: problemId, problemId: problemId,

View File

@ -1,93 +1,140 @@
import * as fs from 'fs-extra';
import { join } from 'path'; import { join } from 'path';
import os = require('os');
import { exec, spawn } from 'child_process'; 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 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); const execPromise = util.promisify(exec);
export type CppPlatform = 'VisualStudio' | 'GCC'; export type CppPlatform = 'VisualStudio' | 'GCC';
export async function runCpp( interface IRunnerParamsCpp extends IRunnerParams {
srcDir: string, srcDir: string;
problemName: string, problemName: string;
input: string, input: string;
cppPlatform: CppPlatform, cppPlatform: CppPlatform;
outputCallback: (data: string) => void, 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);
const configureCommand = `cmake -S ${srcDir} -B ${buildDir}`; 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 { try {
await execPromise(configureCommand); await execPromise(configureCommand);
} catch (error) { } catch (e) {
outputCallback('[Configure Error]\n\n'); const buildErrorText = e?.toString() ?? 'Unknown build errors.';
outputCallback(String(error)); console.log('Build errors: ' + buildErrorText);
return; return {
success: false,
runResult: { kind: 'CompileFailed', resultKindReason: buildErrorText }
};
} }
const compileCommand = `cmake --build ${buildDir} --target ${problemName}`; const compileCommand = `cmake --build ${buildDir} --target ${params.problemName}`;
try { try {
await execPromise(compileCommand); await execPromise(compileCommand);
} catch (error) { } catch (e) {
outputCallback('[Compile Error]\n\n'); const buildErrorText = e?.toString() ?? 'Unknown build errors.';
outputCallback(String(error)); console.log('Build errors: ' + buildErrorText);
return; return {
success: false,
runResult: { kind: 'CompileFailed', resultKindReason: buildErrorText }
};
} }
let runCommand: string = ''; console.log(`- RUN: ${params.problemName}`);
if (cppPlatform === 'VisualStudio') {
runCommand = `${join(buildDir, 'Debug', `${problemName}.exe`)}`; let runCommand = '';
} else if (cppPlatform === 'GCC') { if (params.cppPlatform === 'VisualStudio') {
runCommand = `${(join(buildDir), problemName)}`; 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 }); const runStartTime = performance.now();
child.stdout.setEncoding('utf8'); child.stdin.write(params.input);
child.stdout.on('data', (data) => { child.stdin.end();
outputCallback(data.toString());
});
child.stderr.setEncoding('utf8');
child.stderr.on('data', (data) => {
outputCallback(data.toString());
});
child.stdin.write(input);
child.stdin.end();
let done = false; let timeLimitExceeded = false;
let completedNormally = false;
child.on('close', () => { return {
if (done === false) { success: true,
done = true; runResult: new Promise<RunResult>((resolve) => {
doneCallback(); child.on('close', () => {
} completedNormally = !timeLimitExceeded;
});
setTimeout(() => { const runEndTime = performance.now();
if (done === false) { const runtimeMilliseconds = Math.floor(runEndTime - runStartTime);
console.log('\n[30 seconds reached, killing process...]');
done = true; if (completedNormally) {
if (child.pid) { clearTimeout(timeoutHandle);
kill(child.pid); 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(); } catch (error) {
} return { success: false, runResult: { kind: 'RunError' } };
}, 30000); }
};
return () => {
if (child.pid) {
done = true;
kill(child.pid);
outputCallback('\n[Manually stopped]');
doneCallback();
}
};
}

View File

@ -1,62 +1,89 @@
import * as fs from 'fs-extra';
import { join } from 'path';
import os = require('os');
import { spawn } from 'child_process'; import { spawn } from 'child_process';
import kill = require('tree-kill'); import kill = require('tree-kill');
import { timeoutSeconds, type IRunner, type IRunnerReturn, type RunResult } from './types';
export async function runCSharp( export const runCSharp: IRunner = async function (params: {
srcDir: string, srcDir: string;
input: string, input: string;
outputCallback: (data: string) => void, outputCallback?: (data: string) => void;
doneCallback: () => void }): IRunnerReturn {
): Promise<(() => void) | undefined> { console.log(`- RUN: ${params.srcDir}`);
const tempDir = os.tmpdir(); const child = spawn('dotnet run', { shell: true, cwd: params.srcDir });
const buildDir = join(tempDir, 'bwcontest_csharp');
if (await fs.exists(buildDir)) {
await fs.remove(buildDir);
}
await fs.mkdir(buildDir);
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'); const runStartTime = performance.now();
child.stdout.on('data', (data) => { child.stdin.write(params.input);
outputCallback(data.toString()); child.stdin.end();
});
child.stderr.setEncoding('utf8');
child.stderr.on('data', (data) => {
outputCallback(data.toString());
});
child.stdin.write(input);
child.stdin.end();
let done = false; let timeLimitExceeded = false;
let completedNormally = false;
child.on('close', () => { return {
if (done === false) { success: true,
done = true; runResult: new Promise<RunResult>((resolve) => {
doneCallback(); child.on('close', () => {
} completedNormally = !timeLimitExceeded;
});
setTimeout(() => { const runEndTime = performance.now();
if (done === false) { const runtimeMilliseconds = Math.floor(runEndTime - runStartTime);
console.log('\n[30 seconds reached, killing process...]');
done = true; if (completedNormally) {
if (child.pid) { clearTimeout(timeoutHandle);
kill(child.pid); 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(); } catch (error) {
} return { success: false, runResult: { kind: 'RunError' } };
}, 30000); }
};
return () => {
if (child.pid) {
done = true;
kill(child.pid);
outputCallback('\n[Manually stopped]');
doneCallback();
}
};
}

View File

@ -1,84 +1,112 @@
import * as fs from 'fs-extra';
import { join } from 'path'; import { join } from 'path';
import os = require('os');
import { exec, spawn } from 'child_process'; import { exec, spawn } from 'child_process';
import { extensionSettings } from '../extension'; import * as util from 'util';
import { error } from 'console'; import { timeoutSeconds, type IRunner, type IRunnerParams, type IRunnerReturn, type RunResult } from './types';
import util = require('node:util');
import kill = require('tree-kill'); import kill = require('tree-kill');
const execPromise = util.promisify(exec); const execPromise = util.promisify(exec);
export async function runJava( interface IRunnerParamsJava extends IRunnerParams {
srcDir: string, srcDir: string;
mainFile: string, mainFile: string;
mainClass: string, mainClass: string;
input: string, input: string;
outputCallback: (data: string) => void, outputCallback?: (data: string) => void;
doneCallback: () => void }
): Promise<(() => void) | undefined> {
const javaPath = extensionSettings().javaPath; export const runJava: IRunner<IRunnerParamsJava> = async function (
if (javaPath == '') { params: IRunnerParamsJava
throw error('Java path not set'); ): IRunnerReturn {
} console.log(`- BUILD: ${params.mainFile}`);
const tempDir = os.tmpdir(); const compileCommand = `javac -cp ${join(params.srcDir, 'src')} ${params.mainFile} -d ${join(params.srcDir, 'build')}`;
const buildDir = join(tempDir, 'bwcontest_java');
if (await fs.exists(buildDir)) {
await fs.remove(buildDir);
}
await fs.mkdir(buildDir);
const compileCommand = `${join(javaPath, 'javac')} -cp ${srcDir} ${mainFile} -d ${buildDir}`;
try { try {
await execPromise(compileCommand); await execPromise(compileCommand);
} catch (error) { } catch (e) {
outputCallback('[Compile Error]\n\n'); const buildErrorText = e?.toString() ?? 'Unknown build errors.';
outputCallback(String(error)); console.log('Build errors: ' + buildErrorText);
return; 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'); const runStartTime = performance.now();
child.stdout.on('data', (data) => { child.stdin.write(params.input);
outputCallback(data.toString()); child.stdin.end();
});
child.stderr.setEncoding('utf8');
child.stderr.on('data', (data) => {
outputCallback(data.toString());
});
child.stdin.write(input);
child.stdin.end();
let done = false; let timeLimitExceeded = false;
let completedNormally = false;
child.on('close', () => { return {
if (done === false) { success: true,
done = true; runResult: new Promise<RunResult>((resolve) => {
doneCallback(); child.on('close', () => {
} completedNormally = !timeLimitExceeded;
});
setTimeout(() => { const runEndTime = performance.now();
if (done === false) { const runtimeMilliseconds = Math.floor(runEndTime - runStartTime);
console.log('\n[30 seconds reached, killing process...]');
done = true; if (completedNormally) {
if (child.pid) { clearTimeout(timeoutHandle);
kill(child.pid); 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(); } catch (error) {
} return { success: false, runResult: { kind: 'RunError' } };
}, 30000); }
};
return () => {
if (child.pid) {
done = true;
kill(child.pid);
outputCallback('\n[Manually stopped]');
doneCallback();
}
};
}

View File

@ -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<RunResult> }
| { success: false; runResult: RunResult }
>;
export interface IRunner<T extends IRunnerParams = IRunnerParams> {
(params: T): IRunnerReturn;
}