Compare commits

..

No commits in common. "main" and "v1.1.1" have entirely different histories.
main ... v1.1.1

9 changed files with 88 additions and 509 deletions

21
LICENSE
View File

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2024 Snowykami
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@ -19,46 +19,27 @@ _✨ 服务器状态 - 客户端 ✨_
服务器状态的客户端命令行工具
- 跨平台支持
- 自动上报服务器状态
- 支持自定义标签、地域、链接等信息
## 💿 安装
- 先决条件:`curl` `python3` `pip` `venv` `git`
- Linux 可使用脚本安装,带自动部署和自启动
使用 pip 安装(确保包路径在环境变量下)
```shell
sudo bash -c "$(curl -sSL https://raw.githubusercontent.com/snowykami/server-status-client/refs/heads/main/deploy.sh)"
```
如果位于中国大陆无法访问GitHub可使用中国版脚本
```shell
sudo bash -c "$(curl -sSL https://git.liteyuki.icu/snowykami/server-status-client/raw/branch/main/deploy-cn.sh)"
```
pip install server-status
- 或手动部署
Debian系请使用pipx安装
```shell
# 克隆仓库
git clone https://github.com/snowykami/server-status-client
cd server-status-client
# 配置环境
python3 -m venv venv
source venv/bin/activate
# 安装依赖
pip install pdm
pdm install
# 如需自启动请自行添加到系统服务
```
```bash
sudo apt install pipx
sudo pipx install server-status
```
若出现环境问题,请从 Actions 下载构建好的二进制文件
## 🎉 使用
### 命令
- `python main.py <server> <token> <id> run` - 运行客户端
- `python.main.py <server> <token> <id> rm` - 从服务端移除主机
- `server-status <server> <token> <id> run` - 运行客户端
- `server-status <server> <token> <id> rm` - 从服务端移除主机
#### 可选项
@ -71,20 +52,47 @@ _✨ 服务器状态 - 客户端 ✨_
#### 示例
```shell
python main.py https://status.liteyuki.icu 114514 myhost run -n "MyHost" --labels "标签1,标签2" --interval 5 --location "Chongqing" --link "https://example.com"
server_status https://status.liteyuki.icu 114514 myhost run -n "MyHost" --labels "标签1,标签2" --interval 5 --location "Chongqing" --link "https://example.com"
```
## 📝 其他
### 开机启动
- 安装脚本已自动添加到系统服务
执行以下命令
```shell
sudo pipx ensurepath # 确保pipx路径在环境变量下
sudo touch /etc/systemd/system/server-status-client.service
sudo bash -c 'cat <<EOF > /etc/systemd/system/server-status-client.service
[Unit]
Description=Server Status Client
After=network-online.target
[Service]
Type=simple
ExecStart=server-status <server> <token> <id> run # 请替换为实际参数
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF'
sudo systemctl enable server-status-client
sudo systemctl start server-status-client
```
### 更新
```shell
git pull
sudo systemctl restart server-status-client
#
git pull
systemctl restart server-status-client
```
### 服务端

View File

@ -1,96 +0,0 @@
#!/bin/bash
# 部署脚本中国大陆可用版本
# check if sudo is used
if [ "$EUID" -ne 0 ]; then
echo "Please run as root"
exit 1
fi
# check install dir
install_dir="/opt"
echo -n "安装目录? (默认: $install_dir/server-status-client): " && read -r install_dir_input
if [ -n "$install_dir_input" ]; then
install_dir="$install_dir_input"
fi
# check server
echo -n "服务端地址? (必须): " && read -r server
if [ -z "$server" ]; then
echo "服务端地址是必须的"
exit 1
fi
# check token
echo -n "令牌? (必须或留空): " && read -r token
# check hostname
hostname=$(hostname)
echo -n "此主机名? (默认: $hostname): " && read -r hostname_input
if [ -n "$hostname_input" ]; then
hostname="$hostname_input"
fi
# labels
echo -n "标签们? (空格分隔): " && read -r labels_input
if [ -n "$labels_input" ]; then
labels="$labels_input"
fi
# location
echo -n "地理位置? (可选|自定义): " && read -r location_input
if [ -n "$location_input" ]; then
location="$location_input"
fi
repo2="https://git.liteyuki.icu/snowykami/server-status-client"
# try 1 if failed try 2
git clone "$repo2" "$install_dir/server-status-client"
cd "$install_dir/server-status-client" || { echo "克隆失败"; exit 1; }
# create venv
python3 -m venv venv
python_exe="./venv/bin/python"
# check if venv is created
if [ ! -f "$python_exe" ]; then
echo "创建虚拟环境失败"
exit 1
fi
echo "虚拟环境创建成功"
# install the required packages
echo "正在安装依赖包..."
$python_exe -m pip install pdm -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
$python_exe -m pdm install
# create the systemd service
echo "正在创建服务..."
# generate random id
# shellcheck disable=SC2002
id=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1)
bash -c "cat <<EOF > /etc/systemd/system/server-status-client.service
[Unit]
Description=Server Status Client
After=network-online.target
[Service]
Type=simple
ExecStart=$install_dir/server-status-client/venv/bin/python main.py $server $token $id run -n $hostname --labels $labels --location $location
WorkingDirectory=$install_dir/server-status-client
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF"
# enable and start the service
systemctl enable server-status-client
systemctl start server-status-client
echo "安装完成,服务已启动"

View File

View File

@ -1,99 +0,0 @@
#!/bin/bash
# check if sudo is used
if [ "$EUID" -ne 0 ]; then
echo "Please run as root"
exit 1
fi
# check install dir
install_dir="/opt"
echo -n "Install directory? (default: $install_dir/server-status-client): " && read -r install_dir_input
if [ -n "$install_dir_input" ]; then
install_dir="$install_dir_input"
fi
# check server
echo -n "Server? (required): " && read -r server
if [ -z "$server" ]; then
echo "Server is required"
exit 1
fi
# check token
echo -n "Token? (required): " && read -r token
if [ -z "$token" ]; then
echo "Token is required"
exit 1
fi
# check hostname
hostname=$(hostname)
echo -n "Hostname? (default: $hostname): " && read -r hostname_input
if [ -n "$hostname_input" ]; then
hostname="$hostname_input"
fi
# labels
echo -n "Labels? (space separated): " && read -r labels_input
if [ -n "$labels_input" ]; then
labels="$labels_input"
fi
# location
echo -n "Location? (optional): " && read -r location_input
if [ -n "$location_input" ]; then
location="$location_input"
fi
# clone repo
repo1="https://github.com/snowykami/server-status-client"
repo2="https://git.liteyuki.icu/snowykami/server-status-client"
# try 1 if failed try 2
git clone "$repo1" "$install_dir/server-status-client" || git clone "$repo2" "$install_dir/server-status-client"
cd "$install_dir/server-status-client" || { echo "Failed to clone repo"; exit 1; }
# create venv
python3 -m venv venv
python_exe="./venv/bin/python"
# check if venv is created
if [ ! -f "$python_exe" ]; then
echo "Failed to create venv"
exit 1
fi
echo "venv created successfully"
# install the required packages
echo "Installing the required packages"
$python_exe -m pip install pdm
$python_exe -m pdm install
# create the systemd service
echo "Creating the systemd service"
# generate random id
# shellcheck disable=SC2002
id=$(cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 8 | head -n 1)
bash -c "cat <<EOF > /etc/systemd/system/server-status-client.service
[Unit]
Description=Server Status Client
After=network-online.target
[Service]
Type=simple
ExecStart=$install_dir/server-status-client/venv/bin/python main.py $server $token $id run -n $hostname --labels $labels --location $location
WorkingDirectory=$install_dir/server-status-client
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
EOF"
# enable and start the service
systemctl enable server-status-client
systemctl start server-status-client
echo "server-status-client installed successfully"

View File

@ -9,7 +9,6 @@ dependencies = [
"requests>=2.32.3",
"psutil>=6.0.0",
"arclet-alconna>=1.8.30",
"pytz>=2024.2",
]
requires-python = ">=3.10"
readme = "README.md"
@ -31,5 +30,5 @@ tag_filter = "v*"
tag_regex = '^v(?:\D*)?(?P<version>([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|c|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$)$'
[[tool.pdm.source]]
name = "pypi"
name = "tuna"
url = "https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple"

View File

@ -6,43 +6,7 @@ from typing import Any
import psutil
import requests
from server_status.timezone import get_timezone
excluded_partition_prefix = (
"/var",
"/boot",
"/run",
"/proc",
"/sys",
"/dev",
"/tmp",
"/snap",
"/System",
"/Applications",
"/private",
"/Library",
)
include_partition_prefix_mac = ("/Volumes")
os_name = "" # linux下为发行版名称windows下为Windows macOS下为Darwin
os_version = "" # linux下为发行版版本windows下为Windows版本
try:
# read /etc/os-release
with open("/etc/os-release") as f:
os_release = f.read()
# 找到NAME=和VERSION=的行
for line in os_release.split("\n"):
if line.startswith("NAME="):
os_name = line.split("=")[1].replace('"', "")
elif line.startswith("VERSION_ID="):
os_version = line.split("=")[1].replace('"', "")
except FileNotFoundError:
os_name = platform.system()
os_version = platform.release()
print("Current OS:", os_name, os_version)
excluded_partition_prefix = ("/var", "/boot", "/run", "/proc", "/sys", "/dev", "/tmp", "/snap")
def log(*args):
@ -63,7 +27,6 @@ def get_network_speed(interval) -> tuple[int, int]:
class Hardware:
os_release: str = ""
mem_total: int = psutil.virtual_memory().total
mem_used: int = psutil.virtual_memory().used
@ -76,8 +39,6 @@ class Hardware:
disks: dict[str, dict[str, int]] = {}
timezone: str = get_timezone()
net_up: int = 0
net_down: int = 0
net_type: str = "ethernet"
@ -147,9 +108,7 @@ class Api:
"""
self.headers.update(self.format(headers))
def format(
self, obj: str | list[str] | dict[str, Any]
) -> str | list[str] | dict[str, Any]:
def format(self, obj: str | list[str] | dict[str, Any]) -> str | list[str] | dict[str, Any]:
if isinstance(obj, str):
obj = obj.format(**self.variables)
elif isinstance(obj, dict):
@ -162,17 +121,8 @@ class Api:
class Client:
def __init__(
self,
addr: str,
token: str,
client_id: str,
name: str = "",
location: str = "",
labels: list[str] = [],
link: str = "",
interval: int = 2,
):
def __init__(self, addr: str, token: str, client_id: str, name: str = "", location: str = "", labels: list[str] = [], link: str = "",
interval: int = 2):
self.api = Api(addr, {"token": token, "id": client_id})
self.api = self.api.group("/client")
self.api.add_headers(Authorization="{token}")
@ -186,66 +136,15 @@ class Client:
self.link = link
self.interval = interval
self.start_time: float = psutil.boot_time()
self.hardware = Hardware()
log(
"Client initialized",
f"Name: {self.name}({self.client_id}), Location: {self.location}, Labels: {self.labels}",
)
log("Client initialized",
f"Name: {self.name}({self.client_id}), Location: {self.location}, Labels: {self.labels}")
def start(self):
log("Starting client")
threading.Thread(target=self._start_obs, daemon=True).start()
threading.Thread(target=self._start_post, daemon=True).start()
while True:
time.sleep(1)
self.start_time = time.time()
self.observe()
def _start_obs(self):
"""启动监控记录线程"""
while True:
try:
self.hardware.mem_total = psutil.virtual_memory().total
self.hardware.mem_used = psutil.virtual_memory().used
self.hardware.swap_total = psutil.swap_memory().total
self.hardware.swap_used = psutil.swap_memory().used
self.hardware.cpu_cores = psutil.cpu_count(logical=False)
self.hardware.cpu_logics = psutil.cpu_count(logical=True)
for part in psutil.disk_partitions():
try:
usage = psutil.disk_usage(part.mountpoint)
if (
(
platform.system() in ("Linux", "Darwin")
and (
part.mountpoint.startswith(
excluded_partition_prefix
)
)
)
):
continue
self.hardware.disks[part.device] = {
"mountpoint": part.mountpoint,
"device": part.device,
"fstype": part.fstype,
"total": usage.total,
"used": usage.used,
}
except:
pass
self.hardware.cpu_percent = psutil.cpu_percent(1)
self.hardware.net_up, self.hardware.net_down = get_network_speed(1)
log("Observed")
except Exception as e:
log(f"Failed to observe: {e}")
def _start_post(self):
"""启动上报进程"""
while True:
try:
resp = self.get_ping()
@ -253,13 +152,9 @@ class Client:
log(f"Connected to server {self.addr}")
break
else:
log(
f"Failed to connect to server {self.addr}, retrying in 5 seconds: {resp.text}"
)
log(f"Failed to connect to server {self.addr}, retrying in 5 seconds: {resp.text}")
except Exception as e:
log(
f"Failed to connect to server {self.addr}, retrying in 5 seconds: {e}"
)
log(f"Failed to connect to server {self.addr}, retrying in 5 seconds: {e}")
time.sleep(5)
while True:
@ -286,19 +181,14 @@ class Client:
"id": self.client_id,
"name": self.name,
"os": {
"name": platform.system(), # 系统类型 linux|windows|darwin
"version": os_name + os_version,
# 系统版本复杂描述 #1 SMP PREEMPT_DYNAMIC Fri Sep 13 10:42:50 UTC 2024 (5c05eeb)
"machine": platform.machine(), # 机器类型 x86_64
"release": os_version, # 系统版本
"name": platform.system(),
"version": platform.version(),
},
"labels": self.labels,
"location": self.location,
"uptime": int(time.time() - self.start_time),
"start_time": int(self.start_time), # 系统启动的时间
"link": self.link,
"observed_at": int(time.time()),
"timezone": self.hardware.timezone,
},
"hardware": {
"mem": {
@ -323,5 +213,39 @@ class Client:
},
}
def observe(self):
"""
观察硬件状态并更新
Returns:
"""
def _observe():
while True:
self.hardware.mem_total = psutil.virtual_memory().total
self.hardware.mem_used = psutil.virtual_memory().used
self.hardware.swap_total = psutil.swap_memory().total
self.hardware.swap_used = psutil.swap_memory().used
self.hardware.cpu_cores = psutil.cpu_count(logical=False)
self.hardware.cpu_logics = psutil.cpu_count(logical=True)
for part in psutil.disk_partitions():
try:
usage = psutil.disk_usage(part.mountpoint)
if part.mountpoint.startswith(excluded_partition_prefix) or usage.total == 0:
continue
self.hardware.disks[part.device] = {
"total": usage.total,
"used": usage.used,
}
except:
pass
self.hardware.cpu_percent = psutil.cpu_percent(1)
self.hardware.net_up, self.hardware.net_down = get_network_speed(1)
log("Observed")
threading.Thread(target=_observe, daemon=True).start()
def remove(self, client_id) -> requests.Response:
return self.api.delete("/host", data={"id": client_id})

View File

@ -3,7 +3,7 @@ import socket
from arclet.alconna import Alconna, Subcommand, Option, Args, MultiVar
server_status_alc = Alconna( # type: ignore
server_status_alc = Alconna(
"server_status",
Args["server", str]["token", str]["id", str],
Subcommand(

View File

@ -1,136 +0,0 @@
import os
import platform
from zoneinfo import ZoneInfo
windows_timezone_map = {
"Dateline Standard Time": "Etc/GMT+12",
"UTC-11": "Etc/GMT+11",
"Aleutian Standard Time": "America/Adak",
"Hawaiian Standard Time": "Pacific/Honolulu",
"Marquesas Standard Time": "Pacific/Marquesas",
"Alaskan Standard Time": "America/Anchorage",
"UTC-09": "Etc/GMT+9",
"Pacific Standard Time (Mexico)": "America/Tijuana",
"UTC-08": "Etc/GMT+8",
"Pacific Standard Time": "America/Los_Angeles",
"US Mountain Standard Time": "America/Phoenix",
"Mountain Standard Time (Mexico)": "America/Chihuahua",
"Mountain Standard Time": "America/Denver",
"Central America Standard Time": "America/Guatemala",
"Central Standard Time": "America/Chicago",
"Easter Island Standard Time": "Pacific/Easter",
"Central Standard Time (Mexico)": "America/Mexico_City",
"Canada Central Standard Time": "America/Regina",
"SA Pacific Standard Time": "America/Bogota",
"Eastern Standard Time (Mexico)": "America/Cancun",
"Eastern Standard Time": "America/New_York",
"Haiti Standard Time": "America/Port-au-Prince",
"Cuba Standard Time": "America/Havana",
"US Eastern Standard Time": "America/Indianapolis",
"Turks And Caicos Standard Time": "America/Grand_Turk",
"Paraguay Standard Time": "America/Asuncion",
"Atlantic Standard Time": "America/Halifax",
"Venezuela Standard Time": "America/Caracas",
"Central Brazilian Standard Time": "America/Cuiaba",
"SA Western Standard Time": "America/La_Paz",
"Pacific SA Standard Time": "America/Santiago",
"SA Eastern Standard Time": "America/Cayenne",
"Argentina Standard Time": "America/Buenos_Aires",
"Greenland Standard Time": "America/Godthab",
"Montevideo Standard Time": "America/Montevideo",
"Bahia Standard Time": "America/Bahia",
"UTC-02": "Etc/GMT+2",
"Azores Standard Time": "Atlantic/Azores",
"Cape Verde Standard Time": "Atlantic/Cape_Verde",
"UTC": "Etc/GMT",
"GMT Standard Time": "Europe/London",
"Greenwich Standard Time": "Atlantic/Reykjavik",
"W. Europe Standard Time": "Europe/Berlin",
"Central Europe Standard Time": "Europe/Budapest",
"Romance Standard Time": "Europe/Paris",
"Central European Standard Time": "Europe/Warsaw",
"W. Central Africa Standard Time": "Africa/Lagos",
"Namibia Standard Time": "Africa/Windhoek",
"GTB Standard Time": "Europe/Bucharest",
"Middle East Standard Time": "Asia/Beirut",
"Egypt Standard Time": "Africa/Cairo",
"Syria Standard Time": "Asia/Damascus",
"E. Europe Standard Time": "Europe/Chisinau",
"South Africa Standard Time": "Africa/Johannesburg",
"FLE Standard Time": "Europe/Kiev",
"Turkey Standard Time": "Europe/Istanbul",
"Israel Standard Time": "Asia/Jerusalem",
"Jordan Standard Time": "Asia/Amman",
"Arabic Standard Time": "Asia/Riyadh",
"Kaliningrad Standard Time": "Europe/Kaliningrad",
"Arab Standard Time": "Asia/Riyadh",
"E. Africa Standard Time": "Africa/Nairobi",
"Iran Standard Time": "Asia/Tehran",
"Arabian Standard Time": "Asia/Dubai",
"Astrakhan Standard Time": "Europe/Astrakhan",
"Russian Standard Time": "Europe/Moscow",
"E. Europe Standard Time": "Europe/Chisinau",
"W. Australia Standard Time": "Australia/Perth",
"Moscow Standard Time": "Europe/Moscow",
"Pakistan Standard Time": "Asia/Karachi",
"India Standard Time": "Asia/Kolkata",
"Sri Lanka Standard Time": "Asia/Colombo",
"Nepal Standard Time": "Asia/Kathmandu",
"Bangladesh Standard Time": "Asia/Dhaka",
"Afghanistan Standard Time": "Asia/Kabul",
"Myanmar Standard Time": "Asia/Yangon",
"SE Asia Standard Time": "Asia/Bangkok",
"North Asia Standard Time": "Asia/Krasnoyarsk",
"China Standard Time": "Asia/Shanghai",
"Singapore Standard Time": "Asia/Singapore",
"W. Australia Standard Time": "Australia/Perth",
"Taipei Standard Time": "Asia/Taipei",
"Ulaanbaatar Standard Time": "Asia/Ulaanbaatar",
"North Asia East Standard Time": "Asia/Irkutsk",
"Korea Standard Time": "Asia/Seoul",
"Tokyo Standard Time": "Asia/Tokyo",
"Yakutsk Standard Time": "Asia/Yakutsk",
"Cen. Australia Standard Time": "Australia/Adelaide",
"AUS Central Standard Time": "Australia/Darwin",
"E. Australia Standard Time": "Australia/Brisbane",
"AUS Eastern Standard Time": "Australia/Sydney",
"West Pacific Standard Time": "Pacific/Port_Moresby",
"Tasmania Standard Time": "Australia/Hobart",
"Magadan Standard Time": "Asia/Magadan",
"Vladivostok Standard Time": "Asia/Vladivostok",
"Russia Time Zone 10": "Asia/Srednekolymsk",
"Central Pacific Standard Time": "Pacific/Guadalcanal",
"Fiji Standard Time": "Pacific/Fiji",
"New Zealand Standard Time": "Pacific/Auckland",
"UTC+12": "Etc/GMT-12",
"Kamchatka Standard Time": "Asia/Kamchatka",
"Tonga Standard Time": "Pacific/Tongatapu",
"Samoa Standard Time": "Pacific/Apia",
"Line Islands Standard Time": "Pacific/Kiritimati",
}
def get_timezone() -> str:
try:
# 尝试获取系统的本地时区
if 'TZ' in os.environ:
return os.environ['TZ']
# 如果环境变量中没有TZ尝试获取系统时区
if platform.system() == "Linux":
# Linux:
with open("/etc/timezone", "r") as f:
return f.read().strip()
elif platform.system() == "Darwin":
# macOS:
return ZoneInfo.from_file(open("/etc/localtime")).key
elif platform.system() == "Windows":
# Windows:
import winreg
with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation") as key:
tz = winreg.QueryValueEx(key, "TimeZoneKeyName")[0]
return windows_timezone_map.get(tz) or tz
else:
return "UTC"
except Exception as e:
return "UTC"