[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;
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,

View File

@ -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<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 (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) => {
outputCallback(data.toString());
outputBuffer += data.toString();
params.outputCallback?.(data.toString());
});
child.stderr.setEncoding('utf8');
child.stderr.on('data', (data) => {
outputCallback(data.toString());
outputBuffer += data.toString();
params.outputCallback?.(data.toString());
});
child.stdin.write(input);
const runStartTime = performance.now();
child.stdin.write(params.input);
child.stdin.end();
let done = false;
let timeLimitExceeded = false;
let completedNormally = false;
return {
success: true,
runResult: new Promise<RunResult>((resolve) => {
child.on('close', () => {
if (done === false) {
done = true;
doneCallback();
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`
});
}
});
setTimeout(() => {
if (done === false) {
console.log('\n[30 seconds reached, killing process...]');
done = true;
if (child.pid) {
kill(child.pid);
const timeoutHandle = setTimeout(() => {
if (completedNormally) {
return;
}
outputCallback('\n[Timeout after 30 seconds]');
doneCallback();
}
}, 30000);
return () => {
if (child.pid) {
done = true;
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);
outputCallback('\n[Manually stopped]');
doneCallback();
params.outputCallback?.('\n[Manually stopped]');
}
}
}
};
}
} catch (error) {
return { success: false, runResult: { kind: 'RunError' } };
}
};

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 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);
const child = spawn('dotnet run', { shell: true, cwd: srcDir });
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) => {
outputCallback(data.toString());
outputBuffer += data.toString();
params.outputCallback?.(data.toString());
});
child.stderr.setEncoding('utf8');
child.stderr.on('data', (data) => {
outputCallback(data.toString());
outputBuffer += data.toString();
params.outputCallback?.(data.toString());
});
child.stdin.write(input);
const runStartTime = performance.now();
child.stdin.write(params.input);
child.stdin.end();
let done = false;
let timeLimitExceeded = false;
let completedNormally = false;
return {
success: true,
runResult: new Promise<RunResult>((resolve) => {
child.on('close', () => {
if (done === false) {
done = true;
doneCallback();
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`
});
}
});
setTimeout(() => {
if (done === false) {
console.log('\n[30 seconds reached, killing process...]');
done = true;
if (child.pid) {
kill(child.pid);
const timeoutHandle = setTimeout(() => {
if (completedNormally) {
return;
}
outputCallback('\n[Timeout after 30 seconds]');
doneCallback();
}
}, 30000);
return () => {
if (child.pid) {
done = true;
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);
outputCallback('\n[Manually stopped]');
doneCallback();
}
}, 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,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<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')}`;
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}`;
try {
let outputBuffer = '';
const child = spawn(runCommand, { shell: true });
child.stdout.setEncoding('utf8');
child.stdout.on('data', (data) => {
outputCallback(data.toString());
outputBuffer += data.toString();
params.outputCallback?.(data.toString());
});
child.stderr.setEncoding('utf8');
child.stderr.on('data', (data) => {
outputCallback(data.toString());
outputBuffer += data.toString();
params.outputCallback?.(data.toString());
});
child.stdin.write(input);
const runStartTime = performance.now();
child.stdin.write(params.input);
child.stdin.end();
let done = false;
let timeLimitExceeded = false;
let completedNormally = false;
return {
success: true,
runResult: new Promise<RunResult>((resolve) => {
child.on('close', () => {
if (done === false) {
done = true;
doneCallback();
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`
});
}
});
setTimeout(() => {
if (done === false) {
console.log('\n[30 seconds reached, killing process...]');
done = true;
if (child.pid) {
kill(child.pid);
const timeoutHandle = setTimeout(() => {
if (completedNormally) {
return;
}
outputCallback('\n[Timeout after 30 seconds]');
doneCallback();
}
}, 30000);
return () => {
if (child.pid) {
done = true;
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);
outputCallback('\n[Manually stopped]');
doneCallback();
params.outputCallback?.('\n[Manually stopped]');
}
}
}
};
}
} catch (error) {
return { success: false, runResult: { kind: 'RunError' } };
}
};

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;
}