feat:use putarchive instead of share folder

This commit is contained in:
fly6516 2025-02-28 11:58:21 +08:00
parent f4099a7805
commit 3929c055eb
3 changed files with 454 additions and 250 deletions

490
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -10,12 +10,15 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@types/tar-stream": "^3.1.3",
"axios": "^1.7.9", "axios": "^1.7.9",
"dockerode": "^4.0.2", "dockerode": "^4.0.4",
"memory-streams": "^0.1.3",
"next": "15.1.3", "next": "15.1.3",
"nextjs-node-loader": "^1.1.9", "nextjs-node-loader": "^1.1.9",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0" "react-dom": "^19.0.0",
"tar-stream": "^3.1.7"
}, },
"devDependencies": { "devDependencies": {
"@babel/preset-typescript": "^7.26.0", "@babel/preset-typescript": "^7.26.0",

View File

@ -1,91 +1,166 @@
'use server'; 'use server';
import Docker from 'dockerode'; import Docker from 'dockerode';
import fs from 'fs/promises'; // 使用异步版本的 fs 模块 import { Writable, Readable } from 'stream';
import tar from 'tar-stream';
import path from 'path'; import path from 'path';
import axios from 'axios';
import dockerConfig from './dockerConfig.json'; import dockerConfig from './dockerConfig.json';
import { performance } from 'perf_hooks';
const docker = new Docker(); const docker = new Docker();
/** const createFilesTar = async (files: { name: string; content: string }[]) => {
* const pack = tar.pack();
* @param params
* @returns
*/
export const runMultiFileCodeWithOptionalTestData = async (params: {
files: { name: string; content: string }[];
language: string;
testDataFiles?: string[];
expectedAnswerFiles?: string[];
}) => {
const { files, language, testDataFiles, expectedAnswerFiles } = params;
if (!files || files.length === 0 || !language) { files.forEach(file => {
throw new Error('Files and language are required.'); if (file.name.includes('..') || path.isAbsolute(file.name)) {
throw new Error(`非法文件路径: ${file.name}`);
} }
if (!/\.(c|java|py|txt)$/i.test(file.name)) {
throw new Error(`禁止的文件类型: ${file.name}`);
}
pack.entry({ name: file.name }, file.content);
});
pack.finalize();
return Readable.from(pack) as unknown as NodeJS.ReadableStream;
};
const readContainerFile = async (container: Docker.Container, filePath: string) => {
try {
const archive = await container.getArchive({ path: filePath });
const extract = tar.extract();
let fileContent = '';
extract.on('entry', (header, stream, next) => {
stream.on('data', (chunk) => {
fileContent += chunk.toString();
});
stream.on('end', next);
stream.resume();
});
return new Promise<string>((resolve, reject) => {
extract.on('finish', () => resolve(fileContent));
extract.on('error', reject);
archive.pipe(extract);
});
} catch (error) {
return null;
}
};
export const runMultiFileCodeWithOptionalTestData = async (params: {
files: { name: string; content: string }[];
language: string;
testDataFiles?: string[];
expectedAnswerFiles?: string[];
}) => {
const totalStart = performance.now();
let stageStart: number;
const { files, language } = params;
let container: Docker.Container | null = null;
try {
// 阶段1: 参数校验
stageStart = performance.now();
if (!files?.length || !language) {
throw new Error('必须提供代码文件和语言类型');
}
const supportedLanguages = ['c', 'java', 'python']; const supportedLanguages = ['c', 'java', 'python'];
if (!supportedLanguages.includes(language)) { if (!supportedLanguages.includes(language)) {
throw new Error(`Language '${language}' is not supported. Only 'c', 'java', and 'python' are supported.`); throw new Error(`不支持的语言类型: ${language}`);
} }
console.log(`[PERF] 参数校验耗时: ${(performance.now() - stageStart).toFixed(2)}ms`);
const compileAndRunCmd = // 阶段2: 准备编译命令
language === 'c' stageStart = performance.now();
? 'gcc *.c -o main && ./main' const compileAndRunCmd = {
: language === 'java' c: `cd ${dockerConfig.WorkingDir} && gcc *.c -o main && ./main`,
? 'javac *.java && java Main' java: `cd ${dockerConfig.WorkingDir} && javac *.java && java Main`,
: 'python3 script.py'; python: `cd ${dockerConfig.WorkingDir} && python3 script.py`
}[language];
console.log(`[PERF] 命令准备耗时: ${(performance.now() - stageStart).toFixed(2)}ms`);
const tempDir = path.join('/tmp', `${Date.now()}`); // 阶段3: 创建容器
await fs.mkdir(tempDir, { recursive: true }); stageStart = performance.now();
container = await docker.createContainer({
Image: dockerConfig.Image,
Cmd: ['sh', '-c', compileAndRunCmd],
WorkingDir: dockerConfig.WorkingDir,
HostConfig: {
AutoRemove: false,
Memory: 256 * 1024 * 1024,
NetworkMode: 'none',
Binds: [`${dockerConfig.WorkingDir}:/workspace`]
}
});
console.log(`[PERF] 容器创建耗时: ${(performance.now() - stageStart).toFixed(2)}ms`);
try { // 阶段4: 文件上传
// 写入代码文件 stageStart = performance.now();
await Promise.all(files.map(async (file) => { const tarStream = await createFilesTar(files);
const filePath = path.join(tempDir, file.name); await container.putArchive(tarStream, {
await fs.writeFile(filePath, file.content); path: dockerConfig.WorkingDir
})); });
console.log(`[PERF] 文件上传耗时: ${(performance.now() - stageStart).toFixed(2)}ms`);
// 创建 Docker 容器 // 阶段5: 执行容器
const container = await docker.createContainer({ stageStart = performance.now();
...dockerConfig, await container.start();
Cmd: ['sh', '-c', compileAndRunCmd], let output = '';
HostConfig: { const logs = await container.logs({
Binds: [`${tempDir}:/workspace`], // 挂载临时目录到容器 stdout: true,
}, stderr: true,
}); follow: true
});
await container.start(); const logStream = new Writable({
write(chunk, _, callback) {
output += chunk.toString()
.replace(/[\x00-\x1F\x7F]/g, '')
.trim();
callback();
}
});
const logs = await container.logs({ stdout: true, stderr: true, follow: true }); logs.pipe(logStream);
await new Promise(resolve => logStream.on('finish', resolve));
await container.wait();
console.log(`[PERF] 容器执行耗时: ${(performance.now() - stageStart).toFixed(2)}ms`);
let output = ''; // 阶段6: 读取结果
logs.on('data', (data: Buffer) => { stageStart = performance.now();
output += data.toString().replace(/[\x00-\x1F\x7F]/g, ''); // 去除不可见字符 const resultFilePath = path.posix.join(
}); dockerConfig.WorkingDir,
dockerConfig.ResultOutputFile
);
const resultContent = await readContainerFile(container, resultFilePath);
console.log(`[PERF] 结果读取耗时: ${(performance.now() - stageStart).toFixed(2)}ms`);
const exitCode = await container.wait(); return resultContent
await container.remove(); ? { output, resultFileContent: resultContent }
: { output };
if (exitCode.StatusCode !== 0) { } catch (error) {
return { error: `Execution failed with exit code ${exitCode.StatusCode}. Output: ${output}` }; console.error('执行流程错误:', error);
} return {
error: `执行错误: ${(error as Error).message}`
// 检查结果输出文件是否存在 };
const resultOutputFile = dockerConfig.ResultOutputFile; } finally {
const resultFilePath = path.join(tempDir, resultOutputFile); // 阶段7: 清理容器
stageStart = performance.now();
try { if (container) {
const resultFileContent = await fs.readFile(resultFilePath, 'utf-8'); try {
return { output, resultFileContent }; await container.remove({ force: true });
} catch (error) { } catch (error) {
// 如果结果文件不存在,返回控制台输出 console.error('容器清理失败:', error);
return { output }; }
}
} catch (error) {
return { error: `An error occurred: ${(error as Error).message}` };
} finally {
await fs.rm(tempDir, { recursive: true, force: true });
} }
console.log(`[PERF] 容器清理耗时: ${(performance.now() - stageStart).toFixed(2)}ms`);
// 总耗时统计
const totalTime = performance.now() - totalStart;
console.log(`[PERF] 总运行时间: ${totalTime.toFixed(2)}ms`);
}
}; };