├── .eslintrc.js ├── .github ├── ISSUE_TEMPLATE │ ├── ------.md │ ├── ----.md │ └── bug---.md ├── github.txt └── workflows │ ├── codacy.yml │ └── codeql.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── readme_cn.md ├── src ├── app.ts ├── common │ ├── compress.ts │ ├── global_variable.ts │ ├── instance_stream.ts │ ├── mcping.ts │ ├── process_tools.ts │ ├── query_wrapper.ts │ ├── system_info.ts │ ├── system_storage.ts │ └── typecheck.ts ├── const.ts ├── entity │ ├── commands │ │ ├── base │ │ │ ├── command.ts │ │ │ └── command_parser.ts │ │ ├── cmd.ts │ │ ├── dispatcher.ts │ │ ├── docker │ │ │ ├── docker_resize.ts │ │ │ └── docker_start.ts │ │ ├── general │ │ │ ├── general_command.ts │ │ │ ├── general_kill.ts │ │ │ ├── general_restart.ts │ │ │ ├── general_start.ts │ │ │ ├── general_stop.ts │ │ │ └── general_update.ts │ │ ├── input.ts │ │ ├── kill.ts │ │ ├── nullfunc.ts │ │ ├── process_info.ts │ │ ├── pty │ │ │ ├── pty_start.ts │ │ │ └── pty_stop.ts │ │ ├── restart.ts │ │ ├── start.ts │ │ ├── stop.ts │ │ ├── task │ │ │ ├── openfrp.ts │ │ │ ├── players.ts │ │ │ └── time.ts │ │ └── update.ts │ ├── config.ts │ ├── ctx.ts │ ├── instance │ │ ├── Instance_config.ts │ │ ├── instance.ts │ │ ├── interface.ts │ │ ├── life_cycle.ts │ │ ├── preset.ts │ │ └── process_config.ts │ └── minecraft │ │ ├── mc_getplayer.ts │ │ ├── mc_getplayer_bedrock.ts │ │ └── mc_update.ts ├── i18n │ ├── index.ts │ └── language │ │ ├── en_us.json │ │ └── zh_cn.json ├── routers │ ├── Instance_router.ts │ ├── auth_router.ts │ ├── environment_router.ts │ ├── file_router.ts │ ├── http_router.ts │ ├── info_router.ts │ ├── instance_event_router.ts │ ├── passport_router.ts │ ├── schedule_router.ts │ └── stream_router.ts ├── service │ ├── async_task_service │ │ ├── index.ts │ │ └── quick_install.ts │ ├── docker_service.ts │ ├── download.ts │ ├── file_router_service.ts │ ├── http.ts │ ├── install.ts │ ├── interfaces.ts │ ├── log.ts │ ├── mission_passport.ts │ ├── node_auth.ts │ ├── protocol.ts │ ├── router.ts │ ├── system_file.ts │ ├── system_instance.ts │ ├── system_instance_control.ts │ ├── system_visual_data.ts │ ├── ui.ts │ └── version.ts ├── tools │ └── filepath.ts └── types │ └── properties.d.ts ├── tsconfig.json └── webpack.config.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // .eslintrc.js 2 | module.exports = { 3 | env: { 4 | commonjs: true, 5 | es6: true, 6 | node: true 7 | }, 8 | extends: "eslint:recommended", 9 | globals: { 10 | Atomics: "readonly", 11 | SharedArrayBuffer: "readonly" 12 | }, 13 | parserOptions: { 14 | ecmaVersion: 2018 15 | }, 16 | rules: {} 17 | }; 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/------.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 其他类型模板 3 | about: 除BUG报告与功能建议之外的任何内容,请使用本模板。 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/----.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 功能建议 3 | about: 有任何功能上的想法或意见可以使用此模板发布。 4 | title: "[Feature]" 5 | labels: Feature Request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **面板版本** 11 | 12 | 13 | **想法/建议** 14 | 15 | 16 | **您认为这个想法/建议在每100人中会有多少人遇到/使用** 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug---.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: BUG 报告 3 | about: 创建一个 BUG 报告给予开发团队。 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **操作系统版本** 11 | 12 | **面板版本** 13 | 14 | 15 | **出现概率** 16 | 17 | 18 | **BUG 详情描述** 19 | 20 | 21 | **复现步骤** 22 | -------------------------------------------------------------------------------- /.github/github.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCSManager/Daemon/1e4b5419e123ab6c545f73a2ff026ac32629313b/.github/github.txt -------------------------------------------------------------------------------- /.github/workflows/codacy.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | # This workflow checks out code, performs a Codacy security scan 7 | # and integrates the results with the 8 | # GitHub Advanced Security code scanning feature. For more information on 9 | # the Codacy security scan action usage and parameters, see 10 | # https://github.com/codacy/codacy-analysis-cli-action. 11 | # For more information on Codacy Analysis CLI in general, see 12 | # https://github.com/codacy/codacy-analysis-cli. 13 | 14 | name: Codacy Security Scan 15 | 16 | on: 17 | push: 18 | branches: [ "master" ] 19 | pull_request: 20 | # The branches below must be a subset of the branches above 21 | branches: [ "master" ] 22 | schedule: 23 | - cron: '24 23 * * 5' 24 | 25 | permissions: 26 | contents: read 27 | 28 | jobs: 29 | codacy-security-scan: 30 | permissions: 31 | contents: read # for actions/checkout to fetch code 32 | security-events: write # for github/codeql-action/upload-sarif to upload SARIF results 33 | name: Codacy Security Scan 34 | runs-on: ubuntu-latest 35 | steps: 36 | # Checkout the repository to the GitHub Actions runner 37 | - name: Checkout code 38 | uses: actions/checkout@v3 39 | 40 | # Execute Codacy Analysis CLI and generate a SARIF output with the security issues identified during the analysis 41 | - name: Run Codacy Analysis CLI 42 | uses: codacy/codacy-analysis-cli-action@d840f886c4bd4edc059706d09c6a1586111c540b 43 | with: 44 | # Check https://github.com/codacy/codacy-analysis-cli#project-token to get your project token from your Codacy repository 45 | # You can also omit the token and run the tools that support default configurations 46 | project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} 47 | verbose: true 48 | output: results.sarif 49 | format: sarif 50 | # Adjust severity of non-security issues 51 | gh-code-scanning-compat: true 52 | # Force 0 exit code to allow SARIF file generation 53 | # This will handover control about PR rejection to the GitHub side 54 | max-allowed-issues: 2147483647 55 | 56 | # Upload the SARIF file generated in the previous step 57 | - name: Upload SARIF results file 58 | uses: github/codeql-action/upload-sarif@v2 59 | with: 60 | sarif_file: results.sarif 61 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "master" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "master" ] 20 | schedule: 21 | - cron: '36 3 * * 5' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # myself files 2 | server/ 3 | users/ 4 | data/ 5 | .vscode/ 6 | testServer/ 7 | *.exe 8 | *.zip 9 | docker_temp/ 10 | toprettier.bat 11 | upload/ 12 | lib/ 13 | tmp/ 14 | dist/ 15 | production/ 16 | config.json 17 | *.bat 18 | test_file/ 19 | # config files 20 | users/*.json 21 | server/*.json 22 | public/*.json 23 | McserverConfig.json 24 | core/*.json 25 | public/common/URL.js 26 | property.js 27 | test.txt 28 | #vscode 29 | *.code-workspace 30 | error 31 | 32 | # node 33 | node_modules/ 34 | 35 | # tmp 36 | tmp_files/ 37 | 38 | # Logs 39 | logs 40 | *.log 41 | npm-debug.log* 42 | yarn-debug.log* 43 | yarn-error.log* 44 | log/ 45 | test/ 46 | # Runtime data 47 | pids 48 | *.pid 49 | *.seed 50 | *.pid.lock 51 | 52 | # Directory for instrumented libs generated by jscoverage/JSCover 53 | lib-cov 54 | 55 | # Coverage directory used by tools like istanbul 56 | coverage 57 | 58 | # nyc test coverage 59 | .nyc_output 60 | 61 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 62 | .grunt 63 | 64 | # Bower dependency directory (https://bower.io/) 65 | bower_components 66 | 67 | # node-waf configuration 68 | .lock-wscript 69 | 70 | # Compiled binary addons (http://nodejs.org/api/addons.html) 71 | build/Release 72 | 73 | # Dependency directories 74 | node_modules/ 75 | jspm_packages/ 76 | 77 | # Typescript v1 declaration files 78 | typings/ 79 | 80 | # Optional npm cache directory 81 | .npm 82 | 83 | # Optional eslint cache 84 | .eslintcache 85 | 86 | # Optional REPL history 87 | .node_repl_history 88 | 89 | # Output of 'npm pack' 90 | *.tgz 91 | 92 | # Yarn Integrity file 93 | .yarn-integrity 94 | 95 | # dotenv environment variables file 96 | .env 97 | 98 | Note.txt 99 | 100 | # IntelliJ IDEA project files 101 | /.idea/* 102 | language 103 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | public/common/bootstrap/ 2 | public/common/echarts-dist/ 3 | public/common/vue/ 4 | public/common/jquery-3.2.1.min.js 5 | public/common/xterm.css 6 | public/common/xterm.js 7 | public/common/xterm.js.map 8 | public/common/echarts.js 9 | public/onlinefs_public/ 10 | public/login/static/ 11 | public/login2/static/ 12 | public/login3/static/ 13 | test_file/ 14 | # self files 15 | server/ 16 | users/ 17 | .vscode/ 18 | testServer/ 19 | *.exe 20 | *.zip 21 | docker_temp/ 22 | data/ 23 | production/ 24 | 25 | 26 | # config files 27 | users/*.json 28 | server/*.json 29 | public/*.json 30 | McserverConfig.json 31 | core/*.json 32 | public/common/URL.js 33 | property.js 34 | 35 | #vscode 36 | *.code-workspace 37 | 38 | # node 39 | node_modules/ 40 | 41 | # tmp 42 | tmp_files/ 43 | 44 | # Logs 45 | logs 46 | *.log 47 | npm-debug.log* 48 | yarn-debug.log* 49 | yarn-error.log* 50 | 51 | # Runtime data 52 | pids 53 | *.pid 54 | *.seed 55 | *.pid.lock 56 | 57 | # Directory for instrumented libs generated by jscoverage/JSCover 58 | lib-cov 59 | 60 | # Coverage directory used by tools like istanbul 61 | coverage 62 | 63 | # nyc test coverage 64 | .nyc_output 65 | 66 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 67 | .grunt 68 | 69 | # Bower dependency directory (https://bower.io/) 70 | bower_components 71 | 72 | # node-waf configuration 73 | .lock-wscript 74 | 75 | # Compiled binary addons (http://nodejs.org/api/addons.html) 76 | build/Release 77 | 78 | # Dependency directories 79 | node_modules/ 80 | jspm_packages/ 81 | 82 | # Typescript v1 declaration files 83 | typings/ 84 | 85 | # Optional npm cache directory 86 | .npm 87 | 88 | # Optional eslint cache 89 | .eslintcache 90 | 91 | # Optional REPL history 92 | .node_repl_history 93 | 94 | # Output of 'npm pack' 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | .yarn-integrity 99 | 100 | # dotenv environment variables file 101 | .env 102 | 103 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 142, 3 | "trailingComma": "none" 4 | } 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | MCSManager icon.png 2 | 3 | 4 | ## Deprecated 5 | 6 | **!!! This repository is only applicable to MCSManager 9.X version. Please refer to the latest code here: [https://github.com/MCSManager/MCSManager](https://github.com/MCSManager/MCSManager)** 7 | 8 | 9 |
10 | 11 | ## Manual installation 12 | 13 | Prerequisite: [Web-side program](https://github.com/MCSManager/MCSManager-Web-Production) needs to be installed to use this software normally. 14 | 15 | Install `Node 14+` and `npm` tools and clone the [Daemon code for deployment](https://gitee.com/mcsmanager/MCSManager-Daemon-Production), then use the following commands to initialize and start the Daemon side. 16 | 17 | ```bash 18 | # Download the Daemon program 19 | git clone https://github.com/MCSManager/MCSManager-Daemon-Production.git 20 | # rename the folder and enter 21 | mv MCSManager-Daemon-Production daemon 22 | npm install 23 | node app.js 24 | ```` 25 | 26 | The program will output the following 27 | 28 | ````log 29 | Access address localhost:24444 30 | access key [your key, a string of hexadecimal numbers] 31 | The key is the only means of authentication for the daemon 32 | ```` 33 | 34 | Just add an instance on the web side using the key. 35 | To stop direct input: 36 | 37 | ```bash 38 | Ctrl+C 39 | ```` 40 | 41 | If you need to run in the background for a long time, please use the `Screen` software in conjunction with it, or manually write to the system service. 42 | 43 |
44 | 45 | 46 | ## License 47 | 48 | Copyright 2023 [MCSManager Dev](https://github.com/mcsmanager), Apache-2.0 license. 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mcsmanager-daemon", 3 | "version": "3.4.0", 4 | "description": "Provides remote control capability for MCSManager to manage processes, scheduled tasks, I/O streams, and more", 5 | "main": "src/app.js", 6 | "scripts": { 7 | "start": "ts-node ./src/app.ts", 8 | "build": "tsc && webpack --config webpack.config.js", 9 | "dev": "ts-node ./src/app.ts" 10 | }, 11 | "homepage": "https://mcsmanager.com/", 12 | "author": "https://github.com/unitwk", 13 | "license": "Apache-2.0", 14 | "dependencies": { 15 | "@iarna/toml": "^2.2.5", 16 | "@koa/router": "^10.0.0", 17 | "archiver": "^5.3.1", 18 | "axios": "^1.1.3", 19 | "compressing": "^1.5.1", 20 | "crypto": "^1.0.1", 21 | "dockerode": "3.1.0", 22 | "fs-extra": "^9.0.1", 23 | "i18next": "^21.8.14", 24 | "iconv-lite": "^0.6.2", 25 | "koa": "^2.13.1", 26 | "koa-body": "^4.2.0", 27 | "koa-send": "^5.0.1", 28 | "log4js": "^6.4.0", 29 | "node-disk-info": "^1.3.0", 30 | "node-schedule": "^2.0.0", 31 | "node-stream-zip": "^1.15.0", 32 | "os-utils": "0.0.14", 33 | "pidusage": "^2.0.21", 34 | "properties": "^1.2.1", 35 | "socket.io": "^4.6.1", 36 | "socket.io-client": "^4.6.1", 37 | "uuid": "^8.3.2", 38 | "yaml": "^1.10.2" 39 | }, 40 | "devDependencies": { 41 | "@types/archiver": "^5.3.1", 42 | "@types/axios": "^0.14.0", 43 | "@types/dockerode": "^3.2.7", 44 | "@types/fs-extra": "^9.0.11", 45 | "@types/iconv-lite": "0.0.1", 46 | "@types/koa": "^2.13.4", 47 | "@types/koa__router": "^8.0.7", 48 | "@types/koa-send": "^4.1.3", 49 | "@types/log4js": "^2.3.5", 50 | "@types/mocha": "^8.2.2", 51 | "@types/node": "^18.11.18", 52 | "@types/node-schedule": "^1.3.2", 53 | "@types/os-utils": "0.0.1", 54 | "@types/pidusage": "^2.0.1", 55 | "@types/ssh2": "^1.11.7", 56 | "@types/uuid": "^8.3.0", 57 | "eslint": "^7.13.0", 58 | "nodemon": "^2.0.20", 59 | "ts-node": "^10.9.1", 60 | "typescript": "^4.9.4", 61 | "webpack": "^5.73.0", 62 | "webpack-cli": "^4.10.0", 63 | "webpack-node-externals": "^3.0.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /readme_cn.md: -------------------------------------------------------------------------------- 1 | MCSManager 图标.png 2 | 3 |
4 | 5 | [![Status](https://img.shields.io/badge/npm-v6.14.15-blue.svg)](https://www.npmjs.com/) 6 | [![Status](https://img.shields.io/badge/node-v14.17.6-blue.svg)](https://nodejs.org/en/download/) 7 | [![Status](https://img.shields.io/badge/License-AGPL-red.svg)](https://github.com/Suwings/MCSManager) 8 | 9 | [官方网站](http://mcsmanager.com/) | [使用文档](https://docs.mcsmanager.com/) | [团队主页](https://github.com/MCSManager) | [面板端项目](https://github.com/MCSManager/MCSManager) | [网页前端项目](https://github.com/MCSManager/UI) | [守护进程项目](https://github.com/MCSManager/Daemon) 10 | 11 | 适用于 MCSManager 的分布式守护进程程序,与面板端分离直接管理和控制真实程序。 12 | 13 | 项目主仓库请前往:[https://github.com/MCSManager/MCSManager](https://github.com/MCSManager/MCSManager) 14 | 15 |
16 | 17 | ## MCSManager 简介 18 | 19 | **分布式,稳定可靠,开箱即用,高扩展性,支持 Minecraft 和其他少数游戏的控制面板。** 20 | 21 | MCSManager 面板(简称:MCSM 面板)是一款全中文,轻量级,开箱即用,多实例和支持 Docker 的 Minecraft 服务端管理面板。 22 | 23 | 此软件在 Minecraft 和其他游戏社区内中已有一定的流行程度,它可以帮助你集中管理多个物理服务器,动态在任何主机上创建游戏服务端,并且提供安全可靠的多用户权限系统,可以很轻松的帮助你管理多个服务器。 24 | 25 |
26 | 27 | ## 项目状态 28 | 29 | 项目处于发行状态,如果想促进开发或关注进度您可以点击右上角的 `star` `watch` 给予我们基本的支持。 30 | 31 | 若您想成为本项目的赞助者,请访问官方网站浏览至最底下。 32 | 33 |
34 | 35 | ## 手动安装 36 | 37 | 先决条件:需要安装[Web 端程序](https://github.com/MCSManager/MCSManager-Web-Production)才能正常使用本软件。 38 | 39 | 安装 `Node 14+` 与 `npm` 工具,并克隆[部署用 Daemon 代码](https://gitee.com/mcsmanager/MCSManager-Daemon-Production),然后使用以下命令初始化并启动 Daemon 端。 40 | 41 | > 名词 Daemon 中文代表“守护进程”之意,在此处代表本地或远程主机的守护进程,用于真实运行服务端程序的进程,Web 端面板用于管理与调控,不与服务端程序实际文件进行任何接触。 42 | 43 | ```bash 44 | # cd MCSManager-Daemon-Production 45 | npm install 46 | node app.js 47 | ``` 48 | 49 | 程序会输出以下内容 50 | 51 | ```log 52 | 访问地址 localhost:24444 53 | 访问密钥 [你的密钥,是一串16进制数字] 54 | 密钥作为守护进程唯一认证手段 55 | ``` 56 | 57 | 使用密钥在 web 端添加实例即可。 58 | 如需停止直接输入: 59 | 60 | ```bash 61 | exit 62 | ``` 63 | 64 | 如需长期后台运行请使用 `Screen` 软件配合使用,或者手动写入到系统服务。 65 | 66 |
67 | 68 | ## 贡献 69 | 70 | 如果你在使用过程中发现任何问题,可以提交 Issue 或自行 Fork 修改后提交 Pull Request。 71 | 72 | 代码需要保持现有格式,不得格式化多余代码,具体可[参考这里](https://github.com/MCSManager/MCSManager/issues/544)。 73 | 74 |
75 | 76 | -------------------------------------------------------------------------------- /src/app.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import http from "http"; 4 | import fs from "fs-extra"; 5 | import { $t, i18next } from "./i18n"; 6 | import { getVersion, initVersionManager } from "./service/version"; 7 | import { globalConfiguration } from "./entity/config"; 8 | import { Server, Socket } from "socket.io"; 9 | import { LOCAL_PRESET_LANG_PATH } from "./const"; 10 | import logger from "./service/log"; 11 | 12 | initVersionManager(); 13 | const VERSION = getVersion(); 14 | 15 | console.log(` 16 | ______ _______________________ ___ 17 | ___ |/ /_ ____/_ ___/__ |/ /_____ _____________ _______ _____________ 18 | __ /|_/ /_ / _____ \\__ /|_/ /_ __ \`/_ __ \\ __ \`/_ __ \`/ _ \\_ ___/ 19 | _ / / / / /___ ____/ /_ / / / / /_/ /_ / / / /_/ /_ /_/ // __/ / 20 | /_/ /_/ \\____/ /____/ /_/ /_/ \\__,_/ /_/ /_/\\__,_/ _\\__, / \\___//_/ 21 | /____/ 22 | ________ 23 | ___ __ \\_____ ____________ ________________ 24 | __ / / / __ \`/ _ \\_ __ \`__ \\ __ \\_ __ \\ 25 | _ /_/ // /_/ // __/ / / / / / /_/ / / / / 26 | /_____/ \\__,_/ \\___//_/ /_/ /_/\\____//_/ /_/ 27 | 28 | 29 | + Copyright 2022 MCSManager Dev 30 | + Version ${VERSION} 31 | `); 32 | 33 | // Initialize the global configuration service 34 | globalConfiguration.load(); 35 | const config = globalConfiguration.config; 36 | 37 | // Set language 38 | if (fs.existsSync(LOCAL_PRESET_LANG_PATH)) { 39 | i18next.changeLanguage(fs.readFileSync(LOCAL_PRESET_LANG_PATH, "utf-8")); 40 | } else { 41 | const lang = config.language || "en_us"; 42 | logger.info(`LANGUAGE: ${lang}`); 43 | i18next.changeLanguage(lang); 44 | } 45 | logger.info($t("app.welcome")); 46 | 47 | import * as router from "./service/router"; 48 | import * as koa from "./service/http"; 49 | import * as protocol from "./service/protocol"; 50 | import InstanceSubsystem from "./service/system_instance"; 51 | import { initDependent } from "./service/install"; 52 | import "./service/async_task_service"; 53 | import "./service/async_task_service/quick_install"; 54 | import "./service/system_visual_data"; 55 | 56 | // Initialize HTTP service 57 | const koaApp = koa.initKoa(); 58 | 59 | // Listen for Koa errors 60 | koaApp.on("error", (error) => { 61 | // Block all Koa framework error 62 | // When Koa is attacked by a short connection flood, it is easy for error messages to swipe the screen, which may indirectly affect the operation of some applications 63 | }); 64 | 65 | const httpServer = http.createServer(koaApp.callback()); 66 | httpServer.on("error", (err) => { 67 | logger.error($t("app.httpSetupError")); 68 | logger.error(err); 69 | process.exit(1); 70 | }); 71 | httpServer.listen(config.port, config.ip); 72 | 73 | // Initialize Websocket service to HTTP service 74 | const io = new Server(httpServer, { 75 | serveClient: false, 76 | pingInterval: 5000, 77 | pingTimeout: 5000, 78 | cookie: false, 79 | path: "/socket.io", 80 | cors: { 81 | origin: "*", 82 | methods: ["GET", "POST", "PUT", "DELETE"] 83 | } 84 | }); 85 | 86 | // Initialize optional dependencies 87 | initDependent(); 88 | 89 | // Initialize application instance system 90 | try { 91 | InstanceSubsystem.loadInstances(); 92 | logger.info($t("app.instanceLoad", { n: InstanceSubsystem.getInstances().length })); 93 | } catch (err) { 94 | logger.error($t("app.instanceLoadError"), err); 95 | process.exit(-1); 96 | } 97 | 98 | // Initialize Websocket server 99 | io.on("connection", (socket: Socket) => { 100 | logger.info($t("app.sessionConnect", { ip: socket.handshake.address, uuid: socket.id })); 101 | 102 | protocol.addGlobalSocket(socket); 103 | router.navigation(socket); 104 | 105 | socket.on("disconnect", () => { 106 | protocol.delGlobalSocket(socket); 107 | for (const name of socket.eventNames()) socket.removeAllListeners(name); 108 | logger.info($t("app.sessionDisconnect", { ip: socket.handshake.address, uuid: socket.id })); 109 | }); 110 | }); 111 | 112 | process.on("uncaughtException", function (err) { 113 | logger.error(`Error: UncaughtException:`, err); 114 | }); 115 | 116 | process.on("unhandledRejection", (reason, p) => { 117 | logger.error(`Error: UnhandledRejection:`, reason, p); 118 | }); 119 | 120 | logger.info("----------------------------"); 121 | logger.info($t("app.started")); 122 | logger.info($t("app.doc")); 123 | logger.info($t("app.addr", { port: config.port })); 124 | logger.info($t("app.configPathTip", { path: "" })); 125 | logger.info($t("app.password", { key: config.key })); 126 | logger.info($t("app.passwordTip")); 127 | logger.info($t("app.exitTip")); 128 | logger.info("----------------------------"); 129 | console.log(""); 130 | 131 | async function processExit() { 132 | try { 133 | console.log(""); 134 | logger.warn("Program received EXIT command."); 135 | await InstanceSubsystem.exit(); 136 | logger.info("Exit."); 137 | } catch (err) { 138 | logger.error("ERROR:", err); 139 | } finally { 140 | process.exit(0); 141 | } 142 | } 143 | 144 | ["SIGTERM", "SIGINT", "SIGQUIT"].forEach(function (sig) { 145 | process.on(sig, () => { 146 | logger.warn(`${sig} close process signal detected.`); 147 | processExit(); 148 | }); 149 | }); 150 | 151 | process.stdin.on("data", (v) => { 152 | const command = v.toString().replace("\n", "").replace("\r", "").trim().toLowerCase(); 153 | if (command === "exit") processExit(); 154 | }); 155 | -------------------------------------------------------------------------------- /src/common/compress.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../i18n"; 4 | import fs from "fs-extra"; 5 | import path from "path"; 6 | import * as compressing from "compressing"; 7 | import child_process from "child_process"; 8 | import os from "os"; 9 | import archiver from "archiver"; 10 | import StreamZip, { async } from "node-stream-zip"; 11 | import { processWrapper } from "./process_tools"; 12 | import { PTY_PATH } from "../const"; 13 | // const StreamZip = require('node-stream-zip'); 14 | 15 | // Cross-platform high-efficiency/low-efficiency decompression scheme 16 | const system = os.platform(); 17 | 18 | function checkFileName(fileName: string) { 19 | const disableList = ['"', "/", "\\", "?", "|"]; 20 | for (const iterator of disableList) { 21 | if (fileName.includes(iterator)) return false; 22 | } 23 | return true; 24 | } 25 | 26 | function archiveZip(zipPath: string, files: string[], fileCode: string = "utf-8"): Promise { 27 | return new Promise((resolve, reject) => { 28 | const output = fs.createWriteStream(zipPath); 29 | const archive = archiver("zip", { 30 | zlib: { level: 9 } 31 | // encoding: fileCode 32 | }); 33 | files.forEach((v) => { 34 | const basename = path.normalize(path.basename(v)); 35 | if (!fs.existsSync(v)) return; 36 | if (fs.statSync(v)?.isDirectory()) { 37 | archive.directory(v, basename); 38 | } else { 39 | archive.file(v, { name: basename }); 40 | } 41 | }); 42 | output.on("close", function () { 43 | resolve(true); 44 | }); 45 | archive.on("warning", function (err) { 46 | reject(err); 47 | }); 48 | archive.on("error", function (err) { 49 | reject(err); 50 | }); 51 | archive.pipe(output); 52 | archive.finalize(); 53 | }); 54 | } 55 | 56 | function archiveUnZip(sourceZip: string, destDir: string, fileCode: string = "utf-8"): Promise { 57 | return new Promise(async (resolve, reject) => { 58 | const zip = new StreamZip.async({ file: sourceZip, nameEncoding: fileCode }); 59 | if (!fs.existsSync(destDir)) fs.mkdirsSync(destDir); 60 | try { 61 | await zip.extract(null, destDir); 62 | return resolve(true); 63 | } catch (error) { 64 | reject(error); 65 | } 66 | zip 67 | .close() 68 | .then(() => {}) 69 | .catch(() => {}); 70 | }); 71 | } 72 | 73 | export async function compress(sourceZip: string, files: string[], fileCode?: string): Promise { 74 | // if (system === "linux" && haveLinuxZip()) return await linuxZip(sourceZip, files); 75 | // return await nodeCompress(sourceZip, files, fileCode); 76 | if (hasGolangProcess()) return golangProcessZip(files, sourceZip, fileCode); 77 | return await archiveZip(sourceZip, files, fileCode); 78 | } 79 | 80 | export async function decompress(zipPath: string, dest: string, fileCode?: string): Promise { 81 | // if (system === "linux" && haveLinuxUnzip()) return await linuxUnzip(zipPath, dest); 82 | // return await nodeDecompress(zipPath, dest, fileCode); 83 | if (hasGolangProcess()) return await golangProcessUnzip(zipPath, dest, fileCode); 84 | return await archiveUnZip(zipPath, dest, fileCode); 85 | } 86 | 87 | async function _7zipCompress(zipPath: string, files: string[]) { 88 | const cmd = `7z.exe a ${zipPath} ${files.join(" ")}`.split(" "); 89 | console.log($t("common._7zip"), `${cmd.join(" ")}`); 90 | return new Promise((resolve, reject) => { 91 | const p = cmd.splice(1); 92 | const process = child_process.spawn(cmd[0], [...p], { 93 | cwd: "./7zip/" 94 | }); 95 | if (!process || !process.pid) return reject(false); 96 | process.on("exit", (code) => { 97 | if (code) return reject(false); 98 | return resolve(true); 99 | }); 100 | }); 101 | } 102 | 103 | async function _7zipDecompress(sourceZip: string, destDir: string) { 104 | // ./7z.exe x archive.zip -oD:\7-Zip 105 | const cmd = `7z.exe x ${sourceZip} -o${destDir}`.split(" "); 106 | console.log($t("common._7unzip"), `${cmd.join(" ")}`); 107 | return new Promise((resolve, reject) => { 108 | const process = child_process.spawn(cmd[0], [cmd[1], cmd[2], cmd[3]], { 109 | cwd: "./7zip/" 110 | }); 111 | if (!process || !process.pid) return reject(false); 112 | process.on("exit", (code) => { 113 | if (code) return reject(false); 114 | return resolve(true); 115 | }); 116 | }); 117 | } 118 | 119 | function haveLinuxUnzip() { 120 | try { 121 | const result = child_process.execSync("unzip -hh"); 122 | return result?.toString("utf-8").toLowerCase().includes("extended help for unzip"); 123 | } catch (error) { 124 | return false; 125 | } 126 | } 127 | 128 | function haveLinuxZip() { 129 | try { 130 | const result = child_process.execSync("zip -h2"); 131 | return result?.toString("utf-8").toLowerCase().includes("extended help for zip"); 132 | } catch (error) { 133 | return false; 134 | } 135 | } 136 | 137 | async function linuxUnzip(sourceZip: string, destDir: string) { 138 | return new Promise((resolve, reject) => { 139 | let end = false; 140 | const process = child_process.spawn("unzip", ["-o", sourceZip, "-d", destDir], { 141 | cwd: path.normalize(path.dirname(sourceZip)) 142 | }); 143 | if (!process || !process.pid) return reject(false); 144 | process.on("exit", (code) => { 145 | end = true; 146 | if (code) return reject(false); 147 | return resolve(true); 148 | }); 149 | // timeout, terminate the task 150 | setTimeout(() => { 151 | if (end) return; 152 | process.kill("SIGKILL"); 153 | reject(false); 154 | }, 1000 * 60 * 60); 155 | }); 156 | } 157 | 158 | // zip -r a.zip css css_v1 js 159 | // The ZIP file compressed by this function and the directory where the file is located must be in the same directory 160 | async function linuxZip(sourceZip: string, files: string[]) { 161 | if (!files || files.length == 0) return false; 162 | return new Promise((resolve, reject) => { 163 | let end = false; 164 | files = files.map((v) => path.normalize(path.basename(v))); 165 | const process = child_process.spawn("zip", ["-r", sourceZip, ...files], { 166 | cwd: path.normalize(path.dirname(sourceZip)) 167 | }); 168 | if (!process || !process.pid) return reject(false); 169 | process.on("exit", (code) => { 170 | end = true; 171 | if (code) return reject(false); 172 | return resolve(true); 173 | }); 174 | // timeout, terminate the task 175 | setTimeout(() => { 176 | if (end) return; 177 | process.kill("SIGKILL"); 178 | reject(false); 179 | }, 1000 * 60 * 60); 180 | }); 181 | } 182 | 183 | async function nodeCompress(zipPath: string, files: string[], fileCode: string = "utf-8") { 184 | const stream = new compressing.zip.Stream(); 185 | files.forEach((v) => { 186 | stream.addEntry(v, {}); 187 | }); 188 | const destStream = fs.createWriteStream(zipPath); 189 | stream.pipe(destStream); 190 | } 191 | 192 | async function nodeDecompress(sourceZip: string, destDir: string, fileCode: string = "utf-8") { 193 | return await compressing.zip.uncompress(sourceZip, destDir, { 194 | zipFileNameEncoding: fileCode 195 | }); 196 | } 197 | 198 | function hasGolangProcess() { 199 | return fs.existsSync(PTY_PATH); 200 | } 201 | 202 | // ./pty_linux_arm64 -m unzip /Users/wangkun/Documents/OtherWork/MCSM-Daemon/data/InstanceData/3832159255b042da8cb3fd2012b0a996/tmp.zip /Users/wangkun/Documents/OtherWork/MCSM-Daemon/data/InstanceData/3832159255b042da8cb3fd2012b0a996 203 | async function golangProcessUnzip(zipPath: string, destDir: string, fileCode: string = "utf-8") { 204 | console.log("GO Zip Params", zipPath, destDir, fileCode); 205 | return await new processWrapper(PTY_PATH, ["-coder", fileCode, "-m", "unzip", zipPath, destDir], ".", 60 * 30).start(); 206 | } 207 | 208 | async function golangProcessZip(files: string[], destZip: string, fileCode: string = "utf-8") { 209 | let p = ["-coder", fileCode, "-m", "zip"]; 210 | p = p.concat(files); 211 | p.push(destZip); 212 | console.log("GO Unzip Params", p); 213 | return await new processWrapper(PTY_PATH, p, ".", 60 * 30).start(); 214 | } 215 | 216 | // async function test() { 217 | // console.log( 218 | // "UNZIP::", 219 | // await golangProcessUnzip( 220 | // "/Users/wangkun/Documents/OtherWork/MCSM-Daemon/data/InstanceData/3832159d255b042da8cb3fd2012b0a996/tmp.zip", 221 | // "/Users/wangkun/Documents/OtherWork/MCSM-Daemon/data/InstanceData/3832159255b042da8cb3fd2012b0a996", 222 | // "utf-8" 223 | // ) 224 | // ); 225 | // } 226 | 227 | // test(); 228 | -------------------------------------------------------------------------------- /src/common/global_variable.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | export default class GlobalVariable { 4 | public static readonly map = new Map(); 5 | 6 | public static set(k: string, v: any) { 7 | GlobalVariable.map.set(k, v); 8 | } 9 | 10 | public static get(k: string, def?: any) { 11 | if (GlobalVariable.map.has(k)) { 12 | return GlobalVariable.map.get(k); 13 | } else { 14 | return def; 15 | } 16 | } 17 | 18 | public static del(k: string) { 19 | return GlobalVariable.map.delete(k); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/common/instance_stream.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { Socket } from "socket.io"; 4 | 5 | // Application instance data stream forwarding adapter 6 | export default class InstanceStreamListener { 7 | // Instance uuid -> Socket[] 8 | public readonly listenMap = new Map(); 9 | 10 | public requestForward(socket: Socket, instanceUuid: string) { 11 | if (this.listenMap.has(instanceUuid)) { 12 | const sockets = this.listenMap.get(instanceUuid); 13 | for (const iterator of sockets) if (iterator.id === socket.id) throw new Error(`此 Socket ${socket.id} 已经存在于指定实例监听表中`); 14 | sockets.push(socket); 15 | } else { 16 | this.listenMap.set(instanceUuid, [socket]); 17 | } 18 | } 19 | 20 | public cannelForward(socket: Socket, instanceUuid: string) { 21 | if (!this.listenMap.has(instanceUuid)) throw new Error(`指定 ${instanceUuid} 并不存在于监听表中`); 22 | const socketList = this.listenMap.get(instanceUuid); 23 | socketList.forEach((v, index) => { 24 | if (v.id === socket.id) socketList.splice(index, 1); 25 | }); 26 | } 27 | 28 | public forward(instanceUuid: string, data: any) { 29 | const sockets = this.listenMap.get(instanceUuid); 30 | sockets.forEach((socket) => { 31 | if (socket && socket.connected) socket.emit("instance/stdout", data); 32 | }); 33 | } 34 | 35 | public forwardViaCallback(instanceUuid: string, callback: (socket: Socket) => void) { 36 | if (this.listenMap.has(instanceUuid)) { 37 | const sockets = this.listenMap.get(instanceUuid); 38 | sockets.forEach((socket) => { 39 | if (socket && socket.connected) callback(socket); 40 | }); 41 | } 42 | } 43 | 44 | public hasListenInstance(instanceUuid: string) { 45 | return this.listenMap.has(instanceUuid) && this.listenMap.get(instanceUuid).length > 0; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/common/mcping.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | // Using SLT (Server List Ping) provided by Minecraft. 4 | // Since it is part of the protocol it is always enabled contrary to Query 5 | // More information at: https://wiki.vg/Server_List_Ping#Response 6 | // Github: https://github.com/Vaksty/mcping 7 | 8 | import net from "net"; 9 | 10 | function formatMotd(motd: any) { 11 | let noSpaces = motd.replace(/\u0000/g, ""); 12 | Buffer.from(noSpaces); 13 | // let noColor = noSpaces.toString().replace(/[^\x00-\x7F]/g, ''); 14 | // console.log(Buffer.from(motd, 'utf8').toString('hex')); 15 | // console.log(noColor); 16 | } 17 | 18 | export default class MCServStatus { 19 | public port: number; 20 | public host: string; 21 | public status: any; 22 | 23 | constructor(port: number, host: string) { 24 | this.port = port; 25 | this.host = host; 26 | this.status = { 27 | online: null, 28 | version: null, 29 | motd: null, 30 | current_players: null, 31 | max_players: null, 32 | latency: null 33 | }; 34 | } 35 | 36 | getStatus() { 37 | return new Promise((resolve, reject) => { 38 | var start_time = new Date().getTime(); 39 | const client = net.connect(this.port, this.host, () => { 40 | this.status.latency = Math.round(new Date().getTime() - start_time); 41 | // 0xFE packet identifier for a server list ping 42 | // 0x01 server list ping's payload (always 1) 43 | let data = Buffer.from([0xfe, 0x01]); 44 | client.write(data); 45 | }); 46 | 47 | // The client can also receive data from the server by reading from its socket. 48 | client.on("data", (response: any) => { 49 | // Check the readme for a simple explanation 50 | var server_info = response.toString().split("\x00\x00"); 51 | 52 | this.status = { 53 | host: this.host, 54 | port: this.port, 55 | status: true, 56 | version: server_info[2].replace(/\u0000/g, ""), 57 | motd: server_info[3].replace(/\u0000/g, ""), 58 | current_players: server_info[4].replace(/\u0000/g, ""), 59 | max_players: server_info[5].replace(/\u0000/g, ""), 60 | latency: this.status.latency 61 | }; 62 | formatMotd(server_info[3]); 63 | // Request an end to the connection after the data has been received. 64 | client.end(); 65 | resolve(this.status); 66 | }); 67 | 68 | client.on("end", () => { 69 | // console.log('Requested an end to the TCP connection'); 70 | }); 71 | 72 | client.on("error", (err: any) => { 73 | reject(err); 74 | }); 75 | }); 76 | } 77 | 78 | async asyncStatus() { 79 | let status = await this.getStatus(); 80 | return status; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/common/process_tools.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../i18n"; 4 | import { ChildProcess, exec, execSync, SpawnOptionsWithoutStdio } from "child_process"; 5 | import os from "os"; 6 | import child_process from "child_process"; 7 | import path from "path"; 8 | import EventEmitter from "events"; 9 | import iconv from "iconv-lite"; 10 | 11 | export class processWrapper extends EventEmitter { 12 | public process: ChildProcess; 13 | public pid: number; 14 | 15 | constructor( 16 | public readonly file: string, 17 | public readonly args: string[], 18 | public readonly cwd: string, 19 | public readonly timeout: number = null, 20 | public readonly code = "utf-8", 21 | public readonly option: SpawnOptionsWithoutStdio = {} 22 | ) { 23 | super(); 24 | } 25 | 26 | public start(): Promise { 27 | return new Promise((resolve, reject) => { 28 | let timeTask: NodeJS.Timeout = null; 29 | const process = child_process.spawn(this.file, this.args, { 30 | stdio: "pipe", 31 | windowsHide: true, 32 | cwd: path.normalize(this.cwd), 33 | ...this.option 34 | }); 35 | this.process = process; 36 | this.pid = process.pid; 37 | 38 | this.emit("start", process.pid); 39 | if (!process || !process.pid) return reject(false); 40 | 41 | process.stdout.on("data", (text) => this.emit("data", iconv.decode(text, this.code))); 42 | process.stderr.on("data", (text) => this.emit("data", iconv.decode(text, this.code))); 43 | process.on("exit", (code) => { 44 | try { 45 | this.emit("exit", code); 46 | this.destroy(); 47 | } catch (error) {} 48 | if (timeTask) clearTimeout(timeTask); 49 | if (code != 0) return resolve(false); 50 | return resolve(true); 51 | }); 52 | 53 | // timeout, terminate the task 54 | if (this.timeout) { 55 | timeTask = setTimeout(() => { 56 | killProcess(process.pid, process); 57 | reject(false); 58 | }, 1000 * this.timeout); 59 | } 60 | }); 61 | } 62 | 63 | public getPid() { 64 | return this.process.pid; 65 | } 66 | 67 | public write(data?: string) { 68 | return this.process.stdin.write(iconv.encode(data, this.code)); 69 | } 70 | 71 | public kill() { 72 | killProcess(this.process.pid, this.process); 73 | } 74 | 75 | public status() { 76 | return this.process.exitCode == null; 77 | } 78 | 79 | public exitCode() { 80 | return this.process.exitCode; 81 | } 82 | 83 | private async destroy() { 84 | try { 85 | for (const n of this.eventNames()) this.removeAllListeners(n); 86 | if (this.process.stdout) for (const eventName of this.process.stdout.eventNames()) this.process.stdout.removeAllListeners(eventName); 87 | if (this.process.stderr) for (const eventName of this.process.stderr.eventNames()) this.process.stderr.removeAllListeners(eventName); 88 | if (this.process) for (const eventName of this.process.eventNames()) this.process.removeAllListeners(eventName); 89 | this.process?.stdout?.destroy(); 90 | this.process?.stderr?.destroy(); 91 | if (this.process?.exitCode === null) { 92 | this.process.kill("SIGTERM"); 93 | this.process.kill("SIGKILL"); 94 | } 95 | } catch (error) { 96 | console.log("[ProcessWrapper destroy() Error]", error); 97 | } finally { 98 | this.process = null; 99 | } 100 | } 101 | } 102 | 103 | export function killProcess(pid: string | number, process: ChildProcess, signal?: any) { 104 | try { 105 | if (os.platform() === "win32") { 106 | execSync(`taskkill /PID ${pid} /T /F`); 107 | console.log($t("common.killProcess", { pid: pid })); 108 | return true; 109 | } 110 | if (os.platform() === "linux") { 111 | execSync(`kill -s 9 ${pid}`); 112 | console.log($t("common.killProcess", { pid: pid })); 113 | return true; 114 | } 115 | } catch (err) { 116 | return signal ? process.kill(signal) : process.kill("SIGKILL"); 117 | } 118 | return signal ? process.kill(signal) : process.kill("SIGKILL"); 119 | } 120 | -------------------------------------------------------------------------------- /src/common/query_wrapper.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | interface IMap { 4 | size: number; 5 | forEach: (value: any, key?: any) => void; 6 | } 7 | 8 | interface Page { 9 | page: number; 10 | pageSize: number; 11 | maxPage: number; 12 | total: number; 13 | data: T[]; 14 | } 15 | 16 | // This design can be used to connect to MYSQL, SQLITE and other databases 17 | // Provide the MAP query interface used by the routing layer 18 | export class QueryMapWrapper { 19 | constructor(public map: IMap) {} 20 | 21 | select(condition: (v: T) => boolean): T[] { 22 | const result: T[] = []; 23 | this.map.forEach((v: T) => { 24 | if (condition(v)) result.push(v); 25 | }); 26 | return result; 27 | } 28 | 29 | page(data: T[], page = 1, pageSize = 10) { 30 | const start = (page - 1) * pageSize; 31 | const end = start + pageSize; 32 | let size = data.length; 33 | let maxPage = 0; 34 | while (size > 0) { 35 | size -= pageSize; 36 | maxPage++; 37 | } 38 | return { 39 | page, 40 | pageSize, 41 | maxPage, 42 | data: data.slice(start, end) 43 | }; 44 | } 45 | } 46 | 47 | // Data source interface for QueryWrapper to use 48 | export interface IDataSource { 49 | selectPage: (condition: any, page: number, pageSize: number) => Page; 50 | select: (condition: any) => any[]; 51 | update: (condition: any, data: any) => void; 52 | delete: (condition: any) => void; 53 | insert: (data: any) => void; 54 | } 55 | 56 | // MYSQL data source 57 | export class MySqlSource implements IDataSource { 58 | selectPage: (condition: any, page: number, pageSize: number) => Page; 59 | select: (condition: any) => any[]; 60 | update: (condition: any, data: any) => void; 61 | delete: (condition: any) => void; 62 | insert: (data: any) => void; 63 | } 64 | 65 | // local file data source (embedded microdatabase) 66 | export class LocalFileSource implements IDataSource { 67 | constructor(public data: any) {} 68 | 69 | selectPage(condition: any, page = 1, pageSize = 10) { 70 | const result: T[] = []; 71 | this.data.forEach((v: any) => { 72 | for (const key in condition) { 73 | const dataValue = v[key]; 74 | const targetValue = condition[key]; 75 | if (targetValue[0] == "%") { 76 | if (!dataValue.includes(targetValue.slice(1, targetValue.length - 1))) return false; 77 | } else { 78 | if (targetValue !== dataValue) return false; 79 | } 80 | } 81 | result.push(v); 82 | }); 83 | return this.page(result, page, pageSize); 84 | } 85 | 86 | page(data: T[], page = 1, pageSize = 10) { 87 | const start = (page - 1) * pageSize; 88 | const end = start + pageSize; 89 | let size = data.length; 90 | let maxPage = 0; 91 | while (size > 0) { 92 | size -= pageSize; 93 | maxPage++; 94 | } 95 | return { 96 | page, 97 | pageSize, 98 | maxPage, 99 | total: data.length, 100 | data: data.slice(start, end) 101 | }; 102 | } 103 | 104 | select(condition: any): any[] { 105 | return null; 106 | } 107 | update(condition: any, data: any) {} 108 | delete(condition: any) {} 109 | insert(data: any) {} 110 | } 111 | 112 | // Provide the unified data query interface used by the routing layer 113 | export class QueryWrapper { 114 | constructor(public dataSource: IDataSource) {} 115 | 116 | selectPage(condition: any, page = 1, pageSize = 10) { 117 | return this.dataSource.selectPage(condition, page, pageSize); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/common/system_info.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import os from "os"; 4 | import osUtils from "os-utils"; 5 | import fs from "fs"; 6 | // import systeminformation from "systeminformation"; 7 | 8 | interface IInfoTable { 9 | [key: string]: number; 10 | } 11 | 12 | interface ISystemInfo { 13 | cpuUsage: number; 14 | memUsage: number; 15 | totalmem: number; 16 | freemem: number; 17 | type: string; 18 | hostname: string; 19 | platform: string; 20 | release: string; 21 | uptime: number; 22 | cwd: string; 23 | processCpu: number; 24 | processMem: number; 25 | loadavg: number[]; 26 | } 27 | 28 | // System details are updated every time 29 | const info: ISystemInfo = { 30 | type: os.type(), 31 | hostname: os.hostname(), 32 | platform: os.platform(), 33 | release: os.release(), 34 | uptime: os.uptime(), 35 | cwd: process.cwd(), 36 | loadavg: os.loadavg(), 37 | freemem: 0, 38 | cpuUsage: 0, 39 | memUsage: 0, 40 | totalmem: 0, 41 | processCpu: 0, 42 | processMem: 0 43 | }; 44 | 45 | // periodically refresh the cache 46 | setInterval(() => { 47 | if (os.platform() === "linux") { 48 | return setLinuxSystemInfo(); 49 | } 50 | if (os.platform() === "win32") { 51 | return setWindowsSystemInfo(); 52 | } 53 | return otherSystemInfo(); 54 | }, 3000); 55 | 56 | function otherSystemInfo() { 57 | info.freemem = os.freemem(); 58 | info.totalmem = os.totalmem(); 59 | info.memUsage = (os.totalmem() - os.freemem()) / os.totalmem(); 60 | osUtils.cpuUsage((p) => (info.cpuUsage = p)); 61 | } 62 | 63 | function setWindowsSystemInfo() { 64 | info.freemem = os.freemem(); 65 | info.totalmem = os.totalmem(); 66 | info.memUsage = (os.totalmem() - os.freemem()) / os.totalmem(); 67 | osUtils.cpuUsage((p) => (info.cpuUsage = p)); 68 | } 69 | 70 | function setLinuxSystemInfo() { 71 | try { 72 | // read memory data based on /proc/meminfo 73 | const data = fs.readFileSync("/proc/meminfo", { encoding: "utf-8" }); 74 | const list = data.split("\n"); 75 | const infoTable: IInfoTable = {}; 76 | list.forEach((line) => { 77 | const kv = line.split(":"); 78 | if (kv.length === 2) { 79 | const k = kv[0].replace(/ /gim, "").replace(/\t/gim, "").trim().toLowerCase(); 80 | let v = kv[1].replace(/ /gim, "").replace(/\t/gim, "").trim().toLowerCase(); 81 | v = v.replace(/kb/gim, "").replace(/mb/gim, "").replace(/gb/gim, ""); 82 | let vNumber = parseInt(v); 83 | if (isNaN(vNumber)) vNumber = 0; 84 | infoTable[k] = vNumber; 85 | } 86 | }); 87 | const memAvailable = infoTable["memavailable"] ?? infoTable["memfree"]; 88 | const memTotal = infoTable["memtotal"]; 89 | info.freemem = memAvailable * 1024; 90 | info.totalmem = memTotal * 1024; 91 | info.memUsage = (info.totalmem - info.freemem) / info.totalmem; 92 | osUtils.cpuUsage((p) => (info.cpuUsage = p)); 93 | } catch (error) { 94 | // If the reading is wrong, the default general reading method is automatically used 95 | otherSystemInfo(); 96 | } 97 | } 98 | 99 | export function systemInfo() { 100 | return info; 101 | } 102 | -------------------------------------------------------------------------------- /src/common/system_storage.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../i18n"; 4 | import path from "path"; 5 | import fs from "fs-extra"; 6 | 7 | interface IClassz { 8 | name: string; 9 | } 10 | 11 | class StorageSubsystem { 12 | public static readonly STIRAGE_DATA_PATH = path.normalize(path.join(process.cwd(), "data")); 13 | public static readonly STIRAGE_INDEX_PATH = path.normalize(path.join(process.cwd(), "data", "index")); 14 | 15 | private checkFileName(name: string) { 16 | const blackList = ["\\", "/", ".."]; 17 | for (const ch of blackList) { 18 | if (name.includes(ch)) return false; 19 | } 20 | return true; 21 | } 22 | 23 | /** 24 | * Stored in local file based on class definition and identifier 25 | */ 26 | public store(category: string, uuid: string, object: any) { 27 | const dirPath = path.join(StorageSubsystem.STIRAGE_DATA_PATH, category); 28 | if (!fs.existsSync(dirPath)) fs.mkdirsSync(dirPath); 29 | if (!this.checkFileName(uuid)) throw new Error($t("common.uuidIrregular", { uuid: uuid })); 30 | const filePath = path.join(dirPath, `${uuid}.json`); 31 | const data = JSON.stringify(object, null, 4); 32 | fs.writeFileSync(filePath, data, { encoding: "utf-8" }); 33 | } 34 | 35 | // deep copy of the primitive type with the copy target as the prototype 36 | // target copy target object copy source 37 | protected defineAttr(target: any, object: any): any { 38 | for (const v of Object.keys(target)) { 39 | const objectValue = object[v]; 40 | if (objectValue === undefined) continue; 41 | if (objectValue instanceof Array) { 42 | target[v] = objectValue; 43 | continue; 44 | } 45 | if (objectValue instanceof Object && typeof objectValue === "object") { 46 | this.defineAttr(target[v], objectValue); 47 | continue; 48 | } 49 | target[v] = objectValue; 50 | } 51 | return target; 52 | } 53 | 54 | /** 55 | * Instantiate an object based on the class definition and identifier 56 | */ 57 | public load(category: string, classz: any, uuid: string) { 58 | const dirPath = path.join(StorageSubsystem.STIRAGE_DATA_PATH, category); 59 | if (!fs.existsSync(dirPath)) fs.mkdirsSync(dirPath); 60 | if (!this.checkFileName(uuid)) throw new Error($t("common.uuidIrregular", { uuid: uuid })); 61 | const filePath = path.join(dirPath, `${uuid}.json`); 62 | if (!fs.existsSync(filePath)) return null; 63 | const data = fs.readFileSync(filePath, { encoding: "utf-8" }); 64 | const dataObject = JSON.parse(data); 65 | const target = new classz(); 66 | // for (const v of Object. keys(target)) { 67 | // if (dataObject[v] !== undefined) target[v] = dataObject[v]; 68 | // } 69 | // deep object copy 70 | return this.defineAttr(target, dataObject); 71 | } 72 | 73 | /** 74 | * Return all identifiers related to this class through the class definition 75 | */ 76 | public list(category: string) { 77 | const dirPath = path.join(StorageSubsystem.STIRAGE_DATA_PATH, category); 78 | if (!fs.existsSync(dirPath)) fs.mkdirsSync(dirPath); 79 | const files = fs.readdirSync(dirPath); 80 | const result = new Array(); 81 | files.forEach((name) => { 82 | result.push(name.replace(path.extname(name), "")); 83 | }); 84 | return result; 85 | } 86 | 87 | /** 88 | * Delete an identifier instance of the specified type through the class definition 89 | */ 90 | public delete(category: string, uuid: string) { 91 | const filePath = path.join(StorageSubsystem.STIRAGE_DATA_PATH, category, `${uuid}.json`); 92 | if (!fs.existsSync(filePath)) return; 93 | fs.removeSync(filePath); 94 | } 95 | } 96 | 97 | export default new StorageSubsystem(); 98 | -------------------------------------------------------------------------------- /src/common/typecheck.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | export function configureEntityParams(self: any, args: any, key: string, typeFn?: Function): any { 4 | const selfDefaultValue = self[key] ?? null; 5 | const v = args[key] != null ? args[key] : selfDefaultValue; 6 | 7 | if (typeFn === Number) { 8 | if (v === "" || v == null) { 9 | self[key] = null; 10 | } else { 11 | if (isNaN(Number(v))) throw new Error(`ConfigureEntityParams Error: Expected type to be Number, but got ${typeof v}`); 12 | self[key] = Number(v); 13 | } 14 | return; 15 | } 16 | 17 | if (typeFn === String) { 18 | if (v == null) { 19 | self[key] = null; 20 | } else { 21 | self[key] = String(v); 22 | } 23 | return; 24 | } 25 | 26 | if (typeFn === Boolean) { 27 | if (v == null) { 28 | self[key] = false; 29 | } else { 30 | self[key] = Boolean(v); 31 | } 32 | return; 33 | } 34 | 35 | if (typeFn === Array) { 36 | if (v == null) return (self[key] = null); 37 | if (!(v instanceof Array)) throw new Error(`ConfigureEntityParams Error: Expected type to be Array, but got ${typeof v}`); 38 | return; 39 | } 40 | 41 | if (typeFn) { 42 | self[key] = typeFn(v); 43 | } else { 44 | self[key] = v; 45 | } 46 | } 47 | 48 | export function toText(v: any) { 49 | if (isEmpty(v)) return null; 50 | return String(v); 51 | } 52 | 53 | export function toNumber(v: any) { 54 | if (isEmpty(v)) return null; 55 | if (isNaN(Number(v))) return null; 56 | return Number(v); 57 | } 58 | 59 | export function toBoolean(v: any) { 60 | if (isEmpty(v)) return null; 61 | return Boolean(v); 62 | } 63 | 64 | export function isEmpty(v: any) { 65 | return v === null || v === undefined; 66 | } 67 | 68 | export function supposeValue(v: any, def: any = null) { 69 | if (isEmpty(v)) return def; 70 | return v; 71 | } 72 | -------------------------------------------------------------------------------- /src/const.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import os from "os"; 4 | import path from "path"; 5 | 6 | const SYS_INFO = `${os.platform()}_${os.arch()}${os.platform() === "win32" ? ".exe" : ""}`; 7 | const ptyName = `pty_${SYS_INFO}`; 8 | const frpcName = `frpc_${SYS_INFO}`; 9 | 10 | const PTY_PATH = path.normalize(path.join(process.cwd(), "lib", ptyName)); 11 | 12 | const FRPC_PATH = path.normalize(path.join(process.cwd(), "lib", frpcName)); 13 | 14 | const FILENAME_BLACKLIST = ["\\", "/", ".", "'", '"', "?", "*", "<", ">"]; 15 | 16 | const LOCAL_PRESET_LANG_PATH = path.normalize(path.join(process.cwd(), "language")); 17 | 18 | const IGNORE = "[IGNORE_LOG]"; 19 | 20 | export { FILENAME_BLACKLIST, PTY_PATH, LOCAL_PRESET_LANG_PATH, FRPC_PATH, IGNORE }; 21 | -------------------------------------------------------------------------------- /src/entity/commands/base/command.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | export default class InstanceCommand { 4 | constructor(public info?: string) {} 5 | async exec(instance: any): Promise {} 6 | async stop(instance: any) {} 7 | } 8 | -------------------------------------------------------------------------------- /src/entity/commands/base/command_parser.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../../../i18n"; 4 | 5 | export function commandStringToArray(cmd: string) { 6 | const QUOTES_KEY = "{quotes}"; 7 | let start = 0; 8 | let len = cmd.length; 9 | const cmdArray: string[] = []; 10 | function _analyze() { 11 | for (let index = start; index < len; index++) { 12 | const ch = cmd[index]; 13 | if (ch === " ") { 14 | findSpace(index); 15 | start++; 16 | continue; 17 | } 18 | if (ch === '"') { 19 | index = findQuotes(index); 20 | } 21 | if (index + 1 >= len) { 22 | findEnd(); 23 | break; 24 | } 25 | } 26 | } 27 | 28 | function findEnd() { 29 | return cmdArray.push(cmd.slice(start)); 30 | } 31 | 32 | function findSpace(endPoint: number) { 33 | if (endPoint != start) { 34 | const elem = cmd.slice(start, endPoint); 35 | start = endPoint; 36 | return cmdArray.push(elem); 37 | } 38 | } 39 | 40 | function findQuotes(p: number) { 41 | for (let index = p + 1; index < len; index++) { 42 | const ch = cmd[index]; 43 | if (ch === '"') return index; 44 | } 45 | throw new Error($t("command.quotes")); 46 | } 47 | 48 | _analyze(); 49 | 50 | if (cmdArray.length == 0) { 51 | return []; 52 | } 53 | 54 | for (const index in cmdArray) { 55 | const element = cmdArray[index]; 56 | if (element[0] === '"' && element[element.length - 1] === '"') cmdArray[index] = element.slice(1, element.length - 1); 57 | while (cmdArray[index].indexOf(QUOTES_KEY) != -1) cmdArray[index] = cmdArray[index].replace(QUOTES_KEY, '"'); 58 | } 59 | 60 | return cmdArray; 61 | } 62 | -------------------------------------------------------------------------------- /src/entity/commands/cmd.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import Instance from "../instance/instance"; 4 | import InstanceCommand from "./base/command"; 5 | 6 | export default class SendCommand extends InstanceCommand { 7 | public cmd: string; 8 | 9 | constructor(cmd: string) { 10 | super("SendCommand"); 11 | this.cmd = cmd; 12 | } 13 | 14 | async exec(instance: Instance) { 15 | return await instance.execPreset("command", this.cmd); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/entity/commands/dispatcher.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import path from "path"; 4 | import fs from "fs-extra"; 5 | import Instance from "../instance/instance"; 6 | import InstanceCommand from "./base/command"; 7 | import RefreshPlayer from "./task/players"; 8 | import MinecraftGetPlayersCommand from "../minecraft/mc_getplayer"; 9 | import NullCommand from "./nullfunc"; 10 | import GeneralStartCommand from "./general/general_start"; 11 | import GeneralStopCommand from "./general/general_stop"; 12 | import GeneralKillCommand from "./general/general_kill"; 13 | import GeneralSendCommand from "./general/general_command"; 14 | import GeneralRestartCommand from "./general/general_restart"; 15 | import DockerStartCommand from "./docker/docker_start"; 16 | import DockerResizeCommand from "./docker/docker_resize"; 17 | import TimeCheck from "./task/time"; 18 | import MinecraftBedrockGetPlayersCommand from "../minecraft/mc_getplayer_bedrock"; 19 | import GeneralUpdateCommand from "./general/general_update"; 20 | import PtyStartCommand from "./pty/pty_start"; 21 | import PtyStopCommand from "./pty/pty_stop"; 22 | import { PTY_PATH } from "../../const"; 23 | import OpenFrpTask from "./task/openfrp"; 24 | 25 | // instance function scheduler 26 | // Dispatch and assign different functions according to different types 27 | export default class FunctionDispatcher extends InstanceCommand { 28 | constructor() { 29 | super("FunctionDispatcher"); 30 | } 31 | 32 | async exec(instance: Instance) { 33 | // initialize all modules 34 | instance.lifeCycleTaskManager.clearLifeCycleTask(); 35 | instance.clearPreset(); 36 | 37 | // the component that the instance must mount 38 | instance.lifeCycleTaskManager.registerLifeCycleTask(new TimeCheck()); 39 | instance.lifeCycleTaskManager.registerLifeCycleTask(new OpenFrpTask()); 40 | 41 | // Instance general preset capabilities 42 | instance.setPreset("command", new GeneralSendCommand()); 43 | instance.setPreset("stop", new GeneralStopCommand()); 44 | instance.setPreset("kill", new GeneralKillCommand()); 45 | instance.setPreset("restart", new GeneralRestartCommand()); 46 | instance.setPreset("update", new GeneralUpdateCommand()); 47 | 48 | // Preset the basic operation mode according to the instance startup type 49 | if (!instance.config.processType || instance.config.processType === "general") { 50 | instance.setPreset("start", new GeneralStartCommand()); 51 | } 52 | 53 | // Enable emulated terminal mode 54 | if ( 55 | instance.config.terminalOption.pty && 56 | instance.config.terminalOption.ptyWindowCol && 57 | instance.config.terminalOption.ptyWindowRow && 58 | instance.config.processType === "general" 59 | ) { 60 | instance.setPreset("start", new PtyStartCommand()); 61 | instance.setPreset("stop", new PtyStopCommand()); 62 | instance.setPreset("resize", new NullCommand()); 63 | } 64 | // Whether to enable Docker PTY mode 65 | if (instance.config.processType === "docker") { 66 | instance.setPreset("resize", new NullCommand()); 67 | instance.setPreset("start", new DockerStartCommand()); 68 | } 69 | 70 | // Set different preset functions and functions according to different types 71 | if (instance.config.type.includes(Instance.TYPE_UNIVERSAL)) { 72 | instance.setPreset("getPlayer", new NullCommand()); 73 | } 74 | if (instance.config.type.includes(Instance.TYPE_MINECRAFT_JAVA)) { 75 | instance.setPreset("getPlayer", new MinecraftGetPlayersCommand()); 76 | instance.lifeCycleTaskManager.registerLifeCycleTask(new RefreshPlayer()); 77 | } 78 | if (instance.config.type.includes(Instance.TYPE_MINECRAFT_BEDROCK)) { 79 | instance.setPreset("getPlayer", new MinecraftBedrockGetPlayersCommand()); 80 | instance.lifeCycleTaskManager.registerLifeCycleTask(new RefreshPlayer()); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/entity/commands/docker/docker_resize.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager and RimuruChan 2 | 3 | import Instance from "../../instance/instance"; 4 | import InstanceCommand from "../base/command"; 5 | import { DockerProcessAdapter } from "./docker_start"; 6 | 7 | export interface IResizeOptions { 8 | h: number; 9 | w: number; 10 | } 11 | 12 | export default class DockerResizeCommand extends InstanceCommand { 13 | constructor() { 14 | super("ResizeTTY"); 15 | } 16 | 17 | async exec(instance: Instance, size?: IResizeOptions): Promise { 18 | if (!instance.process || !(instance.config.processType === "docker")) return; 19 | const dockerProcess = instance.process; 20 | await dockerProcess?.container?.resize({ 21 | h: size.h, 22 | w: size.w 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/entity/commands/docker/docker_start.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | import { $t } from "../../../i18n"; 3 | import Instance from "../../instance/instance"; 4 | import InstanceCommand from "../base/command"; 5 | import Docker from "dockerode"; 6 | import logger from "../../../service/log"; 7 | import { EventEmitter } from "events"; 8 | import { IInstanceProcess } from "../../instance/interface"; 9 | import fs from "fs-extra"; 10 | import { commandStringToArray } from "../base/command_parser"; 11 | import path from "path"; 12 | 13 | // user identity function 14 | const processUserUid = process.getuid ? process.getuid : () => 0; 15 | const processGroupGid = process.getgid ? process.getgid : () => 0; 16 | 17 | // Error exception at startup 18 | class StartupDockerProcessError extends Error { 19 | constructor(msg: string) { 20 | super(msg); 21 | } 22 | } 23 | 24 | interface IDockerProcessAdapterStartParam { 25 | isTty: boolean; 26 | h: number; 27 | w: number; 28 | } 29 | 30 | // process adapter 31 | export class DockerProcessAdapter extends EventEmitter implements IInstanceProcess { 32 | pid?: number | string; 33 | 34 | private stream: NodeJS.ReadWriteStream; 35 | 36 | constructor(public container: Docker.Container) { 37 | super(); 38 | } 39 | 40 | // Once the program is actually started, no errors can block the next startup process 41 | public async start(param?: IDockerProcessAdapterStartParam) { 42 | try { 43 | await this.container.start(); 44 | 45 | const { isTty, h, w } = param; 46 | if (isTty) { 47 | this.container.resize({ h, w }); 48 | } 49 | 50 | this.pid = this.container.id; 51 | const stream = (this.stream = await this.container.attach({ 52 | stream: true, 53 | stdout: true, 54 | stderr: true, 55 | stdin: true 56 | })); 57 | stream.on("data", (data) => this.emit("data", data)); 58 | stream.on("error", (data) => this.emit("data", data)); 59 | this.wait(); 60 | } catch (error) { 61 | this.kill(); 62 | throw error; 63 | } 64 | } 65 | 66 | public write(data?: string) { 67 | if (this.stream) this.stream.write(data); 68 | } 69 | 70 | public async kill(s?: string) { 71 | await this.container.kill(); 72 | return true; 73 | } 74 | 75 | public async destroy() { 76 | try { 77 | await this.container.remove(); 78 | } catch (error) {} 79 | } 80 | 81 | private wait() { 82 | this.container.wait(async (v) => { 83 | await this.destroy(); 84 | this.emit("exit", v); 85 | }); 86 | } 87 | } 88 | 89 | export default class DockerStartCommand extends InstanceCommand { 90 | constructor() { 91 | super("DockerStartCommand"); 92 | } 93 | 94 | async exec(instance: Instance, source = "Unknown") { 95 | if (!instance.config.cwd || !instance.config.ie || !instance.config.oe) 96 | return instance.failure(new StartupDockerProcessError($t("instance.dirEmpty"))); 97 | if (!fs.existsSync(instance.absoluteCwdPath())) return instance.failure(new StartupDockerProcessError($t("instance.dirNoE"))); 98 | 99 | // command parsing 100 | const commandList = commandStringToArray(instance.config.startCommand); 101 | const cwd = instance.absoluteCwdPath(); 102 | 103 | // parsing port open 104 | // { 105 | // "PortBindings": { 106 | // "22/tcp": [ 107 | // { 108 | // "HostPort": "11022" 109 | // } 110 | // ] 111 | // } 112 | // } 113 | // 25565:25565/tcp 8080:8080/tcp 114 | const portMap = instance.config.docker.ports; 115 | const publicPortArray: any = {}; 116 | const exposedPorts: any = {}; 117 | for (const iterator of portMap) { 118 | const elemt = iterator.split("/"); 119 | if (elemt.length != 2) continue; 120 | const ports = elemt[0]; 121 | const protocol = elemt[1]; 122 | //Host (host) port: container port 123 | const publicAndPrivatePort = ports.split(":"); 124 | if (publicAndPrivatePort.length != 2) continue; 125 | publicPortArray[`${publicAndPrivatePort[1]}/${protocol}`] = [{ HostPort: publicAndPrivatePort[0] }]; 126 | exposedPorts[`${publicAndPrivatePort[1]}/${protocol}`] = {}; 127 | } 128 | 129 | // resolve extra path mounts 130 | const extraVolumes = instance.config.docker.extraVolumes; 131 | const extraBinds = []; 132 | for (const it of extraVolumes) { 133 | if (!it) continue; 134 | const element = it.split(":"); 135 | if (element.length < 2) continue; 136 | let hostPath = element[0]; 137 | let containerPath = element.slice(1).join(":"); 138 | 139 | if (path.isAbsolute(containerPath)) { 140 | containerPath = path.normalize(containerPath); 141 | } else { 142 | containerPath = path.normalize(path.join("/workspace/", containerPath)); 143 | } 144 | if (path.isAbsolute(hostPath)) { 145 | hostPath = path.normalize(hostPath); 146 | } else { 147 | hostPath = path.normalize(path.join(process.cwd(), hostPath)); 148 | } 149 | extraBinds.push(`${hostPath}:${containerPath}`); 150 | } 151 | 152 | // memory limit 153 | let maxMemory = undefined; 154 | if (instance.config.docker.memory) maxMemory = instance.config.docker.memory * 1024 * 1024; 155 | 156 | // CPU usage calculation 157 | let cpuQuota = undefined; 158 | let cpuPeriod = undefined; 159 | if (instance.config.docker.cpuUsage) { 160 | cpuQuota = instance.config.docker.cpuUsage * 10 * 1000; 161 | cpuPeriod = 1000 * 1000; 162 | } 163 | 164 | // Check the number of CPU cores 165 | let cpusetCpus = undefined; 166 | if (instance.config.docker.cpusetCpus) { 167 | const arr = instance.config.docker.cpusetCpus.split(","); 168 | arr.forEach((v) => { 169 | if (isNaN(Number(v))) throw new Error($t("instance.invalidCpu", { v })); 170 | }); 171 | cpusetCpus = instance.config.docker.cpusetCpus; 172 | // Note: check 173 | } 174 | 175 | // container name check 176 | let containerName = instance.config.docker.containerName; 177 | if (containerName && (containerName.length > 64 || containerName.length < 2)) { 178 | throw new Error($t("instance.invalidContainerName", { v: containerName })); 179 | } 180 | 181 | // output startup log 182 | logger.info("----------------"); 183 | logger.info(`Session ${source}: Request to start an instance`); 184 | logger.info(`UUID: [${instance.instanceUuid}] [${instance.config.nickname}]`); 185 | logger.info(`NAME: [${containerName}]`); 186 | logger.info(`COMMAND: ${commandList.join(" ")}`); 187 | logger.info(`WORKSPACE: ${cwd}`); 188 | logger.info(`NET_MODE: ${instance.config.docker.networkMode}`); 189 | logger.info(`OPEN_PORT: ${JSON.stringify(publicPortArray)}`); 190 | logger.info(`EXT_MOUNT: ${JSON.stringify(extraBinds)}`); 191 | logger.info(`NET_ALIASES: ${JSON.stringify(instance.config.docker.networkAliases)}`); 192 | logger.info(`MEM_LIMIT: ${maxMemory} MB`); 193 | logger.info(`TYPE: Docker Container`); 194 | logger.info("----------------"); 195 | 196 | // Whether to use TTY mode 197 | const isTty = instance.config.terminalOption.pty; 198 | 199 | // Start Docker container creation and running 200 | const docker = new Docker(); 201 | const container = await docker.createContainer({ 202 | name: containerName, 203 | Image: instance.config.docker.image, 204 | AttachStdin: true, 205 | AttachStdout: true, 206 | AttachStderr: true, 207 | Tty: isTty, 208 | User: `${processUserUid()}:${processGroupGid()}`, 209 | WorkingDir: "/workspace/", 210 | Cmd: commandList, 211 | OpenStdin: true, 212 | StdinOnce: false, 213 | ExposedPorts: exposedPorts, 214 | HostConfig: { 215 | Memory: maxMemory, 216 | Binds: [`${cwd}:/workspace/`, ...extraBinds], 217 | AutoRemove: true, 218 | CpusetCpus: cpusetCpus, 219 | CpuPeriod: cpuPeriod, 220 | CpuQuota: cpuQuota, 221 | PortBindings: publicPortArray, 222 | NetworkMode: instance.config.docker.networkMode 223 | }, 224 | NetworkingConfig: { 225 | EndpointsConfig: { 226 | [instance.config.docker.networkMode]: { 227 | Aliases: instance.config.docker.networkAliases 228 | } 229 | } 230 | } 231 | }); 232 | 233 | // Docker docks to the process adapter 234 | const processAdapter = new DockerProcessAdapter(container); 235 | await processAdapter.start({ 236 | isTty, 237 | w: instance.config.terminalOption.ptyWindowCol, 238 | h: instance.config.terminalOption.ptyWindowCol 239 | }); 240 | 241 | instance.started(processAdapter); 242 | logger.info($t("instance.successful", { v: `${instance.config.nickname} ${instance.instanceUuid}` })); 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/entity/commands/general/general_command.ts: -------------------------------------------------------------------------------- 1 | import { $t } from "../../../i18n"; 2 | // Copyright (C) 2022 MCSManager 3 | 4 | import Instance from "../../instance/instance"; 5 | import { encode } from "iconv-lite"; 6 | import InstanceCommand from "../base/command"; 7 | 8 | export default class GeneralSendCommand extends InstanceCommand { 9 | constructor() { 10 | super("SendCommand"); 11 | } 12 | 13 | async exec(instance: Instance, buf?: any): Promise { 14 | // 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. 15 | // So here the command can only be executed by whether the process exists or not 16 | if (!instance.process) instance.failure(new Error($t("command.instanceNotOpen"))); 17 | 18 | instance.process.write(encode(buf, instance.config.oe)); 19 | if (instance.config.crlf === 2) return instance.process.write("\r\n"); 20 | return instance.process.write("\n"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/entity/commands/general/general_kill.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import Instance from "../../instance/instance"; 4 | import InstanceCommand from "../base/command"; 5 | 6 | export default class GeneralKillCommand extends InstanceCommand { 7 | constructor() { 8 | super("KillCommand"); 9 | } 10 | 11 | async exec(instance: Instance) { 12 | if (instance.process) { 13 | await instance.process.kill("SIGKILL"); 14 | } 15 | instance.setLock(false); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/entity/commands/general/general_restart.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | import { $t } from "../../../i18n"; 3 | import Instance from "../../instance/instance"; 4 | import InstanceCommand from "../base/command"; 5 | 6 | export default class GeneralRestartCommand extends InstanceCommand { 7 | constructor() { 8 | super("GeneralRestartCommand"); 9 | } 10 | 11 | async exec(instance: Instance) { 12 | try { 13 | instance.println("INFO", $t("restart.start")); 14 | await instance.execPreset("stop"); 15 | instance.setLock(true); 16 | const startCount = instance.startCount; 17 | // Check the instance status every second, 18 | // if the instance status is stopped, restart the server immediately 19 | const task = setInterval(async () => { 20 | try { 21 | if (startCount !== instance.startCount) { 22 | throw new Error($t("restart.error1")); 23 | } 24 | if (instance.status() !== Instance.STATUS_STOPPING && instance.status() !== Instance.STATUS_STOP) { 25 | throw new Error($t("restart.error2")); 26 | } 27 | if (instance.status() === Instance.STATUS_STOP) { 28 | instance.println("INFO", $t("restart.restarting")); 29 | await instance.execPreset("start"); 30 | instance.setLock(false); 31 | clearInterval(task); 32 | } 33 | } catch (error) { 34 | clearInterval(task); 35 | instance.setLock(false); 36 | instance.failure(error); 37 | } 38 | }, 1000); 39 | } catch (error) { 40 | instance.setLock(false); 41 | instance.failure(error); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/entity/commands/general/general_start.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../../../i18n"; 4 | import os from "os"; 5 | import Instance from "../../instance/instance"; 6 | import logger from "../../../service/log"; 7 | import fs from "fs-extra"; 8 | import InstanceCommand from "../base/command"; 9 | import EventEmitter from "events"; 10 | import { IInstanceProcess } from "../../instance/interface"; 11 | import { ChildProcess, exec, spawn } from "child_process"; 12 | import { commandStringToArray } from "../base/command_parser"; 13 | import { killProcess } from "../../../common/process_tools"; 14 | 15 | // Error exception at startup 16 | class StartupError extends Error { 17 | constructor(msg: string) { 18 | super(msg); 19 | } 20 | } 21 | 22 | // Docker process adapter 23 | class ProcessAdapter extends EventEmitter implements IInstanceProcess { 24 | pid?: number | string; 25 | 26 | constructor(private process: ChildProcess) { 27 | super(); 28 | this.pid = this.process.pid; 29 | process.stdout.on("data", (text) => this.emit("data", text)); 30 | process.stderr.on("data", (text) => this.emit("data", text)); 31 | process.on("exit", (code) => this.emit("exit", code)); 32 | } 33 | 34 | public write(data?: string) { 35 | return this.process.stdin.write(data); 36 | } 37 | 38 | public kill(s?: any) { 39 | return killProcess(this.pid, this.process, s); 40 | } 41 | 42 | public async destroy() { 43 | try { 44 | // remove all dynamically added event listeners 45 | for (const n of this.eventNames()) this.removeAllListeners(n); 46 | if (this.process.stdout) for (const eventName of this.process.stdout.eventNames()) this.process.stdout.removeAllListeners(eventName); 47 | if (this.process.stderr) for (const eventName of this.process.stderr.eventNames()) this.process.stderr.removeAllListeners(eventName); 48 | if (this.process) for (const eventName of this.process.eventNames()) this.process.removeAllListeners(eventName); 49 | this.process?.stdout?.destroy(); 50 | this.process?.stderr?.destroy(); 51 | if (this.process?.exitCode === null) { 52 | this.process.kill("SIGTERM"); 53 | this.process.kill("SIGKILL"); 54 | } 55 | } catch (error) {} 56 | } 57 | } 58 | 59 | export default class GeneralStartCommand extends InstanceCommand { 60 | constructor() { 61 | super("StartCommand"); 62 | } 63 | 64 | async exec(instance: Instance, source = "Unknown") { 65 | if ( 66 | (!instance.config.startCommand && instance.config.processType === "general") || 67 | !instance.config.cwd || 68 | !instance.config.ie || 69 | !instance.config.oe 70 | ) 71 | return instance.failure(new StartupError($t("general_start.instanceConfigErr"))); 72 | if (!fs.existsSync(instance.absoluteCwdPath())) return instance.failure(new StartupError($t("general_start.cwdPathNotExist"))); 73 | 74 | // command parsing 75 | const commandList = commandStringToArray(instance.config.startCommand); 76 | const commandExeFile = commandList[0]; 77 | const commandParameters = commandList.slice(1); 78 | if (commandList.length === 0) { 79 | return instance.failure(new StartupError($t("general_start.cmdEmpty"))); 80 | } 81 | 82 | logger.info("----------------"); 83 | logger.info($t("general_start.startInstance", { source: source })); 84 | logger.info($t("general_start.instanceUuid", { uuid: instance.instanceUuid })); 85 | logger.info($t("general_start.startCmd", { cmdList: JSON.stringify(commandList) })); 86 | logger.info($t("general_start.cwd", { cwd: instance.config.cwd })); 87 | logger.info("----------------"); 88 | 89 | // create child process 90 | // Parameter 1 directly passes the process name or path (including spaces) without double quotes 91 | const process = spawn(commandExeFile, commandParameters, { 92 | cwd: instance.config.cwd, 93 | stdio: "pipe", 94 | windowsHide: true 95 | }); 96 | 97 | // child process creation result check 98 | if (!process || !process.pid) { 99 | instance.println( 100 | "ERROR", 101 | $t("general_start.pidErr", { 102 | startCommand: instance.config.startCommand, 103 | commandExeFile: commandExeFile, 104 | commandParameters: JSON.stringify(commandParameters) 105 | }) 106 | ); 107 | throw new StartupError($t("general_start.startErr")); 108 | } 109 | 110 | // create process adapter 111 | const processAdapter = new ProcessAdapter(process); 112 | 113 | // generate open event 114 | instance.started(processAdapter); 115 | logger.info($t("general_start.startSuccess", { instanceUuid: instance.instanceUuid, pid: process.pid })); 116 | instance.println("INFO", $t("general_start.startOrdinaryTerminal")); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/entity/commands/general/general_stop.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../../../i18n"; 4 | import Instance from "../../instance/instance"; 5 | import InstanceCommand from "../base/command"; 6 | import SendCommand from "../cmd"; 7 | 8 | export default class GeneralStopCommand extends InstanceCommand { 9 | constructor() { 10 | super("StopCommand"); 11 | } 12 | 13 | async exec(instance: Instance) { 14 | const stopCommand = instance.config.stopCommand; 15 | if (instance.status() === Instance.STATUS_STOP || !instance.process) return instance.failure(new Error($t("general_stop.notRunning"))); 16 | 17 | instance.status(Instance.STATUS_STOPPING); 18 | 19 | const stopCommandList = stopCommand.split("\n"); 20 | for (const stopCommandColumn of stopCommandList) { 21 | if (stopCommandColumn.toLocaleLowerCase() == "^c") { 22 | instance.process.kill("SIGINT"); 23 | } else { 24 | await instance.exec(new SendCommand(stopCommandColumn)); 25 | } 26 | } 27 | 28 | instance.println("INFO", $t("general_stop.execCmd", { stopCommand })); 29 | const cacheStartCount = instance.startCount; 30 | 31 | // If the instance is still in the stopped state after 10 minutes, restore the state 32 | setTimeout(() => { 33 | if (instance.status() === Instance.STATUS_STOPPING && instance.startCount === cacheStartCount) { 34 | instance.println("ERROR", $t("general_stop.stopErr")); 35 | instance.status(Instance.STATUS_RUNNING); 36 | } 37 | }, 1000 * 60 * 10); 38 | 39 | return instance; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/entity/commands/general/general_update.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../../../i18n"; 4 | import { killProcess } from "../../../common/process_tools"; 5 | import { ChildProcess, exec, spawn } from "child_process"; 6 | import logger from "../../../service/log"; 7 | import Instance from "../../instance/instance"; 8 | import InstanceCommand from "../base/command"; 9 | import { commandStringToArray } from "../base/command_parser"; 10 | import iconv from "iconv-lite"; 11 | export default class GeneralUpdateCommand extends InstanceCommand { 12 | private pid: number = null; 13 | private process: ChildProcess = null; 14 | 15 | constructor() { 16 | super("GeneralUpdateCommand"); 17 | } 18 | 19 | private stopped(instance: Instance) { 20 | instance.asynchronousTask = null; 21 | instance.setLock(false); 22 | instance.status(Instance.STATUS_STOP); 23 | } 24 | 25 | async exec(instance: Instance) { 26 | if (instance.status() !== Instance.STATUS_STOP) return instance.failure(new Error($t("general_update.statusErr_notStop"))); 27 | if (instance.asynchronousTask !== null) return instance.failure(new Error($t("general_update.statusErr_otherProgress"))); 28 | try { 29 | instance.setLock(true); 30 | let updateCommand = instance.config.updateCommand; 31 | updateCommand = updateCommand.replace(/\$\{mcsm_workspace\}/gm, instance.config.cwd); 32 | logger.info($t("general_update.readyUpdate", { instanceUuid: instance.instanceUuid })); 33 | logger.info($t("general_update.updateCmd", { instanceUuid: instance.instanceUuid })); 34 | logger.info(updateCommand); 35 | 36 | // command parsing 37 | const commandList = commandStringToArray(updateCommand); 38 | const commandExeFile = commandList[0]; 39 | const commnadParameters = commandList.slice(1); 40 | if (commandList.length === 0) { 41 | return instance.failure(new Error($t("general_update.cmdFormatErr"))); 42 | } 43 | 44 | // start the update command 45 | const process = spawn(commandExeFile, commnadParameters, { 46 | cwd: instance.config.cwd, 47 | stdio: "pipe", 48 | windowsHide: true 49 | }); 50 | if (!process || !process.pid) { 51 | this.stopped(instance); 52 | return instance.println($t("general_update.err"), $t("general_update.updateFailed")); 53 | } 54 | 55 | // process & pid 56 | this.pid = process.pid; 57 | this.process = process; 58 | 59 | // Set the asynchronous task that the instance is running 60 | instance.asynchronousTask = this; 61 | instance.status(Instance.STATUS_BUSY); 62 | 63 | process.stdout.on("data", (text) => { 64 | instance.print(iconv.decode(text, instance.config.oe)); 65 | }); 66 | process.stderr.on("data", (text) => { 67 | instance.print(iconv.decode(text, instance.config.oe)); 68 | }); 69 | process.on("exit", (code) => { 70 | this.stopped(instance); 71 | if (code === 0) { 72 | instance.println($t("general_update.update"), $t("general_update.updateSuccess")); 73 | } else { 74 | instance.println($t("general_update.update"), $t("general_update.updateErr")); 75 | } 76 | }); 77 | } catch (err) { 78 | this.stopped(instance); 79 | instance.println($t("general_update.update"), $t("general_update.error", { err: err })); 80 | } 81 | } 82 | 83 | async stop(instance: Instance): Promise { 84 | logger.info($t("general_update.terminateUpdate", { instanceUuid: instance.instanceUuid })); 85 | instance.println($t("general_update.update"), $t("general_update.terminateUpdate", { instanceUuid: instance.instanceUuid })); 86 | instance.println($t("general_update.update"), $t("general_update.killProcess")); 87 | killProcess(this.pid, this.process); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/entity/commands/input.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import Instance from "../instance/instance"; 4 | import InstanceCommand from "./base/command"; 5 | 6 | export default class SendInput extends InstanceCommand { 7 | public cmd: string; 8 | 9 | constructor(cmd: string) { 10 | super("SendInput"); 11 | this.cmd = cmd; 12 | } 13 | 14 | async exec(instance: Instance) { 15 | return await instance.execPreset("input", this.cmd); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/entity/commands/kill.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import Instance from "../instance/instance"; 4 | import InstanceCommand from "./base/command"; 5 | 6 | export default class KillCommand extends InstanceCommand { 7 | constructor() { 8 | super("KillCommand"); 9 | } 10 | 11 | async exec(instance: Instance) { 12 | // If the automatic restart function is enabled, the setting is ignored once 13 | if (instance.config.eventTask && instance.config.eventTask.autoRestart) instance.config.eventTask.ignore = true; 14 | 15 | // send stop command 16 | return await instance.execPreset("kill"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/entity/commands/nullfunc.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import InstanceCommand from "./base/command"; 4 | 5 | export default class NullCommand extends InstanceCommand { 6 | constructor() { 7 | super("NullCommand"); 8 | } 9 | async exec() { 10 | // Do nothing..... 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/entity/commands/process_info.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { ProcessConfig } from "../instance/process_config"; 4 | import pidusage from "pidusage"; 5 | import InstanceCommand from "./base/command"; 6 | import Instance from "../instance/instance"; 7 | 8 | export default class ProcessInfoCommand extends InstanceCommand { 9 | constructor() { 10 | super("ProcessInfo"); 11 | } 12 | async exec(instance: Instance): Promise { 13 | let info: any = { 14 | cpu: 0, // percentage (from 0 to 100*vcore) 15 | memory: 0, // bytes 16 | ppid: 0, // PPID 17 | pid: 0, // PID 18 | ctime: 0, // ms user + system time 19 | elapsed: 0, // ms since the start of the process 20 | timestamp: 0 // ms since epoch 21 | }; 22 | if (instance.process && instance.process.pid) { 23 | info = await pidusage(instance.process.pid); 24 | } 25 | return info; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/entity/commands/pty/pty_start.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../../../i18n"; 4 | import os from "os"; 5 | import Instance from "../../instance/instance"; 6 | import logger from "../../../service/log"; 7 | import fs from "fs-extra"; 8 | import path from "path"; 9 | import readline from "readline"; 10 | import InstanceCommand from "../base/command"; 11 | import EventEmitter from "events"; 12 | import { IInstanceProcess } from "../../instance/interface"; 13 | import { ChildProcess, ChildProcessWithoutNullStreams, exec, spawn } from "child_process"; 14 | import { commandStringToArray } from "../base/command_parser"; 15 | import { killProcess } from "../../../common/process_tools"; 16 | import GeneralStartCommand from "../general/general_start"; 17 | import FunctionDispatcher from "../dispatcher"; 18 | import StartCommand from "../start"; 19 | import { PTY_PATH } from "../../../const"; 20 | 21 | interface IPtySubProcessCfg { 22 | pid: number; 23 | } 24 | 25 | // Error exception at startup 26 | class StartupError extends Error { 27 | constructor(msg: string) { 28 | super(msg); 29 | } 30 | } 31 | 32 | // process adapter 33 | class ProcessAdapter extends EventEmitter implements IInstanceProcess { 34 | pid?: number | string; 35 | 36 | constructor(private process: ChildProcess, ptySubProcessPid: number) { 37 | super(); 38 | this.pid = ptySubProcessPid; 39 | process.stdout.on("data", (text) => this.emit("data", text)); 40 | process.stderr.on("data", (text) => this.emit("data", text)); 41 | process.on("exit", (code) => this.emit("exit", code)); 42 | } 43 | 44 | public write(data?: string) { 45 | return this.process.stdin.write(data); 46 | } 47 | 48 | public kill(s?: any) { 49 | return killProcess(this.pid, this.process, s); 50 | } 51 | 52 | public async destroy() { 53 | try { 54 | // remove all dynamically added event listeners 55 | for (const n of this.eventNames()) this.removeAllListeners(n); 56 | if (this.process.stdout) for (const eventName of this.process.stdout.eventNames()) this.process.stdout.removeAllListeners(eventName); 57 | if (this.process.stderr) for (const eventName of this.process.stderr.eventNames()) this.process.stderr.removeAllListeners(eventName); 58 | if (this.process) for (const eventName of this.process.eventNames()) this.process.stdout.removeAllListeners(eventName); 59 | this.process?.stdout?.destroy(); 60 | this.process?.stderr?.destroy(); 61 | if (this.process?.exitCode === null) { 62 | this.process.kill("SIGTERM"); 63 | this.process.kill("SIGKILL"); 64 | } 65 | } catch (error) {} 66 | } 67 | } 68 | 69 | export default class PtyStartCommand extends InstanceCommand { 70 | constructor() { 71 | super("PtyStartCommand"); 72 | } 73 | 74 | readPtySubProcessConfig(subProcess: ChildProcessWithoutNullStreams): Promise { 75 | return new Promise((r, j) => { 76 | const errConfig = { 77 | pid: 0 78 | }; 79 | const rl = readline.createInterface({ 80 | input: subProcess.stdout, 81 | crlfDelay: Infinity 82 | }); 83 | rl.on("line", (line = "") => { 84 | try { 85 | rl.removeAllListeners(); 86 | const cfg = JSON.parse(line) as IPtySubProcessCfg; 87 | if (cfg.pid == null) throw new Error("Error"); 88 | r(cfg); 89 | } catch (error) { 90 | r(errConfig); 91 | } 92 | }); 93 | setTimeout(() => { 94 | r(errConfig); 95 | }, 1000 * 3); 96 | }); 97 | } 98 | 99 | async exec(instance: Instance, source = "Unknown") { 100 | if (!instance.config.startCommand || !instance.config.cwd || !instance.config.ie || !instance.config.oe) 101 | return instance.failure(new StartupError($t("pty_start.cmdErr"))); 102 | if (!fs.existsSync(instance.absoluteCwdPath())) return instance.failure(new StartupError($t("pty_start.cwdNotExist"))); 103 | if (!path.isAbsolute(path.normalize(instance.config.cwd))) return instance.failure(new StartupError($t("pty_start.mustAbsolutePath"))); 104 | 105 | // PTY mode correctness check 106 | logger.info($t("pty_start.startPty", { source: source })); 107 | let checkPtyEnv = true; 108 | 109 | if (!fs.existsSync(PTY_PATH)) { 110 | instance.println("ERROR", $t("pty_start.startErr")); 111 | checkPtyEnv = false; 112 | } 113 | 114 | if (checkPtyEnv === false) { 115 | // Close the PTY type, reconfigure the instance function group, and restart the instance 116 | instance.config.terminalOption.pty = false; 117 | await instance.forceExec(new FunctionDispatcher()); 118 | await instance.execPreset("start", source); // execute the preset command directly 119 | return; 120 | } 121 | 122 | // Set the startup state & increase the number of startups 123 | instance.setLock(true); 124 | instance.status(Instance.STATUS_STARTING); 125 | instance.startCount++; 126 | 127 | // command parsing 128 | let commandList: string[] = []; 129 | if (os.platform() === "win32") { 130 | // windows: cmd.exe /c {{startCommand}} 131 | commandList = [instance.config.startCommand]; 132 | } else { 133 | commandList = commandStringToArray(instance.config.startCommand); 134 | } 135 | 136 | if (commandList.length === 0) return instance.failure(new StartupError($t("pty_start.cmdEmpty"))); 137 | const ptyParameter = [ 138 | "-dir", 139 | instance.config.cwd, 140 | "-cmd", 141 | JSON.stringify(commandList), 142 | "-size", 143 | `${instance.config.terminalOption.ptyWindowCol},${instance.config.terminalOption.ptyWindowRow}`, 144 | "-color", 145 | "-coder", 146 | instance.config.oe 147 | ]; 148 | 149 | logger.info("----------------"); 150 | logger.info($t("pty_start.sourceRequest", { source: source })); 151 | logger.info($t("pty_start.instanceUuid", { instanceUuid: instance.instanceUuid })); 152 | logger.info($t("pty_start.startCmd", { cmd: commandList.join(" ") })); 153 | logger.info($t("pty_start.ptyPath", { path: PTY_PATH })); 154 | logger.info($t("pty_start.ptyParams", { param: ptyParameter.join(" ") })); 155 | logger.info($t("pty_start.ptyCwd", { cwd: instance.config.cwd })); 156 | logger.info("----------------"); 157 | 158 | // create pty child process 159 | // Parameter 1 directly passes the process name or path (including spaces) without double quotes 160 | const subProcess = spawn(PTY_PATH, ptyParameter, { 161 | cwd: path.dirname(PTY_PATH), 162 | stdio: "pipe", 163 | windowsHide: true 164 | }); 165 | 166 | // pty child process creation result check 167 | if (!subProcess || !subProcess.pid) { 168 | instance.println( 169 | "ERROR", 170 | $t("pty_start.pidErr", { startCommand: instance.config.startCommand, path: PTY_PATH, params: JSON.stringify(ptyParameter) }) 171 | ); 172 | throw new StartupError($t("pty_start.instanceStartErr")); 173 | } 174 | 175 | // create process adapter 176 | const ptySubProcessCfg = await this.readPtySubProcessConfig(subProcess); 177 | const processAdapter = new ProcessAdapter(subProcess, ptySubProcessCfg.pid); 178 | logger.info(`pty.exe response: ${JSON.stringify(ptySubProcessCfg)}`); 179 | 180 | // After reading the configuration, Need to check the process status 181 | // The "processAdapter.pid" here represents the process created by the PTY process 182 | if (subProcess.exitCode !== null || processAdapter.pid == null || processAdapter.pid === 0) { 183 | instance.println( 184 | "ERROR", 185 | $t("pty_start.pidErr", { startCommand: instance.config.startCommand, path: PTY_PATH, params: JSON.stringify(ptyParameter) }) 186 | ); 187 | throw new StartupError($t("pty_start.instanceStartErr")); 188 | } 189 | 190 | // generate open event 191 | instance.started(processAdapter); 192 | 193 | logger.info($t("pty_start.startSuccess", { instanceUuid: instance.instanceUuid, pid: ptySubProcessCfg.pid })); 194 | instance.println("INFO", $t("pty_start.startEmulatedTerminal")); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/entity/commands/pty/pty_stop.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../../../i18n"; 4 | import Instance from "../../instance/instance"; 5 | import InstanceCommand from "../base/command"; 6 | import SendCommand from "../cmd"; 7 | 8 | export default class PtyStopCommand extends InstanceCommand { 9 | constructor() { 10 | super("PtyStopCommand"); 11 | } 12 | 13 | async exec(instance: Instance) { 14 | let stopCommand = instance.config.stopCommand; 15 | 16 | if (instance.status() === Instance.STATUS_STOP || !instance.process) return instance.failure(new Error($t("pty_stop.notRunning"))); 17 | instance.status(Instance.STATUS_STOPPING); 18 | 19 | instance.println("INFO", $t("pty_stop.execCmd", { stopCommand: stopCommand })); 20 | 21 | const stopCommandList = stopCommand.split("\n"); 22 | for (const stopCommandColumn of stopCommandList) { 23 | if (stopCommandColumn.toLocaleLowerCase() == "^c") { 24 | await instance.exec(new SendCommand("\x03")); 25 | } else { 26 | await instance.exec(new SendCommand(stopCommandColumn)); 27 | } 28 | } 29 | 30 | // If the instance is still in the stopped state after 10 minutes, restore the state 31 | const cacheStartCount = instance.startCount; 32 | setTimeout(() => { 33 | if (instance.status() === Instance.STATUS_STOPPING && instance.startCount === cacheStartCount) { 34 | instance.println("ERROR", $t("pty_stop.stopErr")); 35 | instance.status(Instance.STATUS_RUNNING); 36 | } 37 | }, 1000 * 60 * 10); 38 | 39 | return instance; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/entity/commands/restart.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import Instance from "../instance/instance"; 4 | import InstanceCommand from "./base/command"; 5 | 6 | export default class RestartCommand extends InstanceCommand { 7 | constructor() { 8 | super("RestartCommand"); 9 | } 10 | 11 | async exec(instance: Instance) { 12 | // If the automatic restart function is enabled, the setting is ignored once 13 | if (instance.config.eventTask && instance.config.eventTask.autoRestart) instance.config.eventTask.ignore = true; 14 | 15 | return await instance.execPreset("restart"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/entity/commands/start.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../../i18n"; 4 | import Instance from "../instance/instance"; 5 | import logger from "../../service/log"; 6 | import fs from "fs-extra"; 7 | 8 | import InstanceCommand from "./base/command"; 9 | import * as childProcess from "child_process"; 10 | import FunctionDispatcher from "./dispatcher"; 11 | import { start } from "repl"; 12 | 13 | class StartupError extends Error { 14 | constructor(msg: string) { 15 | super(msg); 16 | } 17 | } 18 | 19 | export default class StartCommand extends InstanceCommand { 20 | public source: string; 21 | 22 | constructor(source = "Unknown") { 23 | super("StartCommand"); 24 | this.source = source; 25 | } 26 | 27 | private async sleep() { 28 | return new Promise((ok) => { 29 | setTimeout(ok, 1000 * 3); 30 | }); 31 | } 32 | 33 | async exec(instance: Instance) { 34 | if (instance.status() !== Instance.STATUS_STOP) return instance.failure(new StartupError($t("start.instanceNotDown"))); 35 | try { 36 | instance.setLock(true); 37 | instance.status(Instance.STATUS_STARTING); 38 | instance.startCount++; 39 | 40 | // expiration time check 41 | const endTime = new Date(instance.config.endTime).getTime(); 42 | if (endTime) { 43 | const currentTime = new Date().getTime(); 44 | if (endTime <= currentTime) { 45 | throw new Error($t("start.instanceMaturity")); 46 | } 47 | } 48 | 49 | const currentTimestamp = new Date().getTime(); 50 | instance.startTimestamp = currentTimestamp; 51 | 52 | instance.println("INFO", $t("start.startInstance")); 53 | 54 | // prevent the dead-loop from starting 55 | await this.sleep(); 56 | 57 | return await instance.execPreset("start", this.source); 58 | } catch (error) { 59 | instance.releaseResources(); 60 | instance.status(Instance.STATUS_STOP); 61 | instance.failure(error); 62 | } finally { 63 | instance.setLock(false); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/entity/commands/stop.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import Instance from "../instance/instance"; 4 | import InstanceCommand from "./base/command"; 5 | import SendCommand from "./cmd"; 6 | 7 | export default class StopCommand extends InstanceCommand { 8 | constructor() { 9 | super("StopCommand"); 10 | } 11 | 12 | async exec(instance: Instance) { 13 | // If the automatic restart function is enabled, the setting is ignored once 14 | if (instance.config.eventTask && instance.config.eventTask.autoRestart) instance.config.eventTask.ignore = true; 15 | 16 | // send stop command 17 | return await instance.execPreset("stop"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/entity/commands/task/openfrp.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { v4 } from "uuid"; 4 | import fs from "fs-extra"; 5 | import path from "path"; 6 | import { spawn, ChildProcess } from "child_process"; 7 | import os from "os"; 8 | import { killProcess } from "../../../common/process_tools"; 9 | import { ILifeCycleTask } from "../../instance/life_cycle"; 10 | import Instance from "../../instance/instance"; 11 | import KillCommand from "../kill"; 12 | import logger from "../../../service/log"; 13 | import { $t } from "../../../i18n"; 14 | import { processWrapper } from "../../../common/process_tools"; 15 | import { FRPC_PATH } from "../../../const"; 16 | import { downloadFileToLocalFile } from "../../../service/download"; 17 | export class OpenFrp { 18 | public processWrapper: processWrapper; 19 | 20 | constructor(public readonly token: string, public readonly tunnelId: string) { 21 | // ./frpc -u -p 22 | this.processWrapper = new processWrapper(FRPC_PATH, ["-u", this.token, "-p", this.tunnelId], path.dirname(FRPC_PATH)); 23 | } 24 | 25 | public open() { 26 | logger.info("Start openfrp:", FRPC_PATH); 27 | this.processWrapper.start(); 28 | if (!this.processWrapper.getPid()) { 29 | throw new Error("pid is null"); 30 | } 31 | } 32 | 33 | public stop() { 34 | try { 35 | if (this.processWrapper.exitCode() == null) { 36 | this.processWrapper.kill(); 37 | } 38 | this.processWrapper = null; 39 | } catch (error) {} 40 | } 41 | } 42 | 43 | export default class OpenFrpTask implements ILifeCycleTask { 44 | public status: number = 0; 45 | public name: string = "openfrp"; 46 | public static readonly FRP_EXE_NAME = `frpc_${os.platform()}_${os.arch()}${os.platform() === "win32" ? ".exe" : ""}`; 47 | public static readonly FRP_EXE_PATH = path.normalize(path.join(process.cwd(), "lib", OpenFrpTask.FRP_EXE_NAME)); 48 | public static readonly FRP_DOWNLOAD_ADDR = "https://mcsmanager.oss-cn-guangzhou.aliyuncs.com/"; 49 | 50 | async start(instance: Instance) { 51 | const { openFrpToken, openFrpTunnelId } = instance.config?.extraServiceConfig; 52 | if (!openFrpToken || !openFrpTunnelId) return; 53 | 54 | if (!fs.existsSync(OpenFrpTask.FRP_EXE_PATH)) { 55 | const tmpTask = setInterval(() => { 56 | instance.println("FRP", $t("frp.installing")); 57 | }, 2000); 58 | try { 59 | await downloadFileToLocalFile(OpenFrpTask.FRP_DOWNLOAD_ADDR + OpenFrpTask.FRP_EXE_NAME, OpenFrpTask.FRP_EXE_PATH); 60 | instance.println("FRP", $t("frp.done")); 61 | } catch (error) { 62 | logger.error($t("frp.downloadErr"), error); 63 | instance.println("ERROR", $t("frp.downloadErr") + `: ${error}`); 64 | fs.remove(OpenFrpTask.FRP_EXE_PATH, () => {}); 65 | return; 66 | } finally { 67 | clearInterval(tmpTask); 68 | } 69 | } 70 | 71 | const frpProcess = new OpenFrp(openFrpToken, openFrpTunnelId); 72 | frpProcess.processWrapper.on("start", (pid: number) => { 73 | if (pid) { 74 | logger.info(`Instance ${instance.config.nickname}(${instance.instanceUuid}) ${pid} Frp task started!`); 75 | logger.info(`Params: ${openFrpTunnelId} | ${openFrpToken}`); 76 | instance.openFrp = frpProcess; 77 | instance.info.openFrpStatus = true; 78 | } else { 79 | logger.warn(`Instance ${instance.config.nickname}(${instance.instanceUuid}) Frp task start failed! Process ID is ${pid}`); 80 | } 81 | }); 82 | frpProcess.processWrapper.on("exit", () => { 83 | logger.info(`Instance ${instance.config.nickname}(${instance.instanceUuid}) Frp task stopped!`); 84 | instance.info.openFrpStatus = false; 85 | instance.openFrp = null; 86 | }); 87 | 88 | try { 89 | frpProcess.open(); 90 | } catch (error) { 91 | logger.warn(`Instance ${instance.config.nickname}(${instance.instanceUuid}) Frp task Start failure! ERR:`); 92 | logger.warn(error); 93 | } 94 | } 95 | 96 | async stop(instance: Instance) { 97 | if (instance.openFrp) { 98 | const frpProcess = instance.openFrp; 99 | frpProcess.stop(); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/entity/commands/task/players.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { ILifeCycleTask } from "../../instance/life_cycle"; 4 | import Instance from "../../instance/instance"; 5 | 6 | export default class RefreshPlayer implements ILifeCycleTask { 7 | public name: string = "RefreshPlayer"; 8 | public status: number = 0; 9 | 10 | private task: any = null; 11 | private playersChartTask: any = null; 12 | private playersChart: Array<{ value: string }> = []; 13 | 14 | async start(instance: Instance) { 15 | this.task = setInterval(async () => { 16 | // { 17 | // host: 'localhost', 18 | // port: 28888, 19 | // status: true, 20 | // version: '1.17.1', 21 | // motd: 'A Minecraft Server', 22 | // current_players: '0', 23 | // max_players: '20', 24 | // latency: 1 25 | // } 26 | try { 27 | // Get information such as the number of players, version, etc. 28 | const result = await instance.execPreset("getPlayer"); 29 | if (!result) return; 30 | instance.info.maxPlayers = result.max_players ? result.max_players : -1; 31 | instance.info.currentPlayers = result.current_players ? result.current_players : -1; 32 | instance.info.version = result.version ? result.version : ""; 33 | 34 | // When the number of users is correctly obtained for the first time, initialize the number of players graph 35 | if (this.playersChart.length === 0) { 36 | this.initPlayersChart(instance); 37 | } 38 | } catch (error) {} 39 | }, 3000); 40 | 41 | // Start the timer for querying the online population report data 42 | this.playersChartTask = setInterval(() => { 43 | this.getPlayersChartData(instance); 44 | }, 600000); 45 | } 46 | 47 | initPlayersChart(instance: Instance) { 48 | while (this.playersChart.length < 60) { 49 | this.playersChart.push({ value: "0" }); 50 | } 51 | instance.info.playersChart = this.playersChart; 52 | this.getPlayersChartData(instance); 53 | } 54 | 55 | getPlayersChartData(instance: Instance) { 56 | try { 57 | this.playersChart.shift(); 58 | this.playersChart.push({ 59 | value: String(instance.info.currentPlayers) ?? "0" 60 | }); 61 | instance.info.playersChart = this.playersChart; 62 | } catch (error) {} 63 | } 64 | 65 | async stop(instance: Instance) { 66 | clearInterval(this.task); 67 | clearInterval(this.playersChartTask); 68 | instance.info.maxPlayers = -1; 69 | instance.info.currentPlayers = -1; 70 | instance.info.version = ""; 71 | instance.info.playersChart = []; 72 | this.playersChart = []; 73 | this.playersChartTask = null; 74 | this.task = null; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/entity/commands/task/time.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { ILifeCycleTask } from "../../instance/life_cycle"; 4 | import Instance from "../../instance/instance"; 5 | import KillCommand from "../kill"; 6 | 7 | // When the instance is running, continue to check the expiration time 8 | export default class TimeCheck implements ILifeCycleTask { 9 | public status: number = 0; 10 | public name: string = "TimeCheck"; 11 | 12 | private task: any = null; 13 | 14 | async start(instance: Instance) { 15 | this.task = setInterval(async () => { 16 | const endTime = new Date(instance.config.endTime).getTime(); 17 | if (endTime) { 18 | const currentTime = new Date().getTime(); 19 | if (endTime <= currentTime) { 20 | // Expired, execute the end process command 21 | await instance.exec(new KillCommand()); 22 | clearInterval(this.task); 23 | } 24 | } 25 | }, 1000 * 60 * 60); 26 | } 27 | 28 | async stop(instance: Instance) { 29 | clearInterval(this.task); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/entity/commands/update.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import Instance from "../instance/instance"; 4 | import InstanceCommand from "./base/command"; 5 | import SendCommand from "./cmd"; 6 | 7 | export default class UpdateCommand extends InstanceCommand { 8 | constructor() { 9 | super("UpdateCommand"); 10 | } 11 | 12 | async exec(instance: Instance) { 13 | // Execute the update preset, the preset and function scheduler are set before starting 14 | return await instance.execPreset("update"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/entity/config.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { v4 } from "uuid"; 4 | import StorageSubsystem from "../common/system_storage"; 5 | 6 | function builderPassword() { 7 | const a = `${v4().replace(/\-/gim, "")}`; 8 | const b = a.slice(0, a.length / 2 - 1); 9 | const c = `${v4().replace(/\-/gim, "")}`; 10 | return b + c; 11 | } 12 | 13 | // @Entity 14 | class Config { 15 | public version = 2; 16 | public ip = ""; 17 | public port = 24444; 18 | public key = builderPassword(); 19 | public maxFileTask = 2; 20 | public maxZipFileSize = 60; 21 | public language = "en_us"; 22 | public defaultInstancePath = ""; 23 | } 24 | 25 | // daemon configuration class 26 | class GlobalConfiguration { 27 | public config = new Config(); 28 | private static readonly ID = "global"; 29 | 30 | load() { 31 | let config: Config = StorageSubsystem.load("Config", Config, GlobalConfiguration.ID); 32 | if (config == null) { 33 | config = new Config(); 34 | StorageSubsystem.store("Config", GlobalConfiguration.ID, config); 35 | } 36 | this.config = config; 37 | } 38 | 39 | store() { 40 | StorageSubsystem.store("Config", GlobalConfiguration.ID, this.config); 41 | } 42 | } 43 | 44 | class GlobalEnv { 45 | public fileTaskCount = 0; 46 | } 47 | 48 | const globalConfiguration = new GlobalConfiguration(); 49 | const globalEnv = new GlobalEnv(); 50 | 51 | export { globalConfiguration, Config, globalEnv }; 52 | -------------------------------------------------------------------------------- /src/entity/ctx.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { Socket } from "socket.io"; 4 | 5 | export default class RouterContext { 6 | constructor(public uuid: string, public socket: Socket, public session?: any, public event?: string) {} 7 | 8 | public response(data: any) { 9 | return this; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/entity/instance/Instance_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import Instance from "./instance"; 4 | import { IDockerConfig } from "./interface"; 5 | import os from "os"; 6 | interface IActionCommand { 7 | name: string; 8 | command: string; 9 | } 10 | 11 | type ProcessType = "general" | "docker"; 12 | 13 | // @Entity 14 | export default class InstanceConfig { 15 | public nickname = "Undefined"; 16 | public startCommand = ""; 17 | public stopCommand = "^C"; 18 | public cwd = "."; 19 | public ie = "utf-8"; 20 | public oe = "utf-8"; 21 | public createDatetime = new Date().toLocaleDateString(); 22 | public lastDatetime = "--"; 23 | public type = Instance.TYPE_UNIVERSAL; 24 | public tag: string[] = []; 25 | public endTime: string = ""; 26 | public fileCode: string = "utf-8"; 27 | public processType: ProcessType = "general"; 28 | public updateCommand: string = ""; 29 | public crlf = os.platform() === "win32" ? 2 : 1; // 1: \n 2: \r\n 30 | 31 | // custom command list 32 | public actionCommandList: IActionCommand[] = []; 33 | 34 | // terminal option 35 | public terminalOption = { 36 | haveColor: false, 37 | pty: true, 38 | ptyWindowCol: 140, 39 | ptyWindowRow: 40 40 | }; 41 | 42 | // Event task 43 | public eventTask = { 44 | autoStart: false, 45 | autoRestart: false, 46 | ignore: false 47 | }; 48 | 49 | // Extend 50 | public docker: IDockerConfig = { 51 | containerName: "", 52 | image: "", 53 | ports: [], 54 | extraVolumes: [], 55 | memory: null, 56 | networkMode: "bridge", 57 | networkAliases: [], 58 | cpusetCpus: "", 59 | cpuUsage: null, 60 | maxSpace: null, 61 | io: null, 62 | network: null 63 | }; 64 | 65 | public pingConfig = { 66 | ip: "", 67 | port: 25565, 68 | type: 1 69 | }; 70 | 71 | public extraServiceConfig = { 72 | openFrpTunnelId: "", 73 | openFrpToken: "" 74 | }; 75 | } 76 | -------------------------------------------------------------------------------- /src/entity/instance/interface.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { EventEmitter } from "events"; 4 | 5 | // interface of docker config 6 | export interface IDockerConfig { 7 | containerName: string; 8 | image: string; 9 | memory: number; // Memory limit in bytes. 10 | ports: string[]; 11 | extraVolumes: string[]; 12 | maxSpace: number; 13 | network: number; 14 | io: number; 15 | networkMode: string; 16 | networkAliases: string[]; 17 | cpusetCpus: string; // CPU allowed to execute (eg 0-3, , 0, 1) 18 | cpuUsage: number; 19 | } 20 | 21 | // Instance specific process interface 22 | export interface IInstanceProcess extends EventEmitter { 23 | pid?: number | string; 24 | kill: (signal?: any) => any; 25 | destroy: () => void; 26 | write: (data?: any) => any; 27 | } 28 | -------------------------------------------------------------------------------- /src/entity/instance/life_cycle.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import Instance from "./instance"; 4 | 5 | export interface ILifeCycleTask { 6 | name: string; // task name 7 | status: number; // Running status, the default is 0, the task manager will automatically change 8 | start: (instance: Instance) => Promise; 9 | stop: (instance: Instance) => Promise; 10 | } 11 | 12 | export class LifeCycleTaskManager { 13 | // list of life cycle tasks 14 | public readonly lifeCycleTask: ILifeCycleTask[] = []; 15 | 16 | constructor(private self: any) {} 17 | 18 | registerLifeCycleTask(task: ILifeCycleTask) { 19 | this.lifeCycleTask.push(task); 20 | } 21 | 22 | execLifeCycleTask(type: number) { 23 | if (type == 1) { 24 | this.lifeCycleTask.forEach((v) => { 25 | if (v.status === 0) v.start(this.self); 26 | v.status = 1; 27 | }); 28 | } else { 29 | this.lifeCycleTask.forEach((v) => { 30 | if (v.status === 1) v.stop(this.self); 31 | v.status = 0; 32 | }); 33 | } 34 | } 35 | 36 | clearLifeCycleTask() { 37 | this.execLifeCycleTask(0); 38 | this.lifeCycleTask.splice(0, this.lifeCycleTask.length); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/entity/instance/preset.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../../i18n"; 4 | export interface IExecutable { 5 | exec: (a: any, b?: any) => Promise; 6 | stop?: (a: any) => Promise; 7 | } 8 | 9 | export class PresetCommandManager { 10 | public readonly preset = new Map(); 11 | 12 | constructor(private self: any) {} 13 | 14 | setPreset(action: string, cmd: IExecutable) { 15 | this.preset.set(action, cmd); 16 | } 17 | 18 | getPreset(action: string) { 19 | return this.preset.get(action); 20 | } 21 | 22 | async execPreset(action: string, p?: any) { 23 | const cmd = this.preset.get(action); 24 | if (!cmd) throw new Error($t("preset.actionErr", { action: action })); 25 | return await cmd.exec(this.self, p); 26 | } 27 | 28 | clearPreset() { 29 | this.preset.clear(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/entity/instance/process_config.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../../i18n"; 4 | import yaml from "yaml"; 5 | import toml from "@iarna/toml"; 6 | import properties from "properties"; 7 | import path from "path"; 8 | import fs from "fs-extra"; 9 | 10 | const CONFIG_FILE_ENCODE = "utf-8"; 11 | 12 | export interface IProcessConfig { 13 | fileName: string; 14 | path: string; 15 | type: string; 16 | info: string; 17 | redirect: string; 18 | from?: string; 19 | fromLink?: string; 20 | } 21 | 22 | export class ProcessConfig { 23 | constructor(public iProcessConfig: IProcessConfig) { 24 | iProcessConfig.path = path.normalize(iProcessConfig.path); 25 | } 26 | 27 | // Automatically parse the local file according to the type and return the configuration object 28 | read(): any { 29 | const text = fs.readFileSync(this.iProcessConfig.path, { encoding: CONFIG_FILE_ENCODE }); 30 | if (this.iProcessConfig.type === "yml") { 31 | return yaml.parse(text); 32 | } 33 | if (this.iProcessConfig.type == "toml") { 34 | return toml.parse(text); 35 | } 36 | if (this.iProcessConfig.type === "properties") { 37 | return properties.parse(text); 38 | } 39 | if (this.iProcessConfig.type === "json") { 40 | return JSON.parse(text); 41 | } 42 | if (this.iProcessConfig.type === "txt") { 43 | return text; 44 | } 45 | } 46 | 47 | // Automatically save to the local configuration file according to the parameter object 48 | write(object: Object | toml.JsonMap) { 49 | let text = ""; 50 | if (this.iProcessConfig.type === "yml") { 51 | text = yaml.stringify(object); 52 | } 53 | if (this.iProcessConfig.type === "toml") { 54 | text = toml.stringify(object); 55 | } 56 | if (this.iProcessConfig.type === "properties") { 57 | text = properties.stringify(object, { 58 | unicode: true 59 | }); 60 | text = text.replace(/ = /gim, "="); 61 | if (this.iProcessConfig.fileName == "server.properties") { 62 | text = text.replace(/\\\\u/gim, "\\u"); 63 | } 64 | } 65 | if (this.iProcessConfig.type === "json") { 66 | text = JSON.stringify(object); 67 | } 68 | if (this.iProcessConfig.type === "txt") { 69 | text = object.toString(); 70 | } 71 | if (!text && this.iProcessConfig.type !== "txt") throw new Error($t("process_config.writEmpty")); 72 | fs.writeFileSync(this.iProcessConfig.path, text, { encoding: CONFIG_FILE_ENCODE }); 73 | } 74 | 75 | exists() { 76 | return fs.existsSync(this.iProcessConfig.path); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/entity/minecraft/mc_getplayer.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import Instance from "../instance/instance"; 4 | import InstanceCommand from "../commands/base/command"; 5 | import MCServerStatus from "../../common/mcping"; 6 | 7 | export default class MinecraftGetPlayersCommand extends InstanceCommand { 8 | constructor() { 9 | super("MinecraftGetPlayersCommand"); 10 | } 11 | 12 | async exec(instance: Instance) { 13 | if (instance.config.pingConfig.ip && instance.config.pingConfig.port) { 14 | const player = await new MCServerStatus(instance.config.pingConfig.port, instance.config.pingConfig.ip).getStatus(); 15 | return player; 16 | } 17 | return null; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/entity/minecraft/mc_getplayer_bedrock.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import dgram from "dgram"; 4 | import Instance from "../instance/instance"; 5 | import InstanceCommand from "../commands/base/command"; 6 | 7 | // Get Minecraft Bedrock server MOTD information 8 | // Author: https://github.com/Mcayear 9 | async function request(ip: string, port: number) { 10 | const message = Buffer.from( 11 | "01 00 00 00 00 00 06 18 20 00 FF FF 00 FE FE FE FE FD FD FD FD 12 34 56 78 A3 61 1C F8 BA 8F D5 60".replace(/ /g, ""), 12 | "hex" 13 | ); 14 | const client = dgram.createSocket("udp4"); 15 | var Config = { 16 | ip, 17 | port 18 | }; 19 | return new Promise((r, j) => { 20 | client.on("error", (err: any) => { 21 | try { 22 | client.close(); 23 | } finally { 24 | j(err); 25 | } 26 | }); 27 | client.on("message", (data: any) => { 28 | const result = data.toString().split(";"); 29 | try { 30 | client.close(); 31 | } finally { 32 | r(result); 33 | } 34 | }); 35 | client.send(message, Config.port, Config.ip, (err: any) => { 36 | if (err) { 37 | try { 38 | client.close(); 39 | } finally { 40 | j(err); 41 | } 42 | } 43 | }); 44 | setTimeout(() => { 45 | j("request timeout"); 46 | try { 47 | client.close(); 48 | } catch (error) {} 49 | }, 3000); 50 | }); 51 | } 52 | 53 | // Adapt to MCSManager lifecycle tasks 54 | export default class MinecraftBedrockGetPlayersCommand extends InstanceCommand { 55 | constructor() { 56 | super("MinecraftBedrockGetPlayersCommand"); 57 | } 58 | 59 | async exec(instance: Instance) { 60 | if (instance.config.pingConfig.ip && instance.config.pingConfig.port) { 61 | try { 62 | const info: any = await request(instance.config.pingConfig.ip, instance.config.pingConfig.port); 63 | return { 64 | version: info[3], 65 | motd: info[0], 66 | current_players: info[4], 67 | max_players: info[5] 68 | }; 69 | } catch (error) {} 70 | } 71 | return null; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/entity/minecraft/mc_update.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../../i18n"; 4 | import Instance from "../instance/instance"; 5 | import InstanceCommand from "../commands/base/command"; 6 | 7 | export default class MinecraftUpdateCommand extends InstanceCommand { 8 | constructor() { 9 | super("UpdateCommand"); 10 | } 11 | 12 | async exec(instance: Instance) { 13 | console.log($t("mc_update.updateInstance")); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import i18next from "i18next"; 4 | 5 | import zh_cn from "./language/zh_cn.json"; 6 | import en_us from "./language/en_us.json"; 7 | 8 | i18next.init({ 9 | interpolation: { 10 | escapeValue: false 11 | }, 12 | lng: "en_us", 13 | fallbackLng: "en_us", 14 | resources: { 15 | zh_cn: { 16 | translation: zh_cn 17 | }, 18 | en_us: { 19 | translation: en_us 20 | } 21 | } 22 | }); 23 | 24 | // alias 25 | const $t = i18next.t; 26 | 27 | export { $t, i18next }; 28 | -------------------------------------------------------------------------------- /src/i18n/language/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "welcome": "欢迎使用 MCSManager 守护进程", 4 | "instanceLoad": "所有应用实例已加载,总计 {{n}} 个", 5 | "instanceLoadError": "读取本地实例文件失败:", 6 | "sessionConnect": "会话 {{ip}} {{uuid}} 已连接", 7 | "sessionDisconnect": "会话 {{ip}} {{uuid}} 已断开", 8 | "started": "守护进程现已成功启动", 9 | "doc": "参考文档:https://docs.mcsmanager.com/", 10 | "addr": "访问地址:http://:{{port}}/ 或 ws://:{{port}}", 11 | "configPathTip": "配置文件:data/Config/global.json", 12 | "password": "访问密钥:{{key}}", 13 | "passwordTip": "密钥作为唯一认证方式,请使用 MCSManager 面板的节点功能连接程序", 14 | "exitTip": "你可以使用 Ctrl+C 快捷键即可关闭程序", 15 | "sysinfo": "资源报告", 16 | "httpSetupError": "HTTP/Socket 服务启动错误,可能是端口被占用,权限不足或网卡设备不可用。" 17 | }, 18 | "common": { 19 | "title": "标题", 20 | "_7zip": "[7zip 压缩任务]", 21 | "_7unzip": "[7zip 解压任务]", 22 | "killProcess": "进程 {{pid}} 已使用系统指令强制终止进程", 23 | "uuidIrregular": "UUID {{uuid}} 不符合规范" 24 | }, 25 | "command": { 26 | "quotes": "错误的命令双引号,无法找到成对双引号,如需使用单个双引号请使用 {quotes} 符号", 27 | "errLen": "错误的命令长度,请确保命令格式正确", 28 | "instanceNotOpen": "命令执行失败,因为实例实际进程不存在" 29 | }, 30 | "instance": { 31 | "dirEmpty": "启动命令,输入输出编码或工作目录为空值", 32 | "dirNoE": "工作目录并不存在", 33 | "invalidCpu": "非法的CPU核心指定 {{v}}", 34 | "invalidContainerName": "非法的容器名 {{v}}", 35 | "successful": "实例 {{v}} 启动成功" 36 | }, 37 | "start": { 38 | "instanceNotDown": "实例未处于关闭状态,无法再进行启动", 39 | "instanceMaturity": "实例使用到期时间已到,无法再启动实例", 40 | "startInstance": "正在准备启动实例..." 41 | }, 42 | "restart": { 43 | "start": "重启实例计划开始执行", 44 | "error1": "重启实例状态错误,实例已被启动过,上次状态的重启计划取消", 45 | "error2": "重启实例状态错误,实例状态应该为停止中状态,现在变为正在运行,重启计划取消", 46 | "restarting": "检测到服务器已停止,正在重启实例..." 47 | }, 48 | "general_start": { 49 | "instanceConfigErr": "启动命令,输入输出编码或工作目录为空值", 50 | "cwdPathNotExist": "工作目录并不存在", 51 | "cmdEmpty": "无法启动实例,启动命令为空", 52 | "startInstance": "会话 {{source}}: 请求开启实例.", 53 | "instanceUuid": "实例标识符: [{{uuid}}]", 54 | "startCmd": "启动命令: {{cmdList}}", 55 | "cwd": "工作目录: {{cwd}}", 56 | "pidErr": "检测到实例进程/容器启动失败(PID 为空),其可能的原因是:\n1. 实例启动命令编写错误,请前往实例设置界面检查启动命令与参数。\n2. 系统主机环境不正确或缺少环境,如 Java 环境等。\n\n原生启动命令:\n{{startCommand}}\n\n启动命令解析体:\n程序:{{commandExeFile}}\n参数:{{commandParameters}}\n\n请将此信息报告给管理员,技术人员或自行排查故障。", 57 | "startErr": "实例启动失败,请检查启动命令,主机环境和配置文件等", 58 | "startSuccess": "实例 {{instanceUuid}} 成功启动 PID: {{pid}}", 59 | "startOrdinaryTerminal": "应用实例已运行,您可以在底部的命令输入框发送命令,如果您需要支持 Ctrl,Tab 等快捷键等高级控制台功能,请前往终端设置开启仿真终端功能" 60 | }, 61 | "general_stop": { 62 | "notRunning": "实例未处于运行中状态,无法进行停止.", 63 | "execCmd": "已执行预设的关闭命令:{{stopCommand}}\n如果无法关闭实例请前往实例设置更改关闭实例的正确命令,比如 ^C,stop,end 等", 64 | "stopErr": "关闭命令已发出但长时间未能关闭实例,可能是实例关闭命令错误或实例进程假死导致,现在将恢复到运行中状态,可使用强制终止指令结束进程。" 65 | }, 66 | "general_update": { 67 | "statusErr_notStop": "实例状态不正确,无法执行更新任务,必须停止实例", 68 | "statusErr_otherProgress": "实例状态不正确,有其他任务正在运行中", 69 | "readyUpdate": "实例 {{instanceUuid}} 正在准备进行更新操作...", 70 | "updateCmd": "实例 {{instanceUuid}} 执行更新命令如下:", 71 | "cmdFormatErr": "更新命令格式错误,请联系管理员", 72 | "err": "Error", 73 | "updateFailed": "更新失败,更新命令启动失败,请联系管理员", 74 | "update": "更新", 75 | "updateSuccess": "更新成功!", 76 | "updateErr": "更新程序结束,但结果不正确,可能文件更新损坏或网络不畅通", 77 | "error": "更新错误: {{err}}", 78 | "terminateUpdate": "用户请求终止实例 {{instanceUuid}} 的 update 异步任务", 79 | "killProcess": "正在强制杀死任务进程..." 80 | }, 81 | "pty_start": { 82 | "cmdErr": "启动命令,输入输出编码或工作目录为空值", 83 | "cwdNotExist": "工作目录并不存在", 84 | "startPty": "会话 {{source}}: 请求开启实例,模式为仿真终端", 85 | "startErr": "仿真终端模式失败,可能是依赖程序不存在,已自动降级到普通终端模式...", 86 | "notSupportPty": "仿真终端模式失败,无法支持的架构或系统,已自动降级到普通终端模式...", 87 | "cmdEmpty": "无法启动实例,启动命令为空", 88 | "sourceRequest": "会话 {{source}}: 请求开启实例.", 89 | "instanceUuid": "实例标识符: [{{instanceUuid}}]", 90 | "startCmd": "启动命令: {{cmd}}", 91 | "ptyPath": "PTY 路径: {{path}}", 92 | "ptyParams": "PTY 参数: {{param}}", 93 | "ptyCwd": "工作目录: {{cwd}}", 94 | "pidErr": "检测到实例进程/容器启动失败(PID 为空),其可能的原因是:\n1. 实例启动命令编写错误,请前往实例设置界面检查启动命令与参数。\n2. 系统主机环境不正确或缺少环境,如 Java 环境等。\n\n原生启动命令:\n{{startCommand}}\n\n仿真终端中转命令:\n程序:{{path}}\n参数:{{params}}\n\n请将此信息报告给管理员,技术人员或自行排查故障。\n如果您认为是面板仿真终端导致的问题,请在左侧终端设置中关闭“仿真终端”选项,我们将会采用原始输入输出流的方式监听程序。", 95 | "instanceStartErr": "实例启动失败,请检查启动命令,主机环境和配置文件等", 96 | "startSuccess": "实例 {{instanceUuid}} 成功启动 PID: {{pid}}", 97 | "startEmulatedTerminal": "仿真终端模式已生效,您可以直接在终端内直接输入内容并使用 Ctrl,Tab 等功能键", 98 | "mustAbsolutePath": "仿真终端启动工作目录必须使用绝对路径,请前往实例设置界面重新设置工作路径为绝对路径" 99 | }, 100 | "pty_stop": { 101 | "notRunning": "实例未处于运行中状态,无法进行停止.", 102 | "execCmd": "已执行预设的关闭命令:{{stopCommand}}\n如果无法关闭实例请前往实例设置更改关闭实例的正确命令,比如 exit,stop,end 等", 103 | "stopErr": "关闭命令已发出但长时间未能关闭实例,可能是实例关闭命令错误或实例进程假死导致,现在将恢复到运行中状态,可使用强制终止指令结束进程。" 104 | }, 105 | "instanceConf": { 106 | "initInstanceErr": "初始化实例失败,唯一标识符或配置参数为空", 107 | "cantModifyInstanceType": "正在运行时无法修改此实例类型", 108 | "cantModifyProcessType": "正在运行时无法修改此实例进程类型", 109 | "cantModifyPtyModel": "正在运行时无法修改PTY模式", 110 | "ptyNotExist": "无法启用仿真终端,因为 {{path}} 附属程序不存在,您可以联系管理员重启 Daemon 程序得以重新安装(仅 Linux)", 111 | "instanceLock": "此 {{info}} 操作无法执行,因为实例处于锁定状态,请稍后再试.", 112 | "instanceBusy": "当前实例正处于忙碌状态,无法执行任何操作.", 113 | "info": "信息", 114 | "error": "错误", 115 | "autoRestart": "检测到实例关闭,根据主动事件机制,自动重启指令已发出...", 116 | "autoRestartErr": "自动重启错误: {{err}}", 117 | "instantExit": "检测到实例启动后在极短的时间内退出,原因可能是您的启动命令错误或配置文件错误。" 118 | }, 119 | "preset": { 120 | "actionErr": "预设命令 {{action}} 不可用" 121 | }, 122 | "process_config": { 123 | "writEmpty": "写入内容为空,可能是配置文件类型不支持" 124 | }, 125 | "mc_update": { 126 | "updateInstance": "更新实例....." 127 | }, 128 | "auth_router": { 129 | "notAccess": "会话 {{id}}({{address}}) 试图无权限访问 {{event}} 现已阻止.", 130 | "illegalAccess": "非法访问", 131 | "access": "会话 {{id}}({{address}}) 验证身份成功", 132 | "disconnect": "会话 {{id}}({{address}}) 因长时间未验证身份而断开连接" 133 | }, 134 | "environment_router": { 135 | "dockerInfoErr": "无法获取镜像信息,请确保您已正确安装Docker环境", 136 | "crateImage": "守护进程正在创建镜像 {{name}}:{{tag}} DockerFile 如下:\n{{dockerFileText}}\n", 137 | "crateSuccess": "创建镜像 {{name}}:{{tag}} 完毕", 138 | "crateErr": "创建镜像 {{name}}:{{tag}} 错误:{{error}}", 139 | "delImage": "守护进程正在删除镜像 {{imageId}}" 140 | }, 141 | "file_router": { 142 | "instanceNotExist": "实例 {{instanceUuid}} 不存在", 143 | "unzipLimit": "超出最大同时解压缩任务量,最大准许{{maxFileTask}}个,目前有{{fileLock}}个任务正在进行,请耐心等待" 144 | }, 145 | "http_router": { 146 | "instanceNotExist": "实例不存在", 147 | "fileNameNotSpec": "用户文件下载名不符合规范", 148 | "downloadErr": "下载出错: {{error}}", 149 | "updateErr": "未知原因: 上传失败" 150 | }, 151 | "Instance_router": { 152 | "requestIO": "会话 {{id}} 请求转发实例 {{targetInstanceUuid}} IO 流", 153 | "cancelIO": "会话 {{id}} 请求取消转发实例 {{targetInstanceUuid}} IO 流", 154 | "openInstanceErr": "实例{{instanceUuid}}启动时错误: ", 155 | "performTasks": "会话 {{id}} 要求实例 {{uuid}} 执行异步 {{taskName}} 异步任务", 156 | "performTasksErr": "实例 {{uuid}} {{taskName}} 异步任务执行异常: {{err}}", 157 | "taskEmpty": "无异步任务正在运行", 158 | "accessFileErr": "文件不存在或路径错误,文件访问被拒绝", 159 | "terminalLogNotExist": "终端日志文件不存在" 160 | }, 161 | "passport_router": { 162 | "registerErr": "不可定义任务名或密钥为空" 163 | }, 164 | "stream_router": { 165 | "IGNOREAccess": "非法访问", 166 | "taskNotExist": "任务不存在", 167 | "instanceNotExist": "实例不存在", 168 | "authSuccess": "会话 {{id}} {{address}} 数据流通道身份验证成功", 169 | "establishConnection": "会话 {{id}} {{address}} 已与 {{uuid}} 建立数据通道", 170 | "disconnect": "会话 {{id}} {{address}} 已与 {{uuid}} 断开数据通道", 171 | "unauthorizedAccess": "非法访问" 172 | }, 173 | "file_router_service": { 174 | "instanceNotExit": "实例 {{uuid}} 不存在" 175 | }, 176 | "install": { 177 | "ptyNotSupportSystem": "仿真终端只能支持 Windows/Linux x86_64 架构,已自动降级为普通终端", 178 | "ptySupport": "识别到可选依赖库安装,仿真终端功能已可用", 179 | "skipInstall": "检测到系统不是 Linux 系统,自动跳过依赖库安装", 180 | "installed": "可选依赖程序已自动安装,仿真终端和部分高级功能已自动启用", 181 | "guide": "依赖程序参考:https://github.com/mcsmanager/pty", 182 | "changeModeErr": "修改文件 {{path}} 权限失败,请手动设置其为 chmod 755 以上", 183 | "installErr": "安装可选依赖库失败,全仿真终端和部分可选功能将无法使用,不影响正常功能,将在下次启动时再尝试安装" 184 | }, 185 | "protocol": { 186 | "socketErr": "会话 {{id}}({{address}})/{{event}} 响应数据时异常:\n" 187 | }, 188 | "router": { 189 | "initComplete": "所有功能模块与权限防火墙已初始化完毕" 190 | }, 191 | "system_file": { 192 | "illegalAccess": "非法访问路径", 193 | "unzipLimit": "文件解压缩只支持最大 {{max}}GB 文件的解压缩,如需改变上限请前往 data/Config/global.json 文件", 194 | "execLimit": "超出最大文件编辑限制" 195 | }, 196 | "system_instance_control": { 197 | "execLimit": "无法继续创建计划任务,以达到上限", 198 | "existRepeatTask": "已存在重复的任务", 199 | "illegalName": "非法的计划名,仅支持下划线,数字,字母和部分本地语言", 200 | "crateTask": "创建计划任务 {{name}}:\n{{task}}", 201 | "crateTaskErr": "计划任务创建错误,不正确的时间表达式: \n{{name}}: {{timeArray}}\n请尝试删除 data/TaskConfig/{{name}}.json 文件解决此问题", 202 | "crateSuccess": "创建计划任务 {{name}} 完毕", 203 | "execCmdErr": "实例 {{uuid}} 计划任务 {{name}} 执行错误: \n {{error}}" 204 | }, 205 | "system_instance": { 206 | "autoStart": "实例 {{name}} {{uuid}} 自动启动指令已发出", 207 | "autoStartErr": "实例 {{name}} {{uuid}} 自动启动时错误: {{reason}}", 208 | "readInstanceFailed": "读取 {{uuid}} 应用实例失败: {{error}}", 209 | "checkConf": "请检查或删除文件:data/InstanceConfig/{{uuid}}.json", 210 | "uuidEmpty": "无法新增某实例,因为实例UUID为空" 211 | }, 212 | "ui": { 213 | "help": "[终端] 守护进程拥有基本的交互功能,请输入\"help\"查看更多信息" 214 | }, 215 | "version": { 216 | "versionDetectErr": "版本检查失败" 217 | }, 218 | "quick_install": { 219 | "unzipError": "解压文件失败", 220 | "hiperError": "网络映射进程已存在,不可重复启动!" 221 | }, 222 | "frp": { 223 | "downloadErr": "下载 FRP 应用程序失败,无法正常启动 FRP 内网映射程序。", 224 | "installing": "正在同时下载并安装 FRP 内网映射程序,稍后我们将自动启动映射功能...", 225 | "done": "FRP 内网映射程序安装完毕,您可以从左上角查看映射状态,如果没有显示,则有可能是杀毒软件拦截,文件权限不足,或者 FRP 密钥错误。" 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /src/routers/auth_router.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../i18n"; 4 | import { routerApp } from "../service/router"; 5 | import * as protocol from "../service/protocol"; 6 | import { globalConfiguration } from "../entity/config"; 7 | import logger from "../service/log"; 8 | import RouterContext from "../entity/ctx"; 9 | import { IGNORE } from "../const"; 10 | 11 | // latest verification time 12 | const AUTH_TIMEOUT = 6000; 13 | // authentication type identifier 14 | const TOP_LEVEL = "TOP_LEVEL"; 15 | 16 | // Top-level authority authentication middleware (this is the first place for any authority authentication middleware) 17 | routerApp.use(async (event, ctx, _, next) => { 18 | const socket = ctx.socket; 19 | // release all data flow controllers 20 | if (event.startsWith("stream")) return next(); 21 | // Except for the auth controller, which is publicly accessible, other business controllers must be authorized before they can be accessed 22 | if (event === "auth") return await next(); 23 | if (!ctx.session) throw new Error("Session does not exist in authentication middleware."); 24 | if (ctx.session.key === globalConfiguration.config.key && ctx.session.type === TOP_LEVEL && ctx.session.login && ctx.session.id) { 25 | return await next(); 26 | } 27 | logger.warn($t("auth_router.notAccess", { id: socket.id, address: socket.handshake.address, event: event })); 28 | return protocol.error(ctx, "error", IGNORE); 29 | }); 30 | 31 | // log output middleware 32 | // routerApp.use((event, ctx, data, next) => { 33 | // try { 34 | // const socket = ctx.socket; 35 | // logger.info(`Received ${event} command from ${socket.id}(${socket.handshake.address}).`); 36 | // logger.info(` - data: ${JSON.stringify(data)}.`); 37 | // } catch (err) { 38 | // logger.error("Logging error:", err); 39 | // } finally { 40 | // next(); 41 | // } 42 | // }); 43 | 44 | // authentication controller 45 | routerApp.on("auth", (ctx, data) => { 46 | if (data === globalConfiguration.config.key) { 47 | // The authentication is passed, and the registered session is a trusted session 48 | logger.info($t("auth_router.access", { id: ctx.socket.id, address: ctx.socket.handshake.address })); 49 | loginSuccessful(ctx, data); 50 | protocol.msg(ctx, "auth", true); 51 | } else { 52 | protocol.msg(ctx, "auth", false); 53 | } 54 | }); 55 | 56 | // Connected event for timeout authentication close 57 | routerApp.on("connection", (ctx) => { 58 | const session = ctx.session; 59 | setTimeout(() => { 60 | if (!session.login) { 61 | ctx.socket.disconnect(); 62 | logger.info($t("auth_router.disconnect", { id: ctx.socket.id, address: ctx.socket.handshake.address })); 63 | } 64 | }, AUTH_TIMEOUT); 65 | }); 66 | 67 | // This function must be executed after successful login 68 | function loginSuccessful(ctx: RouterContext, data: string) { 69 | ctx.session.key = data; 70 | ctx.session.login = true; 71 | ctx.session.id = ctx.socket.id; 72 | ctx.session.type = TOP_LEVEL; 73 | return ctx.session; 74 | } 75 | -------------------------------------------------------------------------------- /src/routers/environment_router.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../i18n"; 4 | import { DockerManager } from "../service/docker_service"; 5 | import * as protocol from "../service/protocol"; 6 | import { routerApp } from "../service/router"; 7 | import * as fs from "fs-extra"; 8 | import path from "path"; 9 | import { v4 } from "uuid"; 10 | import logger from "../service/log"; 11 | import os from "os"; 12 | 13 | // Get the image list of this system 14 | routerApp.on("environment/images", async (ctx, data) => { 15 | try { 16 | const docker = new DockerManager().getDocker(); 17 | const result = await docker.listImages(); 18 | protocol.response(ctx, result); 19 | } catch (error) { 20 | protocol.responseError(ctx, $t("environment_router.dockerInfoErr")); 21 | } 22 | }); 23 | 24 | // Get the list of containers in this system 25 | routerApp.on("environment/containers", async (ctx, data) => { 26 | try { 27 | const docker = new DockerManager().getDocker(); 28 | const result = await docker.listContainers(); 29 | protocol.response(ctx, result); 30 | } catch (error) { 31 | protocol.responseError(ctx, error); 32 | } 33 | }); 34 | 35 | // Get the network list of this system 36 | routerApp.on("environment/networkModes", async (ctx, data) => { 37 | try { 38 | const docker = new DockerManager().getDocker(); 39 | const result = await docker.listNetworks(); 40 | protocol.response(ctx, result); 41 | } catch (error) { 42 | protocol.responseError(ctx, error); 43 | } 44 | }); 45 | 46 | // create image 47 | routerApp.on("environment/new_image", async (ctx, data) => { 48 | try { 49 | const dockerFileText = data.dockerFile; 50 | const name = data.name; 51 | const tag = data.tag; 52 | // Initialize the image file directory and Dockerfile 53 | const uuid = v4(); 54 | const dockerFileDir = path.normalize(path.join(process.cwd(), "tmp", uuid)); 55 | if (!fs.existsSync(dockerFileDir)) fs.mkdirsSync(dockerFileDir); 56 | 57 | // write to DockerFile 58 | const dockerFilepath = path.normalize(path.join(dockerFileDir, "Dockerfile")); 59 | await fs.writeFile(dockerFilepath, dockerFileText, { encoding: "utf-8" }); 60 | 61 | logger.info($t("environment_router.crateImage", { name: name, tag: tag, dockerFileText: dockerFileText })); 62 | 63 | // pre-response 64 | protocol.response(ctx, true); 65 | 66 | // start creating 67 | const dockerImageName = `${name}:${tag}`; 68 | try { 69 | await new DockerManager().startBuildImage(dockerFileDir, dockerImageName); 70 | logger.info($t("environment_router.crateSuccess", { name: name, tag: tag })); 71 | } catch (error) { 72 | logger.info($t("environment_router.crateErr", { name: name, tag: tag, error: error })); 73 | } 74 | } catch (error) { 75 | protocol.responseError(ctx, error); 76 | } 77 | }); 78 | 79 | // delete image 80 | routerApp.on("environment/del_image", async (ctx, data) => { 81 | try { 82 | const imageId = data.imageId; 83 | const docker = new DockerManager().getDocker(); 84 | const image = docker.getImage(imageId); 85 | if (image) { 86 | logger.info($t("environment_router.delImage", { imageId: imageId })); 87 | await image.remove(); 88 | } else { 89 | throw new Error("Image does not exist"); 90 | } 91 | protocol.response(ctx, true); 92 | } catch (error) { 93 | protocol.responseError(ctx, error); 94 | } 95 | }); 96 | 97 | // Get the progress of all mirroring tasks 98 | routerApp.on("environment/progress", async (ctx) => { 99 | try { 100 | const data: any = {}; 101 | DockerManager.builderProgress.forEach((v, k) => { 102 | data[k] = v; 103 | }); 104 | protocol.response(ctx, data); 105 | } catch (error) { 106 | protocol.responseError(ctx, error); 107 | } 108 | }); 109 | -------------------------------------------------------------------------------- /src/routers/file_router.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../i18n"; 4 | import * as protocol from "../service/protocol"; 5 | import { routerApp } from "../service/router"; 6 | import InstanceSubsystem from "../service/system_instance"; 7 | import { getFileManager } from "../service/file_router_service"; 8 | import { globalConfiguration, globalEnv } from "../entity/config"; 9 | import os from "os"; 10 | import * as nodeDiskInfo from "node-disk-info"; 11 | 12 | let diskInfos: any[] = []; 13 | if (os.platform() === "win32") { 14 | diskInfos = nodeDiskInfo.getDiskInfoSync(); 15 | } 16 | 17 | // Some routers operate router authentication middleware 18 | routerApp.use((event, ctx, data, next) => { 19 | if (event.startsWith("file/")) { 20 | const instanceUuid = data.instanceUuid; 21 | if (!InstanceSubsystem.exists(instanceUuid)) { 22 | return protocol.error(ctx, event, { 23 | instanceUuid: instanceUuid, 24 | err: $t("file_router.instanceNotExist", { instanceUuid: instanceUuid }) 25 | }); 26 | } 27 | } 28 | next(); 29 | }); 30 | 31 | // List the files in the specified instance working directory 32 | routerApp.on("file/list", (ctx, data) => { 33 | try { 34 | const fileManager = getFileManager(data.instanceUuid); 35 | const { page, pageSize, target, fileName } = data; 36 | fileManager.cd(target); 37 | const overview = fileManager.list(page, pageSize, fileName); 38 | protocol.response(ctx, overview); 39 | } catch (error) { 40 | protocol.responseError(ctx, error); 41 | } 42 | }); 43 | 44 | // File chmod (only Linux) 45 | routerApp.on("file/chmod", async (ctx, data) => { 46 | try { 47 | const fileManager = getFileManager(data.instanceUuid); 48 | const { chmod, target, deep } = data; 49 | await fileManager.chmod(target, chmod, deep); 50 | protocol.response(ctx, true); 51 | } catch (error) { 52 | protocol.responseError(ctx, error); 53 | } 54 | }); 55 | 56 | // Query the status of the file management system 57 | routerApp.on("file/status", async (ctx, data) => { 58 | try { 59 | const instance = InstanceSubsystem.getInstance(data.instanceUuid); 60 | 61 | protocol.response(ctx, { 62 | instanceFileTask: instance.info.fileLock ?? 0, 63 | globalFileTask: globalEnv.fileTaskCount ?? 0, 64 | platform: os.platform(), 65 | isGlobalInstance: data.instanceUuid === InstanceSubsystem.GLOBAL_INSTANCE_UUID, 66 | disks: diskInfos.map((v) => { 67 | return String(v._mounted).replace(":", ""); 68 | }) 69 | }); 70 | } catch (error) { 71 | protocol.responseError(ctx, error); 72 | } 73 | }); 74 | 75 | // Create a new file 76 | routerApp.on("file/touch", (ctx, data) => { 77 | try { 78 | const target = data.target; 79 | const fileManager = getFileManager(data.instanceUuid); 80 | fileManager.newFile(target); 81 | protocol.response(ctx, true); 82 | } catch (error) { 83 | protocol.responseError(ctx, error); 84 | } 85 | }); 86 | 87 | // Create a directory 88 | routerApp.on("file/mkdir", (ctx, data) => { 89 | try { 90 | const target = data.target; 91 | const fileManager = getFileManager(data.instanceUuid); 92 | fileManager.mkdir(target); 93 | protocol.response(ctx, true); 94 | } catch (error) { 95 | protocol.responseError(ctx, error); 96 | } 97 | }); 98 | 99 | // copy the file 100 | routerApp.on("file/copy", async (ctx, data) => { 101 | try { 102 | // [["a.txt","b.txt"],["cxz","zzz"]] 103 | const targets = data.targets; 104 | const fileManager = getFileManager(data.instanceUuid); 105 | for (const target of targets) { 106 | fileManager.copy(target[0], target[1]); 107 | } 108 | protocol.response(ctx, true); 109 | } catch (error) { 110 | protocol.responseError(ctx, error); 111 | } 112 | }); 113 | 114 | // move the file 115 | routerApp.on("file/move", async (ctx, data) => { 116 | try { 117 | // [["a.txt","b.txt"],["cxz","zzz"]] 118 | const targets = data.targets; 119 | const fileManager = getFileManager(data.instanceUuid); 120 | for (const target of targets) { 121 | await fileManager.move(target[0], target[1]); 122 | } 123 | protocol.response(ctx, true); 124 | } catch (error) { 125 | protocol.responseError(ctx, error); 126 | } 127 | }); 128 | 129 | // Delete Files 130 | routerApp.on("file/delete", async (ctx, data) => { 131 | try { 132 | const targets = data.targets; 133 | const fileManager = getFileManager(data.instanceUuid); 134 | for (const target of targets) { 135 | // async delete 136 | fileManager.delete(target); 137 | } 138 | protocol.response(ctx, true); 139 | } catch (error) { 140 | protocol.responseError(ctx, error); 141 | } 142 | }); 143 | 144 | // edit file 145 | routerApp.on("file/edit", async (ctx, data) => { 146 | try { 147 | const target = data.target; 148 | const text = data.text; 149 | const fileManager = getFileManager(data.instanceUuid); 150 | const result = await fileManager.edit(target, text); 151 | protocol.response(ctx, result ? result : true); 152 | } catch (error) { 153 | protocol.responseError(ctx, error); 154 | } 155 | }); 156 | 157 | // compress/decompress the file 158 | routerApp.on("file/compress", async (ctx, data) => { 159 | const maxFileTask = globalConfiguration.config.maxFileTask; 160 | try { 161 | const source = data.source; 162 | const targets = data.targets; 163 | const type = data.type; 164 | const code = data.code; 165 | const fileManager = getFileManager(data.instanceUuid); 166 | const instance = InstanceSubsystem.getInstance(data.instanceUuid); 167 | if (instance.info.fileLock >= maxFileTask) { 168 | throw new Error($t("file_router.unzipLimit", { maxFileTask: maxFileTask, fileLock: instance.info.fileLock })); 169 | } 170 | // Statistics of the number of tasks in a single instance file and the number of tasks in the entire daemon process 171 | function fileTaskStart() { 172 | instance.info.fileLock++; 173 | globalEnv.fileTaskCount++; 174 | } 175 | function fileTaskEnd() { 176 | instance.info.fileLock--; 177 | globalEnv.fileTaskCount--; 178 | } 179 | 180 | protocol.response(ctx, true); 181 | 182 | // start decompressing or compressing the file 183 | fileTaskStart(); 184 | try { 185 | if (type === 1) { 186 | await fileManager.promiseZip(source, targets, code); 187 | } else { 188 | await fileManager.promiseUnzip(source, targets, code); 189 | } 190 | } catch (error) { 191 | throw error; 192 | } finally { 193 | fileTaskEnd(); 194 | } 195 | } catch (error) { 196 | protocol.responseError(ctx, error); 197 | } finally { 198 | } 199 | }); 200 | -------------------------------------------------------------------------------- /src/routers/http_router.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../i18n"; 4 | import Router from "@koa/router"; 5 | import send from "koa-send"; 6 | import fs from "fs-extra"; 7 | import path from "path"; 8 | import { missionPassport } from "../service/mission_passport"; 9 | import InstanceSubsystem from "../service/system_instance"; 10 | import FileManager from "../service/system_file"; 11 | 12 | const router = new Router(); 13 | 14 | // Define the HTTP home page display route 15 | router.all("/", async (ctx) => { 16 | ctx.body = "[MCSManager Daemon] Status: OK | reference: https://mcsmanager.com/"; 17 | ctx.status = 200; 18 | }); 19 | 20 | // file download route 21 | router.get("/download/:key/:fileName", async (ctx) => { 22 | const key = ctx.params.key; 23 | const paramsFileName = ctx.params.fileName; 24 | try { 25 | // Get the task from the task center 26 | const mission = missionPassport.getMission(key, "download"); 27 | if (!mission) throw new Error((ctx.body = "Access denied: No task found")); 28 | const instance = InstanceSubsystem.getInstance(mission.parameter.instanceUuid); 29 | if (!instance) throw new Error($t("http_router.instanceNotExist")); 30 | if (!FileManager.checkFileName(paramsFileName)) throw new Error($t("http_router.fileNameNotSpec")); 31 | 32 | const cwd = instance.config.cwd; 33 | const fileRelativePath = mission.parameter.fileName; 34 | 35 | // Check for file cross-directory security risks 36 | const fileManager = new FileManager(cwd); 37 | if (!fileManager.check(fileRelativePath)) throw new Error((ctx.body = "Access denied: Invalid destination")); 38 | 39 | // send File 40 | const fileAbsPath = fileManager.toAbsolutePath(fileRelativePath); 41 | const fileDir = path.dirname(fileAbsPath); 42 | const fileName = path.basename(fileAbsPath); 43 | ctx.set("Content-Type", "application/octet-stream"); 44 | await send(ctx, fileName, { root: fileDir + "/", hidden: true }); 45 | } catch (error) { 46 | ctx.body = $t("http_router.downloadErr", { error: error.message }); 47 | ctx.status = 500; 48 | } finally { 49 | missionPassport.deleteMission(key); 50 | } 51 | }); 52 | 53 | // file upload route 54 | router.post("/upload/:key", async (ctx) => { 55 | const key = ctx.params.key; 56 | const unzip = ctx.query.unzip; 57 | const zipCode = String(ctx.query.code); 58 | try { 59 | // Get the task & check the task & check if the instance exists 60 | const mission = missionPassport.getMission(key, "upload"); 61 | if (!mission) throw new Error("Access denied: No task found"); 62 | const instance = InstanceSubsystem.getInstance(mission.parameter.instanceUuid); 63 | if (!instance) throw new Error("Access denied: No instance found"); 64 | const uploadDir = mission.parameter.uploadDir; 65 | const cwd = instance.config.cwd; 66 | 67 | const file = ctx.request.files.file; 68 | if (file && !(file instanceof Array)) { 69 | // Confirm storage location 70 | const fullFileName = file.name; 71 | const fileSaveRelativePath = path.normalize(path.join(uploadDir, fullFileName)); 72 | 73 | // File name special character filtering (to prevent any cross-directory intrusion) 74 | if (!FileManager.checkFileName(fullFileName)) throw new Error("Access denied: Malformed file name"); 75 | 76 | // Check for file cross-directory security risks 77 | const fileManager = new FileManager(cwd); 78 | if (!fileManager.checkPath(fileSaveRelativePath)) throw new Error("Access denied: Invalid destination"); 79 | const fileSaveAbsolutePath = fileManager.toAbsolutePath(fileSaveRelativePath); 80 | 81 | // prohibit overwriting the original file 82 | // if (fs.existsSync(fileSaveAbsolutePath)) throw new Error("The file exists and cannot be overwritten"); 83 | 84 | // Copy the file from the temporary folder to the specified directory 85 | const reader = fs.createReadStream(file.path); 86 | const upStream = fs.createWriteStream(fileSaveAbsolutePath); 87 | reader.pipe(upStream); 88 | reader.on("close", () => { 89 | if (unzip) { 90 | // If decompression is required, perform the decompression task 91 | const filemanager = new FileManager(instance.config.cwd); 92 | filemanager.unzip(fullFileName, "", zipCode); 93 | } 94 | }); 95 | return (ctx.body = "OK"); 96 | } 97 | ctx.body = $t("http_router.updateErr"); 98 | ctx.status = 500; 99 | } catch (error) { 100 | ctx.body = error.message; 101 | ctx.status = 500; 102 | } finally { 103 | missionPassport.deleteMission(key); 104 | } 105 | }); 106 | 107 | export default router; 108 | -------------------------------------------------------------------------------- /src/routers/info_router.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import * as protocol from "../service/protocol"; 4 | import { routerApp } from "../service/router"; 5 | import InstanceSubsystem from "../service/system_instance"; 6 | import Instance from "../entity/instance/instance"; 7 | 8 | import { systemInfo } from "../common/system_info"; 9 | import { getVersion } from "../service/version"; 10 | import { globalConfiguration } from "../entity/config"; 11 | import i18next from "i18next"; 12 | import logger from "../service/log"; 13 | import fs from "fs-extra"; 14 | import { LOCAL_PRESET_LANG_PATH } from "../const"; 15 | import VisualDataSubsystem from "../service/system_visual_data"; 16 | 17 | // Get the basic information of the daemon system 18 | routerApp.on("info/overview", async (ctx) => { 19 | const daemonVersion = getVersion(); 20 | let total = 0; 21 | let running = 0; 22 | InstanceSubsystem.getInstances().forEach((v) => { 23 | total++; 24 | if (v.status() == Instance.STATUS_RUNNING) running++; 25 | }); 26 | const info = { 27 | version: daemonVersion, 28 | process: { 29 | cpu: process.cpuUsage().system, 30 | memory: process.memoryUsage().heapUsed, 31 | cwd: process.cwd() 32 | }, 33 | instance: { 34 | running, 35 | total 36 | }, 37 | system: systemInfo(), 38 | cpuMemChart: VisualDataSubsystem.getSystemChartArray() 39 | }; 40 | protocol.response(ctx, info); 41 | }); 42 | 43 | routerApp.on("info/setting", async (ctx, data) => { 44 | const language = String(data.language); 45 | try { 46 | logger.warn("Language change:", language); 47 | i18next.changeLanguage(language); 48 | fs.remove(LOCAL_PRESET_LANG_PATH, () => {}); 49 | globalConfiguration.config.language = language; 50 | globalConfiguration.store(); 51 | protocol.response(ctx, true); 52 | } catch (error) { 53 | protocol.responseError(ctx, error); 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /src/routers/instance_event_router.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import path from "path"; 4 | 5 | import RouterContext from "../entity/ctx"; 6 | import * as protocol from "../service/protocol"; 7 | import InstanceSubsystem from "../service/system_instance"; 8 | import fs from "fs-extra"; 9 | const MAX_LOG_SIZE = 512; 10 | 11 | // buffer 12 | const buffer = new Map(); 13 | setInterval(() => { 14 | buffer.forEach((buf, instanceUuid) => { 15 | if (!buf || !instanceUuid) return; 16 | const logFilePath = path.join(InstanceSubsystem.LOG_DIR, `${instanceUuid}.log`); 17 | if (!fs.existsSync(InstanceSubsystem.LOG_DIR)) fs.mkdirsSync(InstanceSubsystem.LOG_DIR); 18 | try { 19 | const fileInfo = fs.statSync(logFilePath); 20 | if (fileInfo && fileInfo.size > 1024 * MAX_LOG_SIZE) fs.removeSync(logFilePath); 21 | } catch (err) {} 22 | fs.writeFile(logFilePath, buf, { encoding: "utf-8", flag: "a" }, () => { 23 | buffer.set(instanceUuid, ""); 24 | }); 25 | }); 26 | }, 500); 27 | 28 | // output stream record to buffer 29 | async function outputLog(instanceUuid: string, text: string) { 30 | const buf = (buffer.get(instanceUuid) ?? "") + text; 31 | if (buf.length > 1024 * 1024) buffer.set(instanceUuid, ""); 32 | buffer.set(instanceUuid, buf ?? null); 33 | } 34 | 35 | // instance output stream event 36 | // By default, it is added to the data cache to control the sending rate to ensure its stability 37 | InstanceSubsystem.on("data", (instanceUuid: string, text: string) => { 38 | InstanceSubsystem.forEachForward(instanceUuid, (socket) => { 39 | protocol.msg(new RouterContext(null, socket), "instance/stdout", { 40 | instanceUuid: instanceUuid, 41 | text: text 42 | }); 43 | }); 44 | // Append the output to the log file 45 | outputLog(instanceUuid, text) 46 | .then(() => {}) 47 | .catch(() => {}); 48 | }); 49 | 50 | // instance exit event 51 | InstanceSubsystem.on("exit", (obj: any) => { 52 | InstanceSubsystem.forEachForward(obj.instanceUuid, (socket) => { 53 | protocol.msg(new RouterContext(null, socket), "instance/stopped", { 54 | instanceUuid: obj.instanceUuid, 55 | instanceName: obj.instanceName 56 | }); 57 | }); 58 | }); 59 | 60 | // instance start event 61 | InstanceSubsystem.on("open", (obj: any) => { 62 | InstanceSubsystem.forEachForward(obj.instanceUuid, (socket) => { 63 | protocol.msg(new RouterContext(null, socket), "instance/opened", { 64 | instanceUuid: obj.instanceUuid, 65 | instanceName: obj.instanceName 66 | }); 67 | }); 68 | }); 69 | 70 | // Instance failure event (usually used for startup failure, or other operation failures) 71 | InstanceSubsystem.on("failure", (obj: any) => { 72 | InstanceSubsystem.forEachForward(obj.instanceUuid, (socket) => { 73 | protocol.msg(new RouterContext(null, socket), "instance/failure", { 74 | instanceUuid: obj.instanceUuid, 75 | instanceName: obj.instanceName 76 | }); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/routers/passport_router.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../i18n"; 4 | import { routerApp } from "../service/router"; 5 | import { missionPassport } from "../service/mission_passport"; 6 | import * as protocol from "../service/protocol"; 7 | 8 | const ONE_HOUR_TIME = 3600000; 9 | const TASK_MAX_TIME = 1; 10 | 11 | // register temporary task passport 12 | // For example, file upload, file download, these operations that need to bypass the Web side, 13 | // all need to use this route 14 | routerApp.on("passport/register", (ctx, data) => { 15 | const name = data.name; 16 | const password = data.password; 17 | const parameter = data.parameter; 18 | const count = data.count; 19 | const start = new Date().getTime(); 20 | const end = start + ONE_HOUR_TIME * TASK_MAX_TIME; 21 | if (!name || !password) throw new Error($t("passport_router.registerErr")); 22 | missionPassport.registerMission(password, { 23 | name, 24 | parameter, 25 | count, 26 | start, 27 | end 28 | }); 29 | protocol.response(ctx, true); 30 | }); 31 | -------------------------------------------------------------------------------- /src/routers/schedule_router.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { routerApp } from "../service/router"; 4 | import * as protocol from "../service/protocol"; 5 | import InstanceControlSubsystem from "../service/system_instance_control"; 6 | 7 | // create a scheduled task 8 | routerApp.on("schedule/register", (ctx, data) => { 9 | try { 10 | InstanceControlSubsystem.registerScheduleJob(data); 11 | protocol.response(ctx, true); 12 | } catch (error) { 13 | protocol.responseError(ctx, error); 14 | } 15 | }); 16 | 17 | // get the task list 18 | routerApp.on("schedule/list", (ctx, data) => { 19 | protocol.response(ctx, InstanceControlSubsystem.listScheduleJob(data.instanceUuid)); 20 | }); 21 | 22 | // delete the task plan 23 | routerApp.on("schedule/delete", (ctx, data) => { 24 | InstanceControlSubsystem.deleteScheduleTask(data.instanceUuid, data.name); 25 | protocol.response(ctx, true); 26 | }); 27 | -------------------------------------------------------------------------------- /src/routers/stream_router.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../i18n"; 4 | import * as protocol from "../service/protocol"; 5 | import { routerApp } from "../service/router"; 6 | import { missionPassport } from "../service/mission_passport"; 7 | import InstanceSubsystem from "../service/system_instance"; 8 | import logger from "../service/log"; 9 | import SendCommand from "../entity/commands/cmd"; 10 | import SendInput from "../entity/commands/input"; 11 | import { IGNORE } from "../const"; 12 | 13 | // Authorization authentication middleware 14 | routerApp.use(async (event, ctx, data, next) => { 15 | // release data flow authentication route 16 | if (event === "stream/auth") return next(); 17 | // Check other routes for data flow 18 | if (event.startsWith("stream")) { 19 | if (ctx.session.stream && ctx.session.stream.check === true && ctx.session.type === "STREAM") { 20 | return await next(); 21 | } 22 | return protocol.error(ctx, "error", IGNORE); 23 | } 24 | return await next(); 25 | }); 26 | 27 | // Publicly accessible dataflow authentication route 28 | routerApp.on("stream/auth", (ctx, data) => { 29 | try { 30 | const password = data.password; 31 | const mission = missionPassport.getMission(password, "stream_channel"); 32 | if (!mission) throw new Error($t("stream_router.taskNotExist")); 33 | 34 | // The instance UUID parameter must come from the task parameter and cannot be used directly 35 | const instance = InstanceSubsystem.getInstance(mission.parameter.instanceUuid); 36 | if (!instance) throw new Error($t("stream_router.instanceNotExist")); 37 | 38 | // Add the data stream authentication ID 39 | logger.info($t("stream_router.authSuccess", { id: ctx.socket.id, address: ctx.socket.handshake.address })); 40 | ctx.session.id = ctx.socket.id; 41 | ctx.session.login = true; 42 | ctx.session.type = "STREAM"; 43 | ctx.session.stream = { 44 | check: true, 45 | instanceUuid: instance.instanceUuid 46 | }; 47 | 48 | // Start forwarding output stream data to this Socket 49 | InstanceSubsystem.forward(instance.instanceUuid, ctx.socket); 50 | logger.info( 51 | $t("stream_router.establishConnection", { id: ctx.socket.id, address: ctx.socket.handshake.address, uuid: instance.instanceUuid }) 52 | ); 53 | 54 | // Cancel forwarding events when registration is disconnected 55 | ctx.socket.on("disconnect", () => { 56 | InstanceSubsystem.stopForward(instance.instanceUuid, ctx.socket); 57 | logger.info($t("stream_router.disconnect", { id: ctx.socket.id, address: ctx.socket.handshake.address, uuid: instance.instanceUuid })); 58 | }); 59 | protocol.response(ctx, true); 60 | } catch (error) { 61 | protocol.responseError(ctx, error, { 62 | notPrintErr: true 63 | }); 64 | } 65 | }); 66 | 67 | // Get instance details 68 | routerApp.on("stream/detail", async (ctx) => { 69 | try { 70 | const instanceUuid = ctx.session.stream.instanceUuid; 71 | const instance = InstanceSubsystem.getInstance(instanceUuid); 72 | // const processInfo = await instance. forceExec(new ProcessInfoCommand()); 73 | protocol.response(ctx, { 74 | instanceUuid: instance.instanceUuid, 75 | started: instance.startCount, 76 | status: instance.status(), 77 | config: instance.config, 78 | info: instance.info 79 | // processInfo 80 | }); 81 | } catch (error) { 82 | protocol.responseError(ctx, error); 83 | } 84 | }); 85 | 86 | // Execute commands, line-based interactive input and output streams for ordinary processes 87 | routerApp.on("stream/input", async (ctx, data) => { 88 | try { 89 | const command = data.command; 90 | const instanceUuid = ctx.session.stream.instanceUuid; 91 | const instance = InstanceSubsystem.getInstance(instanceUuid); 92 | await instance.exec(new SendCommand(command)); 93 | } catch (error) { 94 | // Ignore potential high frequency exceptions here 95 | // protocol.responseError(ctx, error); 96 | } 97 | }); 98 | 99 | // Process terminal input, suitable for direct connection input and output streams of simulated terminals. 100 | routerApp.on("stream/write", async (ctx, data) => { 101 | try { 102 | const buf = data.input; 103 | const instanceUuid = ctx.session.stream.instanceUuid; 104 | const instance = InstanceSubsystem.getInstance(instanceUuid); 105 | // run without command execution 106 | if (instance.process) instance.process.write(buf); 107 | } catch (error) { 108 | // Ignore potential high frequency exceptions here 109 | // protocol.responseError(ctx, error); 110 | } 111 | }); 112 | 113 | // handle terminal resize 114 | routerApp.on("stream/resize", async (ctx, data) => { 115 | try { 116 | const instanceUuid = ctx.session.stream.instanceUuid; 117 | const instance = InstanceSubsystem.getInstance(instanceUuid); 118 | if (instance.config.processType === "docker") await instance.execPreset("resize", data); 119 | } catch (error) { 120 | // protocol.responseError(ctx, error); 121 | } 122 | }); 123 | -------------------------------------------------------------------------------- /src/service/async_task_service/index.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import EventEmitter from "events"; 4 | import logger from "../log"; 5 | export interface IAsyncTaskJSON { 6 | [key: string]: any; 7 | } 8 | 9 | export interface IAsyncTask extends EventEmitter { 10 | // The taskId must be complex enough to prevent other users from accessing the information 11 | taskId: string; 12 | type: string; 13 | start(): Promise; 14 | stop(): Promise; 15 | status(): number; 16 | toObject(): IAsyncTaskJSON; 17 | } 18 | 19 | export abstract class AsyncTask extends EventEmitter implements IAsyncTask { 20 | constructor() { 21 | super(); 22 | } 23 | 24 | public taskId: string; 25 | public type: string; 26 | 27 | // 0=stop 1=running -1=error 28 | protected _status = 0; 29 | 30 | public start() { 31 | this._status = 1; 32 | const r = this.onStarted(); 33 | this.emit("started"); 34 | return r; 35 | } 36 | 37 | public stop() { 38 | if (this._status !== -1) this._status = 0; 39 | const r = this.onStopped(); 40 | this.emit("stopped"); 41 | return r; 42 | } 43 | 44 | public error(err: Error) { 45 | this._status = -1; 46 | logger.error(`AsyncTask - ID: ${this.taskId} TYPE: ${this.type} Error:`, err); 47 | this.onError(err); 48 | this.emit("error", err); 49 | 50 | this.stop(); 51 | } 52 | 53 | status(): number { 54 | return this._status; 55 | } 56 | 57 | public abstract onStarted(): Promise; 58 | public abstract onStopped(): Promise; 59 | public abstract onError(err: Error): void; 60 | public abstract toObject(): IAsyncTaskJSON; 61 | } 62 | 63 | export class TaskCenter { 64 | public static tasks = new Array(); 65 | 66 | public static addTask(t: IAsyncTask) { 67 | TaskCenter.tasks.push(t); 68 | t.start(); 69 | t.on("stopped", () => TaskCenter.onTaskStopped(t)); 70 | t.on("error", () => TaskCenter.onTaskError(t)); 71 | } 72 | 73 | public static onTaskStopped(t: IAsyncTask) { 74 | logger.info("Async Task:", t.taskId, "Stopped."); 75 | } 76 | 77 | public static onTaskError(t: IAsyncTask) { 78 | logger.info("Async Task:", t.taskId, "Failed."); 79 | } 80 | 81 | public static getTask(taskId: string, type?: string) { 82 | for (const iterator of TaskCenter.tasks) { 83 | if (iterator.taskId === taskId && (type == null || iterator.type === type)) return iterator; 84 | } 85 | } 86 | 87 | public static getTasks(type?: string) { 88 | const result: IAsyncTask[] = []; 89 | for (const iterator of TaskCenter.tasks) { 90 | if (type == null || iterator.type === type) { 91 | result.push(iterator); 92 | } 93 | } 94 | return result; 95 | } 96 | 97 | public static deleteAllStoppedTask() { 98 | TaskCenter.tasks.forEach((v, i, arr) => { 99 | if (v.status() !== 1) { 100 | arr.splice(i, 1); 101 | } 102 | }); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/service/async_task_service/quick_install.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { v4 } from "uuid"; 4 | import axios from "axios"; 5 | import { pipeline, Readable } from "stream"; 6 | import fs from "fs-extra"; 7 | import Instance from "../../entity/instance/instance"; 8 | import InstanceSubsystem from "../system_instance"; 9 | import InstanceConfig from "../../entity/instance/Instance_config"; 10 | import { $t, i18next } from "../../i18n"; 11 | import path from "path"; 12 | import { getFileManager } from "../file_router_service"; 13 | import EventEmitter from "events"; 14 | import { IAsyncTask, IAsyncTaskJSON, TaskCenter, AsyncTask } from "./index"; 15 | import logger from "../log"; 16 | 17 | export class QuickInstallTask extends AsyncTask { 18 | public static TYPE = "QuickInstallTask"; 19 | 20 | public instance: Instance; 21 | public readonly TMP_ZIP_NAME = "mcsm_install_package.zip"; 22 | public readonly ZIP_CONFIG_JSON = "mcsmanager-config.json"; 23 | public zipPath = ""; 24 | 25 | private downloadStream: fs.WriteStream = null; 26 | private JAVA_17_PATH = path.normalize(path.join(process.cwd(), "lib", "jre17", "bin", "java.exe")); 27 | 28 | constructor(public instanceName: string, public targetLink: string) { 29 | super(); 30 | const config = new InstanceConfig(); 31 | config.nickname = instanceName; 32 | config.cwd = null; 33 | config.stopCommand = "stop"; 34 | config.type = Instance.TYPE_MINECRAFT_JAVA; 35 | this.instance = InstanceSubsystem.createInstance(config); 36 | this.taskId = `${QuickInstallTask.TYPE}-${this.instance.instanceUuid}-${v4()}`; 37 | this.type = QuickInstallTask.TYPE; 38 | } 39 | 40 | private download(): Promise { 41 | return new Promise(async (resolve, reject) => { 42 | try { 43 | this.zipPath = path.normalize(path.join(this.instance.config.cwd, this.TMP_ZIP_NAME)); 44 | const writeStream = fs.createWriteStream(this.zipPath); 45 | const response = await axios({ 46 | url: this.targetLink, 47 | responseType: "stream" 48 | }); 49 | this.downloadStream = pipeline(response.data, writeStream, (err) => { 50 | if (err) { 51 | reject(err); 52 | } else { 53 | resolve(true); 54 | } 55 | }); 56 | } catch (error) { 57 | reject(error); 58 | } 59 | }); 60 | } 61 | 62 | private hasJava17() { 63 | return fs.existsSync(this.JAVA_17_PATH); 64 | } 65 | 66 | async onStarted() { 67 | const fileManager = getFileManager(this.instance.instanceUuid); 68 | try { 69 | let result = await this.download(); 70 | result = await fileManager.promiseUnzip(this.TMP_ZIP_NAME, ".", "UTF-8"); 71 | if (!result) throw new Error($t("quick_install.unzipError")); 72 | const config = JSON.parse(await fileManager.readFile(this.ZIP_CONFIG_JSON)) as InstanceConfig; 73 | 74 | if (config.startCommand && config.startCommand.includes("{{java}}")) { 75 | if (this.hasJava17()) { 76 | config.startCommand = config.startCommand.replace("{{java}}", `"${this.JAVA_17_PATH}"`); 77 | } else { 78 | config.startCommand = config.startCommand.replace("{{java}}", "java"); 79 | } 80 | } 81 | 82 | this.instance.parameters(config); 83 | this.stop(); 84 | } catch (error) { 85 | this.error(error); 86 | } finally { 87 | fs.remove(fileManager.toAbsolutePath(this.TMP_ZIP_NAME), () => {}); 88 | } 89 | } 90 | 91 | async onStopped(): Promise { 92 | try { 93 | if (this.downloadStream) this.downloadStream.destroy(new Error("STOP TASK")); 94 | } catch (error) {} 95 | } 96 | 97 | onError(): void {} 98 | 99 | toObject(): IAsyncTaskJSON { 100 | return JSON.parse( 101 | JSON.stringify({ 102 | taskId: this.taskId, 103 | status: this.status(), 104 | instanceUuid: this.instance.instanceUuid, 105 | instanceStatus: this.instance.status(), 106 | instanceConfig: this.instance.config 107 | }) 108 | ); 109 | } 110 | } 111 | 112 | export function createQuickInstallTask(targetLink: string, instanceName: string) { 113 | if (!targetLink || !instanceName) throw new Error("targetLink or instanceName is null!"); 114 | const task = new QuickInstallTask(instanceName, targetLink); 115 | TaskCenter.addTask(task); 116 | return task; 117 | } 118 | -------------------------------------------------------------------------------- /src/service/docker_service.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import dockerode from "dockerode"; 4 | import Docker from "dockerode"; 5 | 6 | export class DockerManager { 7 | // 1=creating 2=creating completed -1=creating error 8 | public static readonly builderProgress = new Map(); 9 | 10 | public docker: Docker = null; 11 | 12 | constructor(p?: any) { 13 | this.docker = new Docker(p); 14 | } 15 | 16 | public getDocker() { 17 | return this.docker; 18 | } 19 | 20 | public static setBuilderProgress(imageName: string, status: number) { 21 | DockerManager.builderProgress.set(imageName, status); 22 | } 23 | 24 | public static getBuilderProgress(imageName: string) { 25 | return DockerManager.builderProgress.get(imageName); 26 | } 27 | 28 | async startBuildImage(dockerFileDir: string, dockerImageName: string) { 29 | try { 30 | // Set the current image creation progress 31 | DockerManager.setBuilderProgress(dockerImageName, 1); 32 | // Issue the create image command 33 | const stream = await this.docker.buildImage( 34 | { 35 | context: dockerFileDir, 36 | src: ["Dockerfile"] 37 | }, 38 | { t: dockerImageName } 39 | ); 40 | // wait for creation to complete 41 | await new Promise((resolve, reject) => { 42 | this.docker.modem.followProgress(stream, (err, res) => (err ? reject(err) : resolve(res))); 43 | }); 44 | // Set the current image creation progress 45 | DockerManager.setBuilderProgress(dockerImageName, 2); 46 | } catch (error) { 47 | // Set the current image creation progress 48 | DockerManager.setBuilderProgress(dockerImageName, -1); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/service/download.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import path from "path"; 4 | import fs from "fs-extra"; 5 | import axios from "axios"; 6 | import { pipeline, Readable } from "stream"; 7 | import logger from "./log"; 8 | 9 | export function downloadFileToLocalFile(url: string, localFilePath: string): Promise { 10 | logger.info(`Download File: ${url} --> ${path.normalize(localFilePath)}`); 11 | return new Promise(async (resolve, reject) => { 12 | try { 13 | if (fs.existsSync(localFilePath)) fs.removeSync(localFilePath); 14 | const response = await axios({ 15 | url, 16 | responseType: "stream", 17 | timeout: 1000 * 10 18 | }); 19 | const writeStream = fs.createWriteStream(path.normalize(localFilePath)); 20 | pipeline(response.data, writeStream, (err) => { 21 | if (err) { 22 | fs.remove(localFilePath, () => {}); 23 | reject(err); 24 | } else { 25 | fs.chmodSync(localFilePath, 0o777); 26 | resolve(true); 27 | } 28 | }); 29 | } catch (error) { 30 | reject(error.message); 31 | } 32 | }); 33 | } 34 | -------------------------------------------------------------------------------- /src/service/file_router_service.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../i18n"; 4 | import InstanceSubsystem from "../service/system_instance"; 5 | import FileManager from "./system_file"; 6 | 7 | export function getFileManager(instanceUuid: string) { 8 | // Initialize a file manager for the instance, and assign codes, restrictions, etc. 9 | const instance = InstanceSubsystem.getInstance(instanceUuid); 10 | if (!instance) throw new Error($t("file_router_service.instanceNotExit", { uuid: instanceUuid })); 11 | const fileCode = instance.config?.fileCode; 12 | const cwd = instance.config.cwd; 13 | return new FileManager(cwd, fileCode); 14 | } 15 | -------------------------------------------------------------------------------- /src/service/http.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import Koa from "koa"; 4 | import koaBody from "koa-body"; 5 | 6 | // Load the HTTP service route 7 | import koaRouter from "../routers/http_router"; 8 | 9 | export function initKoa() { 10 | // Initialize the Koa framework 11 | const koaApp = new Koa(); 12 | koaApp.use( 13 | koaBody({ 14 | multipart: true, 15 | formidable: { 16 | maxFileSize: 1024 * 1024 * 1024 * 1000 17 | } 18 | }) 19 | ); 20 | 21 | // Load Koa top-level middleware 22 | koaApp.use(async (ctx, next) => { 23 | await next(); 24 | // Because all HTTP requests can only be used by creating a task passport on the panel side, cross-domain requests are allowed, and security can also be guaranteed 25 | ctx.response.set("Access-Control-Allow-Origin", "*"); 26 | ctx.response.set("Access-Control-Allow-Methods", "POST, GET, PUT, DELETE, OPTIONS"); 27 | ctx.response.set( 28 | "Access-Control-Allow-Headers", 29 | "Content-Type, Cookie, Accept-Encoding, User-Agent, Host, Referer, " + 30 | "X-Requested-With, Accept, Accept-Language, Cache-Control, Connection" 31 | ); 32 | ctx.response.set("X-Power-by", "MCSManager"); 33 | }); 34 | 35 | koaApp.use(koaRouter.routes()).use(koaRouter.allowedMethods()); 36 | 37 | return koaApp; 38 | } 39 | -------------------------------------------------------------------------------- /src/service/install.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../i18n"; 4 | import os from "os"; 5 | import fs from "fs-extra"; 6 | import https from "https"; 7 | import path from "path"; 8 | import logger from "./log"; 9 | import { downloadFileToLocalFile } from "./download"; 10 | 11 | const PTY_NAME = `pty_${os.platform()}_${os.arch()}${os.platform() === "win32" ? ".exe" : ""}`; 12 | const PTY_PATH = path.normalize(path.join(process.cwd(), "lib", PTY_NAME)); 13 | const PTY_DIR_PATH = path.join(process.cwd(), "lib"); 14 | 15 | function ptyChmod() { 16 | try { 17 | fs.chmodSync(PTY_PATH, 0o755); 18 | return true; 19 | } catch (error) { 20 | logger.warn($t("install.changeModeErr", { path: PTY_PATH })); 21 | fs.remove(PTY_PATH, () => {}); 22 | return false; 23 | } 24 | } 25 | 26 | async function installPty(url: string): Promise { 27 | if (!fs.existsSync(PTY_DIR_PATH)) fs.mkdirsSync(PTY_DIR_PATH); 28 | if (fs.existsSync(PTY_PATH) && fs.statSync(PTY_PATH)?.size > 1024) { 29 | if (!ptyChmod()) throw new Error("ptyChmod error"); 30 | logger.info($t("install.ptySupport")); 31 | return true; 32 | } 33 | await downloadFileToLocalFile(url, PTY_PATH); 34 | return true; 35 | } 36 | 37 | // Emulate terminal-dependent programs, PTY programs based on Go/C++ 38 | // Reference: https://github.com/MCSManager/pty 39 | export function initDependent() { 40 | const ptyUrls = [`https://mcsmanager.oss-cn-guangzhou.aliyuncs.com/${PTY_NAME}`]; 41 | function setup(index = 0) { 42 | installPty(ptyUrls[index]) 43 | .then(() => { 44 | logger.info($t("install.installed")); 45 | logger.info($t("install.guide")); 46 | ptyChmod(); 47 | }) 48 | .catch((err: Error) => { 49 | fs.remove(PTY_PATH, () => {}); 50 | if (index === ptyUrls.length - 1) { 51 | logger.warn($t("install.installErr")); 52 | logger.warn(err.message); 53 | return; 54 | } 55 | return setup(index + 1); 56 | }); 57 | } 58 | 59 | setup(0); 60 | } 61 | -------------------------------------------------------------------------------- /src/service/interfaces.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import InstanceConfig from "../entity/instance/Instance_config"; 4 | export interface IInstanceDetail { 5 | instanceUuid: string; 6 | started: number; 7 | status: number; 8 | config: InstanceConfig; 9 | info?: any; 10 | } 11 | 12 | export interface IJson { 13 | [key: string]: T; 14 | } 15 | 16 | // export interface IForwardInstanceIO { 17 | // sourceSocket: Socket, 18 | // targetUuid: string 19 | // } 20 | -------------------------------------------------------------------------------- /src/service/log.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import fs from "fs-extra"; 4 | import * as log4js from "log4js"; 5 | import os from "os"; 6 | import { systemInfo } from "../common/system_info"; 7 | import { $t } from "../i18n"; 8 | 9 | const LOG_FILE_PATH = "logs/current.log"; 10 | const LOG_SYS_INFO_FILE_PATH = "logs/sysinfo.log"; 11 | 12 | const time = new Date(); 13 | const timeString = 14 | `${time.getFullYear()}-${time.getMonth() + 1}-${time.getDate()}` + `_${time.getHours()}-${time.getMinutes()}-${time.getSeconds()}`; 15 | 16 | if (fs.existsSync(LOG_FILE_PATH)) { 17 | fs.renameSync(LOG_FILE_PATH, `logs/${timeString}.log`); 18 | } 19 | 20 | if (fs.existsSync(LOG_SYS_INFO_FILE_PATH)) { 21 | fs.renameSync(LOG_SYS_INFO_FILE_PATH, `logs/sysinfo_${timeString}.log`); 22 | } 23 | 24 | log4js.configure({ 25 | appenders: { 26 | out: { 27 | type: "stdout", 28 | layout: { 29 | type: "pattern", 30 | pattern: "[%d{MM/dd hh:mm:ss}] [%[%p%]] %m" 31 | } 32 | }, 33 | app: { 34 | type: "file", 35 | filename: LOG_FILE_PATH, 36 | layout: { 37 | type: "pattern", 38 | pattern: "[%d{MM/dd hh:mm:ss}] [%p] %m" 39 | } 40 | }, 41 | sys: { 42 | type: "file", 43 | filename: LOG_SYS_INFO_FILE_PATH, 44 | layout: { 45 | type: "pattern", 46 | pattern: "[%d{MM/dd hh:mm:ss}] [%p] %m" 47 | } 48 | } 49 | }, 50 | categories: { 51 | default: { 52 | appenders: ["out", "app"], 53 | level: "info" 54 | }, 55 | sysinfo: { 56 | appenders: ["sys"], 57 | level: "info" 58 | } 59 | } 60 | }); 61 | 62 | const logger = log4js.getLogger("default"); 63 | const loggerSysInfo = log4js.getLogger("sysinfo"); 64 | function toInt(v: number) { 65 | return parseInt(String(v)); 66 | } 67 | 68 | function systemInfoReport() { 69 | const MB_SIZE = 1024 * 1024; 70 | const info = systemInfo(); 71 | 72 | const self = process.memoryUsage(); 73 | const sysInfo = 74 | `MEM: ${toInt((info.totalmem - info.freemem) / MB_SIZE)}MB/${toInt(info.totalmem / MB_SIZE)}MB` + ` CPU: ${toInt(info.cpuUsage * 100)}%`; 75 | const selfInfo = `Heap: ${toInt(self.heapUsed / MB_SIZE)}MB/${toInt(self.heapTotal / MB_SIZE)}MB`; 76 | const selfInfo2 = `RSS: ${toInt(self.rss / MB_SIZE)}MB`; 77 | const logTip = $t("app.sysinfo"); 78 | loggerSysInfo.info([`[${logTip}]`, sysInfo, selfInfo, selfInfo2].join(" ")); 79 | } 80 | 81 | setInterval(systemInfoReport, 1000 * 5); 82 | 83 | export default logger; 84 | -------------------------------------------------------------------------------- /src/service/mission_passport.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | // task interface 4 | interface IMission { 5 | name: string; 6 | parameter: any; 7 | start: number; 8 | end: number; 9 | count?: number; 10 | } 11 | 12 | // Task passport manager 13 | class MissionPassport { 14 | // temporary task passport list 15 | public readonly missions = new Map(); 16 | 17 | constructor() { 18 | // Set up to check the task expiration every hour 19 | setInterval(() => { 20 | const t = new Date().getTime(); 21 | this.missions.forEach((m, k) => { 22 | if (t > m.end) this.missions.delete(k); 23 | }); 24 | }, 1000); 25 | } 26 | 27 | // register task passport 28 | public registerMission(password: string, mission: IMission) { 29 | if (this.missions.has(password)) throw new Error("Duplicate primary key, failed to create task"); 30 | this.missions.set(password, mission); 31 | } 32 | 33 | // Get the task based on the passport and task name 34 | public getMission(password: string, missionName: string) { 35 | if (!this.missions.has(password)) return null; 36 | const m = this.missions.get(password); 37 | if (m.name === missionName) return m; 38 | return null; 39 | } 40 | 41 | public deleteMission(password: string) { 42 | this.missions.delete(password); 43 | } 44 | } 45 | 46 | const missionPassport = new MissionPassport(); 47 | 48 | export { missionPassport, IMission }; 49 | -------------------------------------------------------------------------------- /src/service/node_auth.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { globalConfiguration } from "../entity/config"; 4 | 5 | export function initApiKey() { 6 | // Initialize the global configuration service 7 | globalConfiguration.load(); 8 | const config = globalConfiguration.config; 9 | } 10 | -------------------------------------------------------------------------------- /src/service/protocol.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../i18n"; 4 | import { Socket } from "socket.io"; 5 | import RouterContext from "../entity/ctx"; 6 | import logger from "./log"; 7 | import { IGNORE } from "../const"; 8 | 9 | // Define network protocols and common send/broadcast/parse functions, the client should also have this file 10 | 11 | const STATUS_OK = 200; 12 | const STATUS_ERR = 500; 13 | 14 | // packet format definition 15 | export interface IPacket { 16 | uuid?: string; 17 | status: number; 18 | event: string; 19 | data: any; 20 | } 21 | 22 | export interface IResponseErrorConfig { 23 | notPrintErr: boolean; 24 | } 25 | 26 | // global socket storage 27 | const globalSocket = new Map(); 28 | 29 | export class Packet implements IPacket { 30 | constructor(public uuid: string = null, public status = 200, public event: string = null, public data: any = null) {} 31 | } 32 | 33 | export function response(ctx: RouterContext, data: any) { 34 | const packet = new Packet(ctx.uuid, STATUS_OK, ctx.event, data); 35 | ctx.socket.emit(ctx.event, packet); 36 | } 37 | 38 | export function responseError(ctx: RouterContext, err: Error | string, config?: IResponseErrorConfig) { 39 | let errinfo: any = ""; 40 | if (err) errinfo = err.toString(); 41 | else errinfo = err; 42 | const packet = new Packet(ctx.uuid, STATUS_ERR, ctx.event, errinfo); 43 | // Ignore 44 | if (err.toString().includes(IGNORE)) return ctx.socket.emit(ctx.event, packet); 45 | 46 | if (!config?.notPrintErr) 47 | logger.warn($t("protocol.socketErr", { id: ctx.socket.id, address: ctx.socket.handshake.address, event: ctx.event }), err); 48 | ctx.socket.emit(ctx.event, packet); 49 | } 50 | 51 | export function msg(ctx: RouterContext, event: string, data: any) { 52 | const packet = new Packet(ctx.uuid, STATUS_OK, event, data); 53 | ctx.socket.emit(event, packet); 54 | } 55 | 56 | export function error(ctx: RouterContext, event: string, err: any) { 57 | const packet = new Packet(ctx.uuid, STATUS_ERR, event, err); 58 | // Ignore 59 | if (err.toString().includes(IGNORE)) return ctx.socket.emit(ctx.event, packet); 60 | 61 | logger.warn($t("protocol.socketErr", { id: ctx.socket.id, address: ctx.socket.handshake.address, event: ctx.event }), err); 62 | ctx.socket.emit(event, packet); 63 | } 64 | 65 | export function parse(text: IPacket) { 66 | if (typeof text == "object") { 67 | return new Packet(text.uuid || null, text.status, text.event, text.data); 68 | } 69 | const obj = JSON.parse(text); 70 | return new Packet(null, obj.status, obj.event, obj.data); 71 | } 72 | 73 | export function stringify(obj: any) { 74 | return JSON.stringify(obj); 75 | } 76 | 77 | export function addGlobalSocket(socket: Socket) { 78 | globalSocket.set(socket.id, socket); 79 | } 80 | 81 | export function delGlobalSocket(socket: Socket) { 82 | globalSocket.delete(socket.id); 83 | } 84 | 85 | export function socketObjects() { 86 | return globalSocket; 87 | } 88 | 89 | // global socket broadcast 90 | export function broadcast(event: string, obj: any) { 91 | globalSocket.forEach((socket) => { 92 | msg(new RouterContext(null, socket), event, obj); 93 | }); 94 | } 95 | -------------------------------------------------------------------------------- /src/service/router.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../i18n"; 4 | import { EventEmitter } from "events"; 5 | import { Socket } from "socket.io"; 6 | import logger from "./log"; 7 | import RouterContext from "../entity/ctx"; 8 | import { IPacket, response, responseError } from "../service/protocol"; 9 | // Routing controller class (singleton class) 10 | class RouterApp extends EventEmitter { 11 | public readonly middlewares: Array; 12 | 13 | constructor() { 14 | super(); 15 | this.middlewares = []; 16 | } 17 | 18 | emitRouter(event: string, ctx: RouterContext, data: any) { 19 | try { 20 | // service logic routing trigger point 21 | super.emit(event, ctx, data); 22 | } catch (error) { 23 | responseError(ctx, error); 24 | } 25 | return this; 26 | } 27 | 28 | on(event: string, fn: (ctx: RouterContext, data: any) => void) { 29 | // logger.info(` Register event: ${event} `); 30 | return super.on(event, fn); 31 | } 32 | 33 | use(fn: (event: string, ctx: RouterContext, data: any, next: Function) => void) { 34 | this.middlewares.push(fn); 35 | } 36 | 37 | getMiddlewares() { 38 | return this.middlewares; 39 | } 40 | } 41 | 42 | // routing controller singleton class 43 | export const routerApp = new RouterApp(); 44 | 45 | /** 46 | * Based on Socket.io for routing decentralization and secondary forwarding 47 | * @param {Socket} socket 48 | */ 49 | export function navigation(socket: Socket) { 50 | // Full-life session variables (Between connection and disconnection) 51 | const session: any = {}; 52 | // Register all middleware with Socket 53 | for (const fn of routerApp.getMiddlewares()) { 54 | socket.use((packet, next) => { 55 | const protocol = packet[1] as IPacket; 56 | if (!protocol) return logger.info(`session ${socket.id} request data protocol format is incorrect`); 57 | const ctx = new RouterContext(protocol.uuid, socket, session); 58 | fn(packet[0], ctx, protocol.data, next); 59 | }); 60 | } 61 | // Register all events with Socket 62 | for (const event of routerApp.eventNames()) { 63 | socket.on(event as string, (protocol: IPacket) => { 64 | if (!protocol) return logger.info(`session ${socket.id} request data protocol format is incorrect`); 65 | const ctx = new RouterContext(protocol.uuid, socket, session, event.toString()); 66 | routerApp.emitRouter(event as string, ctx, protocol.data); 67 | }); 68 | } 69 | // The connected event route is triggered 70 | const ctx = new RouterContext(null, socket, session); 71 | routerApp.emitRouter("connection", ctx, null); 72 | } 73 | 74 | // The authentication routing order must be the first load. These routing orders cannot be changed without authorization 75 | import "../routers/auth_router"; 76 | import "../routers/passport_router"; 77 | import "../routers/info_router"; 78 | import "../routers/Instance_router"; 79 | import "../routers/instance_event_router"; 80 | import "../routers/file_router"; 81 | import "../routers/stream_router"; 82 | import "../routers/environment_router"; 83 | import "../routers/schedule_router"; 84 | 85 | logger.info($t("router.initComplete")); 86 | -------------------------------------------------------------------------------- /src/service/system_instance.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../i18n"; 4 | import fs from "fs-extra"; 5 | import path from "path"; 6 | import os from "os"; 7 | 8 | import Instance from "../entity/instance/instance"; 9 | import EventEmitter from "events"; 10 | import KillCommand from "../entity/commands/kill"; 11 | import logger from "./log"; 12 | 13 | import { v4 } from "uuid"; 14 | import { Socket } from "socket.io"; 15 | import StorageSubsystem from "../common/system_storage"; 16 | import InstanceConfig from "../entity/instance/Instance_config"; 17 | import InstanceStreamListener from "../common/instance_stream"; 18 | import { QueryMapWrapper } from "../common/query_wrapper"; 19 | import FunctionDispatcher from "../entity/commands/dispatcher"; 20 | import InstanceControl from "./system_instance_control"; 21 | import StartCommand from "../entity/commands/start"; 22 | import { globalConfiguration } from "../entity/config"; 23 | 24 | // init instance default install path 25 | let INSTANCE_DATA_DIR = path.join(process.cwd(), "data/InstanceData"); 26 | if (globalConfiguration.config.defaultInstancePath) { 27 | INSTANCE_DATA_DIR = path.normalize(globalConfiguration.config.defaultInstancePath); 28 | } 29 | 30 | if (!fs.existsSync(INSTANCE_DATA_DIR)) { 31 | fs.mkdirsSync(INSTANCE_DATA_DIR); 32 | } 33 | 34 | class InstanceSubsystem extends EventEmitter { 35 | public readonly GLOBAL_INSTANCE = "__MCSM_GLOBAL_INSTANCE__"; 36 | public readonly GLOBAL_INSTANCE_UUID = "global0001"; 37 | 38 | public readonly LOG_DIR = "data/InstanceLog/"; 39 | 40 | public readonly instances = new Map(); 41 | public readonly instanceStream = new InstanceStreamListener(); 42 | 43 | constructor() { 44 | super(); 45 | } 46 | 47 | // start automatically at boot 48 | private autoStart() { 49 | this.instances.forEach((instance) => { 50 | if (instance.config.eventTask.autoStart) { 51 | instance 52 | .exec(new StartCommand()) 53 | .then(() => { 54 | logger.info($t("system_instance.autoStart", { name: instance.config.nickname, uuid: instance.instanceUuid })); 55 | }) 56 | .catch((reason) => { 57 | logger.error($t("system_instance.autoStartErr", { name: instance.config.nickname, uuid: instance.instanceUuid, reason: reason })); 58 | }); 59 | } 60 | }); 61 | } 62 | 63 | // init all instances from local files 64 | loadInstances() { 65 | const instanceConfigs = StorageSubsystem.list("InstanceConfig"); 66 | instanceConfigs.forEach((uuid) => { 67 | if (uuid === this.GLOBAL_INSTANCE_UUID) return; 68 | try { 69 | const instanceConfig = StorageSubsystem.load("InstanceConfig", InstanceConfig, uuid); 70 | const instance = new Instance(uuid, instanceConfig); 71 | 72 | // Fix BUG, reset state 73 | instanceConfig.eventTask.ignore = false; 74 | 75 | // All instances are all function schedulers 76 | instance 77 | .forceExec(new FunctionDispatcher()) 78 | .then((v) => {}) 79 | .catch((v) => {}); 80 | this.addInstance(instance); 81 | } catch (error) { 82 | logger.error($t("system_instance.readInstanceFailed", { uuid: uuid, error: error.message })); 83 | logger.error($t("system_instance.checkConf", { uuid: uuid })); 84 | } 85 | }); 86 | 87 | this.createInstance( 88 | { 89 | nickname: this.GLOBAL_INSTANCE, 90 | cwd: "/", 91 | startCommand: os.platform() === "win32" ? "cmd.exe" : "bash", 92 | stopCommand: "^c", 93 | ie: "utf-8", 94 | oe: "utf-8", 95 | type: Instance.TYPE_UNIVERSAL, 96 | processType: "general" 97 | }, 98 | false, 99 | this.GLOBAL_INSTANCE_UUID 100 | ); 101 | 102 | // handle autostart 103 | this.autoStart(); 104 | } 105 | 106 | createInstance(cfg: any, persistence = true, uuid?: string) { 107 | const newUuid = uuid || v4().replace(/-/gim, ""); 108 | const instance = new Instance(newUuid, new InstanceConfig()); 109 | // Instance working directory verification and automatic creation 110 | if (!cfg.cwd || cfg.cwd === ".") { 111 | cfg.cwd = path.normalize(`${INSTANCE_DATA_DIR}/${instance.instanceUuid}`); 112 | } 113 | if (!fs.existsSync(cfg.cwd)) fs.mkdirsSync(cfg.cwd); 114 | // Set the default input and output encoding 115 | cfg.ie = cfg.oe = cfg.fileCode = "utf8"; 116 | // Build and initialize the type from the parameters 117 | 118 | instance.parameters(cfg, persistence); 119 | instance.forceExec(new FunctionDispatcher()); 120 | this.addInstance(instance); 121 | return instance; 122 | } 123 | 124 | addInstance(instance: Instance) { 125 | if (instance.instanceUuid == null) throw new Error($t("system_instance.uuidEmpty")); 126 | if (this.instances.has(instance.instanceUuid)) { 127 | throw new Error(`The application instance ${instance.instanceUuid} already exists.`); 128 | } 129 | this.instances.set(instance.instanceUuid, instance); 130 | // Dynamically monitor the newly added instance output stream and pass it to its own event stream 131 | instance.on("data", (...arr) => { 132 | this.emit("data", instance.instanceUuid, ...arr); 133 | }); 134 | instance.on("exit", (...arr) => { 135 | this.emit( 136 | "exit", 137 | { 138 | instanceUuid: instance.instanceUuid, 139 | instanceName: instance.config.nickname 140 | }, 141 | ...arr 142 | ); 143 | }); 144 | instance.on("open", (...arr) => { 145 | this.emit( 146 | "open", 147 | { 148 | instanceUuid: instance.instanceUuid, 149 | instanceName: instance.config.nickname 150 | }, 151 | ...arr 152 | ); 153 | }); 154 | instance.on("failure", (...arr) => { 155 | this.emit( 156 | "failure", 157 | { 158 | instanceUuid: instance.instanceUuid, 159 | instanceName: instance.config.nickname 160 | }, 161 | ...arr 162 | ); 163 | }); 164 | } 165 | 166 | removeInstance(instanceUuid: string, deleteFile: boolean) { 167 | const instance = this.getInstance(instanceUuid); 168 | if (instance) { 169 | instance.destroy(); 170 | // destroy record 171 | this.instances.delete(instanceUuid); 172 | StorageSubsystem.delete("InstanceConfig", instanceUuid); 173 | // delete scheduled task 174 | InstanceControl.deleteInstanceAllTask(instanceUuid); 175 | // delete the file asynchronously 176 | if (deleteFile) fs.remove(instance.config.cwd, (err) => {}); 177 | return true; 178 | } 179 | throw new Error("Instance does not exist"); 180 | } 181 | 182 | forward(targetInstanceUuid: string, socket: Socket) { 183 | try { 184 | this.instanceStream.requestForward(socket, targetInstanceUuid); 185 | } catch (err) {} 186 | } 187 | 188 | stopForward(targetInstanceUuid: string, socket: Socket) { 189 | try { 190 | this.instanceStream.cannelForward(socket, targetInstanceUuid); 191 | } catch (err) {} 192 | } 193 | 194 | forEachForward(instanceUuid: string, callback: (socket: Socket) => void) { 195 | this.instanceStream.forwardViaCallback(instanceUuid, (_socket) => { 196 | callback(_socket); 197 | }); 198 | } 199 | 200 | getInstance(instanceUuid: string) { 201 | return this.instances.get(instanceUuid); 202 | } 203 | 204 | getQueryMapWrapper() { 205 | return new QueryMapWrapper(this.instances); 206 | } 207 | 208 | exists(instanceUuid: string) { 209 | return this.instances.has(instanceUuid); 210 | } 211 | 212 | async exit() { 213 | let promises = []; 214 | for (const iterator of this.instances) { 215 | const instance = iterator[1]; 216 | if (instance.status() != Instance.STATUS_STOP) { 217 | logger.info(`Instance ${instance.config.nickname} (${instance.instanceUuid}) is running or busy, and is being forced to end.`); 218 | promises.push( 219 | instance.execCommand(new KillCommand()).then(() => { 220 | if (!this.isGlobalInstance(instance)) StorageSubsystem.store("InstanceConfig", instance.instanceUuid, instance.config); 221 | logger.info(`Instance ${instance.config.nickname} (${instance.instanceUuid}) saved successfully.`); 222 | }) 223 | ); 224 | } 225 | } 226 | await Promise.all(promises); 227 | } 228 | 229 | getInstances() { 230 | let newArr = new Array(); 231 | this.instances.forEach((instance) => { 232 | if (!this.isGlobalInstance(instance)) newArr.push(instance); 233 | }); 234 | newArr = newArr.sort((a, b) => (a.config.nickname > a.config.nickname ? 1 : -1)); 235 | return newArr; 236 | } 237 | 238 | isGlobalInstance(instance: Instance) { 239 | return instance.instanceUuid === this.GLOBAL_INSTANCE_UUID || instance.config.nickname === this.GLOBAL_INSTANCE; 240 | } 241 | } 242 | 243 | export default new InstanceSubsystem(); 244 | -------------------------------------------------------------------------------- /src/service/system_instance_control.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../i18n"; 4 | import schedule from "node-schedule"; 5 | import InstanceSubsystem from "./system_instance"; 6 | import StorageSubsystem from "../common/system_storage"; 7 | import logger from "./log"; 8 | import StartCommand from "../entity/commands/start"; 9 | import StopCommand from "../entity/commands/stop"; 10 | import SendCommand from "../entity/commands/cmd"; 11 | import RestartCommand from "../entity/commands/restart"; 12 | import KillCommand from "../entity/commands/kill"; 13 | import FileManager from "./system_file"; 14 | 15 | // Scheduled task configuration item interface 16 | interface IScheduleTask { 17 | instanceUuid: string; 18 | name: string; 19 | count: number; 20 | time: string; 21 | action: string; 22 | payload: string; 23 | type: number; 24 | } 25 | 26 | // Scheduled task timer/periodic task interface 27 | interface IScheduleJob { 28 | cancel: Function; 29 | } 30 | 31 | // @Entity 32 | // Schedule task configuration data entity class 33 | class TaskConfig implements IScheduleTask { 34 | instanceUuid = ""; 35 | name: string = ""; 36 | count: number = 1; 37 | time: string = ""; 38 | action: string = ""; 39 | payload: string = ""; 40 | type: number = 1; 41 | } 42 | 43 | class IntervalJob implements IScheduleJob { 44 | public job: number = 0; 45 | 46 | constructor(private callback: Function, public time: number) { 47 | this.job = setInterval(callback, time * 1000); 48 | } 49 | 50 | cancel() { 51 | clearInterval(this.job); 52 | } 53 | } 54 | 55 | // Scheduled task instance class 56 | class Task { 57 | constructor(public config: TaskConfig, public job?: IScheduleJob) {} 58 | } 59 | 60 | class InstanceControlSubsystem { 61 | public readonly taskMap = new Map>(); 62 | public readonly taskJobMap = new Map(); 63 | 64 | constructor() { 65 | // Initialize all persistent data and load into memory one by one 66 | StorageSubsystem.list("TaskConfig").forEach((uuid) => { 67 | const config = StorageSubsystem.load("TaskConfig", TaskConfig, uuid) as TaskConfig; 68 | try { 69 | this.registerScheduleJob(config, false); 70 | } catch (error) { 71 | // Some scheduled tasks may be left, but the upper limit will not change 72 | // Ignore the scheduled task registration at startup 73 | } 74 | }); 75 | } 76 | 77 | public registerScheduleJob(task: IScheduleTask, needStore = true) { 78 | const key = `${task.instanceUuid}`; 79 | if (!this.taskMap.has(key)) { 80 | this.taskMap.set(key, []); 81 | } 82 | if (this.taskMap.get(key)?.length >= 8) throw new Error($t("system_instance_control.execLimit")); 83 | if (!this.checkTask(key, task.name)) throw new Error($t("system_instance_control.existRepeatTask")); 84 | if (!FileManager.checkFileName(task.name)) throw new Error($t("system_instance_control.illegalName")); 85 | if (needStore) logger.info($t("system_instance_control.crateTask", { name: task.name, task: JSON.stringify(task) })); 86 | 87 | let job: IScheduleJob; 88 | 89 | // min interval check 90 | if (task.type === 1) { 91 | let internalTime = Number(task.time); 92 | if (isNaN(internalTime) || internalTime < 1) internalTime = 1; 93 | 94 | // task.type=1: Time interval scheduled task, implemented with built-in timer 95 | job = new IntervalJob(() => { 96 | this.action(task); 97 | if (task.count === -1) return; 98 | if (task.count === 1) { 99 | job.cancel(); 100 | this.deleteTask(key, task.name); 101 | } else { 102 | task.count--; 103 | this.updateTaskConfig(key, task.name, task); 104 | } 105 | }, internalTime); 106 | } else { 107 | // Expression validity check: 8 19 14 * * 1,2,3,4 108 | const timeArray = task.time.split(" "); 109 | const checkIndex = [0, 1, 2]; 110 | checkIndex.forEach((item) => { 111 | if (isNaN(Number(timeArray[item])) && Number(timeArray[item]) >= 0) { 112 | throw new Error($t("system_instance_control.crateTaskErr", { name: task.name, timeArray: timeArray })); 113 | } 114 | }); 115 | // task.type=2: Specify time-based scheduled tasks, implemented by node-schedule library 116 | job = schedule.scheduleJob(task.time, () => { 117 | this.action(task); 118 | if (task.count === -1) return; 119 | if (task.count === 1) { 120 | job.cancel(); 121 | this.deleteTask(key, task.name); 122 | } else { 123 | task.count--; 124 | this.updateTaskConfig(key, task.name, task); 125 | } 126 | }); 127 | } 128 | const newTask = new Task(task, job); 129 | this.taskMap.get(key).push(newTask); 130 | if (needStore) { 131 | StorageSubsystem.store("TaskConfig", `${key}_${newTask.config.name}`, newTask.config); 132 | } 133 | if (needStore) logger.info($t("system_instance_control.crateSuccess", { name: task.name })); 134 | } 135 | 136 | public listScheduleJob(instanceUuid: string) { 137 | const key = `${instanceUuid}`; 138 | const arr = this.taskMap.get(key) || []; 139 | const res: IScheduleTask[] = []; 140 | arr.forEach((v) => { 141 | res.push(v.config); 142 | }); 143 | return res; 144 | } 145 | 146 | public async action(task: IScheduleTask) { 147 | try { 148 | const payload = task.payload; 149 | const instanceUuid = task.instanceUuid; 150 | const instance = InstanceSubsystem.getInstance(instanceUuid); 151 | // If the instance has been deleted, it needs to be automatically destroyed 152 | if (!instance) { 153 | return this.deleteScheduleTask(task.instanceUuid, task.name); 154 | } 155 | const instanceStatus = instance.status(); 156 | // logger.info(`Execute scheduled task: ${task.name} ${task.action} ${task.time} ${task.count} `); 157 | if (task.action === "start") { 158 | if (instanceStatus === 0) { 159 | return await instance.exec(new StartCommand("ScheduleJob")); 160 | } 161 | } 162 | if (task.action === "stop") { 163 | if (instanceStatus === 3) { 164 | return await instance.exec(new StopCommand()); 165 | } 166 | } 167 | if (task.action === "restart") { 168 | if (instanceStatus === 3) { 169 | return await instance.exec(new RestartCommand()); 170 | } 171 | } 172 | if (task.action === "command") { 173 | if (instanceStatus === 3) { 174 | return await instance.exec(new SendCommand(payload)); 175 | } 176 | } 177 | if (task.action === "kill") { 178 | return await instance.exec(new KillCommand()); 179 | } 180 | } catch (error) { 181 | logger.error($t("system_instance_control.execCmdErr", { uuid: task.instanceUuid, name: task.name, error: error })); 182 | } 183 | } 184 | 185 | public deleteInstanceAllTask(instanceUuid: string) { 186 | const tasks = this.listScheduleJob(instanceUuid); 187 | if (tasks) 188 | tasks.forEach((v) => { 189 | this.deleteScheduleTask(instanceUuid, v.name); 190 | }); 191 | } 192 | 193 | public deleteScheduleTask(instanceUuid: string, name: string) { 194 | const key = `${instanceUuid}`; 195 | this.deleteTask(key, name); 196 | } 197 | 198 | private deleteTask(key: string, name: string) { 199 | this.taskMap.get(key).forEach((v, index, arr) => { 200 | if (v.config.name === name) { 201 | v.job.cancel(); 202 | arr.splice(index, 1); 203 | } 204 | }); 205 | StorageSubsystem.delete("TaskConfig", `${key}_${name}`); 206 | } 207 | 208 | private checkTask(key: string, name: string) { 209 | let f = true; 210 | this.taskMap.get(key).forEach((v, index, arr) => { 211 | if (v.config.name === name) f = false; 212 | }); 213 | return f; 214 | } 215 | 216 | private updateTaskConfig(key: string, name: string, data: IScheduleTask) { 217 | const list = this.taskMap.get(key); 218 | for (const index in list) { 219 | const t = list[index]; 220 | if (t.config.name === name) { 221 | list[index].config = data; 222 | break; 223 | } 224 | } 225 | } 226 | 227 | private checkScheduledTaskLimit(instanceUuid: string) { 228 | for (const iterator of this.taskMap) { 229 | } 230 | } 231 | } 232 | 233 | export default new InstanceControlSubsystem(); 234 | -------------------------------------------------------------------------------- /src/service/system_visual_data.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2023 MCSManager 2 | 3 | import { systemInfo } from "../common/system_info"; 4 | 5 | // Visual data subsystem: responsible for collecting system data and event data, and providing some methods to display them 6 | class LineQueue { 7 | private readonly arr = new Array(); 8 | 9 | constructor(public readonly maxSize: number, defaultValue?: T) { 10 | for (let index = 0; index < maxSize; index++) { 11 | this.arr.push(defaultValue); 12 | } 13 | } 14 | 15 | push(data: T) { 16 | if (this.arr.length < this.maxSize) { 17 | this.arr.push(data); 18 | } else { 19 | this.arr.shift(); 20 | this.arr.push(data); 21 | } 22 | } 23 | 24 | getArray() { 25 | return this.arr; 26 | } 27 | } 28 | 29 | interface ISystemChartInfo { 30 | cpu: number; 31 | mem: number; 32 | } 33 | 34 | class VisualDataSubsystem { 35 | public readonly countMap: Map = new Map(); 36 | 37 | public readonly systemChart = new LineQueue(200, { cpu: 0, mem: 0 }); 38 | 39 | private requestCount = 0; 40 | 41 | constructor() { 42 | setInterval(() => { 43 | const info = systemInfo(); 44 | if (info) { 45 | this.systemChart.push({ 46 | cpu: Number((info.cpuUsage * 100).toFixed(0)), 47 | mem: Number((info.memUsage * 100).toFixed(0)) 48 | }); 49 | } else { 50 | this.systemChart.push({ 51 | cpu: 0, 52 | mem: 0 53 | }); 54 | } 55 | }, 1000 * 3); 56 | } 57 | 58 | addRequestCount() { 59 | this.requestCount++; 60 | } 61 | 62 | getSystemChartArray() { 63 | return this.systemChart.getArray(); 64 | } 65 | 66 | // Trigger counting event 67 | emitCountEvent(eventName: string) { 68 | const v = this.countMap.get(eventName); 69 | if (v) { 70 | this.countMap.set(eventName, v + 1); 71 | } else { 72 | this.countMap.set(eventName, 1); 73 | } 74 | } 75 | 76 | // Trigger counting event 77 | eventCount(eventName: string) { 78 | return this.countMap.get(eventName); 79 | } 80 | 81 | // Trigger log event 82 | emitLogEvent(eventName: string, source: any) { 83 | const time = new Date().toLocaleString(); 84 | } 85 | } 86 | 87 | export default new VisualDataSubsystem(); 88 | -------------------------------------------------------------------------------- /src/service/ui.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../i18n"; 4 | import readline from "readline"; 5 | 6 | import * as protocol from "./protocol"; 7 | import InstanceSubsystem from "./system_instance"; 8 | import { globalConfiguration } from "../entity/config"; 9 | import logger from "./log"; 10 | import StartCommand from "../entity/commands/start"; 11 | import StopCommand from "../entity/commands/stop"; 12 | import KillCommand from "../entity/commands/kill"; 13 | import SendCommand from "../entity/commands/cmd"; 14 | 15 | const rl = readline.createInterface({ 16 | input: process.stdin, 17 | output: process.stdout 18 | }); 19 | 20 | console.log($t("ui.help")); 21 | 22 | function stdin() { 23 | rl.question("> ", async (answer) => { 24 | try { 25 | const cmds = answer.split(" "); 26 | logger.info(`[Terminal] ${answer}`); 27 | const result = await command(cmds[0], cmds[1], cmds[2], cmds[3]); 28 | if (result) console.log(result); 29 | else console.log(`Command ${answer} does not exist, type help to get help.`); 30 | } catch (err) { 31 | logger.error("[Terminal]", err); 32 | } finally { 33 | // next 34 | stdin(); 35 | } 36 | }); 37 | } 38 | 39 | stdin(); 40 | 41 | /** 42 | * Pass in relevant UI commands and output command results 43 | * @param {String} cmd 44 | * @return {String} 45 | */ 46 | async function command(cmd: string, p1: string, p2: string, p3: string) { 47 | if (cmd === "instance") { 48 | if (p1 === "start") { 49 | InstanceSubsystem.getInstance(p2).exec(new StartCommand("Terminal")); 50 | return "Done."; 51 | } 52 | if (p1 === "stop") { 53 | InstanceSubsystem.getInstance(p2).exec(new StopCommand()); 54 | return "Done."; 55 | } 56 | if (p1 === "kill") { 57 | InstanceSubsystem.getInstance(p2).exec(new KillCommand()); 58 | return "Done."; 59 | } 60 | if (p1 === "send") { 61 | InstanceSubsystem.getInstance(p2).exec(new SendCommand(p3)); 62 | return "Done."; 63 | } 64 | return "Parameter error"; 65 | } 66 | 67 | if (cmd === "instances") { 68 | const objs = InstanceSubsystem.instances; 69 | let result = "instance name | instance UUID | status code\n"; 70 | objs.forEach((v) => { 71 | result += `${v.config.nickname} ${v.instanceUuid} ${v.status()}\n`; 72 | }); 73 | result += "\nStatus Explanation:\n Busy=-1;Stop=0;Stopping=1;Starting=2;Running=3;\n"; 74 | return result; 75 | } 76 | 77 | if (cmd === "sockets") { 78 | const sockets = protocol.socketObjects(); 79 | let result = "IP address | identifier\n"; 80 | sockets.forEach((v) => { 81 | result += `${v.handshake.address} ${v.id}\n`; 82 | }); 83 | result += `Total ${sockets.size} online.\n`; 84 | return result; 85 | } 86 | 87 | if (cmd == "key") { 88 | return globalConfiguration.config.key; 89 | } 90 | 91 | if (cmd == "exit") { 92 | try { 93 | logger.info("Preparing to shut down the daemon..."); 94 | await InstanceSubsystem.exit(); 95 | // logger.info("Data saved, thanks for using, goodbye!"); 96 | logger.info("The data is saved, thanks for using, goodbye!"); 97 | logger.info("closed."); 98 | process.exit(0); 99 | } catch (err) { 100 | logger.error( 101 | "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.", 102 | err 103 | ); 104 | } 105 | } 106 | 107 | if (cmd == "help") { 108 | console.log("----------- Help document -----------"); 109 | console.log(" instances view all instances"); 110 | console.log(" Sockets view all linkers"); 111 | console.log(" key view key"); 112 | console.log(" exit to close this program (recommended method)"); 113 | console.log(" instance start to start the specified instance"); 114 | console.log(" instance stop to start the specified instance"); 115 | console.log(" instance kill to start the specified instance"); 116 | console.log(" instance send to send a command to the instance"); 117 | console.log("----------- Help document -----------"); 118 | return "\n"; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/service/version.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | import { $t } from "../i18n"; 4 | import * as fs from "fs-extra"; 5 | import GlobalVariable from "../common/global_variable"; 6 | import logger from "./log"; 7 | 8 | const PACKAGE_JSON = "package.json"; 9 | 10 | export function initVersionManager() { 11 | try { 12 | const packagePaths = [PACKAGE_JSON, "../package.json"]; 13 | let version = "Unknown"; 14 | 15 | for (const packagePath of packagePaths) { 16 | if (fs.existsSync(packagePath)) { 17 | const data = JSON.parse(fs.readFileSync(packagePath, { encoding: "utf-8" })); 18 | if (data.version) { 19 | version = data.version; 20 | break; 21 | } 22 | } 23 | } 24 | 25 | GlobalVariable.set("version", version); 26 | } catch (error) { 27 | logger.error($t("version.versionDetectErr"), error); 28 | } 29 | } 30 | 31 | export function getVersion() { 32 | return GlobalVariable.get("version", "Unknown"); 33 | } 34 | -------------------------------------------------------------------------------- /src/tools/filepath.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | export function checkFileName(fileName: string) { 4 | const blackKeys = ["/", "\\", "|", "?", "*", ">", "<", ";", '"']; 5 | for (const ch of blackKeys) { 6 | if (fileName.includes(ch)) return false; 7 | } 8 | return true; 9 | } 10 | -------------------------------------------------------------------------------- /src/types/properties.d.ts: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022 MCSManager 2 | 3 | declare module "properties" { 4 | function parse(data: string, options?: any): any; 5 | function stringify(data: any, options?: any): string; 6 | 7 | export { parse, stringify }; 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "resolveJsonModule":true, 4 | "allowJs": true, 5 | "module": "commonjs", 6 | "esModuleInterop": true, 7 | "allowSyntheticDefaultImports": true, 8 | "target": "ES2018", 9 | "noImplicitAny": true, 10 | "moduleResolution": "node", 11 | "sourceMap": true, 12 | "outDir": "dist", 13 | "baseUrl": ".", 14 | "paths": { 15 | "*": [ 16 | "node_modules/*", 17 | "src/types/*" 18 | ] 19 | } 20 | }, 21 | "include": [ 22 | "src/**/*" 23 | ] 24 | } -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | const path = require("path"); 3 | const nodeExternals = require("webpack-node-externals"); 4 | 5 | module.exports = { 6 | mode: "production", 7 | entry: "./dist/app.js", 8 | target: "node", 9 | externalsPresets: { node: true }, 10 | externals: [nodeExternals()], 11 | output: { 12 | filename: "app.js", 13 | path: path.resolve(__dirname, "production") 14 | } 15 | }; 16 | --------------------------------------------------------------------------------