Merge branch 'master' into rollout

This commit is contained in:
Louis Lam 2023-11-25 15:39:37 +08:00
commit 875793d6c2
11 changed files with 281 additions and 25 deletions

View file

@ -30,6 +30,7 @@ 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 childProcess from "child_process";
import { Terminal } from "./terminal";
export class DockgeServer { export class DockgeServer {
app : Express; 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) { async afterLogin(socket : DockgeSocket, user : User) {
@ -292,11 +298,11 @@ export class DockgeServer {
log.info("server", `Listening on ${this.config.port}`); log.info("server", `Listening on ${this.config.port}`);
} }
// Run every 5 seconds // Run every 10 seconds
Cron("*/2 * * * * *", { Cron("*/10 * * * * *", {
protect: true, // Enabled over-run protection. protect: true, // Enabled over-run protection.
}, () => { }, () => {
log.debug("server", "Cron job running"); //log.debug("server", "Cron job running");
this.sendStackList(true); this.sendStackList(true);
}); });

View file

@ -75,7 +75,9 @@ export class DockerSocketHandler extends SocketHandler {
const stack = Stack.getStack(server, stackName); const stack = Stack.getStack(server, stackName);
stack.joinCombinedTerminal(socket); if (stack.isManagedByDockge) {
stack.joinCombinedTerminal(socket);
}
callback({ callback({
ok: true, ok: true,

View file

@ -140,9 +140,26 @@ export class TerminalSocketHandler extends SocketHandler {
} }
}); });
// Close Terminal // Leave Combined Terminal
socket.on("terminalClose", async (terminalName : unknown, callback : unknown) => { 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 // TODO: Resize Terminal

View file

@ -191,6 +191,7 @@ export class Stack {
let stacksDir = server.stacksDir; let stacksDir = server.stacksDir;
let stackList : Map<string, Stack>; let stackList : Map<string, Stack>;
// Use cached stack list?
if (useCacheForManaged && this.managedStackList.size > 0) { if (useCacheForManaged && this.managedStackList.size > 0) {
stackList = this.managedStackList; stackList = this.managedStackList;
} else { } else {
@ -220,22 +221,19 @@ export class Stack {
this.managedStackList = new Map(stackList); this.managedStackList = new Map(stackList);
} }
// Also get the list from `docker compose ls --all --format json` // Get status from docker compose ls
let res = childProcess.execSync("docker compose ls --all --format json"); let res = childProcess.execSync("docker compose ls --all --format json");
let composeList = JSON.parse(res.toString()); let composeList = JSON.parse(res.toString());
for (let composeStack of composeList) { for (let composeStack of composeList) {
// Skip the dockge stack
// TODO: Could be self managed?
if (composeStack.Name === "dockge") {
continue;
}
let stack = stackList.get(composeStack.Name); let stack = stackList.get(composeStack.Name);
// This stack probably is not managed by Dockge, but we still want to show it // This stack probably is not managed by Dockge, but we still want to show it
if (!stack) { if (!stack) {
// Skip the dockge stack if it is not managed by Dockge
if (composeStack.Name === "dockge") {
continue;
}
stack = new Stack(server, composeStack.Name); stack = new Stack(server, composeStack.Name);
stackList.set(composeStack.Name, stack); stackList.set(composeStack.Name, stack);
} }
@ -300,7 +298,7 @@ export class Stack {
} }
} }
} else { } else {
log.debug("getStack", "Skip FS operations"); //log.debug("getStack", "Skip FS operations");
} }
let stack : Stack; let stack : Stack;
@ -376,12 +374,21 @@ export class Stack {
async joinCombinedTerminal(socket: DockgeSocket) { async joinCombinedTerminal(socket: DockgeSocket) {
const terminalName = getCombinedTerminalName(this.name); const terminalName = getCombinedTerminalName(this.name);
const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, "docker", [ "compose", "logs", "-f", "--tail", "100" ], this.path); const terminal = Terminal.getOrCreateTerminal(this.server, terminalName, "docker", [ "compose", "logs", "-f", "--tail", "100" ], this.path);
terminal.enableKeepAlive = true;
terminal.rows = COMBINED_TERMINAL_ROWS; terminal.rows = COMBINED_TERMINAL_ROWS;
terminal.cols = COMBINED_TERMINAL_COLS; terminal.cols = COMBINED_TERMINAL_COLS;
terminal.join(socket); terminal.join(socket);
terminal.start(); 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) { async joinContainerTerminal(socket: DockgeSocket, serviceName: string, shell : string = "sh", index: number = 0) {
const terminalName = getContainerExecTerminalName(this.name, serviceName, index); const terminalName = getContainerExecTerminalName(this.name, serviceName, index);
let terminal = Terminal.getTerminal(terminalName); let terminal = Terminal.getTerminal(terminalName);

View file

@ -34,6 +34,9 @@ export class Terminal {
protected _rows : number = TERMINAL_ROWS; protected _rows : number = TERMINAL_ROWS;
protected _cols : number = TERMINAL_COLS; 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) { constructor(server : DockgeServer, name : string, file : string, args : string | string[], cwd : string) {
this.server = server; this.server = server;
this._name = name; this._name = name;
@ -80,6 +83,25 @@ export class Terminal {
return; 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 { try {
this._ptyProcess = pty.spawn(this.file, this.args, { this._ptyProcess = pty.spawn(this.file, this.args, {
name: this.name, name: this.name,
@ -100,6 +122,8 @@ export class Terminal {
this._ptyProcess.onExit(this.exit); this._ptyProcess.onExit(this.exit);
} catch (error) { } catch (error) {
if (error instanceof Error) { if (error instanceof Error) {
clearInterval(this.keepAliveInterval);
log.error("Terminal", "Failed to start terminal: " + error.message); log.error("Terminal", "Failed to start terminal: " + error.message);
const exitCode = Number(error.message.split(" ").pop()); const exitCode = Number(error.message.split(" ").pop());
this.exit({ this.exit({
@ -122,6 +146,8 @@ export class Terminal {
Terminal.terminalMap.delete(this.name); Terminal.terminalMap.delete(this.name);
log.debug("Terminal", "Terminal " + this.name + " exited with code " + res.exitCode); log.debug("Terminal", "Terminal " + this.name + " exited with code " + res.exitCode);
clearInterval(this.keepAliveInterval);
if (this.callback) { if (this.callback) {
this.callback(res.exitCode); this.callback(res.exitCode);
} }
@ -158,7 +184,9 @@ export class Terminal {
} }
close() { 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(); terminal.start();
}); });
} }
public static getTerminalCount() {
return Terminal.terminalMap.size;
}
} }
/** /**

View file

@ -12,7 +12,6 @@ declare module 'vue' {
ArrayInput: typeof import('./src/components/ArrayInput.vue')['default'] ArrayInput: typeof import('./src/components/ArrayInput.vue')['default']
ArraySelect: typeof import('./src/components/ArraySelect.vue')['default'] ArraySelect: typeof import('./src/components/ArraySelect.vue')['default']
BDropdown: typeof import('bootstrap-vue-next')['BDropdown'] BDropdown: typeof import('bootstrap-vue-next')['BDropdown']
BDropdownDivider: typeof import('bootstrap-vue-next')['BDropdownDivider']
BDropdownItem: typeof import('bootstrap-vue-next')['BDropdownItem'] BDropdownItem: typeof import('bootstrap-vue-next')['BDropdownItem']
BModal: typeof import('bootstrap-vue-next')['BModal'] BModal: typeof import('bootstrap-vue-next')['BModal']
Confirm: typeof import('./src/components/Confirm.vue')['default'] Confirm: typeof import('./src/components/Confirm.vue')['default']

View file

@ -45,11 +45,12 @@ export default {
<style scoped> <style scoped>
.badge { .badge {
min-width: 62px; min-width: 62px;
overflow: hidden;
text-overflow: ellipsis;
} }
.fixed-width { .fixed-width {
width: 62px; width: 62px;
overflow: hidden;
text-overflow: ellipsis;
} }
</style> </style>

View file

@ -13,6 +13,7 @@ const languageList = {
"sl": "Slovenščina", "sl": "Slovenščina",
"tr": "Türkçe", "tr": "Türkçe",
"zh-CN": "简体中文", "zh-CN": "简体中文",
"zh-TW": "繁體中文(台灣)",
"ur": "Urdu", "ur": "Urdu",
"ko-KR": "한국어", "ko-KR": "한국어",
"ru": "Русский", "ru": "Русский",
@ -20,7 +21,8 @@ const languageList = {
"ar": "العربية", "ar": "العربية",
"th":"ไทย", "th":"ไทย",
"it-IT":"Italiano", "it-IT":"Italiano",
"sv-SE":"Svenska" "sv-SE":"Svenska",
"uk-UA":"Українська"
}; };
let messages = { let messages = {

View file

@ -0,0 +1,94 @@
{
"languageName": "Українська",
"Create your admin account": "Створити акаунт адміністратора",
"authIncorrectCreds": "Невірний логін чи пароль",
"PasswordsDoNotMatch": "Паролі не співпадають.",
"Repeat Password": "Повторіть пароль",
"Create": "Створити",
"signedInDisp": "Авторизовано як {0}",
"signedInDispDisabled": "Авторизацію вимкнено.",
"home": "Головна",
"console": "Консоль",
"registry": "Registry",
"compose": "Compose",
"addFirstStackMsg": "Додайте свій перший стек!",
"stackName" : "Назва стеку",
"deployStack": "Розрознути",
"deleteStack": "Видалити",
"stopStack": "Зупинити",
"restartStack": "Перезапустити",
"updateStack": "Оновити",
"startStack": "Запустити",
"editStack": "Редагувати",
"discardStack": "Відмінити",
"saveStackDraft": "Зберегти",
"notAvailableShort" : "Н/Д",
"deleteStackMsg": "Ви впевнені що хочете видалити цей стек?",
"stackNotManagedByDockgeMsg": "Даний стек не управляється Dockge.",
"primaryHostname": "Назва хосту",
"general": "Загальне",
"container": "Контейнер | Контейнери",
"scanFolder": "Сканувати папку зі стеками",
"dockerImage": "Образ",
"restartPolicyUnlessStopped": "Доки не буде зупинено",
"restartPolicyAlways": "Завжди",
"restartPolicyOnFailure": "При падінні",
"restartPolicyNo": "Ніколи",
"environmentVariable": "Змінна середовища | змінні середовища",
"restartPolicy": "Перезапуск",
"containerName": "Назва контейнеру",
"port": "Порт | Порти",
"volume": "Сховище | Сховища",
"network": "Мережа | Мережі",
"dependsOn": "Залежність контейнера | Залежності контейнеру",
"addListItem": "Додати {0}",
"deleteContainer": "Видалити",
"addContainer": "Додати Контейнер",
"addNetwork": "Додати Мережу",
"disableauth.message1": "Ви впевнені що хочете <strong>вимкнути авторизацію</strong>?",
"disableauth.message2": "Це призначено для сценаріїв, <strong>де ви збираєтесь використати сторонню авторизацію</strong> перед Dockge, наприклад Cloudflare Access, Authelia чи інші.",
"passwordNotMatchMsg": "Паролі не співпадають.",
"autoGet": "Отримати",
"add": "Додати",
"Edit": "Змінити",
"applyToYAML": "Застосувати для YAML",
"createExternalNetwork": "Створити",
"addInternalNetwork": "Додати",
"Save": "Зберегти",
"Language": "Мова",
"Current User": "Користувач",
"Change Password": "Змінити пароль",
"Current Password": "Поточний пароль",
"New Password": "Новий пароль",
"Repeat New Password": "Повторіть новий пароль",
"Update Password": "Оновити пароль",
"Advanced": "Розширені опції",
"Please use this option carefully!": "Будь-ласка, використовуйте цю опцію з обережністю!",
"Enable Auth": "Увімкнути автентифікацію",
"Disable Auth": "Вимкнути автентифікацію",
"I understand, please disable": "Зрозуміло, все-одно вимкнути",
"Leave": "Покинути",
"Frontend Version": "Версія інтерфейсу",
"Check Update On GitHub": "Перевірити оновлення на GitHub",
"Show update if available": "Показати оновлення, якщо доступно",
"Also check beta release": "Перевіряти оновлення до бета-версії",
"Remember me": "Запамʼятати мене",
"Login": "Логін",
"Username": "Імʼя користувача",
"Password": "Пароль",
"Settings": "Налаштування",
"Logout": "Вийти",
"Lowercase only": "Тільки нижній регістр",
"Convert to Compose": "Конвертувати в Compose",
"Docker Run": "Запустити Docker",
"active": "активно",
"exited": "завершено",
"inactive": "неактивно",
"Appearance": "Зовнішній вигляд",
"Security": "Безпека",
"About": "Про продукт",
"Allowed commands:": "Дозволені команди:",
"Internal Networks": "Внутрішні мережі",
"External Networks": "Зовнішні мережі",
"No External Networks": "Немає зовнішніх мереж"
}

View file

@ -0,0 +1,94 @@
{
"languageName": "繁體中文(台灣)",
"Create your admin account": "建立您的管理員帳號",
"authIncorrectCreds": "使用者名稱或密碼錯誤",
"PasswordsDoNotMatch": "兩次輸入的密碼不一致。",
"Repeat Password": "重複以確認密碼",
"Create": "建立",
"signedInDisp": "目前使用者:{0}",
"signedInDispDisabled": "已停用身份驗證",
"home": "首頁",
"console": "主控台",
"registry": "映像倉庫",
"compose": "Compose",
"addFirstStackMsg": "組合您的第一個堆疊!",
"stackName": "堆疊名稱",
"deployStack": "部署",
"deleteStack": "刪除",
"stopStack": "停止",
"restartStack": "重啟",
"updateStack": "更新",
"startStack": "啟動",
"editStack": "編輯",
"discardStack": "捨棄",
"saveStackDraft": "儲存",
"notAvailableShort": "不可用",
"deleteStackMsg": "您確定要刪除這個堆疊嗎?",
"stackNotManagedByDockgeMsg": "這個堆疊不由 Dockge 管理",
"primaryHostname": "主機名稱",
"general": "一般",
"container": "容器 | 容器群組",
"scanFolder": "掃描堆疊資料夾",
"dockerImage": "映像",
"restartPolicyUnlessStopped": "除非手動停止",
"restartPolicyAlways": "始終",
"restartPolicyOnFailure": "在失敗時",
"restartPolicyNo": "不重啟",
"environmentVariable": "環境變數 | 環境變數群組",
"restartPolicy": "重啟策略",
"containerName": "容器名稱",
"port": "連接埠 | 連接埠群組",
"volume": "資料卷 | 資料卷群組",
"network": "網路 | 網路群組",
"dependsOn": "容器依賴 | 容器依賴關係",
"addListItem": "新增 {0}",
"deleteContainer": "刪除容器",
"addContainer": "新增容器",
"addNetwork": "新增網路",
"disableauth.message1": "您確定要<strong>停用身份驗證</strong>嗎?",
"disableauth.message2": "該選項設計用於某些場景,<strong>例如在 Dockge 之上接入第三方認證</strong>,如 Cloudflare Access、Authelia 或其他認證機制。如果您不清楚這個選項的作用,請不要停用驗證!",
"passwordNotMatchMsg": "兩次輸入的密碼不一致。",
"autoGet": "自動取得",
"add": "新增",
"Edit": "編輯",
"applyToYAML": "應用到YAML",
"createExternalNetwork": "建立",
"addInternalNetwork": "新增",
"Save": "儲存",
"Language": "語言",
"Current User": "目前使用者",
"Change Password": "更換密碼",
"Current Password": "目前密碼",
"New Password": "新密碼",
"Repeat New Password": "重複以確認新密碼",
"Update Password": "更新密碼",
"Advanced": "進階",
"Please use this option carefully!": "請謹慎使用該選項!",
"Enable Auth": "啟用驗證",
"Disable Auth": "停用驗證",
"I understand, please disable": "我已了解風險,確認停用",
"Leave": "離開",
"Frontend Version": "前端版本",
"Check Update On GitHub": "在 GitHub 上檢查更新",
"Show update if available": "有更新時提醒我",
"Also check beta release": "同時檢查 Beta 渠道更新",
"Remember me": "記住我",
"Login": "登入",
"Username": "使用者名稱",
"Password": "密碼",
"Settings": "設定",
"Logout": "登出",
"Lowercase only": "僅小寫字母",
"Convert to Compose": "轉換為 Compose 格式",
"Docker Run": "Docker 啟動",
"active": "已啟動",
"exited": "已退出",
"inactive": "未啟動",
"Appearance": "外觀",
"Security": "安全",
"About": "關於",
"Allowed commands:": "允許使用的指令:",
"Internal Networks": "內部網路",
"External Networks": "外部網路",
"No External Networks": "無外部網路"
}

View file

@ -319,6 +319,12 @@ export default {
}, },
deep: true, deep: true,
}, },
$route(to, from) {
// Leave Combined Terminal
console.debug("leaveCombinedTerminal", from.params.stackName);
this.$root.getSocket().emit("leaveCombinedTerminal", this.stack.name, () => {});
}
}, },
mounted() { mounted() {
if (this.isAdd) { if (this.isAdd) {
@ -361,7 +367,7 @@ export default {
clearTimeout(serviceStatusTimeout); clearTimeout(serviceStatusTimeout);
serviceStatusTimeout = setTimeout(async () => { serviceStatusTimeout = setTimeout(async () => {
this.requestServiceStatus(); this.requestServiceStatus();
}, 2000); }, 5000);
}, },
requestServiceStatus() { requestServiceStatus() {
@ -544,10 +550,6 @@ export default {
throw new Error("Services must be an object"); throw new Error("Services must be an object");
} }
if (!config.version) {
config.version = "3.8";
}
this.yamlDoc = doc; this.yamlDoc = doc;
this.jsonConfig = config; this.jsonConfig = config;