├── .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 |
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 |
2 |
3 |
4 |
5 | [](https://www.npmjs.com/)
6 | [](https://nodejs.org/en/download/)
7 | [](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