feat(playground): integrate Docker support for code execution and add image pulling functionality

This commit is contained in:
ngc2207 2025-01-05 09:05:48 +08:00
parent c30398a453
commit 5a9a20dee6
5 changed files with 118 additions and 1 deletions

View File

@ -17,6 +17,15 @@ const nextConfig: NextConfig = {
},
],
},
webpack(config, { isServer }) {
if (isServer) {
config.module.rules.push({
test: /\.node$/,
use: "node-loader",
});
}
return config;
},
};
export default nextConfig;

View File

@ -22,6 +22,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"devicons-react": "^1.4.0",
"dockerode": "^4.0.3",
"gitea-js": "^1.22.0",
"lucide-react": "^0.469.0",
"monaco-editor": "0.36.1",
@ -40,6 +41,7 @@
"devDependencies": {
"@eslint/eslintrc": "^3",
"@shikijs/monaco": "^1.26.1",
"@types/dockerode": "^3.3.33",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",

View File

@ -0,0 +1,73 @@
"use server";
import Dockerode from "dockerode";
import { pullImage } from "./image";
export async function runCode(code: string, language: string) {
const docker = new Dockerode({ socketPath: "/var/run/docker.sock" });
let container;
try {
if (language === "c") {
const imageGCC = "gcc:latest";
const images = await docker.listImages();
const image = images.find(
(image) => image.RepoTags && image.RepoTags.includes(imageGCC)
);
if (!image) {
await pullImage(imageGCC);
}
container = await docker.createContainer({
Image: imageGCC,
Cmd: [
"sh",
"-c",
`echo '${code}' > main.c && gcc main.c -o main && ./main`,
],
Tty: false,
});
await container.start();
const stream = await container.attach({
stream: true,
stdout: true,
stderr: true,
});
const output = await new Promise<string>((resolve, reject) => {
let output = "";
stream.on("data", (chunk) => {
output += chunk.toString();
});
stream.on("end", () => {
resolve(output);
});
stream.on("error", (err) => {
reject(err);
});
});
console.log("Output:", output);
return output;
} else {
throw new Error(`Unsupported language: ${language}`);
}
} catch (error) {
console.error("Error running code:", error);
throw error;
} finally {
if (container) {
try {
const containerInfo = await container.inspect();
if (containerInfo.State.Running) {
await container.stop();
}
await container.remove();
} catch (cleanupError) {
console.error("Error cleaning up container:", cleanupError);
}
}
}
}

View File

@ -0,0 +1,22 @@
"use server";
import Dockerode from "dockerode";
export async function pullImage(imageName: string) {
console.log("Pulling image:", imageName);
const docker = new Dockerode({ socketPath: "/var/run/docker.sock" });
return new Promise((resolve, reject) => {
docker.pull(imageName, (err: Error, stream: NodeJS.ReadableStream) => {
if (err) {
reject(err);
return;
}
docker.modem.followProgress(stream, (err, output) => {
if (err) {
reject(err);
}
resolve(output);
});
});
});
}

View File

@ -3,6 +3,7 @@
import { Send } from "lucide-react";
import * as monaco from "monaco-editor";
import { Button } from "@/components/ui/button";
import { runCode } from "@/actions/docker/compile";
import { useEffect, useRef, useState } from "react";
import { Editor, loader } from "@monaco-editor/react";
import { MonacoLanguageClient } from "monaco-languageclient";
@ -150,7 +151,17 @@ export default function PlaygroundPage() {
</ScrollArea>
</Tabs>
<div className="m-3">
<Button variant="outline">
<Button
variant="outline"
onClick={async () => {
const code = editorRef.current?.getValue();
if (!code) {
console.error("No code to compile");
return;
}
await runCode(code, language);
}}
>
Submit
<Send
className="-me-1 ms-2 opacity-60"