Compare commits

..

3 Commits

9 changed files with 121 additions and 188 deletions

View File

@ -12,7 +12,7 @@
"concurrently": "^8.2.2",
"cors": "^2.8.5",
"dotenv": "^16.4.5",
"e2b": "^0.16.2-beta.47",
"e2b": "^0.16.1",
"express": "^4.19.2",
"rate-limiter-flexible": "^5.0.3",
"simple-git": "^3.25.0",
@ -41,28 +41,6 @@
"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",
@ -465,7 +443,6 @@
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
"hasInstallScript": true,
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
@ -587,11 +564,6 @@
"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",
@ -765,19 +737,23 @@
}
},
"node_modules/e2b": {
"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==",
"version": "0.16.2",
"resolved": "https://registry.npmjs.org/e2b/-/e2b-0.16.2.tgz",
"integrity": "sha512-xKmVK4ipgVQPJ/uyyrfH9LnaawERRWt8U2UZhdhGfzdL/QU/OpBjuhoIbFCv1Uy6qXV4nIiJ6Nw4MBC4HmXf1g==",
"dependencies": {
"@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"
"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/ee-first": {
@ -1219,6 +1195,14 @@
"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",
@ -1317,7 +1301,6 @@
"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",
@ -1400,7 +1383,6 @@
"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"
}
@ -1435,19 +1417,15 @@
"node": ">= 0.8"
}
},
"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-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-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",
@ -1456,6 +1434,11 @@
"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",
@ -2070,7 +2053,6 @@
"integrity": "sha512-xu9GQDeFp+eZ6LnCywXN/zBancWvOpUMzgjLPSjy4BRHSmTelvn2E0DG0o1sTiw5hkCKBHo8rwSKncfRfv2EEQ==",
"hasInstallScript": true,
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
@ -2116,6 +2098,26 @@
"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",

View File

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

View File

@ -1,68 +0,0 @@
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,11 +20,7 @@ import {
saveFile,
} from "./fileoperations";
import { LockManager } from "./utils";
import { Sandbox, Filesystem } from "e2b";
import { Terminal } from "./Terminal"
import { Sandbox, Terminal, FilesystemManager } from "e2b";
import {
MAX_BODY_SIZE,
createFileRL,
@ -56,12 +52,12 @@ const terminals: Record<string, Terminal> = {};
const dirName = "/home/user";
const moveFile = async (
filesystem: Filesystem,
filesystem: FilesystemManager,
filePath: string,
newFilePath: string
) => {
const fileContents = await filesystem.read(filePath);
await filesystem.write(newFilePath, fileContents);
const fileContents = await filesystem.readBytes(filePath);
await filesystem.writeBytes(newFilePath, fileContents);
await filesystem.remove(filePath);
};
@ -160,7 +156,7 @@ io.on("connection", async (socket) => {
await lockManager.acquireLock(data.sandboxId, async () => {
try {
if (!containers[data.sandboxId]) {
containers[data.sandboxId] = await Sandbox.create({ timeoutMs: 1200000 });
containers[data.sandboxId] = await Sandbox.create();
console.log("Created container ", data.sandboxId);
}
} catch (e: any) {
@ -171,7 +167,7 @@ io.on("connection", async (socket) => {
// Change the owner of the project directory to user
const fixPermissions = async () => {
await containers[data.sandboxId].commands.run(
await containers[data.sandboxId].process.startAndWait(
`sudo chown -R user "${path.join(dirName, "projects", data.sandboxId)}"`
);
};
@ -179,14 +175,10 @@ io.on("connection", async (socket) => {
const sandboxFiles = await getSandboxFiles(data.sandboxId);
sandboxFiles.fileData.forEach(async (file) => {
const filePath = path.join(dirName, file.id);
try {
await containers[data.sandboxId].files.makeDir(
await containers[data.sandboxId].filesystem.makeDir(
path.dirname(filePath)
);
} catch (e: any) {
console.log("Failed to create directory: " + e);
}
await containers[data.sandboxId].files.write(filePath, file.data);
await containers[data.sandboxId].filesystem.write(filePath, file.data);
});
fixPermissions();
@ -239,7 +231,7 @@ io.on("connection", async (socket) => {
if (!file) return;
file.data = body;
await containers[data.sandboxId].files.write(
await containers[data.sandboxId].filesystem.write(
path.join(dirName, file.id),
body
);
@ -261,7 +253,7 @@ io.on("connection", async (socket) => {
const newFileId = folderId + "/" + parts.pop();
await moveFile(
containers[data.sandboxId].files,
containers[data.sandboxId].filesystem,
path.join(dirName, fileId),
path.join(dirName, newFileId)
);
@ -354,7 +346,7 @@ io.on("connection", async (socket) => {
const id = `projects/${data.sandboxId}/${name}`;
await containers[data.sandboxId].files.write(
await containers[data.sandboxId].filesystem.write(
path.join(dirName, id),
""
);
@ -391,7 +383,7 @@ io.on("connection", async (socket) => {
const id = `projects/${data.sandboxId}/${name}`;
await containers[data.sandboxId].files.makeDir(
await containers[data.sandboxId].filesystem.makeDir(
path.join(dirName, id)
);
@ -420,7 +412,7 @@ io.on("connection", async (socket) => {
parts.slice(0, parts.length - 1).join("/") + "/" + newName;
await moveFile(
containers[data.sandboxId].files,
containers[data.sandboxId].filesystem,
path.join(dirName, fileId),
path.join(dirName, newFileId)
);
@ -443,7 +435,7 @@ io.on("connection", async (socket) => {
const file = sandboxFiles.fileData.find((f) => f.id === fileId);
if (!file) return;
await containers[data.sandboxId].files.remove(
await containers[data.sandboxId].filesystem.remove(
path.join(dirName, fileId)
);
sandboxFiles.fileData = sandboxFiles.fileData.filter(
@ -470,7 +462,7 @@ io.on("connection", async (socket) => {
await Promise.all(
files.map(async (file) => {
await containers[data.sandboxId].files.remove(
await containers[data.sandboxId].filesystem.remove(
path.join(dirName, file)
);
@ -499,36 +491,35 @@ io.on("connection", async (socket) => {
await lockManager.acquireLock(data.sandboxId, async () => {
try {
terminals[id] = new Terminal(containers[data.sandboxId])
await terminals[id].init({
onData: (responseString: string) => {
io.emit("terminalResponse", { id, data: responseString });
terminals[id] = await containers[data.sandboxId].terminal.start({
onData: (responseData: string) => {
io.emit("terminalResponse", { id, data: responseData });
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;
}
const port = parseInt(extractPortNumber(responseString) ?? "");
const port = parseInt(extractPortNumber(responseData) ?? "");
if (port) {
io.emit(
"previewURL",
"https://" + containers[data.sandboxId].getHost(port)
"https://" + containers[data.sandboxId].getHostname(port)
);
}
},
cols: 80,
rows: 20,
//onExit: () => console.log("Terminal exited", id),
size: { cols: 80, rows: 20 },
onExit: () => console.log("Terminal exited", id),
});
await terminals[id].sendData(
`cd "${path.join(dirName, "projects", data.sandboxId)}"\rexport PS1='user> '\rclear\r`
`cd "${path.join(dirName, "projects", data.sandboxId)}"\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);
@ -557,7 +548,7 @@ io.on("connection", async (socket) => {
}
);
socket.on("terminalData", async (id: string, data: string) => {
socket.on("terminalData", (id: string, data: string) => {
try {
if (!terminals[id]) {
return;
@ -576,7 +567,7 @@ io.on("connection", async (socket) => {
return;
}
await terminals[id].close();
await terminals[id].kill();
delete terminals[id];
callback();
@ -645,7 +636,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.close();
await terminal.kill();
delete terminals[key];
})
);
@ -653,7 +644,7 @@ io.on("connection", async (socket) => {
await lockManager.acquireLock(data.sandboxId, async () => {
try {
if (containers[data.sandboxId]) {
await containers[data.sandboxId].kill();
await containers[data.sandboxId].close();
delete containers[data.sandboxId];
console.log("Closed container", data.sandboxId);
}

View File

@ -6,6 +6,7 @@ import { notFound, redirect } from "next/navigation"
import Loading from "@/components/editor/loading"
import dynamic from "next/dynamic"
import fs from "fs"
import { TerminalProvider } from "@/context/TerminalContext"
export const revalidate = 0
@ -87,8 +88,10 @@ export default async function CodePage({ params }: { params: { id: string } }) {
}
return (
<>
<div className="overflow-hidden overscroll-none w-screen flex flex-col h-screen bg-background">
<Room id={sandboxId}>
<TerminalProvider>
<Navbar userData={userData} sandboxData={sandboxData} shared={shared} />
<div className="w-screen flex grow">
<CodeEditor
@ -96,7 +99,9 @@ export default async function CodePage({ params }: { params: { id: string } }) {
sandboxData={sandboxData}
/>
</div>
</TerminalProvider>
</Room>
</div>
</>
)
}

View File

@ -6,7 +6,6 @@ import { ThemeProvider } from "@/components/layout/themeProvider"
import { ClerkProvider } from "@clerk/nextjs"
import { Toaster } from "@/components/ui/sonner"
import { Analytics } from "@vercel/analytics/react"
import { TerminalProvider } from '@/context/TerminalContext';
import { PreviewProvider } from "@/context/PreviewContext";
import { SocketProvider } from '@/context/SocketContext'
@ -32,9 +31,7 @@ export default function RootLayout({
>
<SocketProvider>
<PreviewProvider>
<TerminalProvider>
{children}
</TerminalProvider>
</PreviewProvider>
</SocketProvider>
<Analytics />

View File

@ -51,7 +51,7 @@ export default function Dashboard({
useEffect(() => { // update the dashboard to show a new project
router.refresh()
}, [sandboxes])
}, [])
return (
<>

View File

@ -1,37 +1,48 @@
"use client"
import { SetStateAction, useCallback, useEffect, useRef, useState } from "react"
import monaco from "monaco-editor"
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
import { toast } from "sonner"
import { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types"
// Authentication
import { useClerk } from "@clerk/nextjs"
// Liveblocks
import * as Y from "yjs"
import LiveblocksProvider from "@liveblocks/yjs"
import { MonacoBinding } from "y-monaco"
import { Awareness } from "y-protocols/awareness"
import { TypedLiveblocksProvider, useRoom, useSelf } from "@/liveblocks.config"
// Icons
import { FileJson, Loader2, TerminalSquare } from "lucide-react"
// Contexts
import { PreviewProvider, usePreview } from '@/context/PreviewContext';
import { useSocket } from "@/context/SocketContext"
// External Components
import monaco from "monaco-editor"
import { Terminal } from "@xterm/xterm"
import Editor, { BeforeMount, OnMount } from "@monaco-editor/react"
import { toast } from "sonner"
import {
ResizableHandle,
ResizablePanel,
ResizablePanelGroup,
} from "@/components/ui/resizable"
import { FileJson, Loader2, TerminalSquare } from "lucide-react"
import { ImperativePanelHandle } from "react-resizable-panels"
// Editor Components
import Tab from "../ui/tab"
import Sidebar from "./sidebar"
import GenerateInput from "./generate"
import { Sandbox, User, TFile, TFolder, TTab } from "@/lib/types"
import { addNew, processFileType, validateName, debounce } from "@/lib/utils"
import { Cursors } from "./live/cursors"
import { Terminal } from "@xterm/xterm"
import DisableAccessModal from "./live/disableModal"
import Loading from "./loading"
import PreviewWindow from "./preview"
import Terminals from "./terminals"
import { ImperativePanelHandle } from "react-resizable-panels"
import { PreviewProvider, usePreview } from '@/context/PreviewContext';
import { useSocket } from "@/context/SocketContext"
export default function CodeEditor({
userData,
@ -41,22 +52,17 @@ export default function CodeEditor({
sandboxData: Sandbox
}) {
//SocketContext functions and effects
// Socket to the backend server
const { socket, setUserAndSandboxId } = useSocket();
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
// Pass the user and sandbox ID to the socket, causing the socket to be created and to connect.
setUserAndSandboxId(userData.id, sandboxData.id);
}
}
}, [socket, userData.id, sandboxData.id, setUserAndSandboxId]);
}, []);
//Preview Button state
// Preview panel state
const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true)
// When the owner closes the project, isDisabled gets set for the other users.
const [disableAccess, setDisableAccess] = useState({
isDisabled: false,
message: "",

View File

@ -27,7 +27,7 @@ export default function Navbar({
const [isShareOpen, setIsShareOpen] = useState(false);
const [isRunning, setIsRunning] = useState(false);
const isOwner = sandboxData.userId === userData.id;;
const isOwner = sandboxData.userId === userData.id;
return (
<>