feat: prototype running code
This commit is contained in:
parent
545422cd5f
commit
44e3166339
@ -17,6 +17,16 @@ const nextConfig: NextConfig = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
webpack(config, { isServer }) {
|
||||||
|
// 仅在服务器端处理 .node 文件
|
||||||
|
if (isServer) {
|
||||||
|
config.module.rules.push({
|
||||||
|
test: /\.node$/,
|
||||||
|
use: 'node-loader', // 使用 node-loader 处理 .node 文件
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
@ -20,19 +20,20 @@
|
|||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"devicons-react": "^1.4.0",
|
"devicons-react": "^1.4.0",
|
||||||
|
"dockerode": "^4.0.3",
|
||||||
"gitea-js": "^1.22.0",
|
"gitea-js": "^1.22.0",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"monaco-editor": "0.36.1",
|
"monaco-editor": "0.36.1",
|
||||||
"monaco-languageclient": "5.0.1",
|
"monaco-languageclient": "5.0.1",
|
||||||
"normalize-url": "~8.0.0",
|
|
||||||
"next": "15.1.3",
|
"next": "15.1.3",
|
||||||
"next-auth": "^5.0.0-beta.25",
|
"next-auth": "^5.0.0-beta.25",
|
||||||
"react": "^19.0.0",
|
"normalize-url": "~8.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react": "^18.3.1",
|
||||||
|
"react-dom": "^18.3.1",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vscode-ws-jsonrpc": "3.0.0",
|
|
||||||
"vscode-languageclient": "~8.1.0",
|
"vscode-languageclient": "~8.1.0",
|
||||||
|
"vscode-ws-jsonrpc": "3.0.0",
|
||||||
"zustand": "^5.0.2"
|
"zustand": "^5.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
115
src/actions/enhancedmultiFileRunner.test.ts
Normal file
115
src/actions/enhancedmultiFileRunner.test.ts
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
const { runMultiFileCodeWithOptionalTestData, setGiteaRepoUrl, setGiteaToken, setTestDataPath } = require('./enhancedmultiFileRunner');
|
||||||
|
const Docker = require('dockerode');
|
||||||
|
const fs = require('fs/promises');
|
||||||
|
const path = require('path');
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
jest.mock('dockerode');
|
||||||
|
jest.mock('fs/promises');
|
||||||
|
jest.mock('axios');
|
||||||
|
|
||||||
|
const mockDocker = new Docker();
|
||||||
|
const mockContainer = {
|
||||||
|
start: jest.fn(),
|
||||||
|
logs: jest.fn(),
|
||||||
|
wait: jest.fn(),
|
||||||
|
remove: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockDocker.createContainer = jest.fn().mockResolvedValue(mockContainer);
|
||||||
|
mockContainer.logs.mockResolvedValue({ on: jest.fn() });
|
||||||
|
mockContainer.wait.mockResolvedValue({ StatusCode: 0 });
|
||||||
|
|
||||||
|
describe('runMultiFileCodeWithOptionalTestData', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if files are not provided', async () => {
|
||||||
|
await expect(runMultiFileCodeWithOptionalTestData({ files: [], language: 'c' }))
|
||||||
|
.rejects.toThrow('Files and language are required.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if language is not provided', async () => {
|
||||||
|
await expect(runMultiFileCodeWithOptionalTestData({ files: [{ name: 'main.c', content: '' }], language: '' }))
|
||||||
|
.rejects.toThrow('Files and language are required.');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if an unsupported language is provided', async () => {
|
||||||
|
await expect(runMultiFileCodeWithOptionalTestData({ files: [{ name: 'main.c', content: '' }], language: 'ruby' }))
|
||||||
|
.rejects.toThrow("Language 'ruby' is not supported.");
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully compile and run C code', async () => {
|
||||||
|
const result = await runMultiFileCodeWithOptionalTestData({
|
||||||
|
files: [{ name: 'main.c', content: 'int main() { return 0; }' }],
|
||||||
|
language: 'c',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toHaveProperty('output');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully compile and run Java code', async () => {
|
||||||
|
const result = await runMultiFileCodeWithOptionalTestData({
|
||||||
|
files: [{ name: 'Main.java', content: 'public class Main { public static void main(String[] args) {} }' }],
|
||||||
|
language: 'java',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toHaveProperty('output');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should successfully compile and run Python code', async () => {
|
||||||
|
const result = await runMultiFileCodeWithOptionalTestData({
|
||||||
|
files: [{ name: 'script.py', content: 'print("Hello, World!")' }],
|
||||||
|
language: 'python',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toHaveProperty('output');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fetch test data from Gitea if configured', async () => {
|
||||||
|
await setGiteaRepoUrl('http://example.com');
|
||||||
|
await setGiteaToken('token');
|
||||||
|
await setTestDataPath('testData');
|
||||||
|
|
||||||
|
axios.get.mockResolvedValue({ data: [{ type: 'file', name: 'testData.txt', download_url: 'http://example.com/testData.txt' }] });
|
||||||
|
|
||||||
|
const result = await runMultiFileCodeWithOptionalTestData({
|
||||||
|
files: [{ name: 'main.c', content: '' }],
|
||||||
|
language: 'c',
|
||||||
|
testDataFiles: ['testData.txt'],
|
||||||
|
expectedAnswerFiles: ['expected.txt'],
|
||||||
|
resultOutputFile: 'result.txt',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result).toHaveProperty('output');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors during file operations', async () => {
|
||||||
|
fs.writeFile.mockRejectedValue(new Error('File write error'));
|
||||||
|
|
||||||
|
await expect(runMultiFileCodeWithOptionalTestData({
|
||||||
|
files: [{ name: 'main.c', content: '' }],
|
||||||
|
language: 'c',
|
||||||
|
})).rejects.toThrow('File write error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors during Docker container operations', async () => {
|
||||||
|
mockDocker.createContainer.mockRejectedValue(new Error('Docker error'));
|
||||||
|
|
||||||
|
await expect(runMultiFileCodeWithOptionalTestData({
|
||||||
|
files: [{ name: 'main.c', content: '' }],
|
||||||
|
language: 'c',
|
||||||
|
})).rejects.toThrow('Docker error');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle errors during HTTP requests', async () => {
|
||||||
|
axios.get.mockRejectedValue(new Error('HTTP error'));
|
||||||
|
|
||||||
|
await expect(runMultiFileCodeWithOptionalTestData({
|
||||||
|
files: [{ name: 'main.c', content: '' }],
|
||||||
|
language: 'c',
|
||||||
|
testDataFiles: ['testData.txt'],
|
||||||
|
})).rejects.toThrow('HTTP error');
|
||||||
|
});
|
||||||
|
});
|
205
src/actions/enhancedmultiFileRunner.ts
Normal file
205
src/actions/enhancedmultiFileRunner.ts
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import Docker from 'dockerode';
|
||||||
|
import fs from 'fs/promises'; // 使用异步版本的 fs 模块
|
||||||
|
import path from 'path';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
const docker = new Docker();
|
||||||
|
const DOCKER_IMAGE_URL = process.env.DOCKER_IMAGE_URL || 'fly6516.synology.me:8080/multilang:latest'; // 使用环境变量
|
||||||
|
|
||||||
|
let giteaRepoUrl: string | undefined;
|
||||||
|
let giteaToken: string | undefined;
|
||||||
|
let testDataPath: string = 'testdata'; // 默认测试数据目录
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Gitea 仓库 URL
|
||||||
|
* @param url 仓库 URL
|
||||||
|
*/
|
||||||
|
export const setGiteaRepoUrl = async (url: string) => {
|
||||||
|
if (!url || !url.startsWith('http')) {
|
||||||
|
throw new Error('Invalid repository URL. Please provide a valid URL starting with "http".');
|
||||||
|
}
|
||||||
|
giteaRepoUrl = url;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置 Gitea Token
|
||||||
|
* @param token 仓库 API 访问令牌
|
||||||
|
*/
|
||||||
|
export const setGiteaToken = async (token: string) => {
|
||||||
|
if (!token || token.trim().length === 0) {
|
||||||
|
throw new Error('Invalid token. Please provide a non-empty token.');
|
||||||
|
}
|
||||||
|
giteaToken = token;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置测试数据在仓库中的路径
|
||||||
|
* @param path 测试数据目录路径
|
||||||
|
*/
|
||||||
|
export const setTestDataPath = async (path: string) => {
|
||||||
|
if (!path || path.trim().length === 0) {
|
||||||
|
throw new Error('Invalid test data path. Please provide a non-empty path.');
|
||||||
|
}
|
||||||
|
testDataPath = path;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从 Gitea 仓库中下载指定目录的文件
|
||||||
|
* @param dir 目录路径
|
||||||
|
* @param tempDir 本地存储的临时目录
|
||||||
|
*/
|
||||||
|
const fetchFilesFromGitea = async (dir: string, tempDir: string) => {
|
||||||
|
if (!giteaRepoUrl || !giteaToken) {
|
||||||
|
throw new Error('Gitea repository URL or token is not set. Please configure them using setGiteaRepoUrl and setGiteaToken.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetDir = path.join(tempDir, dir);
|
||||||
|
await fs.mkdir(targetDir, { recursive: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await axios.get(`${giteaRepoUrl}/contents/${dir}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${giteaToken}`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const files = response.data;
|
||||||
|
if (!Array.isArray(files)) {
|
||||||
|
throw new Error(`Invalid directory structure for ${dir} in repository.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 并行下载文件
|
||||||
|
await Promise.all(files.filter(file => file.type === 'file').map(async (file) => {
|
||||||
|
const fileResponse = await axios.get(file.download_url, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `token ${giteaToken}`,
|
||||||
|
},
|
||||||
|
responseType: 'arraybuffer',
|
||||||
|
});
|
||||||
|
|
||||||
|
const filePath = path.join(targetDir, file.name);
|
||||||
|
await fs.writeFile(filePath, fileResponse.data);
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to fetch files from '${dir}': ${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比较运行结果与正确答案文件
|
||||||
|
* @param resultFilePath 运行结果文件路径
|
||||||
|
* @param expectedFilePath 正确答案文件路径
|
||||||
|
* @returns 是否一致的比较结果
|
||||||
|
*/
|
||||||
|
const compareResults = async (resultFilePath: string, expectedFilePath: string): Promise<boolean> => {
|
||||||
|
try {
|
||||||
|
const result = (await fs.readFile(resultFilePath, 'utf-8')).trim();
|
||||||
|
const expected = (await fs.readFile(expectedFilePath, 'utf-8')).trim();
|
||||||
|
return result === expected;
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to read files for comparison: ${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行多文件代码并测试结果
|
||||||
|
* @param params 包含代码文件列表、语言、测试数据文件名和正确答案文件名的参数
|
||||||
|
* @returns 运行结果或比较结果
|
||||||
|
*/
|
||||||
|
export const runMultiFileCodeWithOptionalTestData = async (params: {
|
||||||
|
files: { name: string; content: string }[];
|
||||||
|
language: string;
|
||||||
|
testDataFiles?: string[];
|
||||||
|
expectedAnswerFiles?: string[];
|
||||||
|
resultOutputFile?: string;
|
||||||
|
}) => {
|
||||||
|
const { files, language, testDataFiles, expectedAnswerFiles, resultOutputFile } = params;
|
||||||
|
|
||||||
|
if (!files || files.length === 0 || !language) {
|
||||||
|
throw new Error('Files and language are required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportedLanguages = ['c', 'java', 'python'];
|
||||||
|
if (!supportedLanguages.includes(language)) {
|
||||||
|
throw new Error(`Language '${language}' is not supported. Only 'c', 'java', and 'python' are supported.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const compileAndRunCmd =
|
||||||
|
language === 'c'
|
||||||
|
? 'gcc *.c -o main && ./main'
|
||||||
|
: language === 'java'
|
||||||
|
? 'javac *.java && java Main'
|
||||||
|
: 'python3 script.py';
|
||||||
|
|
||||||
|
const tempDir = path.join('/tmp', `${Date.now()}`);
|
||||||
|
await fs.mkdir(tempDir, { recursive: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 写入代码文件
|
||||||
|
await Promise.all(files.map(async (file) => {
|
||||||
|
const filePath = path.join(tempDir, file.name);
|
||||||
|
await fs.writeFile(filePath, file.content);
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 检查是否设置了 Gitea 仓库信息
|
||||||
|
if (giteaRepoUrl && giteaToken) {
|
||||||
|
try {
|
||||||
|
await fetchFilesFromGitea(testDataPath, tempDir);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`Failed to fetch test data from '${testDataPath}': ${(error as Error).message}`);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.info('No Gitea repository connection set. Proceeding without testdata.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 Docker 容器
|
||||||
|
const container = await docker.createContainer({
|
||||||
|
Image: DOCKER_IMAGE_URL,
|
||||||
|
Cmd: ['sh', '-c', compileAndRunCmd],
|
||||||
|
AttachStdout: true,
|
||||||
|
AttachStderr: true,
|
||||||
|
Tty: false,
|
||||||
|
HostConfig: {
|
||||||
|
Binds: [`${tempDir}:/workspace`], // 挂载临时目录到容器
|
||||||
|
},
|
||||||
|
WorkingDir: '/workspace',
|
||||||
|
});
|
||||||
|
|
||||||
|
await container.start();
|
||||||
|
|
||||||
|
const logs = await container.logs({ stdout: true, stderr: true, follow: true });
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
logs.on('data', (data: Buffer) => {
|
||||||
|
output += data.toString().replace(/[\x00-\x1F\x7F]/g, ''); // 去除不可见字符
|
||||||
|
});
|
||||||
|
|
||||||
|
const exitCode = await container.wait();
|
||||||
|
await container.remove();
|
||||||
|
|
||||||
|
if (exitCode.StatusCode !== 0) {
|
||||||
|
return { error: `Execution failed with exit code ${exitCode.StatusCode}. Output: ${output}` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果指定了结果输出文件和正确答案文件,执行比较
|
||||||
|
if (resultOutputFile && expectedAnswerFiles && expectedAnswerFiles.length > 0) {
|
||||||
|
const resultFilePath = path.join(tempDir, resultOutputFile);
|
||||||
|
|
||||||
|
const comparisonResults = await Promise.all(expectedAnswerFiles.map(async (expectedFile) => {
|
||||||
|
const expectedFilePath = path.join(tempDir, testDataPath, expectedFile);
|
||||||
|
const isMatch = await compareResults(resultFilePath, expectedFilePath);
|
||||||
|
return { expectedFile, isMatch };
|
||||||
|
}));
|
||||||
|
|
||||||
|
return { output, comparisonResults };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { output };
|
||||||
|
} catch (error) {
|
||||||
|
return { error: `An error occurred: ${(error as Error).message}` };
|
||||||
|
} finally {
|
||||||
|
await fs.rm(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
};
|
80
src/actions/index.ts
Normal file
80
src/actions/index.ts
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import Docker from 'dockerode';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
|
||||||
|
// 创建 Docker 客户端实例
|
||||||
|
const docker = new Docker();
|
||||||
|
|
||||||
|
// 定义执行代码的 Server Action
|
||||||
|
export const runCode = async (params: { code: string; language: string }) => {
|
||||||
|
const { code, language } = params;
|
||||||
|
|
||||||
|
if (!code || !language) {
|
||||||
|
throw new Error('Code and language are required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const supportedLanguages = ['c', 'java', 'python'];
|
||||||
|
if (!supportedLanguages.includes(language)) {
|
||||||
|
throw new Error(`Language '${language}' is not supported. Only 'c', 'java', and 'python' are supported.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据语言选择文件名和编译/运行命令
|
||||||
|
const fileName =
|
||||||
|
language === 'c' ? 'main.c' :
|
||||||
|
language === 'java' ? 'Main.java' :
|
||||||
|
'script.py';
|
||||||
|
const runCmd =
|
||||||
|
language === 'c' ? 'gcc main.c -o main && ./main' :
|
||||||
|
language === 'java' ? 'javac Main.java && java Main' :
|
||||||
|
'python3 script.py';
|
||||||
|
|
||||||
|
// 创建临时目录用于存储代码文件
|
||||||
|
const tempDir = path.join('/tmp', `${Date.now()}`);
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
|
||||||
|
const filePath = path.join(tempDir, fileName);
|
||||||
|
fs.writeFileSync(filePath, code);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 创建并启动 Docker 容器
|
||||||
|
const container = await docker.createContainer({
|
||||||
|
Image: 'fly6516.synology.me:8080/multilang:latest',
|
||||||
|
Cmd: ['sh', '-c', runCmd],
|
||||||
|
AttachStdout: true,
|
||||||
|
AttachStderr: true,
|
||||||
|
Tty: false,
|
||||||
|
HostConfig: {
|
||||||
|
Binds: [`${tempDir}:/workspace`], // 将临时目录挂载到容器
|
||||||
|
},
|
||||||
|
WorkingDir: '/workspace',
|
||||||
|
});
|
||||||
|
|
||||||
|
await container.start();
|
||||||
|
|
||||||
|
const logs = await container.logs({ stdout: true, stderr: true, follow: true });
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
logs.on('data', (data: Buffer) => {
|
||||||
|
output += data.toString().replace(/[\x00-\x1F\x7F]/g, ''); // 去除不可见字符
|
||||||
|
});
|
||||||
|
|
||||||
|
const exitCode = await container.wait();
|
||||||
|
await container.remove();
|
||||||
|
|
||||||
|
if (exitCode.StatusCode === 0) {
|
||||||
|
return { output };
|
||||||
|
} else {
|
||||||
|
// 返回详细的错误信息
|
||||||
|
return { error: `Execution failed with exit code ${exitCode.StatusCode}. Output: ${output}` };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 捕获并返回详细的错误信息
|
||||||
|
return { error: `An error occurred: ${(error as Error).message}` };
|
||||||
|
} finally {
|
||||||
|
// 清理临时目录
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
};
|
86
src/actions/multiFileRunner.ts
Normal file
86
src/actions/multiFileRunner.ts
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
'use server';
|
||||||
|
|
||||||
|
import Docker from 'dockerode';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
// 创建 Docker 客户端实例
|
||||||
|
const docker = new Docker();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 执行多文件代码的函数
|
||||||
|
* @param params 包含文件列表和语言的参数
|
||||||
|
* @returns 执行结果或错误信息
|
||||||
|
*/
|
||||||
|
export const runMultiFileCode = async (params: { files: { name: string; content: string }[]; language: string }) => {
|
||||||
|
const { files, language } = params;
|
||||||
|
|
||||||
|
if (!files || files.length === 0 || !language) {
|
||||||
|
throw new Error('Files and language are required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 支持的编程语言
|
||||||
|
const supportedLanguages = ['c', 'java', 'python'];
|
||||||
|
if (!supportedLanguages.includes(language)) {
|
||||||
|
throw new Error(`Language '${language}' is not supported. Only 'c', 'java', and 'python' are supported.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据语言设置编译和运行命令
|
||||||
|
const compileAndRunCmd =
|
||||||
|
language === 'c'
|
||||||
|
? 'gcc *.c -o main && ./main'
|
||||||
|
: language === 'java'
|
||||||
|
? 'javac *.java && java Main'
|
||||||
|
: 'python3 script.py';
|
||||||
|
|
||||||
|
// 创建临时目录存放代码文件
|
||||||
|
const tempDir = path.join('/tmp', `${Date.now()}`);
|
||||||
|
fs.mkdirSync(tempDir, { recursive: true });
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 将所有文件写入临时目录
|
||||||
|
for (const file of files) {
|
||||||
|
const filePath = path.join(tempDir, file.name);
|
||||||
|
fs.writeFileSync(filePath, file.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建 Docker 容器
|
||||||
|
const container = await docker.createContainer({
|
||||||
|
Image: 'fly6516.synology.me:8080/multilang:latest',
|
||||||
|
Cmd: ['sh', '-c', compileAndRunCmd],
|
||||||
|
AttachStdout: true,
|
||||||
|
AttachStderr: true,
|
||||||
|
Tty: false,
|
||||||
|
HostConfig: {
|
||||||
|
Binds: [`${tempDir}:/workspace`], // 挂载临时目录到容器
|
||||||
|
},
|
||||||
|
WorkingDir: '/workspace',
|
||||||
|
});
|
||||||
|
|
||||||
|
await container.start();
|
||||||
|
|
||||||
|
// 获取容器日志
|
||||||
|
const logs = await container.logs({ stdout: true, stderr: true, follow: true });
|
||||||
|
|
||||||
|
let output = '';
|
||||||
|
logs.on('data', (data: Buffer) => {
|
||||||
|
output += data.toString().replace(/[\x00-\x1F\x7F]/g, ''); // 去除不可见字符
|
||||||
|
});
|
||||||
|
|
||||||
|
const exitCode = await container.wait(); // 等待容器执行结束
|
||||||
|
await container.remove(); // 移除容器
|
||||||
|
|
||||||
|
// 返回执行结果或错误信息
|
||||||
|
if (exitCode.StatusCode === 0) {
|
||||||
|
return { output };
|
||||||
|
} else {
|
||||||
|
return { error: `Execution failed with exit code ${exitCode.StatusCode}. Output: ${output}` };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 捕获异常并返回详细错误信息
|
||||||
|
return { error: `An error occurred: ${(error as Error).message}` };
|
||||||
|
} finally {
|
||||||
|
// 清理临时目录
|
||||||
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
};
|
@ -8,11 +8,62 @@ import FontSizeSlider from "./components/font-size-slider";
|
|||||||
import LanguageSelector from "./components/language-selector";
|
import LanguageSelector from "./components/language-selector";
|
||||||
import { useCodeEditorStore } from "@/store/useCodeEditorStore";
|
import { useCodeEditorStore } from "@/store/useCodeEditorStore";
|
||||||
import { highlightMonacoEditor } from "@/constants/editor/themes";
|
import { highlightMonacoEditor } from "@/constants/editor/themes";
|
||||||
|
import { runMultiFileCode } from "@/actions/multiFileRunner";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
loader.config({ monaco });
|
loader.config({ monaco });
|
||||||
|
|
||||||
export default function MainPage() {
|
export default function MainPage() {
|
||||||
const { language, theme, fontSize, setEditor } = useCodeEditorStore();
|
const { language, theme, fontSize, setEditor } = useCodeEditorStore();
|
||||||
|
const [code, setCode] = useState(""); // 存储编辑器代码内容
|
||||||
|
const [output, setOutput] = useState(""); // 存储运行结果
|
||||||
|
const [loading, setLoading] = useState(false); // 控制加载状态
|
||||||
|
|
||||||
|
const handleRunCode = async () => {
|
||||||
|
console.group("Code Execution Debugging");
|
||||||
|
console.log("Execution started...");
|
||||||
|
console.log("Selected language:", language);
|
||||||
|
console.log("Code content:\n", code);
|
||||||
|
|
||||||
|
if (!language || !code.trim()) {
|
||||||
|
console.error("Missing language or code content.");
|
||||||
|
setOutput("Error: Please select a language and enter code.");
|
||||||
|
console.groupEnd();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
setOutput("Running code...");
|
||||||
|
try {
|
||||||
|
const files = [
|
||||||
|
{
|
||||||
|
name: language === "python" ? "script.py" : language === "java" ? "Main.java" : "main.c",
|
||||||
|
content: code,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
console.log("Files to execute:", files);
|
||||||
|
|
||||||
|
const result = await runMultiFileCode({ files, language });
|
||||||
|
console.log("Execution result:", result);
|
||||||
|
|
||||||
|
if (result.output) {
|
||||||
|
console.log("Execution successful. Output:\n", result.output);
|
||||||
|
setOutput(result.output);
|
||||||
|
} else if (result.error) {
|
||||||
|
console.error("Execution error. Details:\n", result.error);
|
||||||
|
setOutput(result.error);
|
||||||
|
} else {
|
||||||
|
console.warn("No output or error returned.");
|
||||||
|
setOutput("No output received from execution.");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error during execution:", error);
|
||||||
|
setOutput(`Error: ${(error as Error).message}`);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
console.groupEnd();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="h-full flex flex-col gap-2">
|
<div className="h-full flex flex-col gap-2">
|
||||||
@ -20,14 +71,35 @@ export default function MainPage() {
|
|||||||
<LanguageSelector />
|
<LanguageSelector />
|
||||||
<ThemeSelector />
|
<ThemeSelector />
|
||||||
<FontSizeSlider />
|
<FontSizeSlider />
|
||||||
|
<button
|
||||||
|
onClick={handleRunCode}
|
||||||
|
className={`px-4 py-2 rounded ${
|
||||||
|
loading ? "bg-gray-400" : "bg-blue-500 hover:bg-blue-600"
|
||||||
|
} text-white`}
|
||||||
|
disabled={loading}
|
||||||
|
>
|
||||||
|
{loading ? "Running..." : "Run Code"}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
<Editor
|
<Editor
|
||||||
language={language}
|
language={language}
|
||||||
theme={theme}
|
theme={theme}
|
||||||
beforeMount={highlightMonacoEditor}
|
beforeMount={highlightMonacoEditor}
|
||||||
onMount={setEditor}
|
onMount={setEditor}
|
||||||
|
onChange={(value) => {
|
||||||
|
console.log("Editor content updated:", value);
|
||||||
|
setCode(value || "");
|
||||||
|
}}
|
||||||
options={{ fontSize, automaticLayout: true }}
|
options={{ fontSize, automaticLayout: true }}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="px-4 py-2 bg-gray-900 text-white rounded mt-2 overflow-auto h-48">
|
||||||
|
<h3 className="font-bold text-yellow-400">Output:</h3>
|
||||||
|
<div className="bg-gray-800 p-2 mt-2 rounded h-32 overflow-y-auto">
|
||||||
|
<pre className="whitespace-pre-wrap">{output || "No output yet."}</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user