diff --git a/apps/dokploy/server/wss/docker-container-logs.ts b/apps/dokploy/server/wss/docker-container-logs.ts index 47868790b..eaefa21f1 100644 --- a/apps/dokploy/server/wss/docker-container-logs.ts +++ b/apps/dokploy/server/wss/docker-container-logs.ts @@ -71,7 +71,9 @@ export const setupDockerContainerLogsWebSocketServer = ( const command = search ? `${baseCommand} 2>&1 | grep --line-buffered -iF "${escapedSearch}"` : baseCommand; - client.exec(command, (err, stream) => { + // Use pty: true to ensure the remote process receives SIGHUP when SSH connection closes + // This is crucial for terminating docker logs processes when the connection is closed + client.exec(command, { pty: true }, (err, stream) => { if (err) { console.error("Execution error:", err); ws.close(); diff --git a/apps/dokploy/server/wss/docker-container-terminal.ts b/apps/dokploy/server/wss/docker-container-terminal.ts index e39db53fa..155d7f0cc 100644 --- a/apps/dokploy/server/wss/docker-container-terminal.ts +++ b/apps/dokploy/server/wss/docker-container-terminal.ts @@ -58,7 +58,12 @@ export const setupDockerContainerTerminalWebSocketServer = ( `docker exec -it -w / ${containerId} ${activeWay}`, { pty: true }, (err, stream) => { - if (err) throw err; + if (err) { + console.error("SSH exec error:", err); + ws.close(); + conn.end(); + return; + } stream .on("close", (code: number, _signal: string) => { @@ -93,10 +98,20 @@ export const setupDockerContainerTerminalWebSocketServer = ( ws.on("close", () => { stream.end(); + // Ensure SSH connection is closed when WebSocket closes + conn.end(); }); }, ); }) + .on("error", (err) => { + console.error("SSH connection error:", err); + if (ws.readyState === ws.OPEN) { + ws.send(`SSH error: ${err.message}`); + ws.close(); + } + conn.end(); + }) .connect({ host: server.ipAddress, port: server.port, diff --git a/apps/dokploy/server/wss/listen-deployment.ts b/apps/dokploy/server/wss/listen-deployment.ts index 47010a565..ca49cea29 100644 --- a/apps/dokploy/server/wss/listen-deployment.ts +++ b/apps/dokploy/server/wss/listen-deployment.ts @@ -31,8 +31,11 @@ export const setupDeploymentLogsWebSocketServer = ( const serverId = url.searchParams.get("serverId"); const { user, session } = await validateRequest(req); + // Generate unique connection ID for tracking + const connectionId = `deployment-logs-${Date.now()}-${Math.random().toString(36).substring(7)}`; + if (!logPath) { - console.log("logPath no provided"); + console.log(`[${connectionId}] logPath no provided`); ws.close(4000, "logPath no provided"); return; } @@ -42,40 +45,55 @@ export const setupDeploymentLogsWebSocketServer = ( return; } + let tailProcess: ReturnType | null = null; + let sshClient: Client | null = null; + try { if (serverId) { const server = await findServerById(serverId); - if (!server.sshKeyId) return; - const client = new Client(); - client + if (!server.sshKeyId) { + ws.close(); + return; + } + + sshClient = new Client(); + sshClient .on("ready", () => { const command = ` tail -n +1 -f ${logPath}; `; - client.exec(command, (err, stream) => { + sshClient!.exec(command, (err, stream) => { if (err) { - console.error("Execution error:", err); + sshClient!.end(); ws.close(); return; } stream .on("close", () => { - client.end(); + sshClient!.end(); ws.close(); }) .on("data", (data: string) => { - ws.send(data.toString()); + if (ws.readyState === ws.OPEN) { + ws.send(data.toString()); + } }) .stderr.on("data", (data) => { - ws.send(data.toString()); + if (ws.readyState === ws.OPEN) { + ws.send(data.toString()); + } }); }); }) .on("error", (err) => { - console.error("SSH connection error:", err); - ws.send(`SSH error: ${err.message}`); - ws.close(); // Cierra el WebSocket si hay un error con SSH + if (ws.readyState === ws.OPEN) { + ws.send(`SSH error: ${err.message}`); + ws.close(); + } + if (sshClient) { + sshClient.end(); + } }) .connect({ host: server.ipAddress, @@ -85,26 +103,75 @@ export const setupDeploymentLogsWebSocketServer = ( }); ws.on("close", () => { - client.end(); + if (sshClient) { + sshClient.end(); + } }); } else { - const tail = spawn("tail", ["-n", "+1", "-f", logPath]); + tailProcess = spawn("tail", ["-n", "+1", "-f", logPath]); - tail.stdout.on("data", (data) => { - ws.send(data.toString()); - }); + const stdout = tailProcess.stdout; + const stderr = tailProcess.stderr; - tail.stderr.on("data", (data) => { - ws.send(new Error(`tail error: ${data.toString()}`).message); - }); - tail.on("close", () => { + if (stdout) { + stdout.on("data", (data) => { + if (ws.readyState === ws.OPEN) { + ws.send(data.toString()); + } + }); + } + + if (stderr) { + stderr.on("data", (data) => { + if (ws.readyState === ws.OPEN) { + ws.send(new Error(`tail error: ${data.toString()}`).message); + } + }); + } + + tailProcess.on("close", () => { ws.close(); }); + + tailProcess.on("error", () => { + if (ws.readyState === ws.OPEN) { + ws.close(); + } + }); + + ws.on("close", () => { + if (tailProcess && !tailProcess.killed) { + tailProcess.kill("SIGTERM"); + // Force kill after a timeout if it doesn't terminate + setTimeout(() => { + if (tailProcess && !tailProcess.killed) { + tailProcess.kill("SIGKILL"); + } else { + } + }, 1000); + } else { + } + }); + } + } catch (error) { + // Clean up resources on error + if (tailProcess && !tailProcess.killed) { + tailProcess.kill("SIGTERM"); + setTimeout(() => { + if (tailProcess && !tailProcess.killed) { + tailProcess.kill("SIGKILL"); + } + }, 1000); + } + if (sshClient) { + sshClient.end(); + } + if (ws.readyState === ws.OPEN) { + // @ts-ignore + const errorMessage = error?.message as unknown as string; + ws.send(errorMessage || "An error occurred"); + ws.close(); } - } catch { - // @ts-ignore - // const errorMessage = error?.message as unknown as string; - // ws.send(errorMessage); } }); };