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 @@ +
+ + MCSManagerLogo.png + + +
+ +

+ Panel de MCSManager +

+ +[![--](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()" >