diff --git a/backend/dockge-server.ts b/backend/dockge-server.ts index 6b2bf1f..9d06782 100644 --- a/backend/dockge-server.ts +++ b/backend/dockge-server.ts @@ -29,7 +29,7 @@ import { Stack } from "./stack"; import { Cron } from "croner"; import gracefulShutdown from "http-graceful-shutdown"; import User from "./models/user"; -import childProcess from "child_process"; +import childProcessAsync from "promisify-child-process"; import { Terminal } from "./terminal"; export class DockgeServer { @@ -483,7 +483,7 @@ export class DockgeServer { return jwtSecretBean; } - sendStackList(useCache = false) { + async sendStackList(useCache = false) { let roomList = this.io.sockets.adapter.rooms.keys(); let map : Map | undefined; @@ -494,7 +494,7 @@ export class DockgeServer { // Get the list only if there is a room if (!map) { map = new Map(); - let stackList = Stack.getStackList(this, useCache); + let stackList = await Stack.getStackList(this, useCache); for (let [ stackName, stack ] of stackList) { map.set(stackName, stack.toSimpleJSON()); @@ -510,8 +510,8 @@ export class DockgeServer { } } - sendStackStatusList() { - let statusList = Stack.getStatusList(); + async sendStackStatusList() { + let statusList = await Stack.getStatusList(); let roomList = this.io.sockets.adapter.rooms.keys(); @@ -529,8 +529,15 @@ export class DockgeServer { } } - getDockerNetworkList() : string[] { - let res = childProcess.spawnSync("docker", [ "network", "ls", "--format", "{{.Name}}" ]); + async getDockerNetworkList() : Promise { + let res = await childProcessAsync.spawn("docker", [ "network", "ls", "--format", "{{.Name}}" ], { + encoding: "utf-8", + }); + + if (!res.stdout) { + return []; + } + let list = res.stdout.toString().split("\n"); // Remove empty string item diff --git a/backend/socket-handlers/docker-socket-handler.ts b/backend/socket-handlers/docker-socket-handler.ts index df6c01b..d4e11c3 100644 --- a/backend/socket-handlers/docker-socket-handler.ts +++ b/backend/socket-handlers/docker-socket-handler.ts @@ -45,7 +45,7 @@ export class DockerSocketHandler extends SocketHandler { if (typeof(name) !== "string") { throw new ValidationError("Name must be a string"); } - const stack = Stack.getStack(server, name); + const stack = await Stack.getStack(server, name); try { await stack.delete(socket); @@ -65,7 +65,7 @@ export class DockerSocketHandler extends SocketHandler { } }); - socket.on("getStack", (stackName : unknown, callback) => { + socket.on("getStack", async (stackName : unknown, callback) => { try { checkLogin(socket); @@ -73,7 +73,7 @@ export class DockerSocketHandler extends SocketHandler { throw new ValidationError("Stack name must be a string"); } - const stack = Stack.getStack(server, stackName); + const stack = await Stack.getStack(server, stackName); if (stack.isManagedByDockge) { stack.joinCombinedTerminal(socket); @@ -111,7 +111,7 @@ export class DockerSocketHandler extends SocketHandler { throw new ValidationError("Stack name must be a string"); } - const stack = Stack.getStack(server, stackName); + const stack = await Stack.getStack(server, stackName); await stack.start(socket); callback({ ok: true, @@ -135,7 +135,7 @@ export class DockerSocketHandler extends SocketHandler { throw new ValidationError("Stack name must be a string"); } - const stack = Stack.getStack(server, stackName); + const stack = await Stack.getStack(server, stackName); await stack.stop(socket); callback({ ok: true, @@ -156,7 +156,7 @@ export class DockerSocketHandler extends SocketHandler { throw new ValidationError("Stack name must be a string"); } - const stack = Stack.getStack(server, stackName); + const stack = await Stack.getStack(server, stackName); await stack.restart(socket); callback({ ok: true, @@ -177,7 +177,7 @@ export class DockerSocketHandler extends SocketHandler { throw new ValidationError("Stack name must be a string"); } - const stack = Stack.getStack(server, stackName); + const stack = await Stack.getStack(server, stackName); await stack.update(socket); callback({ ok: true, @@ -198,7 +198,7 @@ export class DockerSocketHandler extends SocketHandler { throw new ValidationError("Stack name must be a string"); } - const stack = Stack.getStack(server, stackName); + const stack = await Stack.getStack(server, stackName); await stack.down(socket); callback({ ok: true, @@ -219,7 +219,7 @@ export class DockerSocketHandler extends SocketHandler { throw new ValidationError("Stack name must be a string"); } - const stack = Stack.getStack(server, stackName, true); + const stack = await Stack.getStack(server, stackName, true); const serviceStatusList = Object.fromEntries(await stack.getServiceStatusList()); callback({ ok: true, diff --git a/backend/stack.ts b/backend/stack.ts index cda53db..92ffc48 100644 --- a/backend/stack.ts +++ b/backend/stack.ts @@ -16,7 +16,7 @@ import { UNKNOWN } from "./util-common"; import { InteractiveTerminal, Terminal } from "./terminal"; -import childProcess from "child_process"; +import childProcessAsync from "promisify-child-process"; export class Stack { @@ -72,11 +72,15 @@ export class Stack { /** * Get the status of the stack from `docker compose ps --format json` */ - ps() : object { - let res = childProcess.execSync("docker compose ps --format json", { - cwd: this.path + async ps() : Promise { + let res = await childProcessAsync.spawn("docker", [ "compose", "ps", "--format", "json" ], { + cwd: this.path, + encoding: "utf-8", }); - return JSON.parse(res.toString()); + if (!res.stdout) { + return {}; + } + return JSON.parse(res.stdout.toString()); } get isManagedByDockge() : boolean { @@ -192,8 +196,8 @@ export class Stack { return exitCode; } - updateStatus() { - let statusList = Stack.getStatusList(); + async updateStatus() { + let statusList = await Stack.getStatusList(); let status = statusList.get(this.name); if (status) { @@ -203,7 +207,7 @@ export class Stack { } } - static getStackList(server : DockgeServer, useCacheForManaged = false) : Map { + static async getStackList(server : DockgeServer, useCacheForManaged = false) : Promise> { let stacksDir = server.stacksDir; let stackList : Map; @@ -223,7 +227,7 @@ export class Stack { if (!stat.isDirectory()) { continue; } - let stack = this.getStack(server, filename); + let stack = await this.getStack(server, filename); stack._status = CREATED_FILE; stackList.set(filename, stack); } catch (e) { @@ -238,8 +242,15 @@ export class Stack { } // Get status from docker compose ls - let res = childProcess.execSync("docker compose ls --all --format json"); - let composeList = JSON.parse(res.toString()); + let res = await childProcessAsync.spawn("docker", [ "compose", "ls", "--all", "--format", "json" ], { + encoding: "utf-8", + }); + + if (!res.stdout) { + return stackList; + } + + let composeList = JSON.parse(res.stdout.toString()); for (let composeStack of composeList) { let stack = stackList.get(composeStack.Name); @@ -265,10 +276,12 @@ export class Stack { * Get the status list, it will be used to update the status of the stacks * Not all status will be returned, only the stack that is deployed or created to `docker compose` will be returned */ - static getStatusList() : Map { + static async getStatusList() : Promise> { let statusList = new Map(); - let res = childProcess.execSync("docker compose ls --all --format json"); + let res = await childProcessAsync.spawn("docker", [ "compose", "ls", "--all", "--format", "json" ], { + encoding: "utf-8", + }); let composeList = JSON.parse(res.toString()); for (let composeStack of composeList) { @@ -297,13 +310,13 @@ export class Stack { } } - static getStack(server: DockgeServer, stackName: string, skipFSOperations = false) : Stack { + static async getStack(server: DockgeServer, stackName: string, skipFSOperations = false) : Promise { let dir = path.join(server.stacksDir, stackName); if (!skipFSOperations) { if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) { // Maybe it is a stack managed by docker compose directly - let stackList = this.getStackList(server, true); + let stackList = await this.getStackList(server, true); let stack = stackList.get(stackName); if (stack) { @@ -374,7 +387,7 @@ export class Stack { } // If the stack is not running, we don't need to restart it - this.updateStatus(); + await this.updateStatus(); log.debug("update", "Status: " + this.status); if (this.status !== RUNNING) { return exitCode; @@ -422,24 +435,35 @@ export class Stack { async getServiceStatusList() { let statusList = new Map(); - let res = childProcess.spawnSync("docker", [ "compose", "ps", "--format", "json" ], { - cwd: this.path, - }); + try { + let res = await childProcessAsync.spawn("docker", [ "compose", "ps", "--format", "json" ], { + cwd: this.path, + encoding: "utf-8", + }); - let lines = res.stdout.toString().split("\n"); - - for (let line of lines) { - try { - let obj = JSON.parse(line); - if (obj.Health === "") { - statusList.set(obj.Service, obj.State); - } else { - statusList.set(obj.Service, obj.Health); - } - } catch (e) { + if (!res.stdout) { + return statusList; } + + let lines = res.stdout?.toString().split("\n"); + + for (let line of lines) { + try { + let obj = JSON.parse(line); + if (obj.Health === "") { + statusList.set(obj.Service, obj.State); + } else { + statusList.set(obj.Service, obj.Health); + } + } catch (e) { + } + } + + return statusList; + } catch (e) { + log.error("getServiceStatusList", e); + return statusList; } - return statusList; } } diff --git a/package.json b/package.json index dc0935e..69c29f2 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "knex": "~2.5.1", "limiter-es6-compat": "~2.1.2", "mysql2": "~3.6.3", + "promisify-child-process": "~4.1.2", "redbean-node": "~0.3.3", "socket.io": "~4.7.2", "socket.io-client": "~4.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index afd758f..c25b72e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,9 @@ dependencies: mysql2: specifier: ~3.6.3 version: 3.6.3 + promisify-child-process: + specifier: ~4.1.2 + version: 4.1.2 redbean-node: specifier: ~0.3.3 version: 0.3.3(mysql2@3.6.3) @@ -3887,6 +3890,11 @@ packages: dev: false optional: true + /promisify-child-process@4.1.2: + resolution: {integrity: sha512-APnkIgmaHNJpkAn7k+CrJSi9WMuff5ctYFbD0CO2XIPkM8yO7d/ShouU2clywbpHV/DUsyc4bpJCsNgddNtx4g==} + engines: {node: '>=8'} + dev: false + /proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'}