Compare commits

...

2 Commits

Author SHA1 Message Date
Louis Lam
b0e7f5aff5 Fix 2023-12-03 21:23:04 +08:00
Louis Lam
95c958ba9f Fix freeze issue 2023-12-03 21:17:16 +08:00
6 changed files with 89 additions and 49 deletions

View File

@ -29,7 +29,7 @@ import { Stack } from "./stack";
import { Cron } from "croner"; import { Cron } from "croner";
import gracefulShutdown from "http-graceful-shutdown"; import gracefulShutdown from "http-graceful-shutdown";
import User from "./models/user"; import User from "./models/user";
import childProcess from "child_process"; import childProcessAsync from "promisify-child-process";
import { Terminal } from "./terminal"; import { Terminal } from "./terminal";
export class DockgeServer { export class DockgeServer {
@ -483,7 +483,7 @@ export class DockgeServer {
return jwtSecretBean; return jwtSecretBean;
} }
sendStackList(useCache = false) { async sendStackList(useCache = false) {
let roomList = this.io.sockets.adapter.rooms.keys(); let roomList = this.io.sockets.adapter.rooms.keys();
let map : Map<string, object> | undefined; let map : Map<string, object> | undefined;
@ -494,7 +494,7 @@ export class DockgeServer {
// Get the list only if there is a room // Get the list only if there is a room
if (!map) { if (!map) {
map = new Map(); map = new Map();
let stackList = Stack.getStackList(this, useCache); let stackList = await Stack.getStackList(this, useCache);
for (let [ stackName, stack ] of stackList) { for (let [ stackName, stack ] of stackList) {
map.set(stackName, stack.toSimpleJSON()); map.set(stackName, stack.toSimpleJSON());
@ -510,8 +510,8 @@ export class DockgeServer {
} }
} }
sendStackStatusList() { async sendStackStatusList() {
let statusList = Stack.getStatusList(); let statusList = await Stack.getStatusList();
let roomList = this.io.sockets.adapter.rooms.keys(); let roomList = this.io.sockets.adapter.rooms.keys();
@ -529,8 +529,15 @@ export class DockgeServer {
} }
} }
getDockerNetworkList() : string[] { async getDockerNetworkList() : Promise<string[]> {
let res = childProcess.spawnSync("docker", [ "network", "ls", "--format", "{{.Name}}" ]); let res = await childProcessAsync.spawn("docker", [ "network", "ls", "--format", "{{.Name}}" ], {
encoding: "utf-8",
});
if (!res.stdout) {
return [];
}
let list = res.stdout.toString().split("\n"); let list = res.stdout.toString().split("\n");
// Remove empty string item // Remove empty string item

View File

@ -45,7 +45,7 @@ export class DockerSocketHandler extends SocketHandler {
if (typeof(name) !== "string") { if (typeof(name) !== "string") {
throw new ValidationError("Name must be a string"); throw new ValidationError("Name must be a string");
} }
const stack = Stack.getStack(server, name); const stack = await Stack.getStack(server, name);
try { try {
await stack.delete(socket); 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 { try {
checkLogin(socket); checkLogin(socket);
@ -73,7 +73,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string"); 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) { if (stack.isManagedByDockge) {
stack.joinCombinedTerminal(socket); stack.joinCombinedTerminal(socket);
@ -111,7 +111,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string"); 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); await stack.start(socket);
callback({ callback({
ok: true, ok: true,
@ -135,7 +135,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string"); 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); await stack.stop(socket);
callback({ callback({
ok: true, ok: true,
@ -156,7 +156,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string"); 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); await stack.restart(socket);
callback({ callback({
ok: true, ok: true,
@ -177,7 +177,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string"); 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); await stack.update(socket);
callback({ callback({
ok: true, ok: true,
@ -198,7 +198,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string"); 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); await stack.down(socket);
callback({ callback({
ok: true, ok: true,
@ -219,7 +219,7 @@ export class DockerSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string"); 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()); const serviceStatusList = Object.fromEntries(await stack.getServiceStatusList());
callback({ callback({
ok: true, ok: true,

View File

@ -101,7 +101,7 @@ export class TerminalSocketHandler extends SocketHandler {
log.debug("interactiveTerminal", "Service name: " + serviceName); log.debug("interactiveTerminal", "Service name: " + serviceName);
// Get stack // Get stack
const stack = Stack.getStack(server, stackName); const stack = await Stack.getStack(server, stackName);
stack.joinContainerTerminal(socket, serviceName, shell); stack.joinContainerTerminal(socket, serviceName, shell);
callback({ callback({
@ -151,7 +151,7 @@ export class TerminalSocketHandler extends SocketHandler {
throw new ValidationError("Stack name must be a string."); throw new ValidationError("Stack name must be a string.");
} }
const stack = Stack.getStack(server, stackName); const stack = await Stack.getStack(server, stackName);
await stack.leaveCombinedTerminal(socket); await stack.leaveCombinedTerminal(socket);
callback({ callback({

View File

@ -16,7 +16,7 @@ import {
UNKNOWN UNKNOWN
} from "./util-common"; } from "./util-common";
import { InteractiveTerminal, Terminal } from "./terminal"; import { InteractiveTerminal, Terminal } from "./terminal";
import childProcess from "child_process"; import childProcessAsync from "promisify-child-process";
export class Stack { export class Stack {
@ -72,11 +72,15 @@ export class Stack {
/** /**
* Get the status of the stack from `docker compose ps --format json` * Get the status of the stack from `docker compose ps --format json`
*/ */
ps() : object { async ps() : Promise<object> {
let res = childProcess.execSync("docker compose ps --format json", { let res = await childProcessAsync.spawn("docker", [ "compose", "ps", "--format", "json" ], {
cwd: this.path cwd: this.path,
encoding: "utf-8",
}); });
return JSON.parse(res.toString()); if (!res.stdout) {
return {};
}
return JSON.parse(res.stdout.toString());
} }
get isManagedByDockge() : boolean { get isManagedByDockge() : boolean {
@ -192,8 +196,8 @@ export class Stack {
return exitCode; return exitCode;
} }
updateStatus() { async updateStatus() {
let statusList = Stack.getStatusList(); let statusList = await Stack.getStatusList();
let status = statusList.get(this.name); let status = statusList.get(this.name);
if (status) { if (status) {
@ -203,7 +207,7 @@ export class Stack {
} }
} }
static getStackList(server : DockgeServer, useCacheForManaged = false) : Map<string, Stack> { static async getStackList(server : DockgeServer, useCacheForManaged = false) : Promise<Map<string, Stack>> {
let stacksDir = server.stacksDir; let stacksDir = server.stacksDir;
let stackList : Map<string, Stack>; let stackList : Map<string, Stack>;
@ -223,7 +227,7 @@ export class Stack {
if (!stat.isDirectory()) { if (!stat.isDirectory()) {
continue; continue;
} }
let stack = this.getStack(server, filename); let stack = await this.getStack(server, filename);
stack._status = CREATED_FILE; stack._status = CREATED_FILE;
stackList.set(filename, stack); stackList.set(filename, stack);
} catch (e) { } catch (e) {
@ -238,8 +242,15 @@ export class Stack {
} }
// Get status from docker compose ls // Get status from docker compose ls
let res = childProcess.execSync("docker compose ls --all --format json"); let res = await childProcessAsync.spawn("docker", [ "compose", "ls", "--all", "--format", "json" ], {
let composeList = JSON.parse(res.toString()); encoding: "utf-8",
});
if (!res.stdout) {
return stackList;
}
let composeList = JSON.parse(res.stdout.toString());
for (let composeStack of composeList) { for (let composeStack of composeList) {
let stack = stackList.get(composeStack.Name); 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 * 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 * Not all status will be returned, only the stack that is deployed or created to `docker compose` will be returned
*/ */
static getStatusList() : Map<string, number> { static async getStatusList() : Promise<Map<string, number>> {
let statusList = new Map<string, number>(); let statusList = new Map<string, number>();
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()); let composeList = JSON.parse(res.toString());
for (let composeStack of composeList) { 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<Stack> {
let dir = path.join(server.stacksDir, stackName); let dir = path.join(server.stacksDir, stackName);
if (!skipFSOperations) { if (!skipFSOperations) {
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) { if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
// Maybe it is a stack managed by docker compose directly // 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); let stack = stackList.get(stackName);
if (stack) { if (stack) {
@ -374,7 +387,7 @@ export class Stack {
} }
// If the stack is not running, we don't need to restart it // If the stack is not running, we don't need to restart it
this.updateStatus(); await this.updateStatus();
log.debug("update", "Status: " + this.status); log.debug("update", "Status: " + this.status);
if (this.status !== RUNNING) { if (this.status !== RUNNING) {
return exitCode; return exitCode;
@ -422,24 +435,35 @@ export class Stack {
async getServiceStatusList() { async getServiceStatusList() {
let statusList = new Map<string, number>(); let statusList = new Map<string, number>();
let res = childProcess.spawnSync("docker", [ "compose", "ps", "--format", "json" ], { try {
cwd: this.path, let res = await childProcessAsync.spawn("docker", [ "compose", "ps", "--format", "json" ], {
}); cwd: this.path,
encoding: "utf-8",
});
let lines = res.stdout.toString().split("\n"); if (!res.stdout) {
return statusList;
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) {
} }
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;
} }
} }

View File

@ -41,6 +41,7 @@
"knex": "~2.5.1", "knex": "~2.5.1",
"limiter-es6-compat": "~2.1.2", "limiter-es6-compat": "~2.1.2",
"mysql2": "~3.6.3", "mysql2": "~3.6.3",
"promisify-child-process": "~4.1.2",
"redbean-node": "~0.3.3", "redbean-node": "~0.3.3",
"socket.io": "~4.7.2", "socket.io": "~4.7.2",
"socket.io-client": "~4.7.2", "socket.io-client": "~4.7.2",

View File

@ -56,6 +56,9 @@ dependencies:
mysql2: mysql2:
specifier: ~3.6.3 specifier: ~3.6.3
version: 3.6.3 version: 3.6.3
promisify-child-process:
specifier: ~4.1.2
version: 4.1.2
redbean-node: redbean-node:
specifier: ~0.3.3 specifier: ~0.3.3
version: 0.3.3(mysql2@3.6.3) version: 0.3.3(mysql2@3.6.3)
@ -3887,6 +3890,11 @@ packages:
dev: false dev: false
optional: true 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: /proxy-addr@2.0.7:
resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==}
engines: {node: '>= 0.10'} engines: {node: '>= 0.10'}