diff --git a/next.config.ts b/next.config.ts index aa7c1ca..04bb838 100644 --- a/next.config.ts +++ b/next.config.ts @@ -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; diff --git a/package.json b/package.json index d1c623c..630d0b1 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/actions/docker/compile.ts b/src/actions/docker/compile.ts new file mode 100644 index 0000000..cdb83e9 --- /dev/null +++ b/src/actions/docker/compile.ts @@ -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((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); + } + } + } +} diff --git a/src/actions/docker/image.ts b/src/actions/docker/image.ts new file mode 100644 index 0000000..299d2be --- /dev/null +++ b/src/actions/docker/image.ts @@ -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); + }); + }); + }); +} diff --git a/src/app/playground/page.tsx b/src/app/playground/page.tsx index f29447c..0d788b5 100644 --- a/src/app/playground/page.tsx +++ b/src/app/playground/page.tsx @@ -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() {
-