diff --git a/backend/server/package-lock.json b/backend/server/package-lock.json index f7227b5..dd284cb 100644 --- a/backend/server/package-lock.json +++ b/backend/server/package-lock.json @@ -12,7 +12,7 @@ "concurrently": "^8.2.2", "cors": "^2.8.5", "dotenv": "^16.4.5", - "e2b": "^0.16.1", + "e2b": "^0.16.2-beta.47", "express": "^4.19.2", "rate-limiter-flexible": "^5.0.3", "simple-git": "^3.25.0", @@ -41,6 +41,28 @@ "node": ">=6.9.0" } }, + "node_modules/@bufbuild/protobuf": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.10.0.tgz", + "integrity": "sha512-QDdVFLoN93Zjg36NoQPZfsVH9tZew7wKDKyV5qRdj8ntT4wQCOradQjRaTdwMhWUYsgKsvCINKKm87FdEk96Ag==" + }, + "node_modules/@connectrpc/connect": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-1.4.0.tgz", + "integrity": "sha512-vZeOkKaAjyV4+RH3+rJZIfDFJAfr+7fyYr6sLDKbYX3uuTVszhFe9/YKf5DNqrDb5cKdKVlYkGn6DTDqMitAnA==", + "peerDependencies": { + "@bufbuild/protobuf": "^1.4.2" + } + }, + "node_modules/@connectrpc/connect-web": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-web/-/connect-web-1.4.0.tgz", + "integrity": "sha512-13aO4psFbbm7rdOFGV0De2Za64DY/acMspgloDlcOKzLPPs0yZkhp1OOzAQeiAIr7BM/VOHIA3p8mF0inxCYTA==", + "peerDependencies": { + "@bufbuild/protobuf": "^1.4.2", + "@connectrpc/connect": "1.4.0" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -443,6 +465,7 @@ "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", "hasInstallScript": true, "optional": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -564,6 +587,11 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/compare-versions": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/compare-versions/-/compare-versions-6.1.1.tgz", + "integrity": "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -737,23 +765,19 @@ } }, "node_modules/e2b": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/e2b/-/e2b-0.16.2.tgz", - "integrity": "sha512-xKmVK4ipgVQPJ/uyyrfH9LnaawERRWt8U2UZhdhGfzdL/QU/OpBjuhoIbFCv1Uy6qXV4nIiJ6Nw4MBC4HmXf1g==", + "version": "0.16.2-beta.47", + "resolved": "https://registry.npmjs.org/e2b/-/e2b-0.16.2-beta.47.tgz", + "integrity": "sha512-tMPDYLMD+8+JyLPrsWft3NHBhK5YKOFOXzKMwpOKR5KvXOkd1silkArDwplmBUzN/eG/uRzWdtHZs9mHUQ5b9g==", "dependencies": { - "isomorphic-ws": "^5.0.0", - "normalize-path": "^3.0.0", - "openapi-typescript-fetch": "^1.1.3", - "path-browserify": "^1.0.1", - "platform": "^1.3.6", - "ws": "^8.15.1" + "@bufbuild/protobuf": "^1.10.0", + "@connectrpc/connect": "^1.4.0", + "@connectrpc/connect-web": "^1.4.0", + "compare-versions": "^6.1.0", + "openapi-fetch": "^0.9.7", + "platform": "^1.3.6" }, "engines": { "node": ">=18" - }, - "optionalDependencies": { - "bufferutil": "^4.0.8", - "utf-8-validate": "^6.0.3" } }, "node_modules/ee-first": { @@ -1195,14 +1219,6 @@ "node": ">=0.12.0" } }, - "node_modules/isomorphic-ws": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", - "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", - "peerDependencies": { - "ws": "*" - } - }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", @@ -1301,6 +1317,7 @@ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz", "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", "optional": true, + "peer": true, "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", @@ -1383,6 +1400,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -1417,15 +1435,19 @@ "node": ">= 0.8" } }, - "node_modules/openapi-typescript-fetch": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/openapi-typescript-fetch/-/openapi-typescript-fetch-1.1.3.tgz", - "integrity": "sha512-smLZPck4OkKMNExcw8jMgrMOGgVGx2N/s6DbKL2ftNl77g5HfoGpZGFy79RBzU/EkaO0OZpwBnslfdBfh7ZcWg==", - "engines": { - "node": ">= 12.0.0", - "npm": ">= 7.0.0" + "node_modules/openapi-fetch": { + "version": "0.9.8", + "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.9.8.tgz", + "integrity": "sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg==", + "dependencies": { + "openapi-typescript-helpers": "^0.0.8" } }, + "node_modules/openapi-typescript-helpers": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/openapi-typescript-helpers/-/openapi-typescript-helpers-0.0.8.tgz", + "integrity": "sha512-1eNjQtbfNi5Z/kFhagDIaIRj6qqDzhjNJKz8cmMW0CVdGwT6e1GLbAfgI0d28VTJa1A8jz82jm/4dG8qNoNS8g==" + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -1434,11 +1456,6 @@ "node": ">= 0.8" } }, - "node_modules/path-browserify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", - "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" - }, "node_modules/path-to-regexp": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", @@ -2053,6 +2070,7 @@ "integrity": "sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==", "hasInstallScript": true, "optional": true, + "peer": true, "dependencies": { "node-gyp-build": "^4.3.0" }, @@ -2098,26 +2116,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/backend/server/package.json b/backend/server/package.json index 19e5ee5..40c9c18 100644 --- a/backend/server/package.json +++ b/backend/server/package.json @@ -14,7 +14,7 @@ "concurrently": "^8.2.2", "cors": "^2.8.5", "dotenv": "^16.4.5", - "e2b": "^0.16.1", + "e2b": "^0.16.2-beta.47", "express": "^4.19.2", "rate-limiter-flexible": "^5.0.3", "simple-git": "^3.25.0", diff --git a/backend/server/src/Terminal.ts b/backend/server/src/Terminal.ts new file mode 100644 index 0000000..018ad9b --- /dev/null +++ b/backend/server/src/Terminal.ts @@ -0,0 +1,68 @@ +import { Sandbox, ProcessHandle } from "e2b"; + +// Terminal class to manage a pseudo-terminal (PTY) in a sandbox environment +export class Terminal { + private pty: ProcessHandle | undefined; // Holds the PTY process handle + private sandbox: Sandbox; // Reference to the sandbox environment + + // Constructor initializes the Terminal with a sandbox + constructor(sandbox: Sandbox) { + this.sandbox = sandbox; + } + + // Initialize the terminal with specified rows, columns, and data handler + async init({ + rows = 20, + cols = 80, + onData, + }: { + rows?: number; + cols?: number; + onData: (responseData: string) => void; + }): Promise { + // Create a new PTY process + this.pty = await this.sandbox.pty.create({ + rows, + cols, + timeout: 0, + onData: (data: Uint8Array) => { + onData(new TextDecoder().decode(data)); // Convert received data to string and pass to handler + }, + }); + } + + // Send data to the terminal + async sendData(data: string) { + if (this.pty) { + await this.sandbox.pty.sendInput(this.pty.pid, new TextEncoder().encode(data)); + await this.pty.wait(); + } else { + console.log("Cannot send data because pty is not initialized."); + } + } + + // Resize the terminal + async resize(size: { cols: number; rows: number }): Promise { + if (this.pty) { + await this.sandbox.pty.resize(this.pty.pid, size); + } else { + console.log("Cannot resize terminal because pty is not initialized."); + } + } + + // Close the terminal, killing the PTY process and stopping the input stream + async close(): Promise { + if (this.pty) { + await this.pty.kill(); + } else { + console.log("Cannot kill pty because it is not initialized."); + } + } +} + +// Usage example: +// const terminal = new Terminal(sandbox); +// await terminal.init(); +// terminal.sendData('ls -la'); +// await terminal.resize({ cols: 100, rows: 30 }); +// await terminal.close(); \ No newline at end of file diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 6ca6853..548d23e 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -20,7 +20,11 @@ import { saveFile, } from "./fileoperations"; import { LockManager } from "./utils"; -import { Sandbox, Terminal, FilesystemManager } from "e2b"; + +import { Sandbox, Filesystem } from "e2b"; + +import { Terminal } from "./Terminal" + import { MAX_BODY_SIZE, createFileRL, @@ -52,12 +56,12 @@ const terminals: Record = {}; const dirName = "/home/user"; const moveFile = async ( - filesystem: FilesystemManager, + filesystem: Filesystem, filePath: string, newFilePath: string ) => { - const fileContents = await filesystem.readBytes(filePath); - await filesystem.writeBytes(newFilePath, fileContents); + const fileContents = await filesystem.read(filePath); + await filesystem.write(newFilePath, fileContents); await filesystem.remove(filePath); }; @@ -156,7 +160,7 @@ io.on("connection", async (socket) => { await lockManager.acquireLock(data.sandboxId, async () => { try { if (!containers[data.sandboxId]) { - containers[data.sandboxId] = await Sandbox.create(); + containers[data.sandboxId] = await Sandbox.create({ timeoutMs: 1200000 }); console.log("Created container ", data.sandboxId); } } catch (e: any) { @@ -167,7 +171,7 @@ io.on("connection", async (socket) => { // Change the owner of the project directory to user const fixPermissions = async () => { - await containers[data.sandboxId].process.startAndWait( + await containers[data.sandboxId].commands.run( `sudo chown -R user "${path.join(dirName, "projects", data.sandboxId)}"` ); }; @@ -175,10 +179,14 @@ io.on("connection", async (socket) => { const sandboxFiles = await getSandboxFiles(data.sandboxId); sandboxFiles.fileData.forEach(async (file) => { const filePath = path.join(dirName, file.id); - await containers[data.sandboxId].filesystem.makeDir( - path.dirname(filePath) - ); - await containers[data.sandboxId].filesystem.write(filePath, file.data); + try { + await containers[data.sandboxId].files.makeDir( + path.dirname(filePath) + ); + } catch (e: any) { + console.log("Failed to create directory: " + e); + } + await containers[data.sandboxId].files.write(filePath, file.data); }); fixPermissions(); @@ -231,7 +239,7 @@ io.on("connection", async (socket) => { if (!file) return; file.data = body; - await containers[data.sandboxId].filesystem.write( + await containers[data.sandboxId].files.write( path.join(dirName, file.id), body ); @@ -253,7 +261,7 @@ io.on("connection", async (socket) => { const newFileId = folderId + "/" + parts.pop(); await moveFile( - containers[data.sandboxId].filesystem, + containers[data.sandboxId].files, path.join(dirName, fileId), path.join(dirName, newFileId) ); @@ -346,7 +354,7 @@ io.on("connection", async (socket) => { const id = `projects/${data.sandboxId}/${name}`; - await containers[data.sandboxId].filesystem.write( + await containers[data.sandboxId].files.write( path.join(dirName, id), "" ); @@ -383,7 +391,7 @@ io.on("connection", async (socket) => { const id = `projects/${data.sandboxId}/${name}`; - await containers[data.sandboxId].filesystem.makeDir( + await containers[data.sandboxId].files.makeDir( path.join(dirName, id) ); @@ -412,7 +420,7 @@ io.on("connection", async (socket) => { parts.slice(0, parts.length - 1).join("/") + "/" + newName; await moveFile( - containers[data.sandboxId].filesystem, + containers[data.sandboxId].files, path.join(dirName, fileId), path.join(dirName, newFileId) ); @@ -435,7 +443,7 @@ io.on("connection", async (socket) => { const file = sandboxFiles.fileData.find((f) => f.id === fileId); if (!file) return; - await containers[data.sandboxId].filesystem.remove( + await containers[data.sandboxId].files.remove( path.join(dirName, fileId) ); sandboxFiles.fileData = sandboxFiles.fileData.filter( @@ -462,7 +470,7 @@ io.on("connection", async (socket) => { await Promise.all( files.map(async (file) => { - await containers[data.sandboxId].filesystem.remove( + await containers[data.sandboxId].files.remove( path.join(dirName, file) ); @@ -491,35 +499,36 @@ io.on("connection", async (socket) => { await lockManager.acquireLock(data.sandboxId, async () => { try { - terminals[id] = await containers[data.sandboxId].terminal.start({ - onData: (responseData: string) => { - io.emit("terminalResponse", { id, data: responseData }); + terminals[id] = new Terminal(containers[data.sandboxId]) + await terminals[id].init({ + onData: (responseString: string) => { + io.emit("terminalResponse", { id, data: responseString }); function extractPortNumber(inputString: string) { // Remove ANSI escape codes const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, ''); + // Regular expression to match port number const regex = /http:\/\/localhost:(\d+)/; // If a match is found, return the port number const match = cleanedString.match(regex); - return match ? match[1] : null; + return match ? match[1] : null; } - const port = parseInt(extractPortNumber(responseData) ?? ""); + const port = parseInt(extractPortNumber(responseString) ?? ""); if (port) { io.emit( "previewURL", - "https://" + containers[data.sandboxId].getHostname(port) + "https://" + containers[data.sandboxId].getHost(port) ); } - }, - size: { cols: 80, rows: 20 }, - onExit: () => console.log("Terminal exited", id), + cols: 80, + rows: 20, + //onExit: () => console.log("Terminal exited", id), }); await terminals[id].sendData( - `cd "${path.join(dirName, "projects", data.sandboxId)}"\r` + `cd "${path.join(dirName, "projects", data.sandboxId)}"\rexport PS1='user> '\rclear\r` ); - await terminals[id].sendData("export PS1='user> '\rclear\r"); console.log("Created terminal", id); } catch (e: any) { console.error(`Error creating terminal ${id}:`, e); @@ -548,7 +557,7 @@ io.on("connection", async (socket) => { } ); - socket.on("terminalData", (id: string, data: string) => { + socket.on("terminalData", async (id: string, data: string) => { try { if (!terminals[id]) { return; @@ -567,7 +576,7 @@ io.on("connection", async (socket) => { return; } - await terminals[id].kill(); + await terminals[id].close(); delete terminals[id]; callback(); @@ -636,7 +645,7 @@ io.on("connection", async (socket) => { if (data.isOwner && connections[data.sandboxId] <= 0) { await Promise.all( Object.entries(terminals).map(async ([key, terminal]) => { - await terminal.kill(); + await terminal.close(); delete terminals[key]; }) ); @@ -644,7 +653,7 @@ io.on("connection", async (socket) => { await lockManager.acquireLock(data.sandboxId, async () => { try { if (containers[data.sandboxId]) { - await containers[data.sandboxId].close(); + await containers[data.sandboxId].kill(); delete containers[data.sandboxId]; console.log("Closed container", data.sandboxId); }