This commit is contained in:
James Murdza 2024-06-11 10:48:34 -04:00
parent e5a36a4626
commit 7307a6c2ee
5 changed files with 277 additions and 32 deletions

View File

@ -0,0 +1,19 @@
# e2b template build --name "terminal"
# Use a Debian-based base image
FROM ubuntu:22.04
# Install dependencies and customize sandbox
RUN apt update \
&& apt install -y sudo
# Install xterm
RUN apt update \
&& apt install -y xterm
RUN apt update \
&& apt install -y tmux screen
# Clean up
RUN apt clean \
&& rm -rf /var/lib/apt/lists/*

View File

@ -0,0 +1,14 @@
# This is a config for E2B sandbox template.
# You can use 'template_id' (ne8xtb57tq5xw9vwhdyc) or 'template_name (terminal) from this config to spawn a sandbox:
# Python SDK
# from e2b import Sandbox
# sandbox = Sandbox(template='terminal')
# JS SDK
# import { Sandbox } from 'e2b'
# const sandbox = await Sandbox.create({ template: 'terminal' })
dockerfile = "e2b.Dockerfile"
template_name = "terminal"
template_id = "ne8xtb57tq5xw9vwhdyc"

View File

@ -9,6 +9,7 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@e2b/code-interpreter": "^0.0.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",
@ -50,6 +51,39 @@
"node": ">=12" "node": ">=12"
} }
}, },
"node_modules/@e2b/code-interpreter": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@e2b/code-interpreter/-/code-interpreter-0.0.7.tgz",
"integrity": "sha512-e8nAY4zXU2b9nKthqq/pCPlTVD7f01dtzCtvabWmhlx7Wq+AUln14Q1Wf+uRVJXHkwS9BDv2CupdZpUChsjoCA==",
"dependencies": {
"e2b": "^0.16.1",
"isomorphic-ws": "^5.0.0",
"ws": "^8.15.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/@e2b/code-interpreter/node_modules/ws": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
"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/@jridgewell/resolve-uri": { "node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
@ -369,6 +403,19 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/bufferutil": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/bytes": { "node_modules/bytes": {
"version": "3.1.2", "version": "3.1.2",
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
@ -662,6 +709,59 @@
"url": "https://dotenvx.com" "url": "https://dotenvx.com"
} }
}, },
"node_modules/e2b": {
"version": "0.16.1",
"resolved": "https://registry.npmjs.org/e2b/-/e2b-0.16.1.tgz",
"integrity": "sha512-2L1R/REEB+EezD4Q4MmcXXNATjvCYov2lv/69+PY6V95+wl1PZblIMTYAe7USxX6P6sqANxNs+kXqZr6RvXkSw==",
"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"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"bufferutil": "^4.0.8",
"utf-8-validate": "^6.0.3"
}
},
"node_modules/e2b/node_modules/utf-8-validate": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-6.0.4.tgz",
"integrity": "sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==",
"hasInstallScript": true,
"optional": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/e2b/node_modules/ws": {
"version": "8.17.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.0.tgz",
"integrity": "sha512-uJq6108EgZMAl20KagGkzCKfMEjxmKvZHG7Tlq0Z6nOky7YF7aq4mOx6xK8TJ/i1LeK4Qus7INktacctDgY8Ow==",
"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/ee-first": { "node_modules/ee-first": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
@ -1082,6 +1182,14 @@
"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",
@ -1186,6 +1294,17 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/node-gyp-build": {
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.1.tgz",
"integrity": "sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==",
"optional": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/node-pty": { "node_modules/node-pty": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz",
@ -1265,7 +1384,6 @@
"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"
} }
@ -1297,6 +1415,15 @@
"node": ">= 0.8" "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/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",
@ -1305,6 +1432,11 @@
"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",
@ -1322,6 +1454,11 @@
"url": "https://github.com/sponsors/jonschlinkert" "url": "https://github.com/sponsors/jonschlinkert"
} }
}, },
"node_modules/platform": {
"version": "1.3.6",
"resolved": "https://registry.npmjs.org/platform/-/platform-1.3.6.tgz",
"integrity": "sha512-fnWVljUchTro6RiCFvCXBbNhJc2NijN7oIQxbwsyL0buWJPG85v81ehlHI9fXrJsMNgTofEoWIQeClKpgxFLrg=="
},
"node_modules/proxy-addr": { "node_modules/proxy-addr": {
"version": "2.0.7", "version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@ -1835,6 +1972,20 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/utf-8-validate": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
"integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
"hasInstallScript": true,
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/utils-merge": { "node_modules/utils-merge": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz",

View File

@ -11,6 +11,7 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@e2b/code-interpreter": "^0.0.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",

View File

@ -18,7 +18,7 @@ import {
renameFile, renameFile,
saveFile, saveFile,
} from "./utils"; } from "./utils";
import { IDisposable, IPty, spawn } from "node-pty"; import { Sandbox, Process, ProcessMessage } from "e2b";
import { import {
MAX_BODY_SIZE, MAX_BODY_SIZE,
createFileRL, createFileRL,
@ -44,7 +44,10 @@ let inactivityTimeout: NodeJS.Timeout | null = null;
let isOwnerConnected = false; let isOwnerConnected = false;
const terminals: { const terminals: {
[id: string]: { terminal: IPty; onData: IDisposable; onExit: IDisposable }; [id: string]: Process;
} = {};
const containers: {
[id: string]: Sandbox;
} = {}; } = {};
const dirName = path.join(__dirname, ".."); const dirName = path.join(__dirname, "..");
@ -100,6 +103,32 @@ io.use(async (socket, next) => {
next(); next();
}); });
class LockManager {
private locks: { [key: string]: Promise<any> };
constructor() {
this.locks = {};
}
async acquireLock<T>(key: string, task: () => Promise<T>): Promise<T> {
if (!this.locks[key]) {
this.locks[key] = new Promise<T>(async (resolve, reject) => {
try {
const result = await task();
resolve(result);
} catch (error) {
reject(error);
} finally {
delete this.locks[key];
}
});
}
return await this.locks[key];
}
}
const lockManager = new LockManager();
io.on("connection", async (socket) => { io.on("connection", async (socket) => {
if (inactivityTimeout) clearTimeout(inactivityTimeout); if (inactivityTimeout) clearTimeout(inactivityTimeout);
@ -118,6 +147,16 @@ io.on("connection", async (socket) => {
} }
} }
await lockManager.acquireLock(data.sandboxId, async () => {
if (!containers[data.sandboxId]) {
console.log("Creating container ", data.sandboxId);
containers[data.sandboxId] = await Sandbox.create({
template: "terminal",
});
console.log("Created.");
}
});
const sandboxFiles = await getSandboxFiles(data.sandboxId); const sandboxFiles = await getSandboxFiles(data.sandboxId);
sandboxFiles.fileData.forEach((file) => { sandboxFiles.fileData.forEach((file) => {
const filePath = path.join(dirName, file.id); const filePath = path.join(dirName, file.id);
@ -319,42 +358,55 @@ io.on("connection", async (socket) => {
callback(newFiles.files); callback(newFiles.files);
}); });
socket.on("createTerminal", (id: string, callback) => { function toBackslashNotation(input: string) {
return input
.replace(/\\/g, "\\\\") // Escape backslashes
.replace(/\n/g, "\\n") // Escape newlines
.replace(/\r/g, "\\r") // Escape carriage returns
.replace(/\t/g, "\\t") // Escape tabs
.replace(/"/g, '\\"') // Escape double quotes
.replace(/'/g, "\\'") // Escape single quotes
.replace(/\f/g, "\\f") // Escape form feeds
.replace(/\b/g, "\\b") // Escape backspaces
.replace(/\v/g, "\\v") // Escape vertical tabs
.replace(/\0/g, "\\0") // Escape null characters
.replace(/\a/g, "\\a") // Escape alert (bell)
.replace(/\e/g, "\\e"); // Escape escape
}
socket.on("createTerminal", async (id: string, callback) => {
if (terminals[id] || Object.keys(terminals).length >= 4) { if (terminals[id] || Object.keys(terminals).length >= 4) {
return; return;
} }
const pty = spawn(os.platform() === "win32" ? "cmd.exe" : "bash", [], { const onData = (data: ProcessMessage) => {
name: "xterm", console.log("process", toBackslashNotation(data.toString()));
cols: 100,
cwd: path.join(dirName, "projects", data.sandboxId),
});
const onData = pty.onData((data) => {
io.emit("terminalResponse", { io.emit("terminalResponse", {
id, id,
data, data: data.toString(),
}); });
});
const onExit = pty.onExit((code) => console.log("exit :(", code));
pty.write("export PS1='\\u > '\r");
pty.write("clear\r");
terminals[id] = {
terminal: pty,
onData,
onExit,
}; };
await lockManager.acquireLock(data.sandboxId, async () => {
console.log("Creating terminal", id);
terminals[id] = await containers[data.sandboxId].process.start({
cmd: 'TERM=xterm script -c "screen" /dev/null', // xterm vt100
onStdout: onData,
onStderr: onData,
onExit: (code) => console.log("exit :(", code),
});
terminals[id].sendStdin("export PS1='\\u > '\r\n");
terminals[id].sendStdin("clear\r\n");
console.log("Created terminal", id);
});
callback(); callback();
}); });
socket.on("resizeTerminal", (dimensions: { cols: number; rows: number }) => { socket.on("resizeTerminal", (dimensions: { cols: number; rows: number }) => {
Object.values(terminals).forEach((t) => { /*Object.values(terminals).forEach((t) => {
t.terminal.resize(dimensions.cols, dimensions.rows); t.terminal.resize(dimensions.cols, dimensions.rows);
}); });*/
}); });
socket.on("terminalData", (id: string, data: string) => { socket.on("terminalData", (id: string, data: string) => {
@ -363,19 +415,19 @@ io.on("connection", async (socket) => {
} }
try { try {
terminals[id].terminal.write(data); console.log(`Writing ${toBackslashNotation(data)} to ${id}`);
terminals[id].sendStdin(data);
} catch (e) { } catch (e) {
console.log("Error writing to terminal", e); console.log("Error writing to terminal", e);
} }
}); });
socket.on("closeTerminal", (id: string, callback) => { socket.on("closeTerminal", async (id: string, callback) => {
if (!terminals[id]) { if (!terminals[id]) {
return; return;
} }
terminals[id].onData.dispose(); await terminals[id].kill();
terminals[id].onExit.dispose();
delete terminals[id]; delete terminals[id];
callback(); callback();
@ -429,12 +481,20 @@ io.on("connection", async (socket) => {
socket.on("disconnect", async () => { socket.on("disconnect", async () => {
if (data.isOwner) { if (data.isOwner) {
Object.entries(terminals).forEach((t) => { Object.entries(terminals).forEach((t) => {
const { terminal, onData, onExit } = t[1]; const terminal = t[1];
onData.dispose(); terminal.kill();
onExit.dispose();
delete terminals[t[0]]; delete terminals[t[0]];
}); });
await lockManager.acquireLock(data.sandboxId, async () => {
if (containers[data.sandboxId]) {
console.log("Closing container", data.sandboxId);
await containers[data.sandboxId].close();
delete containers[data.sandboxId];
console.log("Closed");
}
});
socket.broadcast.emit( socket.broadcast.emit(
"disableAccess", "disableAccess",
"The sandbox owner has disconnected." "The sandbox owner has disconnected."