From 766e7515220199de95595a2cf605a4e06330b991 Mon Sep 17 00:00:00 2001 From: Louis Lam Date: Sat, 25 Nov 2023 02:04:16 +0800 Subject: [PATCH] Close terminal if there is no clients (#60) * Close terminal if there is no clients connecting * Only enable * Join the terminal only if it is managed by Dockge * Done --- backend/dockge-server.ts | 12 +++++-- .../socket-handlers/docker-socket-handler.ts | 4 ++- .../terminal-socket-handler.ts | 21 ++++++++++-- backend/stack.ts | 11 +++++- backend/terminal.ts | 34 ++++++++++++++++++- frontend/src/pages/Compose.vue | 8 ++++- 6 files changed, 81 insertions(+), 9 deletions(-) diff --git a/backend/dockge-server.ts b/backend/dockge-server.ts index ad9b075..7aaba63 100644 --- a/backend/dockge-server.ts +++ b/backend/dockge-server.ts @@ -30,6 +30,7 @@ import { Cron } from "croner"; import gracefulShutdown from "http-graceful-shutdown"; import User from "./models/user"; import childProcess from "child_process"; +import { Terminal } from "./terminal"; export class DockgeServer { app : Express; @@ -230,6 +231,11 @@ export class DockgeServer { }); + if (isDev) { + setInterval(() => { + log.debug("terminal", "Terminal count: " + Terminal.getTerminalCount()); + }, 5000); + } } async afterLogin(socket : DockgeSocket, user : User) { @@ -292,11 +298,11 @@ export class DockgeServer { log.info("server", `Listening on ${this.config.port}`); } - // Run every 5 seconds - Cron("*/2 * * * * *", { + // Run every 10 seconds + Cron("*/10 * * * * *", { protect: true, // Enabled over-run protection. }, () => { - log.debug("server", "Cron job running"); + //log.debug("server", "Cron job running"); this.sendStackList(true); }); diff --git a/backend/socket-handlers/docker-socket-handler.ts b/backend/socket-handlers/docker-socket-handler.ts index 3683114..047d8f7 100644 --- a/backend/socket-handlers/docker-socket-handler.ts +++ b/backend/socket-handlers/docker-socket-handler.ts @@ -75,7 +75,9 @@ export class DockerSocketHandler extends SocketHandler { const stack = Stack.getStack(server, stackName); - stack.joinCombinedTerminal(socket); + if (stack.isManagedByDockge) { + stack.joinCombinedTerminal(socket); + } callback({ ok: true, diff --git a/backend/socket-handlers/terminal-socket-handler.ts b/backend/socket-handlers/terminal-socket-handler.ts index 0a0485b..bb27a66 100644 --- a/backend/socket-handlers/terminal-socket-handler.ts +++ b/backend/socket-handlers/terminal-socket-handler.ts @@ -140,9 +140,26 @@ export class TerminalSocketHandler extends SocketHandler { } }); - // Close Terminal - socket.on("terminalClose", async (terminalName : unknown, callback : unknown) => { + // Leave Combined Terminal + socket.on("leaveCombinedTerminal", async (stackName : unknown, callback) => { + try { + checkLogin(socket); + log.debug("leaveCombinedTerminal", "Stack name: " + stackName); + + if (typeof(stackName) !== "string") { + throw new ValidationError("Stack name must be a string."); + } + + const stack = Stack.getStack(server, stackName); + await stack.leaveCombinedTerminal(socket); + + callback({ + ok: true, + }); + } catch (e) { + callbackError(e, callback); + } }); // TODO: Resize Terminal diff --git a/backend/stack.ts b/backend/stack.ts index bf7657c..a9af442 100644 --- a/backend/stack.ts +++ b/backend/stack.ts @@ -298,7 +298,7 @@ export class Stack { } } } else { - log.debug("getStack", "Skip FS operations"); + //log.debug("getStack", "Skip FS operations"); } let stack : Stack; @@ -374,12 +374,21 @@ export class Stack { async joinCombinedTerminal(socket: DockgeSocket) { const terminalName = getCombinedTerminalName(this.name); const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, "docker", [ "compose", "logs", "-f", "--tail", "100" ], this.path); + terminal.enableKeepAlive = true; terminal.rows = COMBINED_TERMINAL_ROWS; terminal.cols = COMBINED_TERMINAL_COLS; terminal.join(socket); terminal.start(); } + async leaveCombinedTerminal(socket: DockgeSocket) { + const terminalName = getCombinedTerminalName(this.name); + const terminal = Terminal.getTerminal(terminalName); + if (terminal) { + terminal.leave(socket); + } + } + async joinContainerTerminal(socket: DockgeSocket, serviceName: string, shell : string = "sh", index: number = 0) { const terminalName = getContainerExecTerminalName(this.name, serviceName, index); let terminal = Terminal.getTerminal(terminalName); diff --git a/backend/terminal.ts b/backend/terminal.ts index f1faf18..2b22e69 100644 --- a/backend/terminal.ts +++ b/backend/terminal.ts @@ -34,6 +34,9 @@ export class Terminal { protected _rows : number = TERMINAL_ROWS; protected _cols : number = TERMINAL_COLS; + public enableKeepAlive : boolean = false; + protected keepAliveInterval? : NodeJS.Timeout; + constructor(server : DockgeServer, name : string, file : string, args : string | string[], cwd : string) { this.server = server; this._name = name; @@ -80,6 +83,25 @@ export class Terminal { return; } + if (this.enableKeepAlive) { + log.debug("Terminal", "Keep alive enabled for terminal " + this.name); + + // Close if there is no clients + this.keepAliveInterval = setInterval(() => { + const clients = this.server.io.sockets.adapter.rooms.get(this.name); + const numClients = clients ? clients.size : 0; + + if (numClients === 0) { + log.debug("Terminal", "Terminal " + this.name + " has no client, closing..."); + this.close(); + } else { + log.debug("Terminal", "Terminal " + this.name + " has " + numClients + " client(s)"); + } + }, 60 * 1000); + } else { + log.debug("Terminal", "Keep alive disabled for terminal " + this.name); + } + try { this._ptyProcess = pty.spawn(this.file, this.args, { name: this.name, @@ -100,6 +122,8 @@ export class Terminal { this._ptyProcess.onExit(this.exit); } catch (error) { if (error instanceof Error) { + clearInterval(this.keepAliveInterval); + log.error("Terminal", "Failed to start terminal: " + error.message); const exitCode = Number(error.message.split(" ").pop()); this.exit({ @@ -122,6 +146,8 @@ export class Terminal { Terminal.terminalMap.delete(this.name); log.debug("Terminal", "Terminal " + this.name + " exited with code " + res.exitCode); + clearInterval(this.keepAliveInterval); + if (this.callback) { this.callback(res.exitCode); } @@ -158,7 +184,9 @@ export class Terminal { } close() { - this._ptyProcess?.kill(); + clearInterval(this.keepAliveInterval); + // Send Ctrl+C to the terminal + this.ptyProcess?.write("\x03"); } /** @@ -193,6 +221,10 @@ export class Terminal { terminal.start(); }); } + + public static getTerminalCount() { + return Terminal.terminalMap.size; + } } /** diff --git a/frontend/src/pages/Compose.vue b/frontend/src/pages/Compose.vue index afcab69..025ed0a 100644 --- a/frontend/src/pages/Compose.vue +++ b/frontend/src/pages/Compose.vue @@ -319,6 +319,12 @@ export default { }, deep: true, }, + + $route(to, from) { + // Leave Combined Terminal + console.debug("leaveCombinedTerminal", from.params.stackName); + this.$root.getSocket().emit("leaveCombinedTerminal", this.stack.name, () => {}); + } }, mounted() { if (this.isAdd) { @@ -361,7 +367,7 @@ export default { clearTimeout(serviceStatusTimeout); serviceStatusTimeout = setTimeout(async () => { this.requestServiceStatus(); - }, 2000); + }, 5000); }, requestServiceStatus() {