feat:use putarchive instead of share folder
This commit is contained in:
parent
f4099a7805
commit
3929c055eb
490
package-lock.json
generated
490
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -10,12 +10,15 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/tar-stream": "^3.1.3",
|
||||
"axios": "^1.7.9",
|
||||
"dockerode": "^4.0.2",
|
||||
"dockerode": "^4.0.4",
|
||||
"memory-streams": "^0.1.3",
|
||||
"next": "15.1.3",
|
||||
"nextjs-node-loader": "^1.1.9",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0"
|
||||
"react-dom": "^19.0.0",
|
||||
"tar-stream": "^3.1.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-typescript": "^7.26.0",
|
||||
|
@ -1,91 +1,166 @@
|
||||
'use server';
|
||||
|
||||
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 axios from 'axios';
|
||||
import dockerConfig from './dockerConfig.json';
|
||||
import { performance } from 'perf_hooks';
|
||||
|
||||
const docker = new Docker();
|
||||
|
||||
/**
|
||||
* 执行多文件代码并测试结果
|
||||
* @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;
|
||||
const createFilesTar = async (files: { name: string; content: string }[]) => {
|
||||
const pack = tar.pack();
|
||||
|
||||
if (!files || files.length === 0 || !language) {
|
||||
throw new Error('Files and language are required.');
|
||||
files.forEach(file => {
|
||||
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'];
|
||||
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 =
|
||||
language === 'c'
|
||||
? 'gcc *.c -o main && ./main'
|
||||
: language === 'java'
|
||||
? 'javac *.java && java Main'
|
||||
: 'python3 script.py';
|
||||
// 阶段2: 准备编译命令
|
||||
stageStart = performance.now();
|
||||
const compileAndRunCmd = {
|
||||
c: `cd ${dockerConfig.WorkingDir} && gcc *.c -o main && ./main`,
|
||||
java: `cd ${dockerConfig.WorkingDir} && javac *.java && java Main`,
|
||||
python: `cd ${dockerConfig.WorkingDir} && python3 script.py`
|
||||
}[language];
|
||||
console.log(`[PERF] 命令准备耗时: ${(performance.now() - stageStart).toFixed(2)}ms`);
|
||||
|
||||
const tempDir = path.join('/tmp', `${Date.now()}`);
|
||||
await fs.mkdir(tempDir, { recursive: true });
|
||||
// 阶段3: 创建容器
|
||||
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 {
|
||||
// 写入代码文件
|
||||
await Promise.all(files.map(async (file) => {
|
||||
const filePath = path.join(tempDir, file.name);
|
||||
await fs.writeFile(filePath, file.content);
|
||||
}));
|
||||
// 阶段4: 文件上传
|
||||
stageStart = performance.now();
|
||||
const tarStream = await createFilesTar(files);
|
||||
await container.putArchive(tarStream, {
|
||||
path: dockerConfig.WorkingDir
|
||||
});
|
||||
console.log(`[PERF] 文件上传耗时: ${(performance.now() - stageStart).toFixed(2)}ms`);
|
||||
|
||||
// 创建 Docker 容器
|
||||
const container = await docker.createContainer({
|
||||
...dockerConfig,
|
||||
Cmd: ['sh', '-c', compileAndRunCmd],
|
||||
HostConfig: {
|
||||
Binds: [`${tempDir}:/workspace`], // 挂载临时目录到容器
|
||||
},
|
||||
});
|
||||
// 阶段5: 执行容器
|
||||
stageStart = performance.now();
|
||||
await container.start();
|
||||
let output = '';
|
||||
const logs = await container.logs({
|
||||
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 = '';
|
||||
logs.on('data', (data: Buffer) => {
|
||||
output += data.toString().replace(/[\x00-\x1F\x7F]/g, ''); // 去除不可见字符
|
||||
});
|
||||
// 阶段6: 读取结果
|
||||
stageStart = performance.now();
|
||||
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();
|
||||
await container.remove();
|
||||
return resultContent
|
||||
? { output, resultFileContent: resultContent }
|
||||
: { output };
|
||||
|
||||
if (exitCode.StatusCode !== 0) {
|
||||
return { error: `Execution failed with exit code ${exitCode.StatusCode}. Output: ${output}` };
|
||||
}
|
||||
|
||||
// 检查结果输出文件是否存在
|
||||
const resultOutputFile = dockerConfig.ResultOutputFile;
|
||||
const resultFilePath = path.join(tempDir, resultOutputFile);
|
||||
|
||||
try {
|
||||
const resultFileContent = await fs.readFile(resultFilePath, 'utf-8');
|
||||
return { output, resultFileContent };
|
||||
} catch (error) {
|
||||
// 如果结果文件不存在,返回控制台输出
|
||||
return { output };
|
||||
}
|
||||
} catch (error) {
|
||||
return { error: `An error occurred: ${(error as Error).message}` };
|
||||
} finally {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
} catch (error) {
|
||||
console.error('执行流程错误:', error);
|
||||
return {
|
||||
error: `执行错误: ${(error as Error).message}`
|
||||
};
|
||||
} finally {
|
||||
// 阶段7: 清理容器
|
||||
stageStart = performance.now();
|
||||
if (container) {
|
||||
try {
|
||||
await container.remove({ force: true });
|
||||
} catch (error) {
|
||||
console.error('容器清理失败:', error);
|
||||
}
|
||||
}
|
||||
console.log(`[PERF] 容器清理耗时: ${(performance.now() - stageStart).toFixed(2)}ms`);
|
||||
|
||||
// 总耗时统计
|
||||
const totalTime = performance.now() - totalStart;
|
||||
console.log(`[PERF] 总运行时间: ${totalTime.toFixed(2)}ms`);
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user