diff --git a/README.md b/README.md index 7d4da129..04a5f168 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ View Video: https://youtu.be/AWAlOQeNpgU?t=48 - Update Docker Images - ⌨️ Interactive Editor for `compose.yaml` - 🦦 Interactive Web Terminal -- 🕷️ (1.4.0 NEW!) Multiple agents support - You can manage multiple stacks from different Docker hosts in one single interface +- 🕷️ (1.4.0 🆕) Multiple agents support - You can manage multiple stacks from different Docker hosts in one single interface - 🏪 Convert `docker run ...` commands into `compose.yaml` - 📙 File based structure - Dockge won't kidnap your compose files, they are stored on your drive as usual. You can interact with them using normal `docker compose` commands diff --git a/backend/agent-manager.ts b/backend/agent-manager.ts index 819c9c51..895232a4 100644 --- a/backend/agent-manager.ts +++ b/backend/agent-manager.ts @@ -2,22 +2,30 @@ import { DockgeSocket } from "./util-server"; import { io, Socket as SocketClient } from "socket.io-client"; import { log } from "./log"; import { Agent } from "./models/agent"; -import { isDev, LooseObject } from "../common/util-common"; +import { isDev, LooseObject, sleep } from "../common/util-common"; import semver from "semver"; import { R } from "redbean-node"; +import dayjs, { Dayjs } from "dayjs"; /** * Dockge Instance Manager + * One AgentManager per Socket connection */ export class AgentManager { protected socket : DockgeSocket; protected agentSocketList : Record = {}; + protected agentLoggedInList : Record = {}; + protected _firstConnectTime : Dayjs = dayjs(); constructor(socket: DockgeSocket) { this.socket = socket; } + get firstConnectTime() : Dayjs { + return this._firstConnectTime; + } + test(url : string, username : string, password : string) : Promise { return new Promise((resolve, reject) => { let obj = new URL(url); @@ -131,12 +139,14 @@ export class AgentManager { }, (res : LooseObject) => { if (res.ok) { log.info("agent-manager", "Logged in to the socket server: " + endpoint); + this.agentLoggedInList[endpoint] = true; this.socket.emit("agentStatus", { endpoint: endpoint, status: "online", }); } else { log.error("agent-manager", "Failed to login to the socket server: " + endpoint); + this.agentLoggedInList[endpoint] = false; this.socket.emit("agentStatus", { endpoint: endpoint, status: "offline", @@ -188,6 +198,8 @@ export class AgentManager { } async connectAll() { + this._firstConnectTime = dayjs(); + if (this.socket.endpoint) { log.info("agent-manager", "This connection is connected as an agent, skip connectAll()"); return; @@ -211,7 +223,7 @@ export class AgentManager { } } - emitToEndpoint(endpoint: string, eventName: string, ...args : unknown[]) { + async emitToEndpoint(endpoint: string, eventName: string, ...args : unknown[]) { log.debug("agent-manager", "Emitting event to endpoint: " + endpoint); let client = this.agentSocketList[endpoint]; @@ -220,9 +232,27 @@ export class AgentManager { throw new Error("Socket client not found for endpoint: " + endpoint); } - if (!client.connected) { - log.error("agent-manager", "Socket client not connected for endpoint: " + endpoint); - throw new Error("Socket client not connected for endpoint: " + endpoint); + if (!client.connected || !this.agentLoggedInList[endpoint]) { + // Maybe the request is too quick, the socket is not connected yet, check firstConnectTime + // If it is within 10 seconds, we should apply retry logic here + let diff = dayjs().diff(this.firstConnectTime, "second"); + log.debug("agent-manager", endpoint + ": diff: " + diff); + let ok = false; + while (diff < 10) { + if (client.connected && this.agentLoggedInList[endpoint]) { + log.debug("agent-manager", `${endpoint}: Connected & Logged in`); + ok = true; + break; + } + log.debug("agent-manager", endpoint + ": not ready yet, retrying in 1 second..."); + await sleep(1000); + diff = dayjs().diff(this.firstConnectTime, "second"); + } + + if (!ok) { + log.error("agent-manager", `${endpoint}: Socket client not connected`); + throw new Error("Socket client not connected for endpoint: " + endpoint); + } } client.emit("agent", endpoint, eventName, ...args); @@ -231,7 +261,9 @@ export class AgentManager { emitToAllEndpoints(eventName: string, ...args : unknown[]) { log.debug("agent-manager", "Emitting event to all endpoints"); for (let endpoint in this.agentSocketList) { - this.emitToEndpoint(endpoint, eventName, ...args); + this.emitToEndpoint(endpoint, eventName, ...args).catch((e) => { + log.warn("agent-manager", e.message); + }); } } diff --git a/backend/socket-handlers/agent-proxy-socket-handler.ts b/backend/socket-handlers/agent-proxy-socket-handler.ts index b1ecd19d..b4ce32e8 100644 --- a/backend/socket-handlers/agent-proxy-socket-handler.ts +++ b/backend/socket-handlers/agent-proxy-socket-handler.ts @@ -31,7 +31,7 @@ export class AgentProxySocketHandler extends SocketHandler { } else { log.debug("agent", "Proxying request to " + endpoint + " for " + eventName); - socket.instanceManager.emitToEndpoint(endpoint, eventName, ...args); + await socket.instanceManager.emitToEndpoint(endpoint, eventName, ...args); } } catch (e) { if (e instanceof Error) { diff --git a/backend/socket-handlers/main-socket-handler.ts b/backend/socket-handlers/main-socket-handler.ts index bc9550a0..5d31878a 100644 --- a/backend/socket-handlers/main-socket-handler.ts +++ b/backend/socket-handlers/main-socket-handler.ts @@ -271,8 +271,6 @@ export class MainSocketHandler extends SocketHandler { await doubleCheckPassword(socket, currentPassword); } - console.log(data); - await Settings.setSettings("general", data); callback({ diff --git a/backend/terminal.ts b/backend/terminal.ts index d2220ada..4a5d6b23 100644 --- a/backend/terminal.ts +++ b/backend/terminal.ts @@ -34,6 +34,7 @@ export class Terminal { public enableKeepAlive : boolean = false; protected keepAliveInterval? : NodeJS.Timeout; + protected kickDisconnectedClientsInterval? : NodeJS.Timeout; protected socketList : Record = {}; @@ -84,6 +85,16 @@ export class Terminal { return; } + this.kickDisconnectedClientsInterval = setInterval(() => { + for (const socketID in this.socketList) { + const socket = this.socketList[socketID]; + if (!socket.connected) { + log.debug("Terminal", "Kicking disconnected client " + socket.id + " from terminal " + this.name); + this.leave(socket); + } + } + }, 60 * 1000); + if (this.enableKeepAlive) { log.debug("Terminal", "Keep alive enabled for terminal " + this.name); @@ -152,6 +163,7 @@ export class Terminal { log.debug("Terminal", "Terminal " + this.name + " exited with code " + res.exitCode); clearInterval(this.keepAliveInterval); + clearInterval(this.kickDisconnectedClientsInterval); if (this.callback) { this.callback(res.exitCode); diff --git a/common/util-common.ts b/common/util-common.ts index c85b96f2..587e6dd2 100644 --- a/common/util-common.ts +++ b/common/util-common.ts @@ -293,7 +293,7 @@ function copyYAMLCommentsItems(items : any, srcItems : any) { * @param input * @param hostname */ -export function parseDockerPort(input : string, hostname) { +export function parseDockerPort(input : string, hostname : string) { let port; let display; diff --git a/frontend/src/components/StackListItem.vue b/frontend/src/components/StackListItem.vue index b17a91d0..7e7561b2 100644 --- a/frontend/src/components/StackListItem.vue +++ b/frontend/src/components/StackListItem.vue @@ -3,7 +3,7 @@
{{ stackName }} -
{{ endpointDisplay }}
+
{{ endpointDisplay }}
@@ -54,11 +54,7 @@ export default { }, computed: { endpointDisplay() { - if (this.stack.endpoint) { - return this.stack.endpoint; - } else { - return this.$t("currentEndpoint"); - } + return this.$root.endpointDisplayFunction(this.stack.endpoint); }, url() { if (this.stack.endpoint) { diff --git a/frontend/src/mixins/socket.ts b/frontend/src/mixins/socket.ts index 87c5586f..49cea5fb 100644 --- a/frontend/src/mixins/socket.ts +++ b/frontend/src/mixins/socket.ts @@ -49,6 +49,10 @@ export default defineComponent({ }, computed: { + agentCount() { + return Object.keys(this.agentList).length; + }, + completeStackList() { let list : Record = {}; @@ -125,6 +129,15 @@ export default defineComponent({ }, methods: { + + endpointDisplayFunction(endpoint : string) { + if (endpoint) { + return endpoint; + } else { + return this.$t("currentEndpoint"); + } + }, + /** * Initialize connection to socket server * @param bypass Should the check for if we diff --git a/frontend/src/pages/Compose.vue b/frontend/src/pages/Compose.vue index b76e7c2a..7d378543 100644 --- a/frontend/src/pages/Compose.vue +++ b/frontend/src/pages/Compose.vue @@ -2,7 +2,12 @@

Compose

-

{{ stack.name }}

+

+ {{ stack.name }} + + ({{ endpointDisplay }}) + +

@@ -310,6 +315,10 @@ export default { }, computed: { + endpointDisplay() { + return this.$root.endpointDisplayFunction(this.endpoint); + }, + urls() { if (!this.envsubstJSONConfig["x-dockge"] || !this.envsubstJSONConfig["x-dockge"].urls || !Array.isArray(this.envsubstJSONConfig["x-dockge"].urls)) { return []; @@ -428,9 +437,7 @@ export default { }, $route(to, from) { - // Leave Combined Terminal - console.debug("leaveCombinedTerminal", from.params.stackName); - this.$root.emitAgent(this.endpoint, "leaveCombinedTerminal", this.stack.name, () => {}); + } }, mounted() { @@ -473,11 +480,9 @@ export default { this.requestServiceStatus(); }, unmounted() { - this.stopServiceStatusTimeout = true; - clearTimeout(serviceStatusTimeout); + }, methods: { - startServiceStatusTimeout() { clearTimeout(serviceStatusTimeout); serviceStatusTimeout = setTimeout(async () => { @@ -499,15 +504,27 @@ export default { exitConfirm(next) { if (this.isEditMode) { if (confirm("You are currently editing a stack. Are you sure you want to leave?")) { + this.exitAction(); next(); } else { next(false); } } else { + this.exitAction(); next(); } }, + exitAction() { + console.log("exitAction"); + this.stopServiceStatusTimeout = true; + clearTimeout(serviceStatusTimeout); + + // Leave Combined Terminal + console.debug("leaveCombinedTerminal", this.endpoint, this.stack.name); + this.$root.emitAgent(this.endpoint, "leaveCombinedTerminal", this.stack.name, () => {}); + }, + bindTerminal() { this.$refs.progressTerminal?.bind(this.endpoint, this.terminalName); }, @@ -774,6 +791,8 @@ export default {