Initial release

This commit is contained in:
hibobmaster 2024-03-02 10:54:27 +08:00
commit dd2c38d90c
Signed by: bobmaster
SSH key fingerprint: SHA256:5ZYgd8fg+PcNZNy4SzcSKu5JtqZyBF8kUhY7/k2viDk
17 changed files with 4397 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
*.db
config.json

4
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,4 @@
{
"html.customData": ["./frontend/node_modules/mdui/html-data.zh-cn.json"],
"css.customData": ["./frontend/node_modules/mdui/css-data.zh-cn.json"]
}

2
README.md Normal file
View file

@ -0,0 +1,2 @@
# 我的世界监控台
用于查看我的世界服务器状态,并添加了重启功能

160
backend/.gitignore vendored Normal file
View file

@ -0,0 +1,160 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

17
backend/README.md Normal file
View file

@ -0,0 +1,17 @@
## development
Create virtual environment and install dependencies
```
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
```
Debug the code
```
uvicorn main:app --reload
```
## deployment
```
uvicorn main:app --host "127.0.0.1" --port 10010 --proxy-headers --forwarded-allow-ips "*"
```

19
backend/db.py Normal file
View file

@ -0,0 +1,19 @@
import sqlite3
class MCDB:
def __init__(self) -> None:
self.conn = sqlite3.connect('database.db')
self.cursor = self.conn.cursor()
self.cursor.execute(
"""CREATE TABLE IF NOT EXISTS mcstatus (
time TEXT NOT NULL,
latency REAL NOT NULL
)"""
)
self.conn.commit()
def insert_time_and_latency(self, time, latency):
self.cursor.execute("""
INSERT INTO mcstatus (time, latency)
VALUES (?, ?)
""", (time, latency))
self.conn.commit()

109
backend/main.py Normal file
View file

@ -0,0 +1,109 @@
import json
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from apscheduler.schedulers.asyncio import AsyncIOScheduler
from utils import getJavaServerStatus, schedule_job, get_last_hour_date_info, get_current_day_time_utc8, parse_date_to_h_m_s
from contextlib import asynccontextmanager
from db import MCDB
from pydantic import BaseModel
import aiohttp
from pathlib import Path
class Password(BaseModel):
code: str
mcdb = MCDB()
with open(Path(__file__).parent.parent / "config.json", encoding="utf-8") as f:
config = json.load(f)
HOST = config["mc_host"]
PASSWORD = config["password"]
RCON_SERVER = config["rcon_host"]
scheduler = AsyncIOScheduler()
# 每30s记录一次
scheduler.add_job(schedule_job, 'interval', [HOST, mcdb], seconds=30)
origins = [
"*"
]
@asynccontextmanager
async def lifespan(app: FastAPI):
scheduler.start()
yield
mcdb.cursor.close()
mcdb.conn.close()
app = FastAPI(lifespan=lifespan)
app.add_middleware(
CORSMiddleware,
allow_origins=origins,
allow_methods=["*"],
allow_headers=["*"],
)
@app.get("/")
async def read_root():
try:
status = await getJavaServerStatus(HOST)
except Exception:
return {
"time": get_current_day_time_utc8(),
"description": "请求错误",
"latency": 10000,
"max": "请求错误",
"online": "请求错误"
}
return {
"time": get_current_day_time_utc8(),
"description": status.description,
"latency": round(status.latency, 2),
"max": status.players.max,
"online": status.players.online
}
@app.get("/get_latency")
async def get_latency_info():
# 获取一个小时前的时间
one_hour_ago = get_last_hour_date_info()
mcdb.cursor.execute("SELECT time, latency FROM mcstatus WHERE time >= ?", (one_hour_ago, ))
rows = mcdb.cursor.fetchall()
data = [{"time": parse_date_to_h_m_s(row[0]), "latency": row[1]} for row in rows]
return data
@app.post("/restart-server")
async def restart_mc_server(password: Password):
if PASSWORD != password.code:
return {
"status": "fail",
"msg": "密码错误"
}
try:
async with aiohttp.ClientSession() as asession:
async with asession.post(RCON_SERVER,
json={"code": password.code}) as resp1:
result1 = await resp1.json()
return {
"status": "成功",
"msg": "重启指令已发送请耐心等待1分钟或者查看图表观察服务器是否正常上线"
}
except Exception:
return {
"status": "失败",
"msg": "重启指令发送失败,请重试或者联系管理员"
}
if __name__ == '__main__':
import uvicorn
uvicorn.run(app, host="127.0.0.1", port=10000)

7
backend/requirements.txt Normal file
View file

@ -0,0 +1,7 @@
uvicorn[standard]
fastapi
mcstatus
apscheduler
pytz
python-dateutil
aiohttp

36
backend/utils.py Normal file
View file

@ -0,0 +1,36 @@
from mcstatus import JavaServer
from mcstatus.status_response import JavaStatusResponse
from datetime import datetime, timedelta
from dateutil import parser
from pytz import timezone
from db import MCDB
async def getJavaServerStatus(host: str) -> JavaStatusResponse:
return await (await JavaServer.async_lookup(host)).async_status()
async def getJavaServerLatency(host: str) -> float:
status = await getJavaServerStatus(host)
return status.latency
async def schedule_job(host: str, mcdb: MCDB):
current_time = datetime.now(timezone("Asia/Shanghai"))
try:
latency = await getJavaServerLatency(host)
except Exception as e:
# 服务器连接失败可能宕机了把延迟设为10s秒
latency = 10000
print(e)
mcdb.insert_time_and_latency(current_time, latency)
def get_last_hour_date_info():
return (datetime.now(timezone("Asia/Shanghai")) - timedelta(hours=1))
def get_current_day_time_utc8():
return datetime.now(timezone("Asia/Shanghai")).strftime('%H:%M:%S')
def parse_date_to_h_m_s(timestamp_str) -> str:
timestamp = parser.parse(timestamp_str)
formatted_timestamp = timestamp.strftime("%H:%M:%S")
return formatted_timestamp

5
config.json.example Normal file
View file

@ -0,0 +1,5 @@
{
"mc_host": "xxxxx.xxx.xxx:25565",
"password": "xxxxxxxxxxxxxxx",
"rcon_host": "http://127.0.0.1:10001"
}

133
frontend/.gitignore vendored Normal file
View file

@ -0,0 +1,133 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# parcel
.parcel-cache

10
frontend/README.md Normal file
View file

@ -0,0 +1,10 @@
## development
```
npm install
npx parcel src/index.html
```
## build
```
npx parcel build src/index.html
```

3710
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

11
frontend/package.json Normal file
View file

@ -0,0 +1,11 @@
{
"devDependencies": {
"@parcel/transformer-webmanifest": "^2.12.0",
"parcel": "^2.12.0"
},
"dependencies": {
"@mdui/icons": "^1.0.2",
"chart.js": "^4.4.2",
"mdui": "^2.0.6"
}
}

26
frontend/src/index.html Normal file
View file

@ -0,0 +1,26 @@
<!doctype html>
<html lang="zh-CN" class="mdui-theme-auto">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=no" />
<meta name="renderer" content="webkit" />
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
<link rel="stylesheet" href="main.css">
<script type="module" src="main.js"></script>
<title>MC监控台</title>
</head>
<body>
<main class="mdui-prose center">
<h1>MC状态监测-No-Flesh-Within-Chest(脆骨症)</h1>
<mdui-text-field autocomplete="on" readonly rows="5" id="server-status" helper="如果延迟等于10000且下面图表是一条直线那大概率是服务器中断了"></mdui-text-field>
<canvas id="latency-chart"></canvas>
<mdui-button id="restart-server-btn">重启服务器</mdui-button>
<p>©BobMaster 2024</p>
</main>
</body>
</html>

10
frontend/src/main.css Normal file
View file

@ -0,0 +1,10 @@
body {
display: flex;
flex-flow: column wrap;
align-items: center;
width: 100%;
}
#restart-server-btn {
margin-top: 5px;
}

136
frontend/src/main.js Normal file
View file

@ -0,0 +1,136 @@
import 'mdui/mdui.css';
import 'mdui/components/layout.js';
import 'mdui/components/layout-item.js';
import 'mdui/components/layout-main.js';
import 'mdui/components/text-field.js';
import 'mdui/components/button.js';
import { prompt } from 'mdui/functions/prompt.js';
import { dialog } from 'mdui/functions/dialog.js';
import Chart from 'chart.js/auto';
import '@mdui/icons/copyright.js';
let ctx = document.getElementById("latency-chart").getContext('2d');
let chart = new Chart(
ctx,
{
type: 'line',
data: {
labels: [], // 存储时间信息
datasets: [{
label: '延迟信息',
data: [], // 存储延迟信息
backgroundColor: 'rgba(0, 123, 255, 0.5)',
borderColor: 'rgba(0, 123, 255, 1)',
borderWidth: 1
}]
},
options: {
plugins: {
title: {
display: true,
text: '延迟曲线图'
},
subtitle: {
display: true,
text: '单位: ms'
}
}
}
}
);
const restartBtn = document.getElementById("restart-server-btn")
window.onload = function () {
fetch('http://127.0.0.1:10000/')
.then(response => response.json())
.then(data => {
let compose_string = "";
compose_string += "当前时间: " + data.time;
compose_string += "\n服务器描述: " + data.description;
compose_string += "\n延迟: " + data.latency + "ms";
compose_string += "\n最大玩家容量: " + data.max;
compose_string += "\n当前在线人数: " + data.online;
document.getElementById("server-status").value = compose_string;
})
.catch(error => {
console.log(error);
});
fetch('http://127.0.0.1:10000/get_latency')
.then(response => response.json())
.then(data => {
for (let i = 0; i < data.length; i = i + 2) {
chart.data.labels.push(data[i].time);
chart.data.datasets[0].data.push(data[i].latency);
}
chart.update();
});
setInterval(function () {
fetch('http://127.0.0.1:10000')
.then(response => response.json())
.then(data => {
compose_string = "";
compose_string += "当前时间: " + data.time;
compose_string += "\n服务器描述: " + data.description;
compose_string += "\n延迟: " + data.latency + "ms";
compose_string += "\n最大玩家容量: " + data.max;
compose_string += "\n当前在线人数: " + data.online;
document.getElementById("server-status").value = compose_string;
// 添加新的数据到图表并更新
chart.data.labels.push(data.time);
chart.data.datasets[0].data.push(data.latency);
if (chart.data.labels.length > 30) {
chart.data.labels.shift();
chart.data.datasets[0].data.shift();
}
chart.update();
});
}, 30 * 1000); // 每30s执行一次
};
restartBtn.addEventListener("click", () => {
prompt({
headline: "请输入重置密码",
confirmText: "OK",
cancelText: "Cancel",
closeOnEsc: true,
onConfirm: (value) => {
restartBtn.disabled = true;
restartBtn.loading = true;
fetch("http://127.0.0.1:10000/restart-server", {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
},
body: JSON.stringify({"code": value})
})
.then(response => response.json())
.then(data => {
dialog({
headline: "执行结果",
description: data.status,
body: data.msg,
actions: [
{
"text": "了解",
onClick: () => {
restartBtn.disabled = false;
restartBtn.loading = false;
}
}
]
})
});
}
});
});