diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 50c7260..75ad3d3 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -34,6 +34,9 @@ import { saveFileRL, } from "./ratelimit"; +// The amount of time in ms that a container will stay alive without a hearbeat. +const CONTAINER_TIMEOUT = 60_000; + dotenv.config(); const app: Express = express(); @@ -161,7 +164,7 @@ io.on("connection", async (socket) => { try { // Start a new container if the container doesn't exist or it timed out. if (!containers[data.sandboxId] || !(await containers[data.sandboxId].isRunning())) { - containers[data.sandboxId] = await Sandbox.create({ timeoutMs: 1200_000 }); + containers[data.sandboxId] = await Sandbox.create({ timeoutMs: CONTAINER_TIMEOUT }); console.log("Created container ", data.sandboxId); } } catch (e: any) { @@ -198,6 +201,17 @@ io.on("connection", async (socket) => { socket.emit("loaded", sandboxFiles.files); + socket.on("heartbeat", async () => { + try { + // This keeps the container alive for another CONTAINER_TIMEOUT seconds. + // The E2B docs are unclear, but the timeout is relative to the time of this method call. + await containers[data.sandboxId].setTimeout(CONTAINER_TIMEOUT); + } catch (e: any) { + console.error("Error setting timeout:", e); + io.emit("error", `Error: set timeout. ${e.message ?? e}`); + } + }); + socket.on("getFile", (fileId: string, callback) => { console.log(fileId); try { @@ -656,25 +670,6 @@ 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(); - delete terminals[key]; - }) - ); - - await lockManager.acquireLock(data.sandboxId, async () => { - try { - if (containers[data.sandboxId]) { - await containers[data.sandboxId].kill(); - delete containers[data.sandboxId]; - console.log("Closed container", data.sandboxId); - } - } catch (error) { - console.error("Error closing container ", data.sandboxId, error); - } - }); - socket.broadcast.emit( "disableAccess", "The sandbox owner has disconnected." diff --git a/frontend/components/editor/index.tsx b/frontend/components/editor/index.tsx index ba10bc8..ab497ad 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -59,6 +59,13 @@ export default function CodeEditor({ } }, [socket, userData.id, sandboxData.id, setUserAndSandboxId]) + // This heartbeat is critical to preventing the E2B sandbox from timing out + useEffect(() => { + // 10000 ms = 10 seconds + const interval = setInterval(() => socket?.emit("heartbeat"), 10000); + return () => clearInterval(interval); + }, [socket]); + //Preview Button state const [isPreviewCollapsed, setIsPreviewCollapsed] = useState(true) const [disableAccess, setDisableAccess] = useState({