diff --git a/.dockerignore b/.dockerignore
new file mode 120000
index 00000000..3e4e48b0
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+.gitignore
\ No newline at end of file
diff --git a/.github/panel-custom-layout.gif b/.github/panel-custom-layout.gif
new file mode 100644
index 00000000..09ddb2e0
Binary files /dev/null and b/.github/panel-custom-layout.gif differ
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
new file mode 100644
index 00000000..3b47e7db
--- /dev/null
+++ b/.github/workflows/docker.yml
@@ -0,0 +1,102 @@
+name: Release Docker Build
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ build-web:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Docker meta web
+ id: meta_web
+ uses: docker/metadata-action@v5
+ with:
+ images: |
+ name=ghcr.io/${{ github.repository }}-web
+ name=githubyumao/mcsmanager-web,enable=${{ github.repository == 'MCSManager/MCSManager' }}
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to ghcr.io
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to DockerHub
+ if: ${{ github.repository == 'MCSManager/MCSManager' }}
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_HUB_USERNAME }}
+ password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
+
+ - name: Build and Push Web
+ uses: docker/build-push-action@v6
+ with:
+ file: dockerfile/web.dockerfile
+ push: true
+ platforms: linux/amd64,linux/arm64
+ tags: ${{ steps.meta_web.outputs.tags }}
+ labels: ${{ steps.meta_web.outputs.labels }}
+ build-args: |
+ BUILDPLATFORM=linux/amd64
+
+ build-daemon:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ java_version: [8, 11, 17, 21]
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Docker meta daemon
+ id: meta_daemon
+ uses: docker/metadata-action@v5
+ with:
+ images: |
+ name=ghcr.io/${{ github.repository }}-daemon,enable=${{ matrix.java_version == 21 }}
+ name=githubyumao/mcsmanager-daemon,enable=${{ github.repository == 'MCSManager/MCSManager' && matrix.java_version == 21 }}
+ name=ghcr.io/${{ github.repository }}-daemon-jdk${{ matrix.java_version }}
+ name=githubyumao/mcsmanager-daemon-jdk${{ matrix.java_version }},enable=${{ github.repository == 'MCSManager/MCSManager' }}
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: Login to ghcr.io
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Login to DockerHub
+ if: ${{ github.repository == 'MCSManager/MCSManager' }}
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_HUB_USERNAME }}
+ password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
+
+ - name: Build and Push Daemon
+ uses: docker/build-push-action@v6
+ with:
+ file: dockerfile/daemon.dockerfile
+ push: true
+ platforms: linux/amd64,linux/arm64
+ tags: ${{ steps.meta_daemon.outputs.tags }}
+ labels: ${{ steps.meta_daemon.outputs.labels }}
+ build-args: |
+ BUILDPLATFORM=linux/amd64
+ JAVA_RUNTIME=${{ matrix.java_version }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000..012c340d
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,57 @@
+name: Release Build
+
+on:
+ release:
+ types: [published]
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20.x
+
+ - name: Install dependencies
+ run: |
+ chmod a+x ./install-dependents.sh
+ chmod a+x ./build.sh
+ ./install-dependents.sh
+ ./build.sh
+
+ - name: Add binaries to production files
+ run: wget --input-file=lib-urls.txt --directory-prefix=production-code/daemon/lib/
+
+ - name: Create linux and windows build
+ run: |
+ cp -r production-code dist_linux
+ mv production-code dist_windows
+
+ - name: Copy startup scripts
+ run: |
+ cp prod-scripts/linux/* dist_linux/
+ cp prod-scripts/windows/* dist_windows/
+
+ - name: Copy node runtime to windows build
+ run: |
+ wget https://nodejs.org/download/release/latest-v20.x/win-x64/node.exe -O dist_windows/daemon/node_app.exe
+ cp dist_windows/daemon/node_app.exe dist_windows/web/node_app.exe
+
+ - name: Create archive
+ run: |
+ mv dist_linux/ mcsmanager/
+ tar czf mcsmanager_linux_release.tar.gz mcsmanager/
+ rm -rf mcsmanager/
+ mv dist_windows/ mcsmanager/
+ zip -r mcsmanager_windows_release.zip mcsmanager/
+
+ - name: Upload assets to release
+ uses: softprops/action-gh-release@v2
+ with:
+ files: |
+ mcsmanager_windows_release.zip
+ mcsmanager_linux_release.tar.gz
diff --git a/README.md b/README.md
index 018ede91..4c765d5f 100644
--- a/README.md
+++ b/README.md
@@ -16,8 +16,8 @@
[Official Website](http://mcsmanager.com/) | [Docs](https://docs.mcsmanager.com/) | [Discord](https://discord.gg/BNpYMVX7Cd)
-[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) |
-[日本語](README_JP.md)
+[简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) |
+[日本語](README_JP.md) | [Spanish](README_ES.md)
@@ -32,10 +32,21 @@ MCSManager has already gained a certain level of popularity within the community
MCSManager **supports English, French, German, Italian, Japanese, Portuguese, Simplified Chinese, and Traditional Chinese**, with plans to add support for more languages in the future!
+
+**Terminal**
+
![failed_to_load_screenshot.png](/.github/panel-image.png)
+**Instance List**
+
![failed_to_load_screenshot.png](/.github/panel-instances.png)
+**Custom Layout**
+
+![failed_to_load_screenshot.png](/.github/panel-custom-layout.gif)
+
+
+
## Features
1. One-click deployment of `Minecraft` Java/Bedrock Server
diff --git a/README_ES.md b/README_ES.md
new file mode 100644
index 00000000..bca92240
--- /dev/null
+++ b/README_ES.md
@@ -0,0 +1,199 @@
+
+
+
+
+
+
+
+
+
+[![--](https://img.shields.io/badge/Support-Windows/Linux-green.svg)](https://github.com/MCSManager)
+[![Estado](https://img.shields.io/badge/npm-v8.9.14-blue.svg)](https://www.npmjs.com/)
+[![Estado](https://img.shields.io/badge/node-v16.20.2-blue.svg)](https://nodejs.org/en/download/)
+[![Licencia](https://img.shields.io/badge/License-Apache%202.0-red.svg)](https://github.com/MCSManager)
+
+[Sitio Oficial](http://mcsmanager.com/) | [Documentación](https://docs.mcsmanager.com/) | [Discord](https://discord.gg/BNpYMVX7Cd)
+
+[Inglés](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) | [日本語](README_JP.md)
+
+
+
+
+## ¿Qué es MCSManager?
+
+**Panel de MCSManager** (MCSM) es un **panel de control moderno, seguro y distribuido** diseñado para gestionar servidores de juego de Minecraft y Steam.
+
+MCSManager ha ganado popularidad en la comunidad, especialmente en Minecraft. MCSManager ofrece una solución centralizada para gestionar múltiples instancias de servidor y proporciona un sistema de permisos multiusuario seguro y confiable. Nos comprometemos a apoyar a los administradores de servidores no solo para Minecraft, sino también para Terraria y varios juegos de Steam. Nuestro objetivo es fomentar una comunidad próspera y de apoyo en la gestión de servidores de juego.
+
+MCSManager **admite inglés, francés, alemán, italiano, japonés, portugués, chino simplificado y chino tradicional**, ¡y planea agregar más idiomas en el futuro!
+
+![failed_to_load_screenshot.png](/.github/panel-image.png)
+
+![failed_to_load_screenshot.png](/.github/panel-instances.png)
+
+## Características
+
+1. Implementación con un clic de servidor `Minecraft` Java/Bedrock.
+2. Compatible con la mayoría de servidores de juegos de `Steam` (p. ej., `Palworld`, `Squad`, `Project Zomboid`, `Terraria`, etc.).
+3. Interfaz personalizable; crea tu propio diseño.
+4. Soporte para virtualización con `Docker`, multiusuario y servicios comerciales.
+5. Gestiona múltiples servidores desde una sola interfaz web.
+6. ¡Y más!
+
+
+
+## Entorno de Ejecución
+
+MCSM es compatible con `Windows` y `Linux`. El único requisito es `Node.js` y algunas librerías **para descompresión**.
+
+Requiere [Node.js 16.20.2](https://nodejs.org/en) o superior.
+
+
+
+## Instalación
+
+### Windows
+
+Para Windows, ofrecemos archivos ejecutables empaquetados:
+
+Ir a: [https://mcsmanager.com/](https://mcsmanager.com/)
+
+
+
+### Linux
+
+**Despliegue con un solo comando**
+
+> El script necesita registrar servicios del sistema, requiere permisos de root.
+
+```bash
+sudo su -c "wget -qO- https://script.mcsmanager.com/setup.sh | bash"
+```
+
+**Uso**
+
+```bash
+systemctl start mcsm-{web,daemon}
+systemctl stop mcsm-{web,daemon}
+```
+
+- Solo compatible con Ubuntu/Centos/Debian/Archlinux.
+- Directorio de instalación: `/opt/mcsmanager/`.
+
+
+
+**Instalación Manual en Linux**
+
+- Si el script de instalación falla, puedes intentar instalarlo manualmente.
+
+```bash
+# Crear directorio /opt si no existe
+mkdir /opt
+# Cambiar a /opt
+cd /opt/
+# Descargar Node.js 20.11. Si ya tienes Node.js 16+ instalado, omite este paso.
+wget https://nodejs.org/dist/v20.11.0/node-v20.11.0-linux-x64.tar.xz
+# Descomprimir Node.js
+tar -xvf node-v20.11.0-linux-x64.tar.xz
+# Agregar Node.js al PATH del sistema
+ln -s /opt/node-v20.11.0-linux-x64/bin/node /usr/bin/node
+ln -s /opt/node-v20.11.0-linux-x64/bin/npm /usr/bin/npm
+
+# Preparar el directorio de instalación de MCSM
+mkdir /opt/mcsmanager/
+cd /opt/mcsmanager/
+
+# Descargar MCSManager
+wget https://github.com/MCSManager/MCSManager/releases/latest/download/mcsmanager_linux_release.tar.gz
+tar -zxf mcsmanager_linux_release.tar.gz
+
+# Instalar dependencias
+./install.sh
+
+# Abrir dos terminales o pantallas.
+
+# Iniciar el daemon primero.
+./start-daemon.sh
+
+# Iniciar la interfaz web en la segunda terminal o pantalla.
+./start-web.sh
+
+# Para acceder a la web, ir a http://localhost:23333/
+# En general, la interfaz web escaneará y añadirá automáticamente el daemon local.
+```
+
+Este método de instalación no configura automáticamente MCSManager como un servicio del sistema. Por lo tanto, es necesario usar `screen` para la administración. Para quienes quieran administrar MCSManager a través de un servicio del sistema, por favor consulta nuestra wiki/documentación.
+
+
+
+## Compatibilidad del Navegador
+
+- Compatible con navegadores modernos como `Chrome`, `Firefox` y `Safari`.
+- El soporte para `IE` ha sido discontinuado.
+
+
+
+## Desarrollo
+
+Esta sección está dirigida específicamente a desarrolladores. Los usuarios generales pueden ignorarla sin problema.
+
+### MacOS
+
+```bash
+git clone https://github.com/MCSManager/MCSManager.git
+./install-dependents.sh
+./npm-dev-macos.sh
+```
+
+### Windows
+
+```bash
+git clone https://github.com/MCSManager/MCSManager.git
+./install-dependents.bat
+./npm-dev-windows.bat
+```
+
+### Construir Versión de Producción
+
+```bash
+./build.bat # Windows
+./build.sh # MacOS
+```
+
+Luego, deberás ir a los proyectos [PTY](https://github.com/MCSManager/PTY) y [Zip-Tools](https://github.com/MCSManager/Zip-Tools) para descargar los archivos binarios correspondientes y colocarlos en el directorio `daemon/lib` para asegurar el funcionamiento adecuado del `Terminal de Emulación` y la `Descompresión de Archivos`.
+
+
+
+## Contribución de Código
+
+Si experimentas problemas al usar MCSManager, puedes [enviar un Issue](https://github.com/MCSManager/MCSManager/issues/new/choose). Alternativamente, puedes hacer un fork del proyecto y contribuir directamente enviando un Pull Request.
+
+Asegúrate de que el código enviado siga nuestro estilo de codificación existente. Para más detalles, consulta las pautas en [este issue](https://github.com/MCSManager/MCSManager/issues/544).
+
+
+
+## Reporte de Errores
+
+**Abrir Issue:** [Haz clic aquí](https://github.com/MCSManager/MCSManager/issues/new/choose)
+
+**Reporte de Vulnerabilidades de Seguridad:** [SECURITY.md](SECURITY.md) (en ingles)
+
+
+
+## Internacionalización
+
+Gracias a estos colaboradores por proporcionar una gran cantidad de traducciones:
+
+- [KevinLu2000](https://github.com/KevinLu2000)
+- [Unitwk](https://github.com/unitwk)
+- [JianyueLab](https://github.com/JianyueLab)
+- [IceBrick](https://github.com/IceBrick01)
+
+
+
+## Licencia
+El código fuente de MCSManager está licenciado bajo la [Licencia Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0).
+
+Copyright ©2024 MCSManager.
\ No newline at end of file
diff --git a/README_JP.md b/README_JP.md
index e4737421..e169a0ae 100644
--- a/README_JP.md
+++ b/README_JP.md
@@ -17,7 +17,7 @@
[HP](http://mcsmanager.com/) | [ドキュメント](https://docs.mcsmanager.com/) | [Discord](https://discord.gg/BNpYMVX7Cd)
-[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md)
+[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) | [Spanish](README_ES.md)
diff --git a/README_PTBR.md b/README_PTBR.md
index 0b9207e1..feb47f53 100644
--- a/README_PTBR.md
+++ b/README_PTBR.md
@@ -16,8 +16,8 @@
[Website Oficial](http://mcsmanager.com/) | [Documentação](https://docs.mcsmanager.com/) | [Discord](https://discord.gg/BNpYMVX7Cd)
-[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) |
-[日本語](README_JP.md)
+[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) |
+[日本語](README_JP.md) | [Spanish](README_ES.md)
diff --git a/README_TW.md b/README_TW.md
index 6977bc39..3d9545ec 100644
--- a/README_TW.md
+++ b/README_TW.md
@@ -16,8 +16,8 @@
[官方網站](http://mcsmanager.com/) | [教學說明](https://docs.mcsmanager.com/#/zh-cn/) | [TG 群組](https://t.me/MCSManager_dev) | [成為贊助者](https://afdian.net/a/mcsmanager)
-[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) |
-[日本語](README_JP.md)
+[English](README.md) | [简体中文](README_ZH.md) | [Português BR](README_PTBR.md) |
+[日本語](README_JP.md) | [Spanish](README_ES.md)
diff --git a/README_ZH.md b/README_ZH.md
index b4740804..f04d5e07 100644
--- a/README_ZH.md
+++ b/README_ZH.md
@@ -16,8 +16,8 @@
[官方网站](http://mcsmanager.com/) | [使用文档](https://docs.mcsmanager.com/#/zh-cn/) | [QQ 群](https://jq.qq.com/?_wv=1027&k=Pgl9ScGw) | [TG 群](https://t.me/MCSManager_dev) | [成为赞助者](https://afdian.net/a/mcsmanager)
-[English](README.md) | [简体中文](README_ZH.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) |
-[日本語](README_JP.md)
+[English](README.md) | [繁體中文](README_TW.md) | [Português BR](README_PTBR.md) |
+[日本語](README_JP.md) | [Spanish](README_ES.md)
diff --git a/build.sh b/build.sh
index fe7b36b3..b61cd128 100755
--- a/build.sh
+++ b/build.sh
@@ -2,6 +2,8 @@
set -e
+BASE_PATH=$(pwd)
+
npm run preview-build
rm -rf production-code
@@ -9,42 +11,45 @@ rm -rf ./daemon/dist ./daemon/production
rm -rf ./panel/dist ./panel/production
echo "Build daemon..."
-cd daemon
+cd "${BASE_PATH}/daemon"
npm run build
echo "Build panel..."
-cd ..
-cd panel
+cd "${BASE_PATH}/panel"
npm run build
echo "Build frontend..."
-cd ..
-cd frontend
+cd "${BASE_PATH}/frontend"
npm run build
echo "Collecting files..."
-cd ..
+cd "${BASE_PATH}"
mkdir production-code
mkdir production-code/daemon
mkdir production-code/web
mkdir production-code/web/public
-mv ./daemon/production/app.js ./production-code/daemon
-mv ./daemon/production/app.js.map ./production-code/daemon
-cp -f ./daemon/package.json ./production-code/daemon/package.json
-cp -f ./daemon/package-lock.json ./production-code/daemon/package-lock.json
+mv "${BASE_PATH}/daemon/production/app.js" "${BASE_PATH}/production-code/daemon"
+mv "${BASE_PATH}/daemon/production/app.js.map" "${BASE_PATH}/production-code/daemon"
+cp -f "${BASE_PATH}/daemon/package.json" "${BASE_PATH}/production-code/daemon/package.json"
+cp -f "${BASE_PATH}/daemon/package-lock.json" "${BASE_PATH}/production-code/daemon/package-lock.json"
-mv ./panel/production/app.js ./production-code/web
-mv ./panel/production/app.js.map ./production-code/web
-cp -f ./panel/package.json ./production-code/web/package.json
-cp -f ./panel/package-lock.json ./production-code/web/package-lock.json
+mv "${BASE_PATH}/panel/production/app.js" "${BASE_PATH}/production-code/web"
+mv "${BASE_PATH}/panel/production/app.js.map" "${BASE_PATH}/production-code/web"
+cp -f "${BASE_PATH}/panel/package.json" "${BASE_PATH}/production-code/web/package.json"
+cp -f "${BASE_PATH}/panel/package-lock.json" "${BASE_PATH}/production-code/web/package-lock.json"
-mv ./frontend/dist/* ./production-code/web/public
+mv "${BASE_PATH}"/frontend/dist/* "${BASE_PATH}/production-code/web/public"
-rm -rf ./daemon/dist ./daemon/production
-rm -rf ./panel/dist ./panel/production
-rm -rf ./frontend/dist
+rm -rf "${BASE_PATH}/daemon/dist" "${BASE_PATH}/daemon/production"
+rm -rf "${BASE_PATH}/panel/dist" "${BASE_PATH}/panel/production"
+rm -rf "${BASE_PATH}/frontend/dist"
+
+cd "${BASE_PATH}/production-code/daemon"
+npm install --production --no-fund --no-audit
+cd "${BASE_PATH}/production-code/web"
+npm install --production --no-fund --no-audit
echo "------------"
echo "Compilation completed!"
diff --git a/daemon/package.json b/daemon/package.json
index efd7ddbc..9cbfd29c 100644
--- a/daemon/package.json
+++ b/daemon/package.json
@@ -1,6 +1,6 @@
{
"name": "mcsmanager-daemon",
- "version": "4.5.0",
+ "version": "4.5.2",
"description": "Provides remote control capability for MCSManager to manage processes, scheduled tasks, I/O streams, and more",
"scripts": {
"dev": "ts-node --project tsconfig.json src/app.ts",
diff --git a/daemon/src/entity/commands/cmd.ts b/daemon/src/entity/commands/cmd.ts
deleted file mode 100755
index be003eba..00000000
--- a/daemon/src/entity/commands/cmd.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import Instance from "../instance/instance";
-import InstanceCommand from "./base/command";
-
-export default class SendCommand extends InstanceCommand {
- constructor(public readonly cmd: string) {
- super("SendCommand");
- }
-
- async exec(instance: Instance) {
- return await instance.execPreset("command", this.cmd);
- }
-}
diff --git a/daemon/src/entity/commands/docker/docker_pull.ts b/daemon/src/entity/commands/docker/docker_pull.ts
index 8cfaf1e3..3f1bda09 100644
--- a/daemon/src/entity/commands/docker/docker_pull.ts
+++ b/daemon/src/entity/commands/docker/docker_pull.ts
@@ -1,8 +1,7 @@
-import Docker from "dockerode";
import Instance from "../../instance/instance";
import InstanceCommand from "../base/command";
import { t } from "i18next";
-import { DefaultDocker } from "../../../service/docker_service"
+import { DefaultDocker } from "../../../service/docker_service";
export async function checkImage(name: string) {
const docker = new DefaultDocker();
diff --git a/daemon/src/entity/commands/docker/docker_start.ts b/daemon/src/entity/commands/docker/docker_start.ts
index 777b4883..0dbd7aa7 100755
--- a/daemon/src/entity/commands/docker/docker_start.ts
+++ b/daemon/src/entity/commands/docker/docker_start.ts
@@ -3,7 +3,6 @@ import Instance from "../../instance/instance";
import InstanceCommand from "../base/command";
import logger from "../../../service/log";
import fs from "fs-extra";
-import { t } from "i18next";
import DockerPullCommand from "./docker_pull";
import {
DockerProcessAdapter,
@@ -17,8 +16,8 @@ export default class DockerStartCommand extends InstanceCommand {
}
async exec(instance: Instance, source = "Unknown") {
- if (!instance.config.cwd || !instance.config.ie || !instance.config.oe)
- throw new StartupDockerProcessError($t("TXT_CODE_instance.dirEmpty"));
+ if (!instance.hasCwdPath() || !instance.config.ie || !instance.config.oe)
+ throw new StartupDockerProcessError($t("TXT_CODE_a6424dcc"));
if (!fs.existsSync(instance.absoluteCwdPath()))
throw new StartupDockerProcessError($t("TXT_CODE_instance.dirNoE"));
@@ -30,16 +29,13 @@ export default class DockerStartCommand extends InstanceCommand {
}
// Docker docks to the process adapter
- const isTty = instance.config.terminalOption.pty;
- const workingDir = instance.config.docker.workingDir ?? "/workspace/";
const processAdapter = new DockerProcessAdapter(new SetupDockerContainer(instance));
await processAdapter.start({
- isTty,
+ isTty: instance.config.terminalOption.pty,
w: instance.config.terminalOption.ptyWindowCol,
h: instance.config.terminalOption.ptyWindowCol
});
- instance.println("CONTAINER", t("TXT_CODE_e76e49e9") + workingDir);
instance.started(processAdapter);
logger.info(
$t("TXT_CODE_instance.successful", {
diff --git a/daemon/src/entity/commands/general/general_command.ts b/daemon/src/entity/commands/general/general_command.ts
index 872ee78f..48a71d72 100755
--- a/daemon/src/entity/commands/general/general_command.ts
+++ b/daemon/src/entity/commands/general/general_command.ts
@@ -4,12 +4,27 @@ import Instance from "../../instance/instance";
import { encode } from "iconv-lite";
import InstanceCommand from "../base/command";
+export const CTRL_C = "\x03";
+
+export function isExitCommand(instance: Instance, buf: any) {
+ if (String(buf).toLowerCase() === "^c") {
+ instance.process?.kill("SIGINT");
+ return true;
+ }
+ if (buf == CTRL_C) {
+ instance.process?.write(CTRL_C);
+ return true;
+ }
+ return false;
+}
+
export default class GeneralSendCommand extends InstanceCommand {
constructor() {
super("SendCommand");
}
async exec(instance: Instance, buf?: any): Promise {
+ if (isExitCommand(instance, buf)) return;
// The server shutdown command needs to send a command, but before the server shutdown command is executed, the status will be set to the shutdown state.
// So here the command can only be executed by whether the process exists or not
if (instance?.process) {
diff --git a/daemon/src/entity/commands/general/general_install.ts b/daemon/src/entity/commands/general/general_install.ts
index 3d114f99..1f84da30 100755
--- a/daemon/src/entity/commands/general/general_install.ts
+++ b/daemon/src/entity/commands/general/general_install.ts
@@ -28,9 +28,9 @@ export default class GeneralInstallCommand extends InstanceCommand {
instance.setLock(true);
instance.status(Instance.STATUS_BUSY);
instance.println($t("TXT_CODE_1704ea49"), $t("TXT_CODE_cbc235ad"));
- if (instance.config.cwd.length > 1) {
- fs.removeSync(instance.config.cwd);
- fs.mkdirsSync(instance.config.cwd);
+ if (instance.hasCwdPath()) {
+ await fs.remove(instance.absoluteCwdPath());
+ await fs.mkdirs(instance.absoluteCwdPath());
}
instance.println($t("TXT_CODE_1704ea49"), $t("TXT_CODE_906c5d6a"));
this.process = new QuickInstallTask(
diff --git a/daemon/src/entity/commands/general/general_kill.ts b/daemon/src/entity/commands/general/general_kill.ts
index 0ccd4cc5..bfe86e47 100755
--- a/daemon/src/entity/commands/general/general_kill.ts
+++ b/daemon/src/entity/commands/general/general_kill.ts
@@ -1,3 +1,4 @@
+import { $t } from "../../../i18n";
import logger from "../../../service/log";
import Instance from "../../instance/instance";
import InstanceCommand from "../base/command";
@@ -8,6 +9,16 @@ export default class GeneralKillCommand extends InstanceCommand {
}
async exec(instance: Instance) {
+ if (
+ instance.status() === Instance.STATUS_STOP ||
+ instance.status() === Instance.STATUS_STARTING
+ ) {
+ return instance.failure(new Error($t("TXT_CODE_general_stop.notRunning")));
+ }
+ if (instance.startTimestamp + 6 * 1000 > Date.now()) {
+ return instance.failure(new Error($t("TXT_CODE_6259357c")));
+ }
+
const task = instance?.asynchronousTask;
if (task && task.stop) {
task
@@ -20,6 +31,5 @@ export default class GeneralKillCommand extends InstanceCommand {
if (instance.process) {
await instance.process.kill("SIGKILL");
}
- instance.setLock(false);
}
}
diff --git a/daemon/src/entity/commands/general/general_restart.ts b/daemon/src/entity/commands/general/general_restart.ts
index b8fb148d..cb87a2cb 100755
--- a/daemon/src/entity/commands/general/general_restart.ts
+++ b/daemon/src/entity/commands/general/general_restart.ts
@@ -8,10 +8,14 @@ export default class GeneralRestartCommand extends InstanceCommand {
}
async exec(instance: Instance) {
+ // If the automatic restart function is enabled, the setting is ignored once
+ if (instance.config.eventTask && instance.config.eventTask.autoRestart)
+ instance.config.eventTask.ignore = true;
+
try {
instance.println("INFO", $t("TXT_CODE_restart.start"));
- await instance.execPreset("stop");
instance.setLock(true);
+ await instance.execPreset("stop");
const startCount = instance.startCount;
// Check the instance status every second,
// if the instance status is stopped, restart the server immediately
@@ -28,9 +32,9 @@ export default class GeneralRestartCommand extends InstanceCommand {
}
if (instance.status() === Instance.STATUS_STOP) {
instance.println("INFO", $t("TXT_CODE_restart.restarting"));
- await instance.execPreset("start");
instance.setLock(false);
clearInterval(task);
+ await instance.execPreset("start");
}
} catch (error: any) {
clearInterval(task);
diff --git a/daemon/src/entity/commands/general/general_start.ts b/daemon/src/entity/commands/general/general_start.ts
index 968f3858..0c7c8024 100755
--- a/daemon/src/entity/commands/general/general_start.ts
+++ b/daemon/src/entity/commands/general/general_start.ts
@@ -1,14 +1,13 @@
import { $t } from "../../../i18n";
-import os from "os";
import Instance from "../../instance/instance";
import logger from "../../../service/log";
import fs from "fs-extra";
-import InstanceCommand from "../base/command";
import EventEmitter from "events";
import { IInstanceProcess } from "../../instance/interface";
-import { ChildProcess, exec, spawn } from "child_process";
+import { ChildProcess, spawn } from "child_process";
import { commandStringToArray } from "../base/command_parser";
import { killProcess } from "common";
+import AbsStartCommand from "../start";
// Error exception at startup
class StartupError extends Error {
@@ -57,15 +56,15 @@ class ProcessAdapter extends EventEmitter implements IInstanceProcess {
}
}
-export default class GeneralStartCommand extends InstanceCommand {
+export default class GeneralStartCommand extends AbsStartCommand {
constructor() {
super("StartCommand");
}
- async exec(instance: Instance, source = "Unknown") {
+ async createProcess(instance: Instance, source = "") {
if (
(!instance.config.startCommand && instance.config.processType === "general") ||
- !instance.config.cwd ||
+ !instance.hasCwdPath() ||
!instance.config.ie ||
!instance.config.oe
)
@@ -85,13 +84,13 @@ export default class GeneralStartCommand extends InstanceCommand {
logger.info($t("TXT_CODE_general_start.startInstance", { source: source }));
logger.info($t("TXT_CODE_general_start.instanceUuid", { uuid: instance.instanceUuid }));
logger.info($t("TXT_CODE_general_start.startCmd", { cmdList: JSON.stringify(commandList) }));
- logger.info($t("TXT_CODE_general_start.cwd", { cwd: instance.config.cwd }));
+ logger.info($t("TXT_CODE_general_start.cwd", { cwd: instance.absoluteCwdPath() }));
logger.info("----------------");
// create child process
// Parameter 1 directly passes the process name or path (including spaces) without double quotes
const subProcess = spawn(commandExeFile, commandParameters, {
- cwd: instance.config.cwd,
+ cwd: instance.absoluteCwdPath(),
stdio: "pipe",
windowsHide: true,
env: process.env
diff --git a/daemon/src/entity/commands/general/general_stop.ts b/daemon/src/entity/commands/general/general_stop.ts
index 88b10bd2..c48d27da 100755
--- a/daemon/src/entity/commands/general/general_stop.ts
+++ b/daemon/src/entity/commands/general/general_stop.ts
@@ -1,8 +1,6 @@
import { $t } from "../../../i18n";
import Instance from "../../instance/instance";
import InstanceCommand from "../base/command";
-import SendCommand from "../cmd";
-import RconCommand from "../steam/rcon_command";
export default class GeneralStopCommand extends InstanceCommand {
constructor() {
@@ -10,6 +8,10 @@ export default class GeneralStopCommand extends InstanceCommand {
}
async exec(instance: Instance) {
+ // If the automatic restart function is enabled, the setting is ignored once
+ if (instance.config.eventTask && instance.config.eventTask.autoRestart)
+ instance.config.eventTask.ignore = true;
+
const stopCommand = instance.config.stopCommand;
if (instance.status() === Instance.STATUS_STOP || !instance.process)
return instance.failure(new Error($t("TXT_CODE_general_stop.notRunning")));
@@ -18,13 +20,7 @@ export default class GeneralStopCommand extends InstanceCommand {
const stopCommandList = stopCommand.split("\n");
for (const stopCommand of stopCommandList) {
- if (stopCommand.toLowerCase() == "^c") {
- instance.process.kill("SIGINT");
- } else if (instance.config.enableRcon) {
- await instance.exec(new RconCommand(stopCommand));
- } else {
- await instance.exec(new SendCommand(stopCommand));
- }
+ await instance.execPreset("command", stopCommand);
}
instance.print("\n");
diff --git a/daemon/src/entity/commands/kill.ts b/daemon/src/entity/commands/kill.ts
deleted file mode 100755
index 3442b61a..00000000
--- a/daemon/src/entity/commands/kill.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import Instance from "../instance/instance";
-import InstanceCommand from "./base/command";
-
-export default class KillCommand extends InstanceCommand {
- constructor() {
- super("KillCommand");
- }
-
- async exec(instance: Instance) {
- // If the automatic restart function is enabled, the setting is ignored once
- if (instance.config.eventTask && instance.config.eventTask.autoRestart)
- instance.config.eventTask.ignore = true;
-
- // send stop command
- return await instance.execPreset("kill");
- }
-}
diff --git a/daemon/src/entity/commands/pty/pty_start.ts b/daemon/src/entity/commands/pty/pty_start.ts
index b729805f..546f2981 100755
--- a/daemon/src/entity/commands/pty/pty_start.ts
+++ b/daemon/src/entity/commands/pty/pty_start.ts
@@ -5,7 +5,6 @@ import logger from "../../../service/log";
import fs from "fs-extra";
import path from "path";
import readline from "readline";
-import InstanceCommand from "../base/command";
import EventEmitter from "events";
import { IInstanceProcess } from "../../instance/interface";
import { ChildProcess, ChildProcessWithoutNullStreams, exec, spawn } from "child_process";
@@ -15,6 +14,7 @@ import FunctionDispatcher from "../dispatcher";
import { PTY_PATH } from "../../../const";
import { Writable } from "stream";
import { v4 } from "uuid";
+import AbsStartCommand from "../start";
interface IPtySubProcessCfg {
pid: number;
@@ -44,14 +44,17 @@ export class GoPtyProcessAdapter extends EventEmitter implements IInstanceProces
process.stdout?.on("data", (text) => this.emit("data", text));
process.stderr?.on("data", (text) => this.emit("data", text));
process.on("exit", (code) => this.emit("exit", code));
- try {
- this.initNamedPipe();
- } catch (error: any) {
- logger.error(`Init Pipe Err: ${pipeName}, ${error}`);
- }
+ this.initNamedPipe();
}
private initNamedPipe() {
+ if (!fs.existsSync(this.pipeName)) {
+ throw new Error(
+ $t("TXT_CODE_9d1d244f", {
+ pipeName: this.pipeName
+ })
+ );
+ }
const fd = fs.openSync(this.pipeName, "w");
const writePipe = fs.createWriteStream("", { fd });
writePipe.on("close", () => {});
@@ -111,7 +114,7 @@ export class GoPtyProcessAdapter extends EventEmitter implements IInstanceProces
}
}
-export default class PtyStartCommand extends InstanceCommand {
+export default class PtyStartCommand extends AbsStartCommand {
constructor() {
super("PtyStartCommand");
}
@@ -141,21 +144,21 @@ export default class PtyStartCommand extends InstanceCommand {
});
}
- async exec(instance: Instance, source = "Unknown") {
+ async createProcess(instance: Instance) {
if (
!instance.config.startCommand ||
- !instance.config.cwd ||
+ !instance.hasCwdPath() ||
!instance.config.ie ||
!instance.config.oe
)
throw new StartupError($t("TXT_CODE_pty_start.cmdErr"));
if (!fs.existsSync(instance.absoluteCwdPath()))
throw new StartupError($t("TXT_CODE_pty_start.cwdNotExist"));
- if (!path.isAbsolute(path.normalize(instance.config.cwd)))
+ if (!path.isAbsolute(path.normalize(instance.absoluteCwdPath())))
throw new StartupError($t("TXT_CODE_pty_start.mustAbsolutePath"));
// PTY mode correctness check
- logger.info($t("TXT_CODE_pty_start.startPty", { source: source }));
+ logger.info($t("TXT_CODE_pty_start.startPty", { source: "" }));
let checkPtyEnv = true;
if (!fs.existsSync(PTY_PATH)) {
@@ -167,12 +170,11 @@ export default class PtyStartCommand extends InstanceCommand {
// Close the PTY type, reconfigure the instance function group, and restart the instance
instance.config.terminalOption.pty = false;
await instance.forceExec(new FunctionDispatcher());
- await instance.execPreset("start", source); // execute the preset command directly
+ await instance.execPreset("start"); // execute the preset command directly
return;
}
// Set the startup state & increase the number of startups
- instance.setLock(true);
instance.status(Instance.STATUS_STARTING);
instance.startCount++;
@@ -202,7 +204,7 @@ export default class PtyStartCommand extends InstanceCommand {
"-coder",
instance.config.oe,
"-dir",
- instance.config.cwd,
+ instance.absoluteCwdPath(),
"-fifo",
pipeName,
"-cmd",
@@ -210,12 +212,12 @@ export default class PtyStartCommand extends InstanceCommand {
];
logger.info("----------------");
- logger.info($t("TXT_CODE_pty_start.sourceRequest", { source: source }));
+ logger.info($t("TXT_CODE_pty_start.sourceRequest", { source: "" }));
logger.info($t("TXT_CODE_pty_start.instanceUuid", { instanceUuid: instance.instanceUuid }));
logger.info($t("TXT_CODE_pty_start.startCmd", { cmd: commandList.join(" ") }));
logger.info($t("TXT_CODE_pty_start.ptyPath", { path: PTY_PATH }));
logger.info($t("TXT_CODE_pty_start.ptyParams", { param: ptyParameter.join(" ") }));
- logger.info($t("TXT_CODE_pty_start.ptyCwd", { cwd: instance.config.cwd }));
+ logger.info($t("TXT_CODE_pty_start.ptyCwd", { cwd: instance.absoluteCwdPath() }));
logger.info("----------------");
// create pty child process
@@ -246,7 +248,6 @@ export default class PtyStartCommand extends InstanceCommand {
// create process adapter
const ptySubProcessCfg = await this.readPtySubProcessConfig(subProcess);
const processAdapter = new GoPtyProcessAdapter(subProcess, ptySubProcessCfg.pid, pipeName);
- logger.info(`pty.exe subprocess PID: ${JSON.stringify(ptySubProcessCfg)}`);
// After reading the configuration, Need to check the process status
// The "processAdapter.pid" here represents the process created by the PTY process
diff --git a/daemon/src/entity/commands/pty/pty_stop.ts b/daemon/src/entity/commands/pty/pty_stop.ts
index 25fef7f2..617087a0 100755
--- a/daemon/src/entity/commands/pty/pty_stop.ts
+++ b/daemon/src/entity/commands/pty/pty_stop.ts
@@ -1,7 +1,6 @@
import { $t } from "../../../i18n";
import Instance from "../../instance/instance";
import InstanceCommand from "../base/command";
-import SendCommand from "../cmd";
export default class PtyStopCommand extends InstanceCommand {
constructor() {
@@ -18,12 +17,8 @@ export default class PtyStopCommand extends InstanceCommand {
instance.println("INFO", $t("TXT_CODE_pty_stop.execCmd", { stopCommand: stopCommand }));
const stopCommandList = stopCommand.split("\n");
- for (const stopCommandColumn of stopCommandList) {
- if (stopCommandColumn.toLocaleLowerCase() == "^c") {
- await instance.exec(new SendCommand("\x03"));
- } else {
- await instance.exec(new SendCommand(stopCommandColumn));
- }
+ for (const stopCommand of stopCommandList) {
+ await instance.execPreset("command", stopCommand);
}
// If the instance is still in the stopped state after 10 minutes, restore the state
diff --git a/daemon/src/entity/commands/restart.ts b/daemon/src/entity/commands/restart.ts
deleted file mode 100755
index 3117fc29..00000000
--- a/daemon/src/entity/commands/restart.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { $t } from "../../i18n";
-import Instance from "../instance/instance";
-import InstanceCommand from "./base/command";
-
-export default class RestartCommand extends InstanceCommand {
- constructor() {
- super("RestartCommand");
- }
-
- async exec(instance: Instance) {
- // If the automatic restart function is enabled, the setting is ignored once
- if (instance.config.eventTask && instance.config.eventTask.autoRestart)
- instance.config.eventTask.ignore = true;
-
- if (instance.status() !== Instance.STATUS_RUNNING) {
- throw new Error($t("TXT_CODE_d58ffa0f"));
- }
-
- return await instance.execPreset("restart");
- }
-}
diff --git a/daemon/src/entity/commands/start.ts b/daemon/src/entity/commands/start.ts
index 34b0744f..2e64a311 100755
--- a/daemon/src/entity/commands/start.ts
+++ b/daemon/src/entity/commands/start.ts
@@ -2,23 +2,16 @@ import { $t } from "../../i18n";
import Instance from "../instance/instance";
import InstanceCommand from "./base/command";
-class StartupError extends Error {
+export class StartupError extends Error {
constructor(msg: string) {
super(msg);
}
}
-export default class StartCommand extends InstanceCommand {
- public source: string;
-
- constructor(source = "Unknown") {
- super("StartCommand");
- this.source = source;
- }
-
+export default abstract class AbsStartCommand extends InstanceCommand {
private async sleep() {
return new Promise((ok) => {
- setTimeout(ok, 1000 * 3);
+ setTimeout(ok, 1000 * 2);
});
}
@@ -41,16 +34,16 @@ export default class StartCommand extends InstanceCommand {
}
}
- const currentTimestamp = Date.now();
- instance.startTimestamp = currentTimestamp;
-
- instance.print("\n");
+ instance.print("\n\n");
instance.println("INFO", $t("TXT_CODE_start.startInstance"));
// prevent the dead-loop from starting
await this.sleep();
- return await instance.execPreset("start", this.source);
+ const currentTimestamp = Date.now();
+ instance.startTimestamp = currentTimestamp;
+
+ return await this.createProcess(instance);
} catch (error: any) {
try {
await instance.execPreset("kill");
@@ -62,4 +55,6 @@ export default class StartCommand extends InstanceCommand {
instance.setLock(false);
}
}
+
+ protected abstract createProcess(instance: Instance): Promise;
}
diff --git a/daemon/src/entity/commands/steam/rcon_command.ts b/daemon/src/entity/commands/steam/rcon_command.ts
index b4fcbd1c..3ded7165 100755
--- a/daemon/src/entity/commands/steam/rcon_command.ts
+++ b/daemon/src/entity/commands/steam/rcon_command.ts
@@ -2,6 +2,7 @@ import { t } from "i18next";
import Instance from "../../instance/instance";
import InstanceCommand from "../base/command";
import Rcon from "rcon-srcds";
+import { isExitCommand } from "../general/general_command";
async function sendRconCommand(instance: Instance, command: string) {
const targetIp = instance.config.rconIp || "localhost";
@@ -45,6 +46,7 @@ export default class RconCommand extends InstanceCommand {
}
async exec(instance: Instance, text?: string): Promise {
+ if (isExitCommand(instance, text)) return;
try {
if (text || this.cmd) {
await sendRconCommand(instance, String(text ?? this.cmd));
diff --git a/daemon/src/entity/commands/stop.ts b/daemon/src/entity/commands/stop.ts
deleted file mode 100755
index ab0c33ca..00000000
--- a/daemon/src/entity/commands/stop.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import Instance from "../instance/instance";
-import InstanceCommand from "./base/command";
-import SendCommand from "./cmd";
-
-export default class StopCommand extends InstanceCommand {
- constructor() {
- super("StopCommand");
- }
-
- async exec(instance: Instance) {
- // If the automatic restart function is enabled, the setting is ignored once
- if (instance.config.eventTask && instance.config.eventTask.autoRestart)
- instance.config.eventTask.ignore = true;
-
- // send stop command
- return await instance.execPreset("stop");
- }
-}
diff --git a/daemon/src/entity/commands/task/openfrp.ts b/daemon/src/entity/commands/task/openfrp.ts
index af2f57f2..1a4ba2a6 100755
--- a/daemon/src/entity/commands/task/openfrp.ts
+++ b/daemon/src/entity/commands/task/openfrp.ts
@@ -1,12 +1,8 @@
-import { v4 } from "uuid";
import fs from "fs-extra";
import path from "path";
-import { spawn, ChildProcess } from "child_process";
import os from "os";
-import { killProcess } from "common";
import { ILifeCycleTask } from "../../instance/life_cycle";
import Instance from "../../instance/instance";
-import KillCommand from "../kill";
import logger from "../../../service/log";
import { $t } from "../../../i18n";
import { ProcessWrapper } from "common";
diff --git a/daemon/src/entity/commands/task/time.ts b/daemon/src/entity/commands/task/time.ts
index 8af8bd49..ae8d78d8 100755
--- a/daemon/src/entity/commands/task/time.ts
+++ b/daemon/src/entity/commands/task/time.ts
@@ -1,6 +1,5 @@
import { ILifeCycleTask } from "../../instance/life_cycle";
import Instance from "../../instance/instance";
-import KillCommand from "../kill";
// When the instance is running, continue to check the expiration time
export default class TimeCheck implements ILifeCycleTask {
@@ -17,7 +16,7 @@ export default class TimeCheck implements ILifeCycleTask {
const currentTime = Date.now();
if (endTime <= currentTime) {
// Expired, execute the end process command
- await instance.exec(new KillCommand());
+ await instance.execPreset("kill");
clearInterval(this.task);
}
}
diff --git a/daemon/src/entity/commands/update.ts b/daemon/src/entity/commands/update.ts
deleted file mode 100755
index 1a6d43e6..00000000
--- a/daemon/src/entity/commands/update.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import Instance from "../instance/instance";
-import InstanceCommand from "./base/command";
-import SendCommand from "./cmd";
-
-export default class UpdateCommand extends InstanceCommand {
- constructor() {
- super("UpdateCommand");
- }
-
- async exec(instance: Instance) {
- // Execute the update preset, the preset and function scheduler are set before starting
- return await instance.execPreset("update");
- }
-}
diff --git a/daemon/src/entity/instance/instance.ts b/daemon/src/entity/instance/instance.ts
index 77244efd..5a1b4670 100755
--- a/daemon/src/entity/instance/instance.ts
+++ b/daemon/src/entity/instance/instance.ts
@@ -11,7 +11,7 @@ import { PresetCommandManager } from "./preset";
import FunctionDispatcher, { IPresetCommand } from "../commands/dispatcher";
import { IInstanceProcess } from "./interface";
import StartCommand from "../commands/start";
-import { configureEntityParams } from "common";
+import { configureEntityParams, toText } from "common";
import { OpenFrp } from "../commands/task/openfrp";
import logger from "../../service/log";
import { t } from "i18next";
@@ -228,23 +228,12 @@ export default class Instance extends EventEmitter {
}
setLock(bool: boolean) {
+ if (this.lock === true && bool === true) {
+ throw new Error($t("TXT_CODE_ca030197"));
+ }
this.lock = bool;
}
- // Execute the corresponding command for this instance
- async execCommand(command: InstanceCommand) {
- if (this.lock)
- throw new Error($t("TXT_CODE_instanceConf.instanceLock", { info: command.info }));
- if (this.status() == Instance.STATUS_BUSY)
- throw new Error($t("TXT_CODE_instanceConf.instanceBusy"));
- return await command.exec(this);
- }
-
- // Execute the corresponding command for this instance Alias
- async exec(command: InstanceCommand) {
- return await this.execCommand(command);
- }
-
// force the command to execute
async forceExec(command: InstanceCommand) {
return await command.exec(this);
@@ -299,7 +288,7 @@ export default class Instance extends EventEmitter {
// If automatic restart is enabled, the startup operation is performed immediately
if (this.config.eventTask.autoRestart) {
if (!this.config.eventTask.ignore) {
- this.forceExec(new StartCommand("Event Task: Auto Restart"))
+ this.execPreset("start")
.then(() => {
this.println($t("TXT_CODE_instanceConf.info"), $t("TXT_CODE_instanceConf.autoRestart"));
})
@@ -315,9 +304,9 @@ export default class Instance extends EventEmitter {
// Turn off the warning immediately after startup, usually the startup command is written incorrectly
const currentTimestamp = new Date().getTime();
- const startThreshold = 6 * 1000;
+ const startThreshold = 2 * 1000;
if (currentTimestamp - this.startTimestamp < startThreshold) {
- this.println("ERROR", $t("TXT_CODE_instanceConf.instantExit"));
+ this.println("ERROR", $t("TXT_CODE_aae2918f"));
}
}
@@ -356,6 +345,10 @@ export default class Instance extends EventEmitter {
return date.toLocaleDateString() + " " + date.getHours() + ":" + date.getMinutes();
}
+ hasCwdPath() {
+ return !!this.config.cwd;
+ }
+
absoluteCwdPath() {
if (!this.config || !this.config.cwd) throw new Error("Instance config error, cwd is Null!");
if (path.isAbsolute(this.config.cwd)) return path.normalize(this.config.cwd);
@@ -406,6 +399,13 @@ export default class Instance extends EventEmitter {
this.info.latency = 0;
}
+ public parseTextParams(text: string) {
+ text = text.replace(/\{mcsm_workspace\}/gim, this.absoluteCwdPath());
+ text = text.replace(/\{mcsm_instance_id\}/gim, this.instanceUuid);
+ text = text.replace(/\{mcsm_cwd\}/gim, this.absoluteCwdPath());
+ return text;
+ }
+
private pushOutput(data: string) {
if (data.length > LINE_MAX_SIZE * 100) {
this.outputStack.push(IGNORE_TEXT);
diff --git a/daemon/src/routers/Instance_router.ts b/daemon/src/routers/Instance_router.ts
index 3fe8f6c7..d3b6e2cf 100755
--- a/daemon/src/routers/Instance_router.ts
+++ b/daemon/src/routers/Instance_router.ts
@@ -7,19 +7,14 @@ import Instance from "../entity/instance/instance";
import logger from "../service/log";
import path from "path";
-import StartCommand from "../entity/commands/start";
-import StopCommand from "../entity/commands/stop";
-import SendCommand from "../entity/commands/cmd";
-import KillCommand from "../entity/commands/kill";
import { IInstanceDetail, IJson } from "../service/interfaces";
import ProcessInfoCommand from "../entity/commands/process_info";
import FileManager from "../service/system_file";
import { ProcessConfig } from "../entity/instance/process_config";
-import RestartCommand from "../entity/commands/restart";
import { TaskCenter } from "../service/async_task_service";
import { createQuickInstallTask } from "../service/async_task_service/quick_install";
import { QuickInstallTask } from "../service/async_task_service/quick_install";
-import { toNumber, toText } from "common";
+import { toNumber } from "common";
import { arrayUnique } from "common";
// Some instances operate router authentication middleware
@@ -221,7 +216,7 @@ routerApp.on("instance/open", async (ctx, data) => {
for (const instanceUuid of data.instanceUuids) {
const instance = InstanceSubsystem.getInstance(instanceUuid);
try {
- await instance!.exec(new StartCommand(ctx.socket.id));
+ await instance!.execPreset("start");
if (!disableResponse) protocol.msg(ctx, "instance/open", { instanceUuid });
} catch (err: any) {
if (!disableResponse) {
@@ -242,7 +237,7 @@ routerApp.on("instance/stop", async (ctx, data) => {
const instance = InstanceSubsystem.getInstance(instanceUuid);
try {
if (!instance) throw new Error($t("TXT_CODE_3bfb9e04"));
- await instance.exec(new StopCommand());
+ await instance.execPreset("stop");
//Note: Removing this reply will cause the front-end response to be slow, because the front-end will wait for the panel-side message to be forwarded
if (!disableResponse) protocol.msg(ctx, "instance/stop", { instanceUuid });
} catch (err: any) {
@@ -259,7 +254,7 @@ routerApp.on("instance/restart", async (ctx, data) => {
const instance = InstanceSubsystem.getInstance(instanceUuid);
try {
if (!instance) throw new Error($t("TXT_CODE_3bfb9e04"));
- await instance.exec(new RestartCommand());
+ await instance.execPreset("restart");
if (!disableResponse) protocol.msg(ctx, "instance/restart", { instanceUuid });
} catch (err: any) {
if (!disableResponse)
@@ -275,7 +270,7 @@ routerApp.on("instance/kill", async (ctx, data) => {
const instance = InstanceSubsystem.getInstance(instanceUuid);
if (!instance) continue;
try {
- await instance.forceExec(new KillCommand());
+ await instance.execPreset("kill");
if (!disableResponse) protocol.msg(ctx, "instance/kill", { instanceUuid });
} catch (err: any) {
if (!disableResponse)
@@ -292,7 +287,7 @@ routerApp.on("instance/command", async (ctx, data) => {
const instance = InstanceSubsystem.getInstance(instanceUuid);
try {
if (!instance) throw new Error($t("TXT_CODE_3bfb9e04"));
- await instance.exec(new SendCommand(command));
+ await instance.execPreset("command", command);
if (!disableResponse) protocol.msg(ctx, "instance/command", { instanceUuid });
} catch (err: any) {
if (!disableResponse)
diff --git a/daemon/src/routers/http_router.ts b/daemon/src/routers/http_router.ts
index d6279a11..90d27499 100755
--- a/daemon/src/routers/http_router.ts
+++ b/daemon/src/routers/http_router.ts
@@ -31,7 +31,7 @@ router.get("/download/:key/:fileName", async (ctx) => {
if (!FileManager.checkFileName(paramsFileName))
throw new Error($t("TXT_CODE_http_router.fileNameNotSpec"));
- const cwd = instance.config.cwd;
+ const cwd = instance.absoluteCwdPath();
const fileRelativePath = mission.parameter.fileName;
// Check for file cross-directory security risks
@@ -65,7 +65,7 @@ router.post("/upload/:key", async (ctx) => {
const instance = InstanceSubsystem.getInstance(mission.parameter.instanceUuid);
if (!instance) throw new Error("Access denied: No instance found");
const uploadDir = mission.parameter.uploadDir;
- const cwd = instance.config.cwd;
+ const cwd = instance.absoluteCwdPath();
const tmpFiles = ctx.request.files?.file;
if (tmpFiles) {
let uploadedFile: formidable.File;
@@ -120,7 +120,7 @@ router.post("/upload/:key", async (ctx) => {
});
if (unzip) {
- const fileManager = new FileManager(instance.config.cwd);
+ const fileManager = new FileManager(instance.absoluteCwdPath());
fileManager.unzip(fileSaveAbsolutePath, "", zipCode);
}
ctx.body = "OK";
diff --git a/daemon/src/routers/stream_router.ts b/daemon/src/routers/stream_router.ts
index d13333eb..49d290d9 100755
--- a/daemon/src/routers/stream_router.ts
+++ b/daemon/src/routers/stream_router.ts
@@ -7,7 +7,6 @@ import {
streamLoginSuccessful
} from "../service/mission_passport";
import InstanceSubsystem from "../service/system_instance";
-import SendCommand from "../entity/commands/cmd";
import { IGNORE } from "../const";
// Authorization authentication middleware
@@ -79,7 +78,7 @@ routerApp.on("stream/input", async (ctx, data) => {
const command = data.command;
const instanceUuid = ctx.session?.stream?.instanceUuid;
const instance = InstanceSubsystem.getInstance(instanceUuid);
- await instance?.exec(new SendCommand(command));
+ await instance?.execPreset("command", command);
} catch (error: any) {
// Ignore potential high frequency exceptions here
}
diff --git a/daemon/src/service/async_task_service/quick_install.ts b/daemon/src/service/async_task_service/quick_install.ts
index 3eb58956..476f3322 100644
--- a/daemon/src/service/async_task_service/quick_install.ts
+++ b/daemon/src/service/async_task_service/quick_install.ts
@@ -57,7 +57,9 @@ export class QuickInstallTask extends AsyncTask {
const url = new URL(this.targetLink);
downloadFileName = url.pathname.split("/").pop() || `application${this.extName}`;
}
- this.filePath = path.normalize(path.join(this.instance.config.cwd, downloadFileName));
+ this.filePath = path.normalize(
+ path.join(this.instance.absoluteCwdPath(), downloadFileName)
+ );
this.writeStream = fs.createWriteStream(this.filePath);
const response = await axios({
url: this.targetLink,
@@ -109,7 +111,7 @@ export class QuickInstallTask extends AsyncTask {
let startCommand = this.instance.config.startCommand;
const ENV_MAP: IJsonData = {
java: "java",
- cwd: this.instance.config.cwd,
+ cwd: this.instance.absoluteCwdPath(),
rconIp: this.instance.config.rconIp || "localhost",
rconPort: String(this.instance.config.rconPort),
rconPassword: this.instance.config.rconPassword,
diff --git a/daemon/src/service/docker_process_service.ts b/daemon/src/service/docker_process_service.ts
index 64cbee7e..a474cdf1 100644
--- a/daemon/src/service/docker_process_service.ts
+++ b/daemon/src/service/docker_process_service.ts
@@ -12,6 +12,7 @@ import { EventEmitter } from "stream";
import { IInstanceProcess } from "../entity/instance/interface";
import { AsyncTask } from "./async_task_service";
import iconv from "iconv-lite";
+import { toText } from "common";
// Error exception at startup
export class StartupDockerProcessError extends Error {
@@ -51,8 +52,6 @@ export class SetupDockerContainer extends AsyncTask {
commandList = [];
}
- const cwd = instance.absoluteCwdPath();
-
// Parsing port open
// 25565:25565/tcp 8080:8080/tcp
const portMap = instance.config.docker.ports || [];
@@ -119,7 +118,18 @@ export class SetupDockerContainer extends AsyncTask {
const workingDir = instance.config.docker.workingDir ?? "";
- // output startup log
+ let cwd = instance.absoluteCwdPath();
+ const hostRealPath = toText(process.env.MCSM_DOCKER_WORKSPACE_PATH);
+ if (hostRealPath) {
+ cwd = path.normalize(path.join(hostRealPath, instance.instanceUuid));
+ }
+
+ if (workingDir) {
+ instance.println("CONTAINER", $t("TXT_CODE_e76e49e9") + cwd + " --> " + workingDir + "\n");
+ } else {
+ instance.println("CONTAINER", $t("TXT_CODE_ffa884f9"));
+ }
+
logger.info("----------------");
logger.info(`[SetupDockerContainer]`);
logger.info(`UUID: [${instance.instanceUuid}] [${instance.config.nickname}]`);
@@ -130,7 +140,7 @@ export class SetupDockerContainer extends AsyncTask {
logger.info(`OPEN_PORT: ${JSON.stringify(publicPortArray)}`);
logger.info(
`BINDS: ${JSON.stringify([
- workingDir ? `${cwd}->${workingDir}` : "",
+ workingDir ? `${cwd} --> ${workingDir}` : "",
...extraBinds
])}`
);
@@ -139,6 +149,22 @@ export class SetupDockerContainer extends AsyncTask {
logger.info(`TYPE: Docker Container`);
logger.info("----------------");
+ const mounts: Docker.MountConfig =
+ extraBinds.map((v) => {
+ return {
+ Type: "bind",
+ Source: instance.parseTextParams(v.hostPath),
+ Target: instance.parseTextParams(v.containerPath)
+ };
+ }) || [];
+ if (workingDir && cwd) {
+ mounts.push({
+ Type: "bind",
+ Source: cwd,
+ Target: workingDir
+ });
+ }
+
// Start Docker container creation and running
const docker = new DefaultDocker();
this.container = await docker.createContainer({
@@ -163,20 +189,7 @@ export class SetupDockerContainer extends AsyncTask {
CpuQuota: cpuQuota,
PortBindings: publicPortArray,
NetworkMode: instance.config.docker.networkMode,
- Mounts: [
- {
- Type: "bind",
- Source: cwd,
- Target: workingDir
- },
- ...extraBinds.map((v) => {
- return {
- Type: "bind" as Docker.MountType,
- Source: v.hostPath,
- Target: v.containerPath
- };
- })
- ]
+ Mounts: mounts
},
NetworkingConfig: {
EndpointsConfig: {
diff --git a/daemon/src/service/file_router_service.ts b/daemon/src/service/file_router_service.ts
index fd525654..d29216f5 100755
--- a/daemon/src/service/file_router_service.ts
+++ b/daemon/src/service/file_router_service.ts
@@ -10,8 +10,7 @@ export function getFileManager(instanceUuid: string) {
if (!instance)
throw new Error($t("TXT_CODE_file_router_service.instanceNotExit", { uuid: instanceUuid }));
const fileCode = instance.config?.fileCode;
- const cwd = instance.config.cwd;
- return new FileManager(cwd, fileCode);
+ return new FileManager(instance.absoluteCwdPath(), fileCode);
}
let cacheDisks: string[] = [];
diff --git a/daemon/src/service/instance_update_action.ts b/daemon/src/service/instance_update_action.ts
index 1caca883..1a3287f9 100644
--- a/daemon/src/service/instance_update_action.ts
+++ b/daemon/src/service/instance_update_action.ts
@@ -19,8 +19,7 @@ export class InstanceUpdateAction extends AsyncTask {
}
public async onStart() {
- let updateCommand = this.instance.config.updateCommand;
- updateCommand = updateCommand.replace(/\{mcsm_workspace\}/gm, this.instance.config.cwd);
+ const updateCommand = this.instance.parseTextParams(this.instance.config.updateCommand);
logger.info(
$t("TXT_CODE_general_update.readyUpdate", { instanceUuid: this.instance.instanceUuid })
);
@@ -29,6 +28,7 @@ export class InstanceUpdateAction extends AsyncTask {
);
logger.info(updateCommand);
+ this.instance.print("\n");
this.instance.println(
$t("TXT_CODE_general_update.update"),
$t("TXT_CODE_general_update.readyUpdate", { instanceUuid: this.instance.instanceUuid })
@@ -57,7 +57,7 @@ export class InstanceUpdateAction extends AsyncTask {
// start the update command
const process = spawn(commandExeFile, commandParameters, {
- cwd: this.instance.config.cwd,
+ cwd: this.instance.absoluteCwdPath(),
stdio: "pipe",
windowsHide: true
});
diff --git a/daemon/src/service/system_instance.ts b/daemon/src/service/system_instance.ts
index e98b8388..b29fb4e7 100755
--- a/daemon/src/service/system_instance.ts
+++ b/daemon/src/service/system_instance.ts
@@ -4,7 +4,6 @@ import path from "path";
import os from "os";
import Instance from "../entity/instance/instance";
import EventEmitter from "events";
-import KillCommand from "../entity/commands/kill";
import logger from "./log";
import { v4 } from "uuid";
import { Socket } from "socket.io";
@@ -45,7 +44,7 @@ class InstanceSubsystem extends EventEmitter {
this.instances.forEach((instance) => {
if (instance.config.eventTask.autoStart) {
instance
- .exec(new StartCommand())
+ .execPreset("start")
.then(() => {
logger.info(
$t("TXT_CODE_system_instance.autoStart", {
@@ -180,7 +179,7 @@ class InstanceSubsystem extends EventEmitter {
this.instances.delete(instanceUuid);
StorageSubsystem.delete("InstanceConfig", instanceUuid);
InstanceControl.deleteInstanceAllTask(instanceUuid);
- if (deleteFile) fs.remove(instance.config.cwd, (err) => {});
+ if (deleteFile) fs.remove(instance.absoluteCwdPath(), (err) => {});
return true;
}
throw new Error($t("TXT_CODE_3bfb9e04"));
@@ -228,7 +227,7 @@ class InstanceSubsystem extends EventEmitter {
`Instance ${instance.config.nickname} (${instance.instanceUuid}) is running or busy, and is being forced to end.`
);
promises.push(
- instance.execCommand(new KillCommand()).then(() => {
+ instance.execPreset("kill").then(() => {
if (!this.isGlobalInstance(instance))
StorageSubsystem.store("InstanceConfig", instance.instanceUuid, instance.config);
logger.info(
diff --git a/daemon/src/service/system_instance_control.ts b/daemon/src/service/system_instance_control.ts
index 10ccbb2c..a2c8b10d 100755
--- a/daemon/src/service/system_instance_control.ts
+++ b/daemon/src/service/system_instance_control.ts
@@ -3,11 +3,6 @@ import schedule from "node-schedule";
import InstanceSubsystem from "./system_instance";
import StorageSubsystem from "../common/system_storage";
import logger from "./log";
-import StartCommand from "../entity/commands/start";
-import StopCommand from "../entity/commands/stop";
-import SendCommand from "../entity/commands/cmd";
-import RestartCommand from "../entity/commands/restart";
-import KillCommand from "../entity/commands/kill";
import FileManager from "./system_file";
// Scheduled task configuration item interface
@@ -170,26 +165,26 @@ class InstanceControlSubsystem {
// logger.info(`Execute scheduled task: ${task.name} ${task.action} ${task.time} ${task.count} `);
if (task.action === "start") {
if (instanceStatus === 0) {
- return await instance.exec(new StartCommand("ScheduleJob"));
+ return await instance.execPreset("start");
}
}
if (task.action === "stop") {
if (instanceStatus === 3) {
- return await instance.exec(new StopCommand());
+ return await instance.execPreset("stop");
}
}
if (task.action === "restart") {
if (instanceStatus === 3) {
- return await instance.exec(new RestartCommand());
+ return await instance.execPreset("restart");
}
}
if (task.action === "command") {
if (instanceStatus === 3) {
- return await instance.exec(new SendCommand(payload));
+ return await instance.execPreset("command", payload);
}
}
if (task.action === "kill") {
- return await instance.exec(new KillCommand());
+ return await instance.execPreset("kill");
}
} catch (error: any) {
logger.error(
diff --git a/daemon/src/service/ui.ts b/daemon/src/service/ui.ts
deleted file mode 100755
index e60fc51f..00000000
--- a/daemon/src/service/ui.ts
+++ /dev/null
@@ -1,118 +0,0 @@
-import { $t } from "../i18n";
-import readline from "readline";
-
-import * as protocol from "./protocol";
-import InstanceSubsystem from "./system_instance";
-import { globalConfiguration } from "../entity/config";
-import logger from "./log";
-import StartCommand from "../entity/commands/start";
-import StopCommand from "../entity/commands/stop";
-import KillCommand from "../entity/commands/kill";
-import SendCommand from "../entity/commands/cmd";
-
-const rl = readline.createInterface({
- input: process.stdin,
- output: process.stdout
-});
-
-console.log($t("TXT_CODE_ui.help"));
-
-function stdin() {
- rl.question("> ", async (answer) => {
- try {
- const cmds = answer.split(" ");
- logger.info(`[Terminal] ${answer}`);
- const result = await command(cmds[0], cmds[1], cmds[2], cmds[3]);
- if (result) console.log(result);
- else console.log(`Command ${answer} does not exist, type help to get help.`);
- } catch (err) {
- logger.error("[Terminal]", err);
- } finally {
- // next
- stdin();
- }
- });
-}
-
-stdin();
-
-/**
- * Pass in relevant UI commands and output command results
- * @param {String} cmd
- * @return {String}
- */
-async function command(cmd: string, p1: string, p2: string, p3: string) {
- if (cmd === "instance") {
- if (p1 === "start") {
- InstanceSubsystem?.getInstance(p2)?.exec(new StartCommand("Terminal"));
- return "Done.";
- }
- if (p1 === "stop") {
- InstanceSubsystem?.getInstance(p2)?.exec(new StopCommand());
- return "Done.";
- }
- if (p1 === "kill") {
- InstanceSubsystem?.getInstance(p2)?.exec(new KillCommand());
- return "Done.";
- }
- if (p1 === "send") {
- InstanceSubsystem?.getInstance(p2)?.exec(new SendCommand(p3));
- return "Done.";
- }
- return "Parameter error";
- }
-
- if (cmd === "instances") {
- const objs = InstanceSubsystem.instances;
- let result = "instance name | instance UUID | status code\n";
- objs.forEach((v) => {
- result += `${v.config.nickname} ${v.instanceUuid} ${v.status()}\n`;
- });
- result += "\nStatus Explanation:\n Busy=-1;Stop=0;Stopping=1;Starting=2;Running=3;\n";
- return result;
- }
-
- if (cmd === "sockets") {
- const sockets = protocol.socketObjects();
- let result = "IP address | identifier\n";
- sockets.forEach((v) => {
- result += `${v.handshake.address} ${v.id}\n`;
- });
- result += `Total ${sockets.size} online.\n`;
- return result;
- }
-
- if (cmd == "key") {
- return globalConfiguration.config.key;
- }
-
- if (cmd == "exit") {
- try {
- logger.info("Preparing to shut down the daemon...");
- await InstanceSubsystem.exit();
- // logger.info("Data saved, thanks for using, goodbye!");
- logger.info("The data is saved, thanks for using, goodbye!");
- logger.info("closed.");
- process.exit(0);
- } catch (err) {
- logger.error(
- "Failed to end the program. Please check the file permissions and try again. If you still can't close it, please use Ctrl+C to close.",
- err
- );
- }
- }
-
- if (cmd == "help") {
- console.log("----------- Help document -----------");
- console.log(" instances view all instances");
- console.log(" Sockets view all linkers");
- console.log(" key view key");
- console.log(" exit to close this program (recommended method)");
- console.log(" instance start to start the specified instance");
- console.log(" instance stop to start the specified instance");
- console.log(" instance kill to start the specified instance");
- console.log(" instance send to send a command to the instance");
- console.log("----------- Help document -----------");
- return "\n";
- }
-}
diff --git a/dockerfile/daemon.dockerfile b/dockerfile/daemon.dockerfile
new file mode 100644
index 00000000..3909115b
--- /dev/null
+++ b/dockerfile/daemon.dockerfile
@@ -0,0 +1,36 @@
+ARG EMBEDDED_JAVA_VERSION=21
+ARG BUILDPLATFORM=linux/amd64
+
+FROM --platform=${BUILDPLATFORM} node:lts-alpine AS builder
+
+WORKDIR /src
+COPY . /src
+
+RUN apk add --no-cache wget &&\
+ chmod a+x ./install-dependents.sh &&\
+ chmod a+x ./build.sh &&\
+ ./install-dependents.sh &&\
+ ./build.sh &&\
+ wget --input-file=lib-urls.txt --directory-prefix=production-code/daemon/lib/ &&\
+ chmod a+x production-code/daemon/lib/*
+
+FROM eclipse-temurin:${EMBEDDED_JAVA_VERSION}-jdk
+
+ARG DEBIAN_FRONTEND=noninteractive
+RUN apt-get update && apt-get install -y curl &&\
+ curl -fsSL https://deb.nodesource.com/setup_20.x | bash &&\
+ apt-get update && apt-get install -y nodejs && apt-get clean
+
+WORKDIR /opt/mcsmanager/daemon
+
+COPY --from=builder /src/production-code/daemon/ /opt/mcsmanager/daemon/
+
+RUN npm install --production
+
+EXPOSE 24444
+
+ENV MCSM_INSTANCES_BASE_PATH=/opt/mcsmanager/daemon/data/InstanceData
+
+VOLUME ["/opt/mcsmanager/daemon/data", "/opt/mcsmanager/daemon/logs"]
+
+CMD [ "node", "app.js", "--max-old-space-size=8192" ]
\ No newline at end of file
diff --git a/dockerfile/web.dockerfile b/dockerfile/web.dockerfile
new file mode 100644
index 00000000..b7aa8112
--- /dev/null
+++ b/dockerfile/web.dockerfile
@@ -0,0 +1,24 @@
+ARG BUILDPLATFORM=linux/amd64
+FROM --platform=${BUILDPLATFORM} node:lts-alpine AS builder
+
+WORKDIR /src
+COPY . /src
+
+RUN chmod a+x ./install-dependents.sh &&\
+ chmod a+x ./build.sh &&\
+ ./install-dependents.sh &&\
+ ./build.sh
+
+FROM node:lts-alpine
+
+WORKDIR /opt/mcsmanager/web
+
+COPY --from=builder /src/production-code/web/ /opt/mcsmanager/web/
+
+RUN npm install --production
+
+EXPOSE 23333
+
+VOLUME ["/opt/mcsmanager/web/data", "/opt/mcsmanager/web/logs"]
+
+CMD [ "app.js", "--max-old-space-size=8192" ]
diff --git a/example.docker-compose.yml b/example.docker-compose.yml
new file mode 100644
index 00000000..98c2595c
--- /dev/null
+++ b/example.docker-compose.yml
@@ -0,0 +1,24 @@
+# docker-compose.yml
+services:
+ web:
+ image: githubyumao/mcsmanager-web:latest
+ ports:
+ - "23333:23333"
+ volumes:
+ - /etc/localtime:/etc/localtime:ro
+ - /web/data:/opt/mcsmanager/web/data
+ - /web/logs:/opt/mcsmanager/web/logs
+
+ daemon:
+ image: githubyumao/mcsmanager-daemon:latest
+ restart: unless-stopped
+ network_mode: host # if you want run instance in daemon container
+ # ports:
+ # - "24444:24444"
+ environment:
+ - MCSM_DOCKER_WORKSPACE_PATH=/daemon/data/InstanceData
+ volumes:
+ - /etc/localtime:/etc/localtime:ro
+ - /daemon/data:/opt/mcsmanager/daemon/data
+ - /daemon/logs:/opt/mcsmanager/daemon/logs
+ - /var/run/docker.sock:/var/run/docker.sock
diff --git a/frontend/src/components/TerminalCore.vue b/frontend/src/components/TerminalCore.vue
index 824fc4e2..cde1e2c1 100644
--- a/frontend/src/components/TerminalCore.vue
+++ b/frontend/src/components/TerminalCore.vue
@@ -2,7 +2,7 @@
import { onMounted, ref } from "vue";
import { t } from "@/lang/i18n";
import { CodeOutlined, DeleteOutlined, LoadingOutlined } from "@ant-design/icons-vue";
-import { useTerminal } from "../hooks/useTerminal";
+import { encodeConsoleColor, useTerminal } from "../hooks/useTerminal";
import { getInstanceOutputLog } from "@/services/apis/instance";
import { message } from "ant-design-vue";
import connectErrorImage from "@/assets/daemon_connection_error.png";
@@ -29,7 +29,7 @@ const {
clickHistoryItem
} = useCommandHistory();
-const { execute, initTerminalWindow, sendCommand, events, isConnect, socketAddress } =
+const { execute, initTerminalWindow, sendCommand, state, events, isConnect, socketAddress } =
useTerminal();
const instanceId = props.instanceId;
@@ -64,7 +64,14 @@ const initTerminal = async () => {
const { value } = await getInstanceOutputLog().execute({
params: { uuid: instanceId || "", daemonId: daemonId || "" }
});
- if (value) term.write(value);
+
+ if (value) {
+ if (state.value?.config?.terminalOption?.haveColor) {
+ term.write(encodeConsoleColor(value));
+ } else {
+ term.write(value);
+ }
+ }
} catch (error: any) {}
return term;
}
diff --git a/frontend/src/components/fc/KvOptionsDialog.vue b/frontend/src/components/fc/KvOptionsDialog.vue
index 5e7c29d9..d5b762e7 100644
--- a/frontend/src/components/fc/KvOptionsDialog.vue
+++ b/frontend/src/components/fc/KvOptionsDialog.vue
@@ -14,6 +14,7 @@ interface Props extends MountComponent {
keyTitle?: string;
valueTitle?: string;
data: any[];
+ subTitle?: string;
columns?: AntColumnsType[];
}
@@ -100,6 +101,10 @@ const operation = (type: "add" | "del", index = 0) => {
@cancel="cancel"
>
+
+
+
+
{{ t("TXT_CODE_dfc17a0c") }}
@@ -132,7 +137,7 @@ const operation = (type: "add" | "del", index = 0) => {
>
diff --git a/frontend/src/components/fc/index.ts b/frontend/src/components/fc/index.ts
index c65c27f4..d2e40623 100644
--- a/frontend/src/components/fc/index.ts
+++ b/frontend/src/components/fc/index.ts
@@ -65,17 +65,20 @@ export async function usePortEditDialog(data: PortConfigItem[] = []) {
{
align: "center",
dataIndex: "host",
- title: t("TXT_CODE_534db0b2")
+ title: t("TXT_CODE_534db0b2"),
+ placeholder: "eg: 8080"
},
{
align: "center",
dataIndex: "container",
- title: t("TXT_CODE_b729d2e")
+ title: t("TXT_CODE_b729d2e"),
+ placeholder: "eg: 25565"
},
{
align: "center",
dataIndex: "protocol",
- title: t("TXT_CODE_ad1c674c")
+ title: t("TXT_CODE_ad1c674c"),
+ placeholder: "tcp/udp"
}
] as AntColumnsType[]
}).mount(KvOptionsDialogVue)) || []
@@ -86,6 +89,7 @@ export async function useVolumeEditDialog(data: DockerConfigItem[] = []) {
return (
(await useMountComponent({
data,
+ subTitle: t("TXT_CODE_6c232c9c"),
title: t("TXT_CODE_820ebc92"),
columns: [
{
diff --git a/frontend/src/hooks/useFileManager.ts b/frontend/src/hooks/useFileManager.ts
index d767275c..f4a75ac1 100644
--- a/frontend/src/hooks/useFileManager.ts
+++ b/frontend/src/hooks/useFileManager.ts
@@ -500,12 +500,17 @@ export const useFileManager = (instanceId?: string, daemonId?: string) => {
const handleTableChange = (e: { current: number; pageSize: number }) => {
selectedRowKeys.value = [];
selectionData.value = [];
- operationForm.value.name = "";
+ // operationForm.value.name = "";
operationForm.value.current = e.current;
operationForm.value.pageSize = e.pageSize;
getFileList();
};
+ const handleSearchChange = () =>{
+ operationForm.value.current = 1;
+ getFileList();
+ }
+
const getFileStatus = async () => {
const { state, execute } = getFileStatusApi();
try {
@@ -627,6 +632,7 @@ export const useFileManager = (instanceId?: string, daemonId?: string) => {
downloadFile,
handleChangeDir,
handleTableChange,
+ handleSearchChange,
getFileStatus,
changePermission,
toDisk,
diff --git a/frontend/src/hooks/useSocketIo.ts b/frontend/src/hooks/useSocketIo.ts
index 3a047185..37c89701 100644
--- a/frontend/src/hooks/useSocketIo.ts
+++ b/frontend/src/hooks/useSocketIo.ts
@@ -1,9 +1,49 @@
import { io } from "socket.io-client";
import type { Socket } from "socket.io-client";
import type { DefaultEventsMap } from "@socket.io/component-emitter";
+import type { ComputedNodeInfo } from "./useOverviewInfo";
+import { ref } from "vue";
+import { removeTrail } from "@/tools/string";
+
+// eslint-disable-next-line no-unused-vars
+export enum SocketStatus {
+ // eslint-disable-next-line no-unused-vars
+ Connected = 1,
+ // eslint-disable-next-line no-unused-vars
+ Connecting = 2,
+ // eslint-disable-next-line no-unused-vars
+ Error = 0
+}
export function useSocketIoClient() {
let socket: Socket | undefined;
+ const socketStatus = ref(SocketStatus.Connecting);
+ const parseIp = (ip: string) => {
+ if (ip.toLowerCase() === "localhost" || ip === "127.0.0.1") {
+ return window.location.hostname;
+ }
+ return ip;
+ };
+
+ const testFrontendSocket = async (remoteNode?: Partial) => {
+ const nodeCfg = remoteNode;
+
+ if (!nodeCfg?.available || !nodeCfg.ip) {
+ socketStatus.value = SocketStatus.Error;
+ } else {
+ try {
+ socketStatus.value = SocketStatus.Connecting;
+ await testConnect(
+ parseIp(nodeCfg.ip) + ":" + nodeCfg.port,
+ removeTrail(nodeCfg.prefix || "", "/") + "/socket.io"
+ );
+ socketStatus.value = SocketStatus.Connected;
+ } catch (error) {
+ console.error("Socket error: ", error);
+ socketStatus.value = SocketStatus.Error;
+ }
+ }
+ };
const testConnect = (addr: string, path: string) => {
socket = io(addr, {
@@ -32,5 +72,5 @@ export function useSocketIoClient() {
});
};
- return { testConnect };
+ return { testConnect, parseIp, testFrontendSocket, socketStatus };
}
diff --git a/frontend/src/tools/version.ts b/frontend/src/tools/version.ts
new file mode 100644
index 00000000..a03e91e9
--- /dev/null
+++ b/frontend/src/tools/version.ts
@@ -0,0 +1,10 @@
+export function hasVersionUpdate(oldVersion?: string, newVersion?: string) {
+ if (!oldVersion || !newVersion) return true;
+
+ // eslint-disable-next-line no-unused-vars
+ const [oldMajor, oldMinor, _oldPatch] = oldVersion.split(".");
+ // eslint-disable-next-line no-unused-vars
+ const [newMajor, newMinor, _newPatch] = newVersion.split(".");
+
+ return oldMajor !== newMajor || oldMinor !== newMinor;
+}
diff --git a/frontend/src/widgets/InstanceList.vue b/frontend/src/widgets/InstanceList.vue
index f50fba19..a985222c 100644
--- a/frontend/src/widgets/InstanceList.vue
+++ b/frontend/src/widgets/InstanceList.vue
@@ -500,7 +500,7 @@ onMounted(async () => {
:target-instance-info="item"
:target-daemon-id="currentRemoteNode?.uuid"
@click="handleSelectInstance(item)"
- @refrsh-list="initInstancesData()"
+ @refresh-list="initInstancesData()"
/>
diff --git a/frontend/src/widgets/instance/BaseInfo.vue b/frontend/src/widgets/instance/BaseInfo.vue
index 2d039751..b866e92a 100644
--- a/frontend/src/widgets/instance/BaseInfo.vue
+++ b/frontend/src/widgets/instance/BaseInfo.vue
@@ -99,10 +99,16 @@ onMounted(async () => {
- {{ t("TXT_CODE_855c4a1c") }}{{ instanceGameServerInfo.players }}
+ {{ t("TXT_CODE_855c4a1c") }}
+ {{ instanceGameServerInfo.players }}
- {{ t("TXT_CODE_e260a220") }}{{ instanceGameServerInfo.version }}
+
+ {{ t("TXT_CODE_e260a220") }}
+
+
+ {{ instanceGameServerInfo.version }}
+
diff --git a/frontend/src/widgets/instance/FileManager.vue b/frontend/src/widgets/instance/FileManager.vue
index ec6d7879..5631251d 100644
--- a/frontend/src/widgets/instance/FileManager.vue
+++ b/frontend/src/widgets/instance/FileManager.vue
@@ -70,6 +70,7 @@ const {
beforeUpload,
downloadFile,
handleChangeDir,
+ handleSearchChange,
selectedFile,
rowClickTable,
handleTableChange,
@@ -383,7 +384,7 @@ onUnmounted(() => {
v-model:value.trim.lazy="operationForm.name"
:placeholder="t('TXT_CODE_7cad42a5')"
allow-clear
- @change="getFileList()"
+ @change="handleSearchChange()"
>
diff --git a/frontend/src/widgets/instance/Shortcut.vue b/frontend/src/widgets/instance/Shortcut.vue
index b407cc66..0ad6172b 100644
--- a/frontend/src/widgets/instance/Shortcut.vue
+++ b/frontend/src/widgets/instance/Shortcut.vue
@@ -32,6 +32,7 @@ import { arrayFilter } from "@/tools/array";
import { useLayoutContainerStore } from "@/stores/useLayoutContainerStore";
import { reportErrorMsg } from "@/tools/validator";
import { openInstanceTagsEditor } from "@/components/fc/index";
+import _ from "lodash";
const props = defineProps<{
card: LayoutCard;
@@ -39,7 +40,7 @@ const props = defineProps<{
targetDaemonId?: string;
}>();
-const emits = defineEmits(["refrshList"]);
+const emits = defineEmits(["refreshList"]);
const { containerState } = useLayoutContainerStore();
const { getMetaOrRouteValue } = useLayoutCardTools(props.card);
@@ -69,7 +70,7 @@ const { isLoading: updateLoading, execute: executeUpdate } = updateInstance();
const refreshList = () => {
setTimeout(() => {
- emits("refrshList");
+ emits("refreshList");
}, 500);
};
@@ -194,7 +195,8 @@ const instanceOperations = computed(() =>
},
loading: killLoading.value,
disabled: containerState.isDesignMode,
- danger: true
+ danger: true,
+ condition: () => !isStopped.value
},
{
area: true
@@ -206,8 +208,8 @@ const instanceOperations = computed(() =>
event.stopPropagation();
if (instanceId && daemonId) {
const tags = instanceInfo.value?.config.tag || [];
- await openInstanceTagsEditor(instanceId, daemonId, tags);
- refreshList();
+ const newTags = await openInstanceTagsEditor(instanceId, daemonId, tags);
+ if (!_.isEqual(newTags, tags)) refreshList();
}
},
disabled: containerState.isDesignMode
diff --git a/frontend/src/widgets/instance/dialogs/InstanceDetail.vue b/frontend/src/widgets/instance/dialogs/InstanceDetail.vue
index ea29c1f0..d7f6d68d 100644
--- a/frontend/src/widgets/instance/dialogs/InstanceDetail.vue
+++ b/frontend/src/widgets/instance/dialogs/InstanceDetail.vue
@@ -1,6 +1,6 @@
diff --git a/languages/de_DE.json b/languages/de_DE.json
index 4248871e..9cc766ea 100644
--- a/languages/de_DE.json
+++ b/languages/de_DE.json
@@ -364,7 +364,6 @@
"TXT_CODE_40ee4eaf": "Parallelität erzeugen",
"TXT_CODE_413b9c01": "Knotenumgebungsversion",
"TXT_CODE_41406a5f": "Datenkarte",
- "TXT_CODE_41763172": "Dieser Systembefehl wird ausgeführt, wenn der Benutzer einen Update-/Installationsvorgang durchführt.",
"TXT_CODE_41d79430": "Geeignet für die Serverbetriebsumgebung MC Bedrock Edition oder andere Linux-Programme",
"TXT_CODE_41dd4d19": "Offizielle Website",
"TXT_CODE_42036f92": "------Benutzeranmeldeereignis ------",
@@ -1917,7 +1916,7 @@
"TXT_CODE_40241d8e": "Erwerb des Minecraft-Status",
"TXT_CODE_57d1929e": "MCSManager verwendet das Minecraft-Ping-Protokoll, um die Anzahl der Personen, die auf dem Server online sind, die Version, die Latenz und andere Informationen zu ermitteln.",
"TXT_CODE_6b175558": "Nach Abschluss der Konfiguration wird der Serverstatus jede Minute aktualisiert, wenn die Instanz ausgeführt wird.",
- "TXT_CODE_e260a220": "Spielversion:",
+ "TXT_CODE_e260a220": "Spielversion: ",
"TXT_CODE_13411df7": "Die SSO-Anmeldung ist fehlgeschlagen. Bitte kehren Sie zur ursprünglichen Seite zurück und versuchen Sie erneut, sich anzumelden!",
"TXT_CODE_2082f659": "Hinweis: Ist dieser leer, werden die Dateien in der Dateiverwaltung nicht an den Container gebunden!",
"TXT_CODE_2c1337d": "Liste der Tags für diese Instanz",
@@ -1931,7 +1930,7 @@
"TXT_CODE_6da85509": "Sind Sie sicher, dass Sie der Instanz einen Befehl zum Herunterfahren erteilen möchten?",
"TXT_CODE_7333c7f7": "Klar",
"TXT_CODE_78e88c3f": "Etikettengruppierung",
- "TXT_CODE_855c4a1c": "Anzahl Spieler:",
+ "TXT_CODE_855c4a1c": "Anzahl Spieler: ",
"TXT_CODE_893567ac": "Zweite Bestätigung",
"TXT_CODE_a2544278": "Instanzbezeichnung",
"TXT_CODE_a8b0dfab": "Führen Sie eine 2x1-Berechnung für diesen Wert durch, um den Schutzradius des Spawnpunkts zu bestimmen. Wenn Sie ihn auf 0 setzen, wird nur der Block unterhalb des Spawnpunkts geschützt. \nAb Version 1.5 deaktiviert die Einstellung auf 0 oder einen negativen Wert den Build-Schutz.",
@@ -1948,5 +1947,7 @@
"TXT_CODE_2fb14927": "Dieser Fehler bedeutet normalerweise, dass die Webseite keine direkte Verbindung zum Backend-Prozess herstellen kann. \nWenn Sie Portweiterleitung, Mapping, Reverse-Proxy haben, überprüfen Sie bitte die Konfiguration!",
"TXT_CODE_46c48969": "Anscheinend versuchen Sie, HTTPS zu verwenden, aber wir können über das WSS-Protokoll keine Verbindung zum Daemon herstellen. \nBitte versuchen Sie, über das WSS://-Protokoll eine Verbindung zum Daemon herzustellen.",
"TXT_CODE_9b3ce825": "Beschreibung des Reverse-Proxys",
- "TXT_CODE_d4c8fb3b": "Zugehörige Dokumente:"
+ "TXT_CODE_d4c8fb3b": "Zugehörige Dokumente: ",
+ "TXT_CODE_a6424dcc": "Das Arbeitsverzeichnis oder der Startbefehl darf nicht leer sein",
+ "TXT_CODE_ffa884f9": "Warnung: Das Arbeitsverzeichnis ist nicht im Container gemountet und der Container kann nicht auf Dateien im Arbeitsverzeichnis der Instanz zugreifen"
}
diff --git a/languages/en_US.json b/languages/en_US.json
index 58bf0ec8..0dd98f51 100644
--- a/languages/en_US.json
+++ b/languages/en_US.json
@@ -380,7 +380,7 @@
"TXT_CODE_4ab6a0b5": "Browser Time",
"TXT_CODE_856bd2f3": "Temporary Banned IPs",
"TXT_CODE_da8f97a7": "Blocked Accesses",
- "TXT_CODE_190ecd56": "Linux Load",
+ "TXT_CODE_190ecd56": "Unix Load Average",
"TXT_CODE_77d038f7": "Panel Memory Usage",
"TXT_CODE_4df7e9bd": "Hostname",
"TXT_CODE_b4d8588": "OS Version",
@@ -1197,7 +1197,6 @@
"TXT_CODE_ee67e1a3": "Working Directory",
"TXT_CODE_962d9320": "The working directory where the instance runs. You can provide either an absolute or relative path.",
"TXT_CODE_bb0b9711": "Update or Install Command",
- "TXT_CODE_41763172": "This system command will be executed when the user performs an update/installation operation, {mcsm_workspace} represents the working directory.",
"TXT_CODE_f041de90": "File Encoding",
"TXT_CODE_6e69b5a5": "For file management functions.",
"TXT_CODE_fa920c0": "Expiration Time",
@@ -1723,7 +1722,7 @@
"TXT_CODE_434786c9": "After uploading a background image, the panel will switch to dark theme with a blurred translucent effect, you can switch back anytime.",
"TXT_CODE_7ceebc05": "Enter text content, support Markdown syntax, and can wrap lines.\n\nDon’t use other people’s copywriting easily, otherwise malicious code may be injected to attack you.",
"TXT_CODE_4b6e951": "Input",
- "TXT_CODE_61ca492b": "For Example:",
+ "TXT_CODE_61ca492b": "For Example: ",
"TXT_CODE_ae09d79d": "Upload",
"TXT_CODE_bb888626": "Output",
"TXT_CODE_1b1b2934": "Failed to send RCON command due to incorrect password, please check the access password set in the Steam Rcon protocol!",
@@ -1736,8 +1735,8 @@
"TXT_CODE_cc561947": "Scan the QR code using Google Authentication or other universal 2FA tools.",
"TXT_CODE_af2a6972": "Click the 'I have scanned' button.",
"TXT_CODE_b0a18c20": "I have scanned",
- "TXT_CODE_30051f9b": "Instance ID:",
- "TXT_CODE_5f2d2e30": "Node ID:",
+ "TXT_CODE_30051f9b": "Instance ID: ",
+ "TXT_CODE_5f2d2e30": "Node ID: ",
"TXT_CODE_8f47d95": "Saved via shortcut keys!",
"TXT_CODE_282b0721": "Steam Rcon Protocol Settings",
"TXT_CODE_32d87bf1": "Most Steam game servers require RCON protocol to support command execution, including Minecraft game servers. If you find that the built-in console cannot execute commands when running a Steam game server, try configuring RCON protocol to support command execution.",
@@ -1911,14 +1910,14 @@
"TXT_CODE_4aaec75c": "Wrong request type, please try again!",
"TXT_CODE_903b6c50": "User does not exist, please try again",
"TXT_CODE_7e9727bd": "Players",
- "TXT_CODE_855c4a1c": "Players:",
- "TXT_CODE_e260a220": "Version:",
+ "TXT_CODE_855c4a1c": "Players: ",
+ "TXT_CODE_e260a220": "Version: ",
"TXT_CODE_40241d8e": "Minecraft Players Query",
"TXT_CODE_57d1929e": "MCSManager will use the \"Minecraft Ping protocol\" to try to obtain the number of online servers, versions, delays and other information.",
"TXT_CODE_6b175558": "After completing the configuration, the server status will be refreshed every minute when the instance is running.",
"TXT_CODE_2f59807a": "Server address",
"TXT_CODE_ddc2de99": "Default: localhost",
- "TXT_CODE_33a09033": "Players:",
+ "TXT_CODE_33a09033": "Players: ",
"TXT_CODE_ba717ff3": "You can freely design the layout, order and display of most elements on the web page.",
"TXT_CODE_bc46c15b": "Start Designing",
"TXT_CODE_3b24a247": "Click the Save button in the upper right corner to save the layout design.",
@@ -1948,5 +1947,13 @@
"TXT_CODE_9b3ce825": "Reverse proxy description",
"TXT_CODE_10cc2794": "Network architecture description",
"TXT_CODE_46c48969": "It looks like you are trying to use HTTPS, but we cannot connect to the daemon using the WSS protocol. Please try to connect to the daemon using the WSS:// protocol.",
- "TXT_CODE_2fb14927": "This error usually means that the web page cannot connect directly to the backend process. If you have port forwarding, mapping, reverse proxy, please check the configuration!"
+ "TXT_CODE_2fb14927": "This error usually means that the web page cannot connect directly to the backend process. If you have port forwarding, mapping, reverse proxy, please check the configuration!",
+ "TXT_CODE_a6424dcc": "The working directory or startup command cannot be empty",
+ "TXT_CODE_ffa884f9": "Warning: The working directory is not mounted to the container, and the container will not be able to access files in the instance's working directory",
+ "TXT_CODE_6c232c9c": "Used to mount additional folders on the host into the container, supports two variable strings: {mcsm_workspace} (working directory) and {mcsm_instance_id} (instance ID).",
+ "TXT_CODE_fa487a47": "Provide the following variable strings: {mcsm_instance_id} = instance ID, {mcsm_workspace} = instance installation directory",
+ "TXT_CODE_6259357c": "The program must run for more than 6 seconds before it can be forced to stop!",
+ "TXT_CODE_9d1d244f": "Program startup failed, input and output streams are unreadable: {{pipeName}}",
+ "TXT_CODE_ca030197": "The instance has other tasks in progress, and this operation cannot be performed!",
+ "TXT_CODE_aae2918f": "The instance process failed to start, please check the startup command and operating environment configuration!"
}
diff --git a/languages/es_ES.json b/languages/es_ES.json
index dd63b6a5..779fe595 100644
--- a/languages/es_ES.json
+++ b/languages/es_ES.json
@@ -364,7 +364,6 @@
"TXT_CODE_40ee4eaf": "producir paralelismo",
"TXT_CODE_413b9c01": "Versión del entorno de nodo",
"TXT_CODE_41406a5f": "tarjeta de datos",
- "TXT_CODE_41763172": "Este comando del sistema se ejecutará cuando el usuario realice una operación de actualización/instalación.",
"TXT_CODE_41d79430": "Adecuado para el entorno operativo del servidor MC Bedrock Edition u otros programas Linux",
"TXT_CODE_41dd4d19": "Página web oficial",
"TXT_CODE_42036f92": "------Evento de inicio de sesión de usuario ------",
@@ -1917,7 +1916,7 @@
"TXT_CODE_40241d8e": "Adquisición de estado de Minecraft",
"TXT_CODE_57d1929e": "MCSManager utilizará el protocolo Minecraft Ping para intentar obtener la cantidad de personas en línea en el servidor, la versión, la latencia y otra información.",
"TXT_CODE_6b175558": "Después de completar la configuración, cuando la instancia se está ejecutando, el estado del servidor se actualiza cada minuto.",
- "TXT_CODE_e260a220": "Versión del juego:",
+ "TXT_CODE_e260a220": "Versión del juego: ",
"TXT_CODE_13411df7": "Error al iniciar sesión en SSO. Vuelva a la página original e intente iniciar sesión nuevamente.",
"TXT_CODE_2082f659": "Nota: Si está vacío, los archivos en la administración de archivos no estarán vinculados al contenedor.",
"TXT_CODE_2c1337d": "Lista de etiquetas para esta instancia",
@@ -1931,7 +1930,7 @@
"TXT_CODE_6da85509": "¿Está seguro de que desea emitir un comando de apagado para la instancia?",
"TXT_CODE_7333c7f7": "Claro",
"TXT_CODE_78e88c3f": "Agrupación de etiquetas",
- "TXT_CODE_855c4a1c": "Número de jugadores:",
+ "TXT_CODE_855c4a1c": "Número de jugadores: ",
"TXT_CODE_893567ac": "Segunda confirmación",
"TXT_CODE_a2544278": "etiqueta de instancia",
"TXT_CODE_a8b0dfab": "Realice un cálculo de 2x 1 en este valor para determinar el radio de protección del punto de generación. Establecerlo en 0 solo protegerá el bloque debajo del punto de generación. \nA partir de la versión 1.5, establecer en 0 o un valor negativo deshabilita la protección de compilación.",
@@ -1948,5 +1947,7 @@
"TXT_CODE_2fb14927": "Este error generalmente significa que la página web no puede conectarse directamente al proceso backend. \nSi tiene reenvío de puertos, mapeo o proxy inverso, verifique la configuración.",
"TXT_CODE_46c48969": "Parece que está intentando utilizar HTTPS, pero no podemos conectarnos al demonio mediante el protocolo WSS. \nIntente conectarse al demonio utilizando el protocolo WSS://.",
"TXT_CODE_9b3ce825": "Descripción del proxy inverso",
- "TXT_CODE_d4c8fb3b": "Documentos relacionados:"
+ "TXT_CODE_d4c8fb3b": "Documentos relacionados: ",
+ "TXT_CODE_a6424dcc": "El directorio de trabajo o el comando de inicio no pueden estar vacíos",
+ "TXT_CODE_ffa884f9": "Advertencia: el directorio de trabajo no está montado en el contenedor y el contenedor no podrá acceder a los archivos en el directorio de trabajo de la instancia."
}
diff --git a/languages/fr_FR.json b/languages/fr_FR.json
index 7c5fafa0..0f10d88e 100644
--- a/languages/fr_FR.json
+++ b/languages/fr_FR.json
@@ -364,7 +364,6 @@
"TXT_CODE_40ee4eaf": "produire du parallélisme",
"TXT_CODE_413b9c01": "Version de l'environnement du nœud",
"TXT_CODE_41406a5f": "carte de données",
- "TXT_CODE_41763172": "Cette commande système sera exécutée lorsque l'utilisateur effectuera une opération de mise à jour/installation.",
"TXT_CODE_41d79430": "Convient à l'environnement d'exploitation du serveur MC Bedrock Edition ou à d'autres programmes Linux",
"TXT_CODE_41dd4d19": "Site officiel",
"TXT_CODE_42036f92": "------Événement de connexion utilisateur ------",
@@ -1931,7 +1930,7 @@
"TXT_CODE_6da85509": "Êtes-vous sûr de vouloir émettre une commande d'arrêt sur l'instance ?",
"TXT_CODE_7333c7f7": "Clair",
"TXT_CODE_78e88c3f": "Regroupement d'étiquettes",
- "TXT_CODE_855c4a1c": "Nombre de joueurs :",
+ "TXT_CODE_855c4a1c": "Nombre de joueurs: ",
"TXT_CODE_893567ac": "Deuxième confirmation",
"TXT_CODE_a2544278": "Étiquette d'instance",
"TXT_CODE_a8b0dfab": "Effectuez un calcul 2x 1 sur cette valeur pour déterminer le rayon de protection du point d'apparition. Le régler sur 0 ne protégera que le bloc situé en dessous du point d'apparition. \nÀ partir de la version 1.5, la définition sur 0 ou sur une valeur négative désactive la protection de build.",
@@ -1948,5 +1947,7 @@
"TXT_CODE_2fb14927": "Cette erreur signifie généralement que la page Web ne peut pas se connecter directement au processus backend. \nSi vous disposez d'une redirection de port, d'un mappage, d'un proxy inverse, veuillez vérifier la configuration !",
"TXT_CODE_46c48969": "Il semble que vous essayez d'utiliser HTTPS, mais nous ne parvenons pas à nous connecter au démon à l'aide du protocole WSS. \nVeuillez essayer de vous connecter au démon en utilisant le protocole WSS://.",
"TXT_CODE_9b3ce825": "Description du proxy inverse",
- "TXT_CODE_d4c8fb3b": "Documents associés :"
+ "TXT_CODE_d4c8fb3b": "Documents associés :",
+ "TXT_CODE_a6424dcc": "Le répertoire de travail ou la commande de démarrage ne peut pas être vide",
+ "TXT_CODE_ffa884f9": "Avertissement : Le répertoire de travail n'est pas monté sur le conteneur et le conteneur ne pourra pas accéder aux fichiers du répertoire de travail de l'instance."
}
diff --git a/languages/ja_JP.json b/languages/ja_JP.json
index 4f719f0b..fb8a818c 100644
--- a/languages/ja_JP.json
+++ b/languages/ja_JP.json
@@ -364,7 +364,6 @@
"TXT_CODE_40ee4eaf": "並列性を生み出す",
"TXT_CODE_413b9c01": "ノード環境のバージョン",
"TXT_CODE_41406a5f": "データカード",
- "TXT_CODE_41763172": "このシステム コマンドは、ユーザーがアップデート/インストール操作を実行するときに実行されます。",
"TXT_CODE_41d79430": "MC Bedrock Edition サーバーのオペレーティング環境またはその他の Linux プログラムに適しています",
"TXT_CODE_41dd4d19": "公式ウェブサイト",
"TXT_CODE_42036f92": "------ユーザーログインイベント ------",
@@ -1948,5 +1947,7 @@
"TXT_CODE_2fb14927": "このエラーは通常、Web ページがバックエンド プロセスに直接接続できないことを意味します。\nポートフォワーディング、マッピング、リバースプロキシを使用している場合は、設定を確認してください。",
"TXT_CODE_46c48969": "HTTPS を使用しようとしているようですが、WSS プロトコルを使用してデーモンに接続できません。 \nWSS:// プロトコルを使用してデーモンへの接続を試行してください。",
"TXT_CODE_9b3ce825": "リバースプロキシの説明",
- "TXT_CODE_d4c8fb3b": "関連文書:"
+ "TXT_CODE_d4c8fb3b": "関連文書:",
+ "TXT_CODE_a6424dcc": "作業ディレクトリまたは起動コマンドを空にすることはできません",
+ "TXT_CODE_ffa884f9": "警告: 作業ディレクトリがコンテナにマウントされていないため、コンテナはインスタンスの作業ディレクトリ内のファイルにアクセスできません"
}
diff --git a/languages/ko_KR.json b/languages/ko_KR.json
index d1eefbbb..f2e498d3 100644
--- a/languages/ko_KR.json
+++ b/languages/ko_KR.json
@@ -364,7 +364,6 @@
"TXT_CODE_40ee4eaf": "병렬성을 생성",
"TXT_CODE_413b9c01": "노드 환경 버전",
"TXT_CODE_41406a5f": "데이터 카드",
- "TXT_CODE_41763172": "이 시스템 명령은 사용자가 업데이트/설치 작업을 수행할 때 실행됩니다.",
"TXT_CODE_41d79430": "MC Bedrock Edition 서버 운영 환경 또는 기타 Linux 프로그램에 적합",
"TXT_CODE_41dd4d19": "공식 웹 사이트",
"TXT_CODE_42036f92": "------사용자 로그인 이벤트 ------",
@@ -1948,5 +1947,7 @@
"TXT_CODE_2fb14927": "이 오류는 일반적으로 웹페이지가 백엔드 프로세스에 직접 연결할 수 없음을 의미합니다. \n포트포워딩, 매핑, 리버스프록시가 있는 경우 구성을 확인해주세요!",
"TXT_CODE_46c48969": "HTTPS를 사용하려는 것 같지만 WSS 프로토콜을 사용하여 데몬에 연결할 수 없습니다. \nWSS:// 프로토콜을 사용하여 데몬에 연결해 보십시오.",
"TXT_CODE_9b3ce825": "역방향 프록시 설명",
- "TXT_CODE_d4c8fb3b": "관련 문서:"
+ "TXT_CODE_d4c8fb3b": "관련 문서:",
+ "TXT_CODE_a6424dcc": "작업 디렉터리 또는 시작 명령은 비워둘 수 없습니다.",
+ "TXT_CODE_ffa884f9": "경고: 작업 디렉터리가 컨테이너에 마운트되지 않았으므로 컨테이너는 인스턴스의 작업 디렉터리에 있는 파일에 액세스할 수 없습니다."
}
diff --git a/languages/pt_BR.json b/languages/pt_BR.json
index 3bf4ead8..69f59eaa 100644
--- a/languages/pt_BR.json
+++ b/languages/pt_BR.json
@@ -1195,7 +1195,6 @@
"TXT_CODE_ee67e1a3": "Diretório de Trabalho",
"TXT_CODE_962d9320": "O diretório de trabalho onde a instância é executada. Você pode fornecer um caminho absoluto ou relativo.",
"TXT_CODE_bb0b9711": "Comando de Atualização ou Instalação",
- "TXT_CODE_41763172": "Este comando do sistema será executado quando o usuário realizar uma operação de atualização/instalação, {mcsm_workspace} representa o diretório de trabalho.",
"TXT_CODE_f041de90": "Codificação de Arquivo",
"TXT_CODE_6e69b5a5": "Para funções de gerenciamento de arquivos.",
"TXT_CODE_fa920c0": "Tempo de Expiração",
@@ -1948,5 +1947,7 @@
"TXT_CODE_2fb14927": "Este erro geralmente significa que a página da web não pode se conectar diretamente ao processo de back-end. \nSe você possui encaminhamento de porta, mapeamento, proxy reverso, verifique a configuração!",
"TXT_CODE_46c48969": "Parece que você está tentando usar HTTPS, mas não conseguimos nos conectar ao daemon usando o protocolo WSS. \nTente se conectar ao daemon usando o protocolo WSS://.",
"TXT_CODE_9b3ce825": "Descrição do proxy reverso",
- "TXT_CODE_d4c8fb3b": "Documentos relacionados:"
+ "TXT_CODE_d4c8fb3b": "Documentos relacionados:",
+ "TXT_CODE_a6424dcc": "O diretório de trabalho ou comando de inicialização não pode estar vazio",
+ "TXT_CODE_ffa884f9": "Aviso: o diretório de trabalho não está montado no contêiner e o contêiner não poderá acessar arquivos no diretório de trabalho da instância"
}
diff --git a/languages/ru_RU.json b/languages/ru_RU.json
index e4fad89f..217bbd0d 100644
--- a/languages/ru_RU.json
+++ b/languages/ru_RU.json
@@ -364,7 +364,6 @@
"TXT_CODE_40ee4eaf": "производить параллелизм",
"TXT_CODE_413b9c01": "Версия среды узла",
"TXT_CODE_41406a5f": "данные карты",
- "TXT_CODE_41763172": "Эта системная команда будет выполнена, когда пользователь выполнит операцию обновления/установки.",
"TXT_CODE_41d79430": "Подходит для операционной среды сервера MC Bedrock Edition или других программ Linux.",
"TXT_CODE_41dd4d19": "Официальный веб-сайт",
"TXT_CODE_42036f92": "------Событие входа пользователя ------",
@@ -1948,5 +1947,7 @@
"TXT_CODE_2fb14927": "Эта ошибка обычно означает, что веб-страница не может напрямую подключиться к серверному процессу. \nЕсли у вас есть переадресация портов, сопоставление, обратный прокси-сервер, проверьте конфигурацию!",
"TXT_CODE_46c48969": "Похоже, вы пытаетесь использовать HTTPS, но мы не можем подключиться к демону по протоколу WSS. \nПопробуйте подключиться к демону по протоколу WSS://.",
"TXT_CODE_9b3ce825": "Описание обратного прокси",
- "TXT_CODE_d4c8fb3b": "Сопутствующие документы:"
+ "TXT_CODE_d4c8fb3b": "Сопутствующие документы:",
+ "TXT_CODE_a6424dcc": "Рабочий каталог или команда запуска не могут быть пустыми.",
+ "TXT_CODE_ffa884f9": "Предупреждение. Рабочий каталог не подключен к контейнеру, и контейнер не сможет получить доступ к файлам в рабочем каталоге экземпляра."
}
diff --git a/languages/tr_TR.json b/languages/tr_TR.json
index 76511c4b..6ae0d307 100644
--- a/languages/tr_TR.json
+++ b/languages/tr_TR.json
@@ -1196,7 +1196,6 @@
"TXT_CODE_ee67e1a3": "Çalışma Dizini",
"TXT_CODE_962d9320": "Örneğin çalıştığı çalışma dizini. Mutlak veya göreli bir yol sağlayabilirsiniz.",
"TXT_CODE_bb0b9711": "Güncelleme veya Yükleme Komutu",
- "TXT_CODE_41763172": "Bu sistem komutu, kullanıcı bir güncelleme/kurulum işlemi gerçekleştirdiğinde çalıştırılacaktır, {mcsm_workspace} çalışma dizinini temsil eder.",
"TXT_CODE_f041de90": "Dosya Kodlaması",
"TXT_CODE_6e69b5a5": "Dosya yönetimi işlevleri için.",
"TXT_CODE_fa920c0": "Son Kullanma Süresi",
@@ -1948,5 +1947,7 @@
"TXT_CODE_2fb14927": "Bu hata genellikle web sayfasının doğrudan arka uç işlemine bağlanamadığı anlamına gelir. \nBağlantı noktası yönlendirme, eşleme, ters proxy'niz varsa lütfen yapılandırmayı kontrol edin!",
"TXT_CODE_46c48969": "Görünüşe göre HTTPS kullanmaya çalışıyorsunuz ancak arka plan programına WSS protokolünü kullanarak bağlanamıyoruz. \nLütfen WSS:// protokolünü kullanarak arka plan programına bağlanmayı deneyin.",
"TXT_CODE_9b3ce825": "Ters proxy açıklaması",
- "TXT_CODE_d4c8fb3b": "İlgili belgeler:"
+ "TXT_CODE_d4c8fb3b": "İlgili belgeler:",
+ "TXT_CODE_a6424dcc": "Çalışma dizini veya başlatma komutu boş olamaz",
+ "TXT_CODE_ffa884f9": "Uyarı: Çalışma dizini kapsayıcıya bağlanmamıştır ve kapsayıcı, örneğin çalışma dizinindeki dosyalara erişemeyecektir."
}
diff --git a/languages/zh_CN.json b/languages/zh_CN.json
index 970a8f73..edda61c2 100644
--- a/languages/zh_CN.json
+++ b/languages/zh_CN.json
@@ -290,7 +290,7 @@
"TXT_CODE_d07742fe": "服务端配置文件",
"TXT_CODE_1deaa2dd": "用户",
"TXT_CODE_236f70aa": "用户配置",
- "TXT_CODE_342a04a9": "故障中",
+ "TXT_CODE_342a04a9": "维护中",
"TXT_CODE_15f2e564": "未运行",
"TXT_CODE_a409b8a9": "停止中",
"TXT_CODE_175b570d": "启动中",
@@ -380,7 +380,7 @@
"TXT_CODE_4ab6a0b5": "浏览器时间",
"TXT_CODE_856bd2f3": "临时封禁IP数",
"TXT_CODE_da8f97a7": "已阻止的访问",
- "TXT_CODE_190ecd56": "Linux 负载",
+ "TXT_CODE_190ecd56": "負載平均值",
"TXT_CODE_77d038f7": "面板端使用内存量",
"TXT_CODE_4df7e9bd": "主机名",
"TXT_CODE_b4d8588": "系统版本",
@@ -1198,7 +1198,6 @@
"TXT_CODE_ee67e1a3": "工作目录",
"TXT_CODE_962d9320": "实例运行的工作目录,可填绝对路径与相对路径",
"TXT_CODE_bb0b9711": "更新或安装命令",
- "TXT_CODE_41763172": "当用户执行更新/安装操作时,将会执行此系统命令,{mcsm_workspace} 代表工作目录",
"TXT_CODE_f041de90": "文件管理编码",
"TXT_CODE_6e69b5a5": "文件管理功能的解压缩,编辑等编码",
"TXT_CODE_fa920c0": "到期时间",
@@ -1948,5 +1947,13 @@
"TXT_CODE_9b3ce825": "反向代理说明",
"TXT_CODE_10cc2794": "网络架构说明",
"TXT_CODE_46c48969": "看起来您正在尝试使用 HTTPS,但是我们无法使用 WSS 协议连接到守护进程,请尝试使用 WSS:// 协议连接到守护进程。",
- "TXT_CODE_2fb14927": "此错误通常意味着网页无法直接连接到后端进程,如果您有端口转发,映射,反向代理,请检查配置!"
+ "TXT_CODE_2fb14927": "此错误通常意味着网页无法直接连接到后端进程,如果您有端口转发,映射,反向代理,请检查配置!",
+ "TXT_CODE_a6424dcc": "工作目录和启动命令不可为空",
+ "TXT_CODE_ffa884f9": "警告:工作目录没有挂载到容器中,容器将无法访问到实例工作目录中的文件",
+ "TXT_CODE_6c232c9c": "用于挂载主机上额外的文件夹到容器中,支持 {mcsm_workspace}(工作目录)和 {mcsm_instance_id}(实例ID)两个变量字符串。",
+ "TXT_CODE_fa487a47": "提供以下变量字符串:{mcsm_instance_id} = 实例ID,{mcsm_workspace} = 实例安装目录",
+ "TXT_CODE_6259357c": "程序必须运行超过6秒才能被强制停止!",
+ "TXT_CODE_9d1d244f": "程序启动失败,输入输出流不可读:{{pipeName}}",
+ "TXT_CODE_ca030197": "实例有其他任务正在进行中,无法进行此操作!",
+ "TXT_CODE_aae2918f": "实例进程启动失败,请检查启动命令和运行环境等配置!"
}
diff --git a/languages/zh_TW.json b/languages/zh_TW.json
index 23d3e94f..6bdca24c 100644
--- a/languages/zh_TW.json
+++ b/languages/zh_TW.json
@@ -1198,7 +1198,6 @@
"TXT_CODE_ee67e1a3": "工作目錄",
"TXT_CODE_962d9320": "實例運行的工作目錄,可填入絕對路徑與相對路徑",
"TXT_CODE_bb0b9711": "更新或安裝指令",
- "TXT_CODE_41763172": "當使用者執行更新/安裝作業時,將會執行此系統指令,{mcsm_workspace} 代表工作目錄",
"TXT_CODE_f041de90": "檔案管理編碼",
"TXT_CODE_6e69b5a5": "檔案管理功能的解壓縮,編輯等編碼",
"TXT_CODE_fa920c0": "到期時間",
@@ -1779,7 +1778,7 @@
"TXT_CODE_db37b7f9": "映像下載錯誤,請確保此映像名正確,或在節點管理的控制台處手動透過 docker pull 拉取您需要的映像,錯誤訊息:",
"TXT_CODE_1cf6fc4b": "此容器的開放端口配置有誤!",
"TXT_CODE_2029027e": "此容器的開放端口配置有誤,分隔符號左右兩邊不存在值!",
- "TXT_CODE_e76e49e9": "已掛載工作目錄:",
+ "TXT_CODE_e76e49e9": "已載入工作目錄:",
"TXT_CODE_90a9d317": "容器環境變數",
"TXT_CODE_a42984e": "變數名稱",
"TXT_CODE_115e8a25": "變數值",
diff --git a/lib-urls.txt b/lib-urls.txt
new file mode 100644
index 00000000..0368d7b8
--- /dev/null
+++ b/lib-urls.txt
@@ -0,0 +1,6 @@
+https://github.com/MCSManager/Zip-Tools/releases/download/latest/file_zip_linux_arm64
+https://github.com/MCSManager/Zip-Tools/releases/download/latest/file_zip_linux_x64
+https://github.com/MCSManager/Zip-Tools/releases/download/latest/file_zip_win32_x64.exe
+https://github.com/MCSManager/PTY/releases/download/latest/pty_linux_arm64
+https://github.com/MCSManager/PTY/releases/download/latest/pty_linux_x64
+https://github.com/MCSManager/PTY/releases/download/latest/pty_win32_x64.exe
\ No newline at end of file
diff --git a/panel/package.json b/panel/package.json
index 9e012a40..f231f92e 100644
--- a/panel/package.json
+++ b/panel/package.json
@@ -1,6 +1,6 @@
{
"name": "mcsmanager-panel",
- "version": "10.4.0",
+ "version": "10.4.3",
"daemonVersion": "4.5.0",
"description": "Provide MCSManager with the ability to connect and control all Daemons, and provide API services for the front end.",
"scripts": {
diff --git a/panel/src/app/routers/instance_admin_router.ts b/panel/src/app/routers/instance_admin_router.ts
index 4ce7b413..81c4bcc9 100755
--- a/panel/src/app/routers/instance_admin_router.ts
+++ b/panel/src/app/routers/instance_admin_router.ts
@@ -12,6 +12,7 @@ import { getUserUuid } from "../service/passport_service";
import { isHaveInstanceByUuid, isTopPermissionByUuid } from "../service/permission_service";
import { ROLE } from "../entity/user";
import { removeTrail } from "common";
+import userSystem from "../service/user_service";
const router = new Router({ prefix: "/instance" });
@@ -131,6 +132,12 @@ router.delete(
const instanceUuids = ctx.request.body.uuids;
const deleteFile = ctx.request.body.deleteFile;
const remoteService = RemoteServiceSubsystem.getInstance(daemonId);
+ if (!instanceUuids || !Array.isArray(instanceUuids))
+ throw new Error("Type error, invalid uuids or daemonId");
+ const instanceIds = instanceUuids.map((uuid: string) => {
+ return { instanceUuid: uuid, daemonId };
+ });
+ userSystem.deleteUserInstances(null, instanceIds, true);
const result = await new RemoteRequest(remoteService).request("instance/delete", {
instanceUuids,
deleteFile
diff --git a/panel/src/app/service/user_service.ts b/panel/src/app/service/user_service.ts
index 403c25f6..6f6a0d61 100755
--- a/panel/src/app/service/user_service.ts
+++ b/panel/src/app/service/user_service.ts
@@ -108,6 +108,27 @@ class UserSubsystem {
});
});
}
+ deleteUserInstances(uuid: string | null, instanceIds: IUserApp[], allUsers = false) {
+ if (uuid && allUsers) {
+ throw new Error("Type error, The uuid and allUsers cannot be true at the same time.");
+ }
+ const users = allUsers ? Array.from(this.objects.values()) : [this.getInstance(uuid!)];
+ if (!users || users.length === 0) return;
+ instanceIds.forEach((value) => {
+ if (!value.daemonId || !value.instanceUuid)
+ throw new Error("Type error, The instances of user must be IUserHaveInstance array.");
+ });
+ users.forEach((user) => {
+ if (!user) return;
+ user.instances = user.instances.filter((value) => {
+ for (const instance of instanceIds) {
+ if (instance.daemonId === value.daemonId && instance.instanceUuid === value.instanceUuid)
+ return false;
+ }
+ return true;
+ });
+ });
+ }
getUserByUserName(userName: string) {
for (const map of this.objects) {
diff --git a/prod-scripts/linux/install.sh b/prod-scripts/linux/install.sh
new file mode 100644
index 00000000..77d88033
--- /dev/null
+++ b/prod-scripts/linux/install.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+
+BASE_PATH=$(pwd)
+
+cd "${BASE_PATH}/daemon"
+npm install --production --no-fund --no-audit
+
+cd "${BASE_PATH}/web"
+npm install --production --no-fund --no-audit
diff --git a/prod-scripts/linux/start-daemon.sh b/prod-scripts/linux/start-daemon.sh
new file mode 100755
index 00000000..6ead6cb5
--- /dev/null
+++ b/prod-scripts/linux/start-daemon.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+cd daemon || exit
+node --max-old-space-size=8192 --enable-source-maps app.js
diff --git a/prod-scripts/linux/start-web.sh b/prod-scripts/linux/start-web.sh
new file mode 100755
index 00000000..5721146d
--- /dev/null
+++ b/prod-scripts/linux/start-web.sh
@@ -0,0 +1,4 @@
+#!/bin/bash
+
+cd web || exit
+node --max-old-space-size=8192 --enable-source-maps app.js
diff --git a/prod-scripts/windows/start.bat b/prod-scripts/windows/start.bat
new file mode 100644
index 00000000..80929714
--- /dev/null
+++ b/prod-scripts/windows/start.bat
@@ -0,0 +1,5 @@
+cd daemon
+start node_app.exe --enable-source-maps --max-old-space-size=8192 app.js
+ping localhost
+cd ../web
+start node_app.exe --enable-source-maps --max-old-space-size=8192 app.js --open
\ No newline at end of file