From 585dcb469ec42a34d4427d57911dd70fee7a734a Mon Sep 17 00:00:00 2001 From: James Murdza Date: Sun, 15 Sep 2024 10:46:01 -0700 Subject: [PATCH 01/10] fix: skip creating a directory in the container when it already exists --- backend/server/src/index.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index fe0690f..c635fbd 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -176,17 +176,20 @@ io.on("connection", async (socket) => { ); }; + // Copy all files from the project to the container const sandboxFiles = await getSandboxFiles(data.sandboxId); + const containerFiles = containers[data.sandboxId].files; const promises = sandboxFiles.fileData.map(async (file) => { - const filePath = path.join(dirName, file.id); try { - await containers[data.sandboxId].files.makeDir( - path.dirname(filePath) - ); + const filePath = path.join(dirName, file.id); + const parentDirectory = path.dirname(filePath); + if (!containerFiles.exists(parentDirectory)) { + await containerFiles.makeDir(parentDirectory); + } + await containerFiles.write(filePath, file.data); } catch (e: any) { - console.log("Failed to create directory: " + e); + console.log("Failed to create file: " + e); } - await containers[data.sandboxId].files.write(filePath, file.data); }); await Promise.all(promises); From c94678c430c17ceea74579e2753b9a82e725d127 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Sun, 15 Sep 2024 13:11:59 -0700 Subject: [PATCH 02/10] feat: watch container for file changes --- backend/server/src/index.ts | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index c635fbd..0e7d4aa 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -21,7 +21,7 @@ import { } from "./fileoperations"; import { LockManager } from "./utils"; -import { Sandbox, Filesystem } from "e2b"; +import { Sandbox, Filesystem, FilesystemEvent } from "e2b"; import { Terminal } from "./Terminal" @@ -170,9 +170,9 @@ io.on("connection", async (socket) => { }); // Change the owner of the project directory to user - const fixPermissions = async () => { + const fixPermissions = async (projectDirectory: string) => { await containers[data.sandboxId].commands.run( - `sudo chown -R user "${path.join(dirName, "projects", data.sandboxId)}"` + `sudo chown -R user "${projectDirectory}"` ); }; @@ -193,7 +193,26 @@ io.on("connection", async (socket) => { }); await Promise.all(promises); - fixPermissions(); + const projectDirectory = path.join(dirName, "projects", data.sandboxId); + + // Make the logged in user the owner of all project files + fixPermissions(projectDirectory); + + // Start filesystem watcher for the /home directory + containerFiles.watch(projectDirectory, (event: FilesystemEvent) => { + const filePath = path.join(projectDirectory, event.name); + if (event.type === "create") { + sandboxFiles.files.push({ + id: filePath, + name: event.name, + type: "file", + }); + console.log(`Create ${filePath}`); + } else if (event.type === "remove") { + console.log(`Remove ${filePath}`); + } + socket.emit("loaded", sandboxFiles.files); + }); socket.emit("loaded", sandboxFiles.files); @@ -248,7 +267,7 @@ io.on("connection", async (socket) => { path.join(dirName, file.id), body ); - fixPermissions(); + fixPermissions(projectDirectory); } catch (e: any) { console.error("Error saving file:", e); io.emit("error", `Error: file saving. ${e.message ?? e}`); @@ -270,7 +289,7 @@ io.on("connection", async (socket) => { path.join(dirName, fileId), path.join(dirName, newFileId) ); - fixPermissions(); + fixPermissions(projectDirectory); file.id = newFileId; @@ -363,7 +382,7 @@ io.on("connection", async (socket) => { path.join(dirName, id), "" ); - fixPermissions(); + fixPermissions(projectDirectory); sandboxFiles.files.push({ id, @@ -429,7 +448,7 @@ io.on("connection", async (socket) => { path.join(dirName, fileId), path.join(dirName, newFileId) ); - fixPermissions(); + fixPermissions(projectDirectory); await renameFile(fileId, newFileId, file.data); } catch (e: any) { console.error("Error renaming folder:", e); From 69b128734935aa9d9219e2450cf0e7abeda7780c Mon Sep 17 00:00:00 2001 From: James Murdza Date: Sun, 29 Sep 2024 17:40:09 -0700 Subject: [PATCH 03/10] fix: handle errors when fixing permissions --- backend/server/src/index.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 0e7d4aa..b81d213 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -171,9 +171,13 @@ io.on("connection", async (socket) => { // Change the owner of the project directory to user const fixPermissions = async (projectDirectory: string) => { - await containers[data.sandboxId].commands.run( - `sudo chown -R user "${projectDirectory}"` - ); + try { + await containers[data.sandboxId].commands.run( + `sudo chown -R user "${projectDirectory}"` + ); + } catch (e: any) { + console.log("Failed to fix permissions: " + e); + } }; // Copy all files from the project to the container From 7a00d24ab96744654982680bf66f79eebcc1e232 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Sun, 29 Sep 2024 20:54:09 -0700 Subject: [PATCH 04/10] feat: sync changes to the filesystem --- backend/server/src/index.ts | 206 ++++++++++++++++++++++++++++-------- 1 file changed, 164 insertions(+), 42 deletions(-) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index b81d213..0a4fc72 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -6,10 +6,15 @@ import { createServer } from "http"; import { Server } from "socket.io"; import { DokkuClient } from "./DokkuClient"; import { SecureGitClient, FileData } from "./SecureGitClient"; -import fs from "fs"; +import fs, { readFile } from "fs"; import { z } from "zod"; -import { User } from "./types"; +import { + TFile, + TFileData, + TFolder, + User +} from "./types"; import { createFile, deleteFile, @@ -21,7 +26,7 @@ import { } from "./fileoperations"; import { LockManager } from "./utils"; -import { Sandbox, Filesystem, FilesystemEvent } from "e2b"; +import { Sandbox, Filesystem, FilesystemEvent, EntryInfo } from "e2b"; import { Terminal } from "./Terminal" @@ -55,14 +60,14 @@ const terminals: Record = {}; const dirName = "/home/user"; -const moveFile = async ( - filesystem: Filesystem, - filePath: string, - newFilePath: string -) => { - const fileContents = await filesystem.read(filePath); - await filesystem.write(newFilePath, fileContents); - await filesystem.remove(filePath); +const moveFile = async (filesystem: Filesystem, filePath: string, newFilePath: string) => { + try { + const fileContents = await filesystem.read(filePath); + await filesystem.write(newFilePath, fileContents); + await filesystem.remove(filePath); + } catch (e) { + console.error(`Error moving file from ${filePath} to ${newFilePath}:`, e); + } }; io.use(async (socket, next) => { @@ -157,11 +162,12 @@ io.on("connection", async (socket) => { } } - await lockManager.acquireLock(data.sandboxId, async () => { + const createdContainer = await lockManager.acquireLock(data.sandboxId, async () => { try { if (!containers[data.sandboxId]) { containers[data.sandboxId] = await Sandbox.create({ timeoutMs: 1200000 }); console.log("Created container ", data.sandboxId); + return true; } } catch (e: any) { console.error(`Error creating container ${data.sandboxId}:`, e); @@ -169,6 +175,10 @@ io.on("connection", async (socket) => { } }); + const sandboxFiles = await getSandboxFiles(data.sandboxId); + const projectDirectory = path.join(dirName, "projects", data.sandboxId); + const containerFiles = containers[data.sandboxId].files; + // Change the owner of the project directory to user const fixPermissions = async (projectDirectory: string) => { try { @@ -180,44 +190,156 @@ io.on("connection", async (socket) => { } }; - // Copy all files from the project to the container - const sandboxFiles = await getSandboxFiles(data.sandboxId); - const containerFiles = containers[data.sandboxId].files; - const promises = sandboxFiles.fileData.map(async (file) => { + // Check if the given path is a directory + const isDirectory = async (projectDirectory: string): Promise => { try { - const filePath = path.join(dirName, file.id); - const parentDirectory = path.dirname(filePath); - if (!containerFiles.exists(parentDirectory)) { - await containerFiles.makeDir(parentDirectory); - } - await containerFiles.write(filePath, file.data); + const result = await containers[data.sandboxId].commands.run( + `[ -d "${projectDirectory}" ] && echo "true" || echo "false"` + ); + return result.stdout.trim() === "true"; } catch (e: any) { - console.log("Failed to create file: " + e); + console.log("Failed to check if directory: " + e); + return false; } - }); - await Promise.all(promises); + }; - const projectDirectory = path.join(dirName, "projects", data.sandboxId); + // Only continue to container setup if a new container was created + if (createdContainer) { - // Make the logged in user the owner of all project files - fixPermissions(projectDirectory); + // Copy all files from the project to the container + const promises = sandboxFiles.fileData.map(async (file) => { + try { + const filePath = path.join(dirName, file.id); + const parentDirectory = path.dirname(filePath); + if (!containerFiles.exists(parentDirectory)) { + await containerFiles.makeDir(parentDirectory); + } + await containerFiles.write(filePath, file.data); + } catch (e: any) { + console.log("Failed to create file: " + e); + } + }); + await Promise.all(promises); - // Start filesystem watcher for the /home directory - containerFiles.watch(projectDirectory, (event: FilesystemEvent) => { - const filePath = path.join(projectDirectory, event.name); - if (event.type === "create") { - sandboxFiles.files.push({ - id: filePath, - name: event.name, - type: "file", - }); - console.log(`Create ${filePath}`); - } else if (event.type === "remove") { - console.log(`Remove ${filePath}`); + // Make the logged in user the owner of all project files + fixPermissions(projectDirectory); + + } + + // Start filesystem watcher for the project directory + const watchDirectory = (directory: string) => { + try { + containerFiles.watch(directory, async (event: FilesystemEvent) => { + try { + + function removeDirName(path : string, dirName : string) { + return path.startsWith(dirName) ? path.slice(dirName.length) : path; + } + + // This is the absolute file path in the container + const containerFilePath = path.join(directory, event.name); + // This is the file path relative to the home directory + const sandboxFilePath = removeDirName(containerFilePath, dirName + "/"); + // This is the directory being watched relative to the home directory + const sandboxDirectory = removeDirName(directory, dirName + "/"); + + // Helper function to find a folder by id + function findFolderById(files: (TFolder | TFile)[], folderId : string) { + return files.find((file : TFolder | TFile) => file.type === "folder" && file.id === folderId); + } + + // A new file or directory was created. + if (event.type === "create") { + const folder = findFolderById(sandboxFiles.files, sandboxDirectory) as TFolder; + const isDir = await isDirectory(containerFilePath); + + const newItem = isDir + ? { id: sandboxFilePath, name: event.name, type: "folder", children: [] } as TFolder + : { id: sandboxFilePath, name: event.name, type: "file" } as TFile; + + if (folder) { + // If the folder exists, add the new item (file/folder) as a child + folder.children.push(newItem); + } else { + // If folder doesn't exist, add the new item to the root + sandboxFiles.files.push(newItem); + } + + if (!isDir) { + const fileData = await containers[data.sandboxId].files.read(containerFilePath); + const fileContents = typeof fileData === "string" ? fileData : ""; + sandboxFiles.fileData.push({ id: sandboxFilePath, data: fileContents }); + } + + console.log(`Create ${sandboxFilePath}`); + } + + // A file or directory was removed or renamed. + else if (event.type === "remove" || event.type == "rename") { + const folder = findFolderById(sandboxFiles.files, sandboxDirectory) as TFolder; + const isDir = await isDirectory(containerFilePath); + + const isFileMatch = (file: TFolder | TFile | TFileData) => file.id === sandboxFilePath || file.id.startsWith(containerFilePath + '/'); + + if (folder) { + // Remove item from its parent folder + folder.children = folder.children.filter((file: TFolder | TFile) => !isFileMatch(file)); + } else { + // Remove from the root if it's not inside a folder + sandboxFiles.files = sandboxFiles.files.filter((file: TFolder | TFile) => !isFileMatch(file)); + } + + // Also remove any corresponding file data + sandboxFiles.fileData = sandboxFiles.fileData.filter((file: TFileData) => !isFileMatch(file)); + + console.log(`Removed: ${sandboxFilePath}`); + } + + // The contents of a file were changed. + else if (event.type === "write") { + const folder = findFolderById(sandboxFiles.files, sandboxDirectory) as TFolder; + const fileToWrite = sandboxFiles.fileData.find(file => file.id === sandboxFilePath); + + if (fileToWrite) { + fileToWrite.data = await containers[data.sandboxId].files.read(containerFilePath); + console.log(`Write to ${sandboxFilePath}`); + } else { + // If the file is part of a folder structure, locate it and update its data + const fileInFolder = folder?.children.find(file => file.id === sandboxFilePath); + if (fileInFolder) { + const fileData = await containers[data.sandboxId].files.read(containerFilePath); + const fileContents = typeof fileData === "string" ? fileData : ""; + sandboxFiles.fileData.push({ id: sandboxFilePath, data: fileContents }); + console.log(`Write to ${sandboxFilePath}`); + } + } + } + + // Tell the client to reload the file list + socket.emit("loaded", sandboxFiles.files); + + } catch (error) { + console.error(`Error handling ${event.type} event for ${event.name}:`, error); + } + }) + } catch (error) { + console.error(`Error watching filesystem:`, error); } - socket.emit("loaded", sandboxFiles.files); - }); + }; + // Watch the project directory + watchDirectory(projectDirectory); + + // Watch all subdirectories of the project directory, but not deeper + // This also means directories created after the container is created won't be watched + const dirContent = await containerFiles.list(projectDirectory); + dirContent.forEach((item : EntryInfo) => { + if (item.type === "dir") { + console.log("Watching " + item.path); + watchDirectory(item.path); + } + }) + socket.emit("loaded", sandboxFiles.files); socket.on("getFile", (fileId: string, callback) => { From 13be78dee82a6fe3891f15210350fb22c7df76d9 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Mon, 30 Sep 2024 02:55:22 -0700 Subject: [PATCH 05/10] fix: don't exit the script when exceptions occur --- backend/server/src/index.ts | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 0a4fc72..c4ee2e0 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -39,6 +39,18 @@ import { saveFileRL, } from "./ratelimit"; +process.on('uncaughtException', (error) => { + console.error('Uncaught Exception:', error); + // Do not exit the process + // You can add additional logging or recovery logic here +}); + +process.on('unhandledRejection', (reason, promise) => { + console.error('Unhandled Rejection at:', promise, 'reason:', reason); + // Do not exit the process + // You can also handle the rejected promise here if needed +}); + dotenv.config(); const app: Express = express(); From 023b3bdc5e273eb0b70d0d90cdaa57edd71dc18b Mon Sep 17 00:00:00 2001 From: James Murdza Date: Mon, 30 Sep 2024 04:20:14 -0700 Subject: [PATCH 06/10] fix: add missing await keywords --- backend/server/src/index.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index c4ee2e0..c58aea4 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -239,9 +239,9 @@ io.on("connection", async (socket) => { } // Start filesystem watcher for the project directory - const watchDirectory = (directory: string) => { + const watchDirectory = async (directory: string) => { try { - containerFiles.watch(directory, async (event: FilesystemEvent) => { + await containerFiles.watch(directory, async (event: FilesystemEvent) => { try { function removeDirName(path : string, dirName : string) { @@ -340,17 +340,17 @@ io.on("connection", async (socket) => { }; // Watch the project directory - watchDirectory(projectDirectory); + await watchDirectory(projectDirectory); // Watch all subdirectories of the project directory, but not deeper // This also means directories created after the container is created won't be watched const dirContent = await containerFiles.list(projectDirectory); - dirContent.forEach((item : EntryInfo) => { + await Promise.all(dirContent.map(async (item : EntryInfo) => { if (item.type === "dir") { console.log("Watching " + item.path); - watchDirectory(item.path); + await watchDirectory(item.path); } - }) + })) socket.emit("loaded", sandboxFiles.files); From 8e3a6d1aa6cb7823cd1c0e792db3b1c1ba99e737 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Tue, 17 Sep 2024 16:52:31 -0700 Subject: [PATCH 07/10] fix: recreate timed out E2B sandboxes on page load --- backend/server/src/index.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index c58aea4..2277071 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -176,8 +176,9 @@ io.on("connection", async (socket) => { const createdContainer = await lockManager.acquireLock(data.sandboxId, async () => { try { - if (!containers[data.sandboxId]) { - containers[data.sandboxId] = await Sandbox.create({ timeoutMs: 1200000 }); + // 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 }); console.log("Created container ", data.sandboxId); return true; } From 63f3b082d530c154cfc939c9d15f75fc2e98328e Mon Sep 17 00:00:00 2001 From: James Murdza Date: Sun, 29 Sep 2024 17:23:16 -0700 Subject: [PATCH 08/10] fix: don't limit the number of terminals on the backend --- backend/server/src/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 2277071..f52aea2 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -656,7 +656,8 @@ io.on("connection", async (socket) => { socket.on("createTerminal", async (id: string, callback) => { try { - if (terminals[id] || Object.keys(terminals).length >= 4) { + // Note: The number of terminals per window is limited on the frontend, but not backend + if (terminals[id]) { return; } From 9d0680813757ecf23331db0c1af9be1bb839268e Mon Sep 17 00:00:00 2001 From: James Murdza Date: Mon, 30 Sep 2024 03:41:33 -0700 Subject: [PATCH 09/10] feat: keep containers alive for 60s of inactivity instead of killing them on disconnect --- backend/server/src/index.ts | 35 ++++++++++++---------------- frontend/components/editor/index.tsx | 7 ++++++ 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index f52aea2..568c453 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -51,6 +51,9 @@ process.on('unhandledRejection', (reason, promise) => { // You can also handle the rejected promise here if needed }); +// 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(); @@ -178,7 +181,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); return true; } @@ -355,6 +358,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 { @@ -807,25 +821,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 f5750ee..610a089 100644 --- a/frontend/components/editor/index.tsx +++ b/frontend/components/editor/index.tsx @@ -57,6 +57,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({ From 7e48faa1b5f676cea1ce845d52c2f4ba38086e37 Mon Sep 17 00:00:00 2001 From: James Murdza Date: Wed, 2 Oct 2024 13:44:55 -0700 Subject: [PATCH 10/10] fix: prevent the file sync from timing out after the default timeout --- backend/server/src/index.ts | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/backend/server/src/index.ts b/backend/server/src/index.ts index 568c453..dedc57f 100644 --- a/backend/server/src/index.ts +++ b/backend/server/src/index.ts @@ -26,7 +26,7 @@ import { } from "./fileoperations"; import { LockManager } from "./utils"; -import { Sandbox, Filesystem, FilesystemEvent, EntryInfo } from "e2b"; +import { Sandbox, Filesystem, FilesystemEvent, EntryInfo, WatchHandle } from "e2b"; import { Terminal } from "./Terminal" @@ -194,6 +194,7 @@ io.on("connection", async (socket) => { const sandboxFiles = await getSandboxFiles(data.sandboxId); const projectDirectory = path.join(dirName, "projects", data.sandboxId); const containerFiles = containers[data.sandboxId].files; + const fileWatchers: WatchHandle[] = []; // Change the owner of the project directory to user const fixPermissions = async (projectDirectory: string) => { @@ -243,9 +244,9 @@ io.on("connection", async (socket) => { } // Start filesystem watcher for the project directory - const watchDirectory = async (directory: string) => { + const watchDirectory = async (directory: string): Promise => { try { - await containerFiles.watch(directory, async (event: FilesystemEvent) => { + return await containerFiles.watch(directory, async (event: FilesystemEvent) => { try { function removeDirName(path : string, dirName : string) { @@ -337,14 +338,16 @@ io.on("connection", async (socket) => { } catch (error) { console.error(`Error handling ${event.type} event for ${event.name}:`, error); } - }) + }, { "timeout": 0 } ) } catch (error) { console.error(`Error watching filesystem:`, error); } }; // Watch the project directory - await watchDirectory(projectDirectory); + const handle = await watchDirectory(projectDirectory); + // Keep track of watch handlers to close later + if (handle) fileWatchers.push(handle); // Watch all subdirectories of the project directory, but not deeper // This also means directories created after the container is created won't be watched @@ -352,7 +355,9 @@ io.on("connection", async (socket) => { await Promise.all(dirContent.map(async (item : EntryInfo) => { if (item.type === "dir") { console.log("Watching " + item.path); - await watchDirectory(item.path); + // Keep track of watch handlers to close later + const handle = await watchDirectory(item.path); + if (handle) fileWatchers.push(handle); } })) @@ -820,6 +825,11 @@ io.on("connection", async (socket) => { connections[data.sandboxId]--; } + // Stop watching file changes in the container + Promise.all(fileWatchers.map(async (handle : WatchHandle) => { + await handle.close(); + })); + if (data.isOwner && connections[data.sandboxId] <= 0) { socket.broadcast.emit( "disableAccess",