fix: wait until the owner is disconnected from all sockets to close terminals and file manager

This commit is contained in:
James Murdza 2024-10-25 14:14:50 -06:00
parent 3ad7e5d9bc
commit e229dab826
2 changed files with 34 additions and 12 deletions

View File

@ -46,61 +46,81 @@ export class Sandbox {
socket: Socket; socket: Socket;
sandboxId: string; sandboxId: string;
userId: string; userId: string;
isOwner: boolean;
constructor(sandboxId: string, userId: string, { aiWorker, dokkuClient, gitClient, socket }: ServerContext) { constructor(sandboxId: string, userId: string, isOwner: boolean, { aiWorker, dokkuClient, gitClient, socket }: ServerContext) {
this.fileManager = null; this.fileManager = null;
this.terminalManager = null; this.terminalManager = null;
this.container = null; this.container = null;
this.sandboxId = sandboxId; this.sandboxId = sandboxId;
this.userId = userId; this.userId = userId;
this.isOwner = isOwner;
this.aiWorker = aiWorker; this.aiWorker = aiWorker;
this.dokkuClient = dokkuClient; this.dokkuClient = dokkuClient;
this.gitClient = gitClient; this.gitClient = gitClient;
this.socket = socket; this.socket = socket;
} }
// Initializes the container for the sandbox environment
async initializeContainer() { async initializeContainer() {
// Acquire a lock to ensure exclusive access to the sandbox environment
await lockManager.acquireLock(this.sandboxId, async () => { await lockManager.acquireLock(this.sandboxId, async () => {
// Check if a container already exists and is running
if (this.container && await this.container.isRunning()) { if (this.container && await this.container.isRunning()) {
console.log(`Found existing container ${this.sandboxId}`) console.log(`Found existing container ${this.sandboxId}`)
} else { } else {
console.log("Creating container", this.sandboxId) console.log("Creating container", this.sandboxId)
// Create a new container with a specified timeout
this.container = await E2BSandbox.create({ this.container = await E2BSandbox.create({
timeoutMs: CONTAINER_TIMEOUT, timeoutMs: CONTAINER_TIMEOUT,
}) })
} }
}) })
// Ensure a container was successfully created
if (!this.container) throw new Error("Failed to create container") if (!this.container) throw new Error("Failed to create container")
// Initialize the terminal manager if it hasn't been set up yet
if (!this.terminalManager) { if (!this.terminalManager) {
this.terminalManager = new TerminalManager(this.container) this.terminalManager = new TerminalManager(this.container)
console.log(`Terminal manager set up for ${this.sandboxId}`) console.log(`Terminal manager set up for ${this.sandboxId}`)
} }
// Initialize the file manager if it hasn't been set up yet
if (!this.fileManager) { if (!this.fileManager) {
this.fileManager = new FileManager( this.fileManager = new FileManager(
this.sandboxId, this.sandboxId,
this.container, this.container,
(files: (TFolder | TFile)[]) => { (files: (TFolder | TFile)[]) => {
// Emit an event to the socket when files are loaded
this.socket.emit("loaded", files) this.socket.emit("loaded", files)
} }
) )
// Initialize the file manager and emit the initial files
this.fileManager.initialize() this.fileManager.initialize()
this.socket.emit("loaded", this.fileManager.files) this.socket.emit("loaded", this.fileManager.files)
} }
} }
// Called when the client disconnects from the Sandbox
async disconnect() { async disconnect() {
// Close all terminals managed by the terminal manager
await this.terminalManager?.closeAllTerminals() await this.terminalManager?.closeAllTerminals()
// This way the terminal manager will be set up again if we reconnect
this.terminalManager = null;
// Close all file watchers managed by the file manager
await this.fileManager?.closeWatchers() await this.fileManager?.closeWatchers()
// This way the file manager will be set up again if we reconnect
this.fileManager = null;
} }
handlers() { handlers() {
// Handle heartbeat from a socket connection // Handle heartbeat from a socket connection
const handleHeartbeat: SocketHandler = (_: any) => { const handleHeartbeat: SocketHandler = (_: any) => {
this.container?.setTimeout(CONTAINER_TIMEOUT) // Only keep the sandbox alive if the owner is still connected
if (this.isOwner) {
this.container?.setTimeout(CONTAINER_TIMEOUT)
}
} }
// Handle getting a file // Handle getting a file

View File

@ -112,10 +112,12 @@ io.on("connection", async (socket) => {
const sandboxManager = sandboxes[data.sandboxId] ?? new Sandbox( const sandboxManager = sandboxes[data.sandboxId] ?? new Sandbox(
data.sandboxId, data.sandboxId,
data.userId, data.userId,
data.isOwner,
{ aiWorker, dokkuClient, gitClient, socket } { aiWorker, dokkuClient, gitClient, socket }
) )
// Initialize the sandbox container // Initialize the sandbox container
// The file manager and terminal managers will be set up if they have been closed
sandboxManager.initializeContainer() sandboxManager.initializeContainer()
// Register event handlers for the sandbox // Register event handlers for the sandbox
@ -134,15 +136,15 @@ io.on("connection", async (socket) => {
try { try {
if (data.isOwner) { if (data.isOwner) {
connections.ownerDisconnected(data.sandboxId) connections.ownerDisconnected(data.sandboxId)
} // If the owner has disconnected from all sockets, close open terminals and file watchers.o
// The sandbox itself will timeout after the heartbeat stops.
await sandboxManager.disconnect() if (!connections.ownerIsConnected(data.sandboxId)) {
await sandboxManager.disconnect()
if (data.isOwner && !connections.ownerIsConnected(data.sandboxId)) { socket.broadcast.emit(
socket.broadcast.emit( "disableAccess",
"disableAccess", "The sandbox owner has disconnected."
"The sandbox owner has disconnected." )
) }
} }
} catch (e: any) { } catch (e: any) {
handleErrors("Error disconnecting:", e, socket); handleErrors("Error disconnecting:", e, socket);