Compare commits

..

12 Commits

10 changed files with 286 additions and 184 deletions

View File

@ -12,7 +12,7 @@
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"e2b": "^0.16.1", "e2b": "^0.16.2-beta.47",
"express": "^4.19.2", "express": "^4.19.2",
"rate-limiter-flexible": "^5.0.3", "rate-limiter-flexible": "^5.0.3",
"simple-git": "^3.25.0", "simple-git": "^3.25.0",
@ -41,6 +41,28 @@
"node": ">=6.9.0" "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": { "node_modules/@cspotcode/source-map-support": {
"version": "0.8.1", "version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@ -443,6 +465,7 @@
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==", "integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"node-gyp-build": "^4.3.0" "node-gyp-build": "^4.3.0"
}, },
@ -564,6 +587,11 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" "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": { "node_modules/concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -737,23 +765,19 @@
} }
}, },
"node_modules/e2b": { "node_modules/e2b": {
"version": "0.16.2", "version": "0.16.2-beta.47",
"resolved": "https://registry.npmjs.org/e2b/-/e2b-0.16.2.tgz", "resolved": "https://registry.npmjs.org/e2b/-/e2b-0.16.2-beta.47.tgz",
"integrity": "sha512-xKmVK4ipgVQPJ/uyyrfH9LnaawERRWt8U2UZhdhGfzdL/QU/OpBjuhoIbFCv1Uy6qXV4nIiJ6Nw4MBC4HmXf1g==", "integrity": "sha512-tMPDYLMD+8+JyLPrsWft3NHBhK5YKOFOXzKMwpOKR5KvXOkd1silkArDwplmBUzN/eG/uRzWdtHZs9mHUQ5b9g==",
"dependencies": { "dependencies": {
"isomorphic-ws": "^5.0.0", "@bufbuild/protobuf": "^1.10.0",
"normalize-path": "^3.0.0", "@connectrpc/connect": "^1.4.0",
"openapi-typescript-fetch": "^1.1.3", "@connectrpc/connect-web": "^1.4.0",
"path-browserify": "^1.0.1", "compare-versions": "^6.1.0",
"platform": "^1.3.6", "openapi-fetch": "^0.9.7",
"ws": "^8.15.1" "platform": "^1.3.6"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
},
"optionalDependencies": {
"bufferutil": "^4.0.8",
"utf-8-validate": "^6.0.3"
} }
}, },
"node_modules/ee-first": { "node_modules/ee-first": {
@ -1195,14 +1219,6 @@
"node": ">=0.12.0" "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": { "node_modules/lodash": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "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", "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz",
"integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==", "integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==",
"optional": true, "optional": true,
"peer": true,
"bin": { "bin": {
"node-gyp-build": "bin.js", "node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js", "node-gyp-build-optional": "optional.js",
@ -1383,6 +1400,7 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
"dev": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@ -1417,15 +1435,19 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/openapi-typescript-fetch": { "node_modules/openapi-fetch": {
"version": "1.1.3", "version": "0.9.8",
"resolved": "https://registry.npmjs.org/openapi-typescript-fetch/-/openapi-typescript-fetch-1.1.3.tgz", "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.9.8.tgz",
"integrity": "sha512-smLZPck4OkKMNExcw8jMgrMOGgVGx2N/s6DbKL2ftNl77g5HfoGpZGFy79RBzU/EkaO0OZpwBnslfdBfh7ZcWg==", "integrity": "sha512-zM6elH0EZStD/gSiNlcPrzXcVQ/pZo3BDvC6CDwRDUt1dDzxlshpmQnpD6cZaJ39THaSmwVCxxRrPKNM1hHrDg==",
"engines": { "dependencies": {
"node": ">= 12.0.0", "openapi-typescript-helpers": "^0.0.8"
"npm": ">= 7.0.0"
} }
}, },
"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": { "node_modules/parseurl": {
"version": "1.3.3", "version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@ -1434,11 +1456,6 @@
"node": ">= 0.8" "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": { "node_modules/path-to-regexp": {
"version": "0.1.7", "version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@ -2053,6 +2070,7 @@
"integrity": "sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==", "integrity": "sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==",
"hasInstallScript": true, "hasInstallScript": true,
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"node-gyp-build": "^4.3.0" "node-gyp-build": "^4.3.0"
}, },
@ -2098,26 +2116,6 @@
"url": "https://github.com/chalk/wrap-ansi?sponsor=1" "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": { "node_modules/y18n": {
"version": "5.0.8", "version": "5.0.8",
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",

View File

@ -14,7 +14,7 @@
"concurrently": "^8.2.2", "concurrently": "^8.2.2",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"e2b": "^0.16.1", "e2b": "^0.16.2-beta.47",
"express": "^4.19.2", "express": "^4.19.2",
"rate-limiter-flexible": "^5.0.3", "rate-limiter-flexible": "^5.0.3",
"simple-git": "^3.25.0", "simple-git": "^3.25.0",

View File

@ -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<void> {
// 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<void> {
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<void> {
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();

View File

@ -20,7 +20,11 @@ import {
saveFile, saveFile,
} from "./fileoperations"; } from "./fileoperations";
import { LockManager } from "./utils"; import { LockManager } from "./utils";
import { Sandbox, Terminal, FilesystemManager } from "e2b";
import { Sandbox, Filesystem } from "e2b";
import { Terminal } from "./Terminal"
import { import {
MAX_BODY_SIZE, MAX_BODY_SIZE,
createFileRL, createFileRL,
@ -52,12 +56,12 @@ const terminals: Record<string, Terminal> = {};
const dirName = "/home/user"; const dirName = "/home/user";
const moveFile = async ( const moveFile = async (
filesystem: FilesystemManager, filesystem: Filesystem,
filePath: string, filePath: string,
newFilePath: string newFilePath: string
) => { ) => {
const fileContents = await filesystem.readBytes(filePath); const fileContents = await filesystem.read(filePath);
await filesystem.writeBytes(newFilePath, fileContents); await filesystem.write(newFilePath, fileContents);
await filesystem.remove(filePath); await filesystem.remove(filePath);
}; };
@ -156,7 +160,7 @@ io.on("connection", async (socket) => {
await lockManager.acquireLock(data.sandboxId, async () => { await lockManager.acquireLock(data.sandboxId, async () => {
try { try {
if (!containers[data.sandboxId]) { if (!containers[data.sandboxId]) {
containers[data.sandboxId] = await Sandbox.create(); containers[data.sandboxId] = await Sandbox.create({ timeoutMs: 1200000 });
console.log("Created container ", data.sandboxId); console.log("Created container ", data.sandboxId);
} }
} catch (e: any) { } catch (e: any) {
@ -167,7 +171,7 @@ io.on("connection", async (socket) => {
// Change the owner of the project directory to user // Change the owner of the project directory to user
const fixPermissions = async () => { 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)}"` `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); const sandboxFiles = await getSandboxFiles(data.sandboxId);
sandboxFiles.fileData.forEach(async (file) => { sandboxFiles.fileData.forEach(async (file) => {
const filePath = path.join(dirName, file.id); const filePath = path.join(dirName, file.id);
await containers[data.sandboxId].filesystem.makeDir( try {
await containers[data.sandboxId].files.makeDir(
path.dirname(filePath) path.dirname(filePath)
); );
await containers[data.sandboxId].filesystem.write(filePath, file.data); } catch (e: any) {
console.log("Failed to create directory: " + e);
}
await containers[data.sandboxId].files.write(filePath, file.data);
}); });
fixPermissions(); fixPermissions();
@ -231,7 +239,7 @@ io.on("connection", async (socket) => {
if (!file) return; if (!file) return;
file.data = body; file.data = body;
await containers[data.sandboxId].filesystem.write( await containers[data.sandboxId].files.write(
path.join(dirName, file.id), path.join(dirName, file.id),
body body
); );
@ -253,7 +261,7 @@ io.on("connection", async (socket) => {
const newFileId = folderId + "/" + parts.pop(); const newFileId = folderId + "/" + parts.pop();
await moveFile( await moveFile(
containers[data.sandboxId].filesystem, containers[data.sandboxId].files,
path.join(dirName, fileId), path.join(dirName, fileId),
path.join(dirName, newFileId) path.join(dirName, newFileId)
); );
@ -346,7 +354,7 @@ io.on("connection", async (socket) => {
const id = `projects/${data.sandboxId}/${name}`; const id = `projects/${data.sandboxId}/${name}`;
await containers[data.sandboxId].filesystem.write( await containers[data.sandboxId].files.write(
path.join(dirName, id), path.join(dirName, id),
"" ""
); );
@ -383,7 +391,7 @@ io.on("connection", async (socket) => {
const id = `projects/${data.sandboxId}/${name}`; const id = `projects/${data.sandboxId}/${name}`;
await containers[data.sandboxId].filesystem.makeDir( await containers[data.sandboxId].files.makeDir(
path.join(dirName, id) path.join(dirName, id)
); );
@ -412,7 +420,7 @@ io.on("connection", async (socket) => {
parts.slice(0, parts.length - 1).join("/") + "/" + newName; parts.slice(0, parts.length - 1).join("/") + "/" + newName;
await moveFile( await moveFile(
containers[data.sandboxId].filesystem, containers[data.sandboxId].files,
path.join(dirName, fileId), path.join(dirName, fileId),
path.join(dirName, newFileId) path.join(dirName, newFileId)
); );
@ -435,7 +443,7 @@ io.on("connection", async (socket) => {
const file = sandboxFiles.fileData.find((f) => f.id === fileId); const file = sandboxFiles.fileData.find((f) => f.id === fileId);
if (!file) return; if (!file) return;
await containers[data.sandboxId].filesystem.remove( await containers[data.sandboxId].files.remove(
path.join(dirName, fileId) path.join(dirName, fileId)
); );
sandboxFiles.fileData = sandboxFiles.fileData.filter( sandboxFiles.fileData = sandboxFiles.fileData.filter(
@ -462,7 +470,7 @@ io.on("connection", async (socket) => {
await Promise.all( await Promise.all(
files.map(async (file) => { files.map(async (file) => {
await containers[data.sandboxId].filesystem.remove( await containers[data.sandboxId].files.remove(
path.join(dirName, file) path.join(dirName, file)
); );
@ -491,35 +499,36 @@ io.on("connection", async (socket) => {
await lockManager.acquireLock(data.sandboxId, async () => { await lockManager.acquireLock(data.sandboxId, async () => {
try { try {
terminals[id] = await containers[data.sandboxId].terminal.start({ terminals[id] = new Terminal(containers[data.sandboxId])
onData: (responseData: string) => { await terminals[id].init({
io.emit("terminalResponse", { id, data: responseData }); onData: (responseString: string) => {
io.emit("terminalResponse", { id, data: responseString });
function extractPortNumber(inputString: string) { function extractPortNumber(inputString: string) {
// Remove ANSI escape codes // Remove ANSI escape codes
const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, ''); const cleanedString = inputString.replace(/\x1B\[[0-9;]*m/g, '');
// Regular expression to match port number // Regular expression to match port number
const regex = /http:\/\/localhost:(\d+)/; const regex = /http:\/\/localhost:(\d+)/;
// If a match is found, return the port number // If a match is found, return the port number
const match = cleanedString.match(regex); 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) { if (port) {
io.emit( io.emit(
"previewURL", "previewURL",
"https://" + containers[data.sandboxId].getHostname(port) "https://" + containers[data.sandboxId].getHost(port)
); );
} }
}, },
size: { cols: 80, rows: 20 }, cols: 80,
onExit: () => console.log("Terminal exited", id), rows: 20,
//onExit: () => console.log("Terminal exited", id),
}); });
await terminals[id].sendData( 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); console.log("Created terminal", id);
} catch (e: any) { } catch (e: any) {
console.error(`Error creating terminal ${id}:`, e); 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 { try {
if (!terminals[id]) { if (!terminals[id]) {
return; return;
@ -567,7 +576,7 @@ io.on("connection", async (socket) => {
return; return;
} }
await terminals[id].kill(); await terminals[id].close();
delete terminals[id]; delete terminals[id];
callback(); callback();
@ -636,7 +645,7 @@ io.on("connection", async (socket) => {
if (data.isOwner && connections[data.sandboxId] <= 0) { if (data.isOwner && connections[data.sandboxId] <= 0) {
await Promise.all( await Promise.all(
Object.entries(terminals).map(async ([key, terminal]) => { Object.entries(terminals).map(async ([key, terminal]) => {
await terminal.kill(); await terminal.close();
delete terminals[key]; delete terminals[key];
}) })
); );
@ -644,7 +653,7 @@ io.on("connection", async (socket) => {
await lockManager.acquireLock(data.sandboxId, async () => { await lockManager.acquireLock(data.sandboxId, async () => {
try { try {
if (containers[data.sandboxId]) { if (containers[data.sandboxId]) {
await containers[data.sandboxId].close(); await containers[data.sandboxId].kill();
delete containers[data.sandboxId]; delete containers[data.sandboxId];
console.log("Closed container", data.sandboxId); console.log("Closed container", data.sandboxId);
} }

View File

@ -7,7 +7,8 @@ import { ClerkProvider } from "@clerk/nextjs"
import { Toaster } from "@/components/ui/sonner" import { Toaster } from "@/components/ui/sonner"
import { Analytics } from "@vercel/analytics/react" import { Analytics } from "@vercel/analytics/react"
import { TerminalProvider } from '@/context/TerminalContext'; import { TerminalProvider } from '@/context/TerminalContext';
import { PreviewProvider } from "@/context/PreviewContext" import { PreviewProvider } from "@/context/PreviewContext";
import { SocketProvider } from '@/context/SocketContext'
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Sandbox", title: "Sandbox",
@ -15,7 +16,7 @@ export const metadata: Metadata = {
} }
export default function RootLayout({ export default function RootLayout({
children, children
}: Readonly<{ }: Readonly<{
children: React.ReactNode children: React.ReactNode
}>) { }>) {
@ -29,11 +30,13 @@ export default function RootLayout({
forcedTheme="dark" forcedTheme="dark"
disableTransitionOnChange disableTransitionOnChange
> >
<SocketProvider>
<PreviewProvider> <PreviewProvider>
<TerminalProvider> <TerminalProvider>
{children} {children}
</TerminalProvider> </TerminalProvider>
</PreviewProvider> </PreviewProvider>
</SocketProvider>
<Analytics /> <Analytics />
<Toaster position="bottom-left" richColors /> <Toaster position="bottom-left" richColors />
</ThemeProvider> </ThemeProvider>

View File

@ -49,10 +49,8 @@ export default function Dashboard({
const q = searchParams.get("q") const q = searchParams.get("q")
const router = useRouter() const router = useRouter()
useEffect(() => { useEffect(() => { // update the dashboard to show a new project
if (!sandboxes) {
router.refresh() router.refresh()
}
}, [sandboxes]) }, [sandboxes])
return ( return (

View File

@ -3,7 +3,6 @@
import { SetStateAction, useCallback, useEffect, useRef, useState } from "react" import { SetStateAction, useCallback, useEffect, useRef, useState } from "react"
import monaco from "monaco-editor" import monaco from "monaco-editor"
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react" import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
import { Socket, io } from "socket.io-client"
import { toast } from "sonner" import { toast } from "sonner"
import { useClerk } from "@clerk/nextjs" import { useClerk } from "@clerk/nextjs"
@ -32,7 +31,7 @@ import PreviewWindow from "./preview"
import Terminals from "./terminals" import Terminals from "./terminals"
import { ImperativePanelHandle } from "react-resizable-panels" import { ImperativePanelHandle } from "react-resizable-panels"
import { PreviewProvider, usePreview } from '@/context/PreviewContext'; import { PreviewProvider, usePreview } from '@/context/PreviewContext';
import { useTerminal } from '@/context/TerminalContext'; import { useSocket } from "@/context/SocketContext"
export default function CodeEditor({ export default function CodeEditor({
userData, userData,
@ -41,24 +40,20 @@ export default function CodeEditor({
userData: User userData: User
sandboxData: Sandbox sandboxData: Sandbox
}) { }) {
const socketRef = useRef<Socket | null>(null);
// Initialize socket connection if it doesn't exist //SocketContext functions and effects
if (!socketRef.current) { const { socket, setUserAndSandboxId } = useSocket();
socketRef.current = io(
`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userData.id}&sandboxId=${sandboxData.id}`,
{
timeout: 2000,
}
);
}
//Terminalcontext functionsand effects
const { setUserAndSandboxId } = useTerminal();
useEffect(() => { useEffect(() => {
// Ensure userData.id and sandboxData.id are available before attempting to connect
if (userData.id && sandboxData.id) {
// Check if the socket is not initialized or not connected
if (!socket || (socket && !socket.connected)) {
// Initialize socket connection
setUserAndSandboxId(userData.id, sandboxData.id); setUserAndSandboxId(userData.id, sandboxData.id);
}, [userData.id, sandboxData.id, setUserAndSandboxId]); }
}
}, [socket, userData.id, sandboxData.id, setUserAndSandboxId]);
//Preview Button state //Preview Button state
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true) const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
@ -333,9 +328,9 @@ export default function CodeEditor({
); );
console.log(`Saving file...${activeFileId}`); console.log(`Saving file...${activeFileId}`);
console.log(`Saving file...${value}`); console.log(`Saving file...${value}`);
socketRef.current?.emit("saveFile", activeFileId, value); socket?.emit("saveFile", activeFileId, value);
}, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000), }, Number(process.env.FILE_SAVE_DEBOUNCE_DELAY) || 1000),
[socketRef] [socket]
); );
useEffect(() => { useEffect(() => {
@ -432,11 +427,11 @@ export default function CodeEditor({
// Connection/disconnection effect // Connection/disconnection effect
useEffect(() => { useEffect(() => {
socketRef.current?.connect() socket?.connect()
return () => { return () => {
socketRef.current?.disconnect() socket?.disconnect()
} }
}, []) }, [socket])
// Socket event listener effect // Socket event listener effect
useEffect(() => { useEffect(() => {
@ -469,25 +464,24 @@ export default function CodeEditor({
}) })
} }
socketRef.current?.on("connect", onConnect) socket?.on("connect", onConnect)
socketRef.current?.on("disconnect", onDisconnect) socket?.on("disconnect", onDisconnect)
socketRef.current?.on("loaded", onLoadedEvent) socket?.on("loaded", onLoadedEvent)
socketRef.current?.on("error", onError) socket?.on("error", onError)
socketRef.current?.on("terminalResponse", onTerminalResponse) socket?.on("terminalResponse", onTerminalResponse)
socketRef.current?.on("disableAccess", onDisableAccess) socket?.on("disableAccess", onDisableAccess)
socketRef.current?.on("previewURL", loadPreviewURL) socket?.on("previewURL", loadPreviewURL)
return () => { return () => {
socketRef.current?.off("connect", onConnect) socket?.off("connect", onConnect)
socketRef.current?.off("disconnect", onDisconnect) socket?.off("disconnect", onDisconnect)
socketRef.current?.off("loaded", onLoadedEvent) socket?.off("loaded", onLoadedEvent)
socketRef.current?.off("error", onError) socket?.off("error", onError)
socketRef.current?.off("terminalResponse", onTerminalResponse) socket?.off("terminalResponse", onTerminalResponse)
socketRef.current?.off("disableAccess", onDisableAccess) socket?.off("disableAccess", onDisableAccess)
socketRef.current?.off("previewURL", loadPreviewURL) socket?.off("previewURL", loadPreviewURL)
} }
// }, []); }, [socket, terminals, setTerminals, setFiles, toast, setDisableAccess, isOwner, loadPreviewURL]);
}, [terminals])
// Helper functions for tabs: // Helper functions for tabs:
@ -497,14 +491,13 @@ export default function CodeEditor({
const fileCache = useRef(new Map()); const fileCache = useRef(new Map());
// Debounced function to get file content // Debounced function to get file content
const debouncedGetFile = useCallback( const debouncedGetFile =
debounce((tabId, callback) => { (tabId: any, callback: any) => {
socketRef.current?.emit('getFile', tabId, callback); socket?.emit('getFile', tabId, callback);
}, 300), // 300ms debounce delay, adjust as needed } // 300ms debounce delay, adjust as needed
[]
); const selectFile = (tab: TTab) => {
const selectFile = useCallback((tab: TTab) => {
if (tab.id === activeFileId) return; if (tab.id === activeFileId) return;
setGenerate((prev) => ({ ...prev, show: false })); setGenerate((prev) => ({ ...prev, show: false }));
@ -529,7 +522,7 @@ export default function CodeEditor({
setEditorLanguage(processFileType(tab.name)); setEditorLanguage(processFileType(tab.name));
setActiveFileId(tab.id); setActiveFileId(tab.id);
}, [activeFileId, tabs, debouncedGetFile]); };
// Close tab and remove from tabs // Close tab and remove from tabs
const closeTab = (id: string) => { const closeTab = (id: string) => {
@ -603,7 +596,7 @@ export default function CodeEditor({
return false return false
} }
socketRef.current?.emit("renameFile", id, newName) socket?.emit("renameFile", id, newName)
setTabs((prev) => setTabs((prev) =>
prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab)) prev.map((tab) => (tab.id === id ? { ...tab, name: newName } : tab))
) )
@ -612,7 +605,7 @@ export default function CodeEditor({
} }
const handleDeleteFile = (file: TFile) => { const handleDeleteFile = (file: TFile) => {
socketRef.current?.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => { socket?.emit("deleteFile", file.id, (response: (TFolder | TFile)[]) => {
setFiles(response) setFiles(response)
}) })
closeTab(file.id) closeTab(file.id)
@ -622,11 +615,11 @@ export default function CodeEditor({
setDeletingFolderId(folder.id) setDeletingFolderId(folder.id)
console.log("deleting folder", folder.id) console.log("deleting folder", folder.id)
socketRef.current?.emit("getFolder", folder.id, (response: string[]) => socket?.emit("getFolder", folder.id, (response: string[]) =>
closeTabs(response) closeTabs(response)
) )
socketRef.current?.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => { socket?.emit("deleteFolder", folder.id, (response: (TFolder | TFile)[]) => {
setFiles(response) setFiles(response)
setDeletingFolderId("") setDeletingFolderId("")
}) })
@ -654,7 +647,7 @@ export default function CodeEditor({
{generate.show && ai ? ( {generate.show && ai ? (
<GenerateInput <GenerateInput
user={userData} user={userData}
socket={socketRef.current} socket={socket!}
width={generate.width - 90} width={generate.width - 90}
data={{ data={{
fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "", fileName: tabs.find((t) => t.id === activeFileId)?.name ?? "",
@ -714,7 +707,7 @@ export default function CodeEditor({
handleRename={handleRename} handleRename={handleRename}
handleDeleteFile={handleDeleteFile} handleDeleteFile={handleDeleteFile}
handleDeleteFolder={handleDeleteFolder} handleDeleteFolder={handleDeleteFolder}
socket={socketRef.current} socket={socket!}
setFiles={setFiles} setFiles={setFiles}
addNew={(name, type) => addNew(name, type, setFiles, sandboxData)} addNew={(name, type) => addNew(name, type, setFiles, sandboxData)}
deletingFolderId={deletingFolderId} deletingFolderId={deletingFolderId}

View File

@ -8,12 +8,15 @@ import { toast } from "sonner";
import EditorTerminal from "./terminal"; import EditorTerminal from "./terminal";
import { useTerminal } from "@/context/TerminalContext"; import { useTerminal } from "@/context/TerminalContext";
import { useEffect } from "react"; import { useEffect } from "react";
import { useSocket } from "@/context/SocketContext"
export default function Terminals() { export default function Terminals() {
const { socket } = useSocket();
const { const {
terminals, terminals,
setTerminals, setTerminals,
socket,
createNewTerminal, createNewTerminal,
closeTerminal, closeTerminal,
activeTerminalId, activeTerminalId,

View File

@ -0,0 +1,63 @@
"use client";
import React, { createContext, useContext, useEffect, useState } from 'react';
import { io, Socket } from 'socket.io-client';
interface SocketContextType {
socket: Socket | null;
setUserAndSandboxId: (userId: string, sandboxId: string) => void;
}
const SocketContext = createContext<SocketContextType | undefined>(undefined);
export const SocketProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [socket, setSocket] = useState<Socket | null>(null);
const [userId, setUserId] = useState<string | null>(null);
const [sandboxId, setSandboxId] = useState<string | null>(null);
useEffect(() => {
if (userId && sandboxId) {
console.log("Initializing socket connection...");
const newSocket = io(`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`);
console.log("Socket instance:", newSocket);
setSocket(newSocket);
newSocket.on('connect', () => {
console.log("Socket connected:", newSocket.id);
});
newSocket.on('disconnect', () => {
console.log("Socket disconnected");
});
return () => {
console.log("Disconnecting socket...");
newSocket.disconnect();
};
}
}, [userId, sandboxId]);
const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => {
setUserId(newUserId);
setSandboxId(newSandboxId);
};
const value = {
socket,
setUserAndSandboxId,
};
return (
<SocketContext.Provider value={ value }>
{children}
</SocketContext.Provider>
);
};
export const useSocket = (): SocketContextType => {
const context = useContext(SocketContext);
if (!context) {
throw new Error('useSocket must be used within a SocketProvider');
}
return context;
};

View File

@ -1,12 +1,11 @@
"use client"; "use client";
import React, { createContext, useContext, useState, useEffect } from 'react'; import React, { createContext, useContext, useState } from 'react';
import { io, Socket } from 'socket.io-client';
import { Terminal } from '@xterm/xterm'; import { Terminal } from '@xterm/xterm';
import { createTerminal as createTerminalHelper, closeTerminal as closeTerminalHelper } from '@/lib/terminal'; import { createTerminal as createTerminalHelper, closeTerminal as closeTerminalHelper } from '@/lib/terminal';
import { useSocket } from '@/context/SocketContext';
interface TerminalContextType { interface TerminalContextType {
socket: Socket | null;
terminals: { id: string; terminal: Terminal | null }[]; terminals: { id: string; terminal: Terminal | null }[];
setTerminals: React.Dispatch<React.SetStateAction<{ id: string; terminal: Terminal | null }[]>>; setTerminals: React.Dispatch<React.SetStateAction<{ id: string; terminal: Terminal | null }[]>>;
activeTerminalId: string; activeTerminalId: string;
@ -15,41 +14,16 @@ interface TerminalContextType {
setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>; setCreatingTerminal: React.Dispatch<React.SetStateAction<boolean>>;
createNewTerminal: (command?: string) => Promise<void>; createNewTerminal: (command?: string) => Promise<void>;
closeTerminal: (id: string) => void; closeTerminal: (id: string) => void;
setUserAndSandboxId: (userId: string, sandboxId: string) => void;
deploy: (callback: () => void) => void; deploy: (callback: () => void) => void;
} }
const TerminalContext = createContext<TerminalContextType | undefined>(undefined); const TerminalContext = createContext<TerminalContextType | undefined>(undefined);
export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const [socket, setSocket] = useState<Socket | null>(null); const { socket } = useSocket();
const [terminals, setTerminals] = useState<{ id: string; terminal: Terminal | null }[]>([]); const [terminals, setTerminals] = useState<{ id: string; terminal: Terminal | null }[]>([]);
const [activeTerminalId, setActiveTerminalId] = useState<string>(''); const [activeTerminalId, setActiveTerminalId] = useState<string>('');
const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false); const [creatingTerminal, setCreatingTerminal] = useState<boolean>(false);
const [userId, setUserId] = useState<string | null>(null);
const [sandboxId, setSandboxId] = useState<string | null>(null);
useEffect(() => {
if (userId && sandboxId) {
console.log("Initializing socket connection...");
const newSocket = io(`${process.env.NEXT_PUBLIC_SERVER_URL}?userId=${userId}&sandboxId=${sandboxId}`);
console.log("Socket instance:", newSocket);
setSocket(newSocket);
newSocket.on('connect', () => {
console.log("Socket connected:", newSocket.id);
});
newSocket.on('disconnect', () => {
console.log("Socket disconnected");
});
return () => {
console.log("Disconnecting socket...");
newSocket.disconnect();
};
}
}, [userId, sandboxId]);
const createNewTerminal = async (command?: string): Promise<void> => { const createNewTerminal = async (command?: string): Promise<void> => {
if (!socket) return; if (!socket) return;
@ -85,11 +59,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
} }
}; };
const setUserAndSandboxId = (newUserId: string, newSandboxId: string) => {
setUserId(newUserId);
setSandboxId(newSandboxId);
};
const deploy = (callback: () => void) => { const deploy = (callback: () => void) => {
if (!socket) console.error("Couldn't deploy: No socket"); if (!socket) console.error("Couldn't deploy: No socket");
console.log("Deploying...") console.log("Deploying...")
@ -99,7 +68,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
} }
const value = { const value = {
socket,
terminals, terminals,
setTerminals, setTerminals,
activeTerminalId, activeTerminalId,
@ -108,7 +76,6 @@ export const TerminalProvider: React.FC<{ children: React.ReactNode }> = ({ chil
setCreatingTerminal, setCreatingTerminal,
createNewTerminal, createNewTerminal,
closeTerminal, closeTerminal,
setUserAndSandboxId,
deploy deploy
}; };