Compare commits

..
371 Commits
Author SHA1 Message Date
cpplaandGitHub 5322a9105e Update README.md
CI / validate (push) Has been cancelled
1.1.9 screen
2026-07-09 19:15:34 +08:00
cppla 3b0705edc1 update client linux and psutil 2026-07-09 18:01:57 +08:00
cppla 2b799a524f raw域名被限流,替换为api 2026-07-09 17:13:58 +08:00
cppla 783d27732d cpu short 2026-07-09 17:07:59 +08:00
cppla efb2768fa8 1.1.9 2026-07-09 16:09:02 +08:00
cppla a8fc0b81ab 1.1.9
CI / validate (push) Has been cancelled
2026-07-09 10:43:40 +08:00
cppla 66e6179332 update 2026-07-09 10:10:08 +08:00
cppla 6ad3a8c294 update
CI / validate (push) Has been cancelled
2026-07-07 17:39:32 +08:00
cppla 03db91c873 update 2026-07-07 17:27:29 +08:00
cppla 007c9bccd5 new style 2026-07-07 16:52:04 +08:00
cppla e8e90275a2 增加月流量重置按钮
CI / validate (push) Has been cancelled
2026-07-06 14:02:12 +08:00
cppla 8e4590699b update css for nomac
CI / validate (push) Has been cancelled
2026-07-02 16:24:04 +08:00
cppla 51ae186243 update 2026-07-02 13:07:35 +08:00
cppla bbec425c51 少于10时折叠筛选 2026-07-02 12:02:36 +08:00
cppla e0aae47efc update css
CI / validate (push) Has been cancelled
2026-07-01 19:17:07 +08:00
cppla a31fb176c4 update
CI / validate (push) Has been cancelled
2026-07-01 17:53:28 +08:00
cppla f19d10966d update version 1.1.8 2026-07-01 17:13:30 +08:00
cppla 9ac276d443 更新说明 2026-07-01 15:32:17 +08:00
cppla c58eaf6ff5 version 1.1.8 2026-07-01 11:55:45 +08:00
cppla f24a70b9d7 修复传递参数优先级的问题,防止错误获取系统变量 2026-03-20 11:51:05 +08:00
cppla 23a620274d linux尝试从os中获取信息 2026-02-28 14:10:35 +08:00
cppla e903200d66 默认psutil,容器下获取更精准的宿主机读写情况 2026-02-28 11:58:00 +08:00
cppla 57c74b0c9e update 2026-02-27 18:40:30 +08:00
cppla caf5d5c34d update 2026-02-27 18:35:57 +08:00
cppla ed64656a06 修复bug 2026-02-27 17:57:31 +08:00
cppla bbde4cb1e9 fix bug 2026-02-27 17:52:03 +08:00
cppla 678cf43077 fix bug 2026-02-27 17:24:33 +08:00
cppla 4bbd54dd06 fix bug 2026-02-27 17:12:31 +08:00
cppla eea08f529e fix bug 2026-02-27 17:12:12 +08:00
cppla d1ad6b0f43 client alpine images 2026-02-27 15:36:01 +08:00
cppla 570e14c1fa update docker compose 2026-02-27 13:27:58 +08:00
cppla 7b9da31db0 add client docker 2026-02-26 18:10:24 +08:00
cppla 6552959ef3 解决总流量pb显示异常的问题 2026-02-10 16:56:33 +08:00
cppla 14ee075853 ai fix a bug 2025-10-16 10:03:06 +08:00
cppla c1955d7ca5 AI fix AI Bug 2025-10-09 10:46:35 +08:00
cppla 41ec81bed3 del not used variable 2025-09-12 10:38:56 +08:00
cppla 1557673db2 ssl expire tips bug 2025-09-12 10:33:09 +08:00
cppla 565f8c7ce0 修复ssl提醒的bug 2025-09-11 11:49:59 +08:00
cppla 163c46f4de 1.1.7 latest 2025-09-03 17:20:36 +08:00
cppla a46ed4ea1a update readme 2025-09-03 16:22:22 +08:00
cppla 4d9372768f update 2025-09-03 16:03:37 +08:00
cppla eed03f641d 1.1.7 2025-09-03 16:01:34 +08:00
cppla 25effc0e2f update 1.1.7 2025-09-02 17:44:30 +08:00
cppla dcec9598c1 1.1.7 update 2025-09-02 17:43:43 +08:00
cppla f8527cc297 update config 2025-09-01 19:39:33 +08:00
cppla a4fc285be6 系统着色更改 2025-09-01 15:26:05 +08:00
cppla 195594b8c4 update 2025-09-01 14:40:18 +08:00
cppla 405e95bff8 update 2025-09-01 14:39:44 +08:00
cppla 72b621e277 硬盘仪表盘初始化颜色为青色 2025-09-01 10:20:55 +08:00
cppla c089ac7834 update 2025-08-31 22:38:03 +08:00
cppla ae648fbe66 fix macos color 2025-08-29 23:41:04 +08:00
cppla 68a1d3719f update 2025-08-29 19:32:51 +08:00
cppla 9cd86ccaa3 update 2025-08-29 17:34:12 +08:00
cppla 956a5ace26 1.1.6 patch 2025-08-29 17:05:20 +08:00
cppla 15fe753a0f 1.1.6修复大部分bug 2025-08-15 19:05:28 +08:00
cppla 0d185f628b update appjs 2025-08-15 18:51:01 +08:00
cppla 83977b55bf update gpt agent 2025-08-15 14:14:23 +08:00
cppla 49decdfed1 gpt commit 2025-08-15 13:59:12 +08:00
cppla 227e15fe48 Merge branch 'master' of github.com:cppla/ServerStatus 2025-08-15 13:34:55 +08:00
cppla 73b3d16ff6 fix some bug 2025-08-15 13:33:39 +08:00
cpplaandGitHub c04256535d Update README.md 2025-08-14 20:51:14 +08:00
cppla 3c8ebcf710 update fix some 2025-08-14 19:45:14 +08:00
cpplaandGitHub 9a19d2f4bf Update README.md 2025-08-14 14:02:56 +08:00
cppla d3c5e4a02a update 1.1.6 screen 2025-08-14 13:34:21 +08:00
cppla a03cda0ef0 update 2025-08-14 10:12:23 +08:00
cppla d25e7871ba update Dockerfile 2025-08-13 19:46:57 +08:00
cppla fd6c3532d9 update 2025-08-13 14:16:29 +08:00
cppla 89745f3c3a 3600 2025-08-13 13:27:37 +08:00
cppla 266230f5f0 update theme 2025-08-12 19:04:44 +08:00
cppla 4c17623dff update bug 2025-08-12 17:56:55 +08:00
cppla 34e30d9873 del 2025-08-12 17:23:20 +08:00
cppla 1e190878dd update theme 2025-08-12 17:19:19 +08:00
cppla 874c122a62 del pch 2025-08-12 15:42:17 +08:00
cppla fa984cfae6 update 2025-08-12 15:26:45 +08:00
cppla a544f72db0 restore makefile 2025-08-12 14:55:54 +08:00
cppla eea993c6cc update 2025-08-12 14:30:01 +08:00
cppla a4a3734c30 update 2025-08-12 14:01:50 +08:00
cppla a10e39b6f2 update readme 2025-08-12 13:54:17 +08:00
cppla 360d8d008d add ssl check 2025-08-12 13:49:25 +08:00
cpplaandGitHub 830938eac9 Update README.md 2025-04-01 10:35:26 +08:00
cppla 1a764ed104 1111 2024-12-02 14:13:00 +08:00
cppla b0c543b44b update Dockerfile 2024-11-25 17:39:07 +08:00
cppla f85c0a87f9 update tcp example 2024-11-25 16:50:32 +08:00
cpplaandGitHub 67d7c5ea0f Merge pull request #261 from ArashPartow/arashpartow/update_exprtk_0.0.3
Update ExprTk to 0.0.3
2024-11-25 16:42:00 +08:00
cppla 8576f81404 some update for probe tcp slow 2024-11-18 13:56:01 +08:00
cppla fa0ee2bf57 重要线路丢包率告警例子 2024-09-18 13:24:06 +08:00
cppla e9776b0e69 增加重要节点丢包率监控 2024-09-18 10:07:43 +08:00
cppla e01cc118dc update css 2024-09-13 09:26:07 +08:00
cpplaandGitHub 784c40b153 Update README.md 2024-09-11 18:01:05 +08:00
cppla 6068a2ba17 yes 2024-09-11 17:54:24 +08:00
cppla ced526824f 增加阿里云免费20G告警示例,限制username是aliyun1和aliyun2 2024-09-09 19:18:42 +08:00
cppla 7d2a73bf2e 修复月流量监控的bug,月流量加入表达式计算 2024-09-09 18:55:11 +08:00
cppla 51e2f664cd update theme set bug 2024-08-18 21:16:38 +08:00
cppla 937b1ea90f fix some bug 2024-08-18 21:13:53 +08:00
cpplaandGitHub b95d1945bf Update README.md 2024-08-18 19:44:57 +08:00
cpplaandGitHub e8446cd52f 我震惊的是AI已经能够帮助重构前端页面
我震惊的是AI已经能够帮助重构前端页面
2024-08-18 13:09:25 +08:00
cppla 6098f0fb1a commit new css by ai 2024-08-18 13:00:45 +08:00
cppla fc89a76cc2 resize css 2024-08-17 18:50:43 +08:00
cppla f290947b2c update theme 2024-08-17 17:36:57 +08:00
cppla b3adc6b782 update all 2024-08-17 17:31:40 +08:00
cppla 705a957606 bai piao copilot 2024-08-15 19:32:50 +08:00
cppla a307ad7d19 ai niu bi 2024-08-15 15:59:26 +08:00
cppla ad95e00723 不更新代码 无法续Copilot 2024-08-15 14:46:49 +08:00
cppla 31330168f3 ai modify 2024-08-15 14:18:34 +08:00
cppla d9e8f8a7c1 del ie 2024-08-15 14:02:33 +08:00
cppla 08f15ebdc5 15s -> 25s 2024-04-18 15:34:43 +08:00
cppla adb05818b9 改为15秒再次判断是否离线 2024-04-08 14:25:53 +08:00
cpplaandGitHub 4e73e8185e Update README.md 2024-04-03 22:06:15 +08:00
cpplaandGitHub 0fe01064a4 Update README.md 2024-04-03 21:59:23 +08:00
cppla 510567eaec 修复驴头不对马嘴的bug 2024-04-03 21:31:35 +08:00
cppla 503037c7e2 add two logs 2024-04-03 19:53:04 +08:00
cppla d75d5438a3 docker compose healthcheck 2024-04-03 17:44:43 +08:00
cppla 91f11dad76 屏蔽无用的nginx日志,增加健康检查wq 2024-04-03 17:16:32 +08:00
cppla fdc5abacfc fix bug for offline 2024-04-03 16:42:40 +08:00
cppla 388938e02b 解决了网络闪断导致的错误报警 2024-04-03 16:39:08 +08:00
cppla f912794068 add todo 2024-04-02 10:24:12 +08:00
cppla 6331d7d45b update 2024-03-11 15:06:47 +08:00
cppla 25f878a38a 5分钟一次probe service 2024-03-11 15:03:57 +08:00
cppla a8b9b2d00d 默认一天在线率 2024-02-04 14:43:19 +08:00
cppla dc3868998a 256->512 2024-02-04 14:37:39 +08:00
cppla 75d06c8666 add some ua 2024-01-27 15:27:10 +08:00
cppla 360252182d Merge branch 'dev' 2024-01-23 18:51:49 +08:00
cppla aa0ccd254c update readme 2024-01-23 18:50:15 +08:00
cpplaandGitHub f7b2e7db42 Merge pull request #242 from cppla/dev
1.1.2 测试
2024-01-23 18:10:19 +08:00
cppla 052c75ef23 update 2024-01-23 18:09:23 +08:00
cppla 1f7827cc21 change 2024-01-23 18:00:24 +08:00
cppla c236aee5dc todo list 2024-01-23 17:52:59 +08:00
cppla fb3ecd1796 增加一个todo 2024-01-23 17:43:27 +08:00
cppla 810f6a0d8a fix 2024-01-23 16:47:26 +08:00
cppla eddf37c413 fix bug 2024-01-23 10:30:43 +08:00
cppla 8dfc6d4718 update 2024-01-22 19:49:29 +08:00
cppla 03446b8f29 update some for cl 2024-01-22 15:34:44 +08:00
cppla 6ab046be51 update 2024-01-22 15:32:20 +08:00
cppla 94708c588c css resize 2024-01-22 15:21:06 +08:00
cppla 144987bcf1 .2f 2024-01-21 22:55:07 +08:00
cppla 7e46b1062e 新增说明,比较粗糙不建议更新 2024-01-21 21:34:07 +08:00
cppla 71cadcfebe 测试监控重要服务 2024-01-21 21:09:37 +08:00
cppla d96e711a34 change ms and lost rate 2024-01-19 16:03:43 +08:00
Arash Partow 5ebf076330 Update ExprTk to 0.0.3 2024-01-01 00:00:00 +00:00
cpplaandGitHub 4a9f5adb6b Update index3.html
cppla/Serverstatus version modify
2023-10-17 09:49:44 +08:00
cppla 59fa2d8eab fix light bug 2023-10-17 01:46:19 +00:00
cpplaandGitHub bd4c22edff Merge pull request #233 from cppla/dev
内置一个light theme
2023-10-16 19:36:58 +08:00
windows11 88229b2f4a 内置一个light theme 2023-10-16 19:35:22 +08:00
cpplaandGitHub d70a56a767 更加丰富的限制条件和规则
更加丰富的限制条件和规则
2023-10-16 18:35:03 +08:00
cpplaandGitHub a177345858 添加静态字符串的计算支持
添加静态字符串的计算支持
username / name / type / host / location
2023-10-16 17:53:46 +08:00
cpplaandGitHub 11a03a020c Merge pull request #232 from cppla/dev
静态字符串参与计算,同时给出样例
2023-10-16 17:42:09 +08:00
windows11 ed330bd785 c style 2023-10-16 17:07:57 +08:00
windows11 77cb62ae80 测试是否支持静态字符串断言 2023-10-16 16:57:02 +08:00
cpplaandGitHub 7bbca5e881 Update README.md 2023-10-13 13:54:02 +08:00
cpplaandGitHub 2e0d1f7fb9 Update README.md 2023-10-13 13:53:20 +08:00
cpplaandGitHub da83c54e5b Update README.md 2023-10-13 13:34:18 +08:00
cpplaandGitHub 3ef6cb52c1 Update README.md 2023-10-13 13:13:05 +08:00
cpplaandGitHub 67877bacfc Update README.md 2023-10-13 13:11:46 +08:00
cpplaandGitHub a8383efe93 Update README.md 2023-10-13 12:47:24 +08:00
cpplaandGitHub 03682772a1 静态字符串无法参与计算
提交一个表达式说明,静态字符串无法参与计算
2023-10-13 12:46:25 +08:00
cpplaandGitHub fdca4fd353 静态字符串变量无法参与计算。
静态字符串变量无法参与计算。
2023-10-13 12:30:03 +08:00
windows11 a5330dcf2f 修复username等静态变量的表达式支持 2023-10-13 11:58:46 +08:00
cpplaandGitHub be78a7cc88 Update README.md 2023-10-13 11:45:12 +08:00
cpplaandGitHub 46d43c247f Update light.css 2023-10-09 15:51:45 +08:00
cpplaandGitHub 6ae278857a Merge pull request #229 from cppla/cppla-patch-1
Update dark.css
2023-10-09 15:48:30 +08:00
cpplaandGitHub d5e59453f1 Update dark.css 2023-10-09 15:47:45 +08:00
cpplaandGitHub d242c6cea1 1.1.0
修复一些bug.
2023-10-09 14:24:17 +08:00
windows11 d7614ab168 width css bt3 2023-10-09 14:08:47 +08:00
windows11 ff06df8263 修复告警信息和URL特殊字符冲突的问题 2023-10-09 13:05:59 +08:00
cpplaandGitHub 9ee1516f72 Update README.md
up theme
2023-10-05 15:29:32 +08:00
cpplaandGitHub 7603ce37af add light theme
add light theme link and demo .
2023-10-05 15:19:02 +08:00
cppla 5cfd533daf flush dns , every time 2023-05-31 11:06:28 +08:00
cppla 9077f1058a add layui theme. 2023-05-04 15:59:37 +08:00
cppla e34ed05901 make -j,多核心编译docker image 2023-04-12 11:10:10 +08:00
cppla 4ab545f646 fix bug about tupt 2023-04-12 10:17:51 +08:00
cppla a6b4248967 jquery 3.5.0 to 3.5.1 2023-04-04 19:06:35 +08:00
cppla 8ad927d88d jquery v3.3.1 to v3.5.0 2023-04-03 15:22:59 +08:00
cppla 4b1ec57c6d 增加一个月出口流量示例 2023-04-03 15:12:33 +08:00
cppla 979a9b5d87 加一个ddcc告警示例 2023-03-13 07:23:44 +00:00
cppla 52d62afa59 优化curl 2023-02-28 17:19:49 +08:00
cppla 806b60bd6b get macos infomation build 2022-11-30 11:17:12 +08:00
cppla d5af5445df disk io not in win 2022-11-30 11:09:21 +08:00
cppla c77e00476c get macos tudp 2022-11-30 11:02:09 +08:00
cppla b551998eb8 get macos load 2022-11-30 10:31:04 +08:00
cppla 1c606cc79e update 2022-11-08 12:52:16 +08:00
cppla d602791b11 macos disk info 2022-11-03 16:38:22 +08:00
cppla a695ef8b3a update for macos 2022-11-03 16:33:16 +08:00
cppla 9f60b7962b beta macos disk info 2022-11-03 16:25:23 +08:00
cppla 34b1bca5b1 The io_counters is not available on OS X, 兼容处理 2022-11-03 14:49:47 +08:00
cppla 25917f0883 update readme 2022-11-01 13:47:36 +08:00
cppla 333bc29c88 rename old tg to plugin 2022-11-01 13:38:14 +08:00
cppla c116067c39 get to post 2022-10-31 18:36:33 +08:00
cppla 14db7ec943 update config template 2022-10-31 16:48:54 +08:00
cppla af8244f1d7 int memory expand 2022-10-31 14:53:12 +08:00
cppla 1004c9a6bd update 2022-10-31 14:25:36 +08:00
cppla 7f78af03f0 100 ping lost 2022-10-31 14:15:37 +08:00
cppla 02bbdb18de update lost style 2022-10-31 14:01:42 +08:00
cpplaandGitHub 6d813f932d Merge pull request #191 from zqcccc/master
展开栏添加丢包信息
2022-10-31 13:43:34 +08:00
c9cu 19d6a5ea8b 展开栏添加丢包信息 2022-10-29 19:07:33 +08:00
cpplaandGitHub 35de279e89 Update README.md 2022-08-26 14:53:47 +08:00
cpplaandGitHub d5a047c781 Update README.md 2022-08-25 16:23:02 +08:00
cpplaandGitHub 10872059c5 build 1.0.9
build 1.0.9
2022-08-25 16:14:46 +08:00
cpplaandGitHub 44656e565f 更改watchdog callback说明
更改watchdog callback说明
2022-08-25 16:13:24 +08:00
cpplaandGitHub 78c7da7361 经测试邮箱 和wechat不能稳定发送
经测试邮箱 和wechat不能稳定发送
2022-08-25 16:03:46 +08:00
cpplaandGitHub b5edeea057 Update README.md
添加一个邮箱服务
2022-08-25 14:40:56 +08:00
cpplaandGitHub c2259f347d Update README.md 2022-08-23 19:39:54 +08:00
cpplaandGitHub bb5f03047d Update README.md 2022-08-23 19:39:10 +08:00
cppla 6ab6e16d0f 1080 to 1200 2022-08-01 19:37:26 +08:00
cppla 615cec88c2 300s 2022-07-21 10:53:08 +08:00
cppla 94896bac80 change docker to beijing time 2022-07-18 10:36:36 +08:00
cppla a788b5da90 . 2022-07-17 10:11:11 +08:00
cpplaandGitHub de0bc9dd74 Update README.md 2022-07-17 10:08:04 +08:00
cpplaandGitHub ec16fc1fac Update README.md 2022-07-16 19:47:33 +08:00
cpplaandGitHub cbd803b686 Create README.md
update tips
2022-07-16 19:46:59 +08:00
cppla 3eddb27d51 update 2022-07-16 19:30:29 +08:00
cppla 522809483e Compatible with older versions config.json 2022-07-16 19:11:29 +08:00
cpplaandGitHub 9d706f4da8 Update README.md
update watchdog 说明
2022-07-16 18:44:09 +08:00
cpplaandGitHub a0fe8ee33c Update README.md
callback说明
2022-07-16 18:35:08 +08:00
cpplaandGitHub c5eed8e4fa Update README.md
加入watchdog 说明
2022-07-16 18:00:10 +08:00
cppla 32d302580e update 2022-07-16 17:53:17 +08:00
cppla b03d090a5c update readme 2022-07-16 17:47:57 +08:00
cppla 6151806141 c++ eval build use c++11 2022-07-16 14:42:32 +08:00
cppla 2703993272 add timeout for libcurl 2022-07-16 13:45:37 +08:00
cppla 022e5edb28 formate 2022-07-16 00:41:25 +08:00
cppla 135eb180c8 escape 2022-07-16 00:32:33 +08:00
cppla 2ead43a0d8 update 2022-07-15 23:14:53 +08:00
cppla baae11de3b tg sms 2022-07-15 22:34:44 +08:00
cppla 2e62ffa593 link libcurl static lib 2022-07-15 21:14:39 +08:00
cppla f70705d872 next add callback 2022-07-13 17:11:22 +08:00
cppla 7316dbdddb update 2022-07-13 16:55:38 +08:00
cppla a1689acbf0 trigger msg for watchdog 2022-07-12 16:58:30 +08:00
cppla e2a59a5465 eval 2022-07-12 15:59:24 +08:00
cppla 962d564c80 copy msg 2022-07-12 13:50:57 +08:00
cppla 77973a5309 0 and 1 2022-07-11 19:03:29 +08:00
cppla a9128b137d read config. 2022-07-07 18:30:31 +08:00
cppla 0a2a007e24 support for python 3.10 2022-05-30 13:08:34 +08:00
cpplaandGitHub ab5d6f7d2d Update serverstatus.js
change style for io speed
2022-05-10 15:20:58 +08:00
cppla dae7c07772 1.0.8 build 2022-05-10 11:56:38 +08:00
cpplaandGitHub 14447399a0 Update README.md 2022-05-06 10:26:46 +08:00
cppla 3327c5fd25 修复增删节点月流量匹配的问题 2022-05-05 10:15:06 +08:00
cppla 217b14bf55 old style 2022-04-29 16:07:56 +08:00
cppla d7b8f27082 IO Speed as Tips 2022-04-29 13:58:38 +08:00
cpplaandGitHub 8f3736e4fc 增加开源的支持
just do it .
2022-04-13 15:40:46 +08:00
windows11 22c1905acb style 2022-04-02 15:55:58 +08:00
cppla f73970262b test 2022-04-02 15:39:41 +08:00
cppla f45d9be9fc ui 2022-04-02 15:34:01 +08:00
cppla cb1313f5b5 test 2022-04-02 15:25:10 +08:00
cppla b3f7ab45c5 blue 2022-04-02 14:47:23 +08:00
cppla 54451cab85 no float for IO 2022-04-02 11:39:37 +08:00
cppla 0f26eb502a 1080p change 2022-04-01 18:55:41 +08:00
cppla 472196d1ea 1.0.8 beta 2022-04-01 17:42:43 +08:00
cppla 7e84c230e6 Merge branch 'dev' 2022-04-01 17:36:22 +08:00
cppla 76cc15e84f css 2022-04-01 17:35:58 +08:00
cpplaandGitHub 4c0f4b94e8 Merge pull request #149 from cppla/dev
新增实时IO统计
2022-04-01 16:29:53 +08:00
cppla 36040be11e add 2022-04-01 16:28:22 +08:00
cppla 776ad68392 io css style 2022-04-01 16:24:52 +08:00
cppla 39c92f4788 bug 2022-04-01 16:07:30 +08:00
cppla 6a15832966 bug test 2022-04-01 16:04:08 +08:00
cppla 654ecd7a3e update disk io style 2022-04-01 15:50:09 +08:00
cppla 47cf7a1818 持续获取io 2022-04-01 15:37:43 +08:00
cppla 5176cb0340 table control 2022-04-01 15:19:06 +08:00
cppla 1f47bcdb6f beta 2022-04-01 14:55:19 +08:00
cppla 7698ce0e4b 开源让编程更加美好 2022-03-31 19:53:17 +08:00
cppla 2d457c66ad push disk io for client-linux.py 2022-03-31 19:48:26 +08:00
cppla d8c8d8fd3f add todo 2022-03-24 17:24:01 +08:00
cppla d64beb7ba8 psutil add io total 2022-03-23 11:45:16 +08:00
cppla 30f9999fc6 rename 2022-03-23 11:13:18 +08:00
cpplaandGitHub c46d0c06cd Merge pull request #139 from cppla/revert-135-master
Revert "修改丢包率分隔符💻为 |"
2022-03-02 18:51:29 +08:00
cpplaandGitHub 589c8812de Revert "修改丢包率分隔符💻为 |" 2022-03-02 18:48:07 +08:00
cpplaandGitHub 110a2d97d8 Merge pull request #135 from jwstaceyOvO/master
修改丢包率分隔符💻为 |
2022-03-02 11:02:02 +08:00
windows11 0914175200 load tofixed 2 2022-02-24 19:31:35 +08:00
windows11 830ea094a3 some change css for view 2022-02-24 19:17:20 +08:00
jwstaceyOvOandGitHub 2f4611bdd8 修改丢包率分隔符💻为 |
💻太占空间,导致当丢包率位数较多时,无法正常显示全部数字
2022-02-24 12:33:31 +08:00
cpplaandGitHub 0b9e747cc9 Merge pull request #132 from jwstaceyOvO/master
ServerStatus一键安装脚本
2022-02-23 15:42:29 +08:00
cpplaandGitHub 22847aa590 Update status.sh
auto install bash.
2022-02-23 15:41:08 +08:00
jwstaceyOvOandGitHub e8482bf8ac Add files via upload 2022-02-22 17:39:14 +08:00
jwstaceyOvOandGitHub be95052d8f Update status-server.service 2022-02-22 17:38:41 +08:00
jwstaceyOvOandGitHub 4c8006d183 Add files via upload 2022-02-22 17:01:04 +08:00
jwstaceyOvOandGitHub f1559bfb55 Update status-server.service 2022-02-22 17:00:13 +08:00
jwstaceyOvOandGitHub cab0f8bc91 Update status.sh 2022-02-16 19:49:16 +08:00
jwstaceyOvOandGitHub 4e1e92e406 Update status.sh 2022-02-16 15:47:27 +08:00
jwstaceyOvOandGitHub fcb4b3cdfe Update status.sh 2022-02-15 18:47:44 +08:00
jwstaceyOvOandGitHub 260b7b3a21 Update status-server.service 2022-02-15 18:31:39 +08:00
jwstaceyOvOandGitHub 64c028f9ce Update status.sh 2022-02-14 16:57:08 +08:00
jwstaceyOvOandGitHub ec4218f1b0 Update status.sh 2022-02-14 16:41:56 +08:00
jwstaceyOvOandGitHub 00bc87c2c2 Add files via upload 2022-02-14 16:37:05 +08:00
jwstaceyOvOandGitHub f36b4e66d2 Delete status.sh 2022-02-14 16:32:02 +08:00
jwstaceyOvOandGitHub 3871c2c3d9 Update status.sh 2022-02-14 16:29:32 +08:00
jwstaceyOvOandGitHub 4a0b15b61d Update status-client.service 2022-02-14 15:37:58 +08:00
jwstaceyOvOandGitHub e4fdacb2b5 Update status-client.service 2022-02-14 15:34:54 +08:00
jwstaceyOvOandGitHub d1bdec5e30 Update status-client.service 2022-02-14 15:33:39 +08:00
jwstaceyOvOandGitHub d0f3507add Update status-client.service 2022-02-14 15:31:31 +08:00
jwstaceyOvOandGitHub a3dab0dc0f Update status-client.service 2022-02-14 15:30:08 +08:00
jwstaceyOvOandGitHub 050a6f78dc Update status-server.service 2022-02-14 13:23:25 +08:00
jwstaceyOvOandGitHub 0219d21ad5 Add files via upload 2022-02-14 12:39:10 +08:00
jwstaceyOvOandGitHub fc977367ba Add files via upload 2022-02-14 12:30:17 +08:00
jwstaceyOvOandGitHub 7d02f16fff Create status-client.service 2022-02-14 12:30:04 +08:00
oc 4792821e8e docker compose for X86 and ARM, good luck 2022-01-27 17:49:35 +08:00
oc 6c926209bd docker compose support for X86 or ARM64 2022-01-27 17:10:07 +08:00
windows11 e274b09c0f 修复增删节点月流量错误适配,置0, todo:等待全适配 2022-01-21 15:48:49 +08:00
windows11 5078381215 copy dark theme color from jk.sunpma.com 2022-01-19 19:29:15 +08:00
oc 4e175d0a81 change max client 128 to 256 2022-01-07 18:45:17 +08:00
cpplaandGitHub 0dc82096f5 update 1.0.7 readme
update 1.0.7 readme
2021-12-21 19:37:44 +08:00
oc 43c1c1279f 主题默认跟随系统,直至你手动选择了模式 2021-12-21 17:51:12 +08:00
oc ef0229b2d7 Unified style 2021-12-20 15:09:19 +08:00
aws 77443663d6 command 2021-12-17 11:21:40 +08:00
cpplaandGitHub 4c2bcfbe20 Update README.md 2021-12-17 11:07:57 +08:00
aws 8e06385152 docker install and docker-compose install 2021-12-17 11:02:07 +08:00
aws cc5aec11ba update README 2021-12-17 10:51:37 +08:00
cpplaandGitHub 7bf4a22457 Update bot-telegram.py 2021-12-16 19:25:15 +08:00
root f68d3a8963 telegram for serverstatus 2021-12-16 19:00:48 +08:00
windows11 0834ff138d emoji to computer 2021-11-24 11:17:27 +08:00
windows11 cbbd71d037 update 2021-11-12 15:49:25 +08:00
cpplaandGitHub 614c6d323c Update README.md 2021-11-12 15:27:09 +08:00
windows11 e6d9edee2d Merge branch 'dev' 2021-11-12 15:09:21 +08:00
windows11 e00a374f03 update readme 2021-11-12 15:07:11 +08:00
windows11 feef07a6d8 Merge branch 'dev' 2021-11-11 18:06:20 +08:00
windows11 a83a2ff5a3 update 2021-11-11 18:05:51 +08:00
windows11 8925de8fc9 Merge branch 'dev' 2021-11-10 18:28:40 +08:00
windows11 700287125a default light 2021-11-10 18:28:20 +08:00
windows11 ec86ae7ed8 Merge branch 'dev' 2021-11-03 18:47:53 +08:00
windows11 0254971ec7 css 2021-11-03 18:47:32 +08:00
windows11 4966fe7daa Merge branch 'dev' 2021-11-03 18:02:49 +08:00
windows11 e5d22ad2db css 2021-11-03 18:02:31 +08:00
windows11 b348032855 Merge branch 'dev' 2021-11-03 15:00:59 +08:00
windows11 3cb0ea4777 css防止挤兑 2021-11-03 14:48:12 +08:00
windows11 f6cfdc9e6b fix 2021-11-03 14:45:00 +08:00
windows11 47a9ba60cb fix a bug 2021-11-03 13:39:55 +08:00
windows11 37dc23ee45 Merge branch 'dev' 2021-11-03 11:37:11 +08:00
windows11 ff51c48c93 update css 2021-11-03 11:36:50 +08:00
windows11 3edefd45ce Merge branch 'dev' 2021-11-03 11:34:56 +08:00
windows11 bde340a7e8 720px with 10px font 2021-11-03 11:34:27 +08:00
windows11 5323b28304 Merge branch 'dev' 2021-11-03 11:15:09 +08:00
windows11 28f61460c3 version beta 1.0.5 2021-11-03 11:13:57 +08:00
windows11 fd382957ca some change for css 2021-11-02 20:00:36 +08:00
windows11 c0ea66171a reduce month traffic diff 2021-11-02 16:35:42 +08:00
windows11 ab0a19b244 test bug for negative monthtraffic 2021-11-02 15:56:33 +08:00
cpplaandGitHub d6878854b1 Update README.md 2021-10-22 11:53:39 +08:00
windows11 3aac3ccd0d 避免歧义 2021-10-22 11:51:43 +08:00
cpplaandGitHub 87824ce25a update to beta 1.0.4
update to beta 1.0.4
2021-10-18 17:05:36 +08:00
windows11 9d2a2e1c94 fix some bug for month traffic 2021-10-18 17:04:04 +08:00
windows11 6ee766df87 not remove json file 2021-10-18 15:42:23 +08:00
cpplaandGitHub 5afdb61e88 Update README.md 2021-10-14 20:41:24 +08:00
cpplaandGitHub 9592ba7b1d Update README.md 2021-10-14 20:30:25 +08:00
windows11 ecf19ae3c0 change length for css 2021-10-14 20:28:43 +08:00
windows11 c512ede1f7 add length for network 2021-10-14 19:54:16 +08:00
windows11 45a0723bea testing 2021-10-14 19:33:17 +08:00
windows11 b8a717a374 add info for change 2021-10-14 11:42:15 +08:00
windows11 3f0790db81 fix bug , no testing 2021-10-13 19:33:23 +08:00
windows11 8acb6ec0df 提交一个bug 2021-10-13 19:08:58 +08:00
windows11 7486919dbe 修复重启服务端,月流量统计丢失的问题 2021-10-13 18:45:34 +08:00
cpplaandGitHub 6188192951 Update README.md 2021-10-10 20:02:06 +08:00
cpplaandGitHub e50ca41061 Bug说明,todo说明
Bug说明,todo说明
2021-10-10 19:01:11 +08:00
windows11 dc06c90e94 update kb 2 to 1 2021-10-10 14:14:04 +08:00
cpplaandGitHub dcb2246d94 Update README.md 2021-10-10 12:46:49 +08:00
cpplaandGitHub 20d938401c Merge pull request #110 from cppla/dev
css show month_traffic
2021-10-10 11:40:05 +08:00
windows11 87895de2cf change css show month_traffic 2021-10-10 11:33:00 +08:00
cpplaandGitHub 427303d9ae Update README.md 2021-10-09 23:03:30 +08:00
cpplaandGitHub e2f4714641 1.0.2 版本,beta
1.0.2 版本,beta
2021-10-09 22:46:07 +08:00
windows11 34236b4c5f update readme 2021-10-09 22:22:24 +08:00
windows11 474e7addc9 update 2021-10-09 22:13:53 +08:00
windows11 4c042a3fc7 兼容旧版本,删除ip_status旧版本显示MH370 2021-10-09 22:13:13 +08:00
windows11 a0842a020b version beta 1.0.2 2021-10-09 22:07:07 +08:00
windows11 c98dc92256 去除ip_status检测,和丢包率检测重复。默认为python3, python2已经 end of life 2021-10-09 21:28:05 +08:00
windows11 60264204cb test web ui 0.7 2021-10-09 20:06:26 +08:00
windows11 e145feb1cd test web ui 0.6 2021-10-09 19:36:53 +08:00
windows11 2359890ca0 test web ui 0.5 2021-10-09 19:24:18 +08:00
windows11 430bf2d2e0 test web ui 0.4 2021-10-09 17:54:04 +08:00
windows11 cfd472357f test web ui 0.3 2021-10-09 17:35:39 +08:00
windows11 895206baa4 test web ui 0.2 2021-10-09 16:59:50 +08:00
windows11 1828a476f7 test web ui 0.1 2021-10-09 16:37:35 +08:00
windows98 7cf9d969bc dev: add month traffic , no completed 2021-10-09 14:27:31 +08:00
root 55ee9ca6fb emoji location 2021-08-09 13:00:07 +08:00
windows98 9601ef1655 update ssview.py for view ServerStatus in console when attacked 2021-07-14 12:02:35 +08:00
cpplaandGitHub a77f94c8d6 Merge pull request #103 from paulzzh/patch-1
fix #102
2021-07-13 21:15:05 +08:00
paulzzhandGitHub ab65e0fc26 fix #102
修正依赖
2021-07-13 18:42:06 +08:00
46 changed files with 51751 additions and 1432 deletions
+11
View File
@@ -0,0 +1,11 @@
.git
.github
.idea
*.sublime-workspace
server/obj
server/sergate
web/json/stats.json
web/json/stats.json~
*.bak-*
__pycache__
*.pyc
+33
View File
@@ -0,0 +1,33 @@
name: CI
on:
push:
pull_request:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install build dependencies
run: sudo apt-get update && sudo apt-get install -y gcc g++ make libcurl4-openssl-dev python3
- name: Build server
run: make -C server -j2
- name: Check scripts
run: |
python3 -m py_compile server/manage_api.py clients/client-linux.py clients/client-psutil.py plugin/bot-telegram.py
sh -n server/entrypoint-server.sh clients/entrypoint.sh status.sh
node --check web/js/app.js
- name: Validate compose files
run: |
docker compose -f docker-compose-server.yml config
docker compose -f docker-compose-client.yml config
- name: Build Docker images
run: |
docker build -f Dockerfile.server -t serverstatus-server:test .
docker build -f Dockerfile.client -t serverstatus-client:test .
+29
View File
@@ -0,0 +1,29 @@
FROM alpine:3.13
WORKDIR /app
# Runtime deps for client-linux.py and client-psutil.py
RUN apk add --no-cache python3 py3-psutil iproute2 procps
COPY clients/client-linux.py /app/client-linux.py
COPY clients/client-psutil.py /app/client-psutil.py
COPY clients/entrypoint.sh /app/entrypoint.sh
# Default client and defaults (can be overridden by docker env)
ENV SERVER=127.0.0.1 \
USER=s01 \
PORT=35601 \
PASSWORD=USER_DEFAULT_PASSWORD \
INTERVAL=1 \
PROBEPORT=80 \
PROBE_PROTOCOL_PREFER=ipv4 \
PING_PACKET_HISTORY_LEN=100 \
CU=cu.tz.cloudcpp.com \
CT=ct.tz.cloudcpp.com \
CM=cm.tz.cloudcpp.com \
CLIENT=psutil
RUN chmod +x /app/entrypoint.sh
ENTRYPOINT ["/app/entrypoint.sh"]
CMD []
+42
View File
@@ -0,0 +1,42 @@
FROM python:3.12-slim-bookworm AS builder
LABEL maintainer="cppla <https://cpp.la>"
RUN sed -i 's|http://deb.debian.org|https://deb.debian.org|g' /etc/apt/sources.list.d/debian.sources \
&& apt-get -o Acquire::Retries=3 update -y \
&& apt-get install -y --no-install-recommends gcc g++ make libcurl4-openssl-dev ca-certificates \
&& rm -rf /var/lib/apt/lists/*
COPY server/ /server/
WORKDIR /server
RUN make -j && strip /server/sergate
FROM debian:bookworm-slim
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
RUN sed -i 's|http://deb.debian.org|https://deb.debian.org|g' /etc/apt/sources.list.d/debian.sources \
&& apt-get -o Acquire::Retries=3 update -y \
&& apt-get install -y --no-install-recommends nginx-light python3 openssl libcurl4 libstdc++6 tzdata \
&& rm -rf /var/lib/apt/lists/* \
&& mkdir -p /ServerStatus/server/ \
&& ln -sf /dev/null /var/log/nginx/access.log \
&& ln -sf /dev/null /var/log/nginx/error.log \
&& rm -f /etc/nginx/sites-enabled/default
COPY --from=builder /server/sergate /ServerStatus/server/sergate
COPY server/config.json /ServerStatus/server/config.json
COPY server/manage_api.py /ServerStatus/server/manage_api.py
COPY server/entrypoint-server.sh /ServerStatus/server/entrypoint-server.sh
COPY web /usr/share/nginx/html/
COPY server/nginx-serverstatus.conf /etc/nginx/conf.d/default.conf
RUN chmod +x /ServerStatus/server/entrypoint-server.sh
ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
EXPOSE 80 35601
HEALTHCHECK --interval=10s --timeout=3s --retries=3 CMD python3 -c "import os,urllib.request; pid=int(open('/tmp/serverstatus-sergate.pid').read().strip()); os.kill(pid,0); urllib.request.urlopen('http://127.0.0.1/',timeout=2).read(1)"
CMD ["/ServerStatus/server/entrypoint-server.sh"]
+304 -103
View File
@@ -1,140 +1,341 @@
# ServerStatus中文版 # ServerStatus 中文版
* ServerStatus中文版是一个酷炫高逼格的云探针云监控、服务器云监控、多服务器探针~ ServerStatus 是一个轻量的服务器探针云监控面板,支持多节点在线状态、资源占用、三网延迟、服务监测、SSL 证书检查、Watchdog 告警和 Web 配置管理
* 在线演示:https://tz.cloudcpp.com
[![Python Support](https://img.shields.io/badge/python-2.7%2B%20-blue.svg)](https://github.com/cppla/ServerStatus) 在线演示:https://tz.cloudcpp.com
[![Python Support](https://img.shields.io/badge/python-3.6%2B%20-blue.svg)](https://github.com/cppla/ServerStatus)
[![C++ Compiler](http://img.shields.io/badge/C++-GNU-blue.svg?style=flat&logo=cplusplus)](https://github.com/cppla/ServerStatus) [![C++ Compiler](http://img.shields.io/badge/C++-GNU-blue.svg?style=flat&logo=cplusplus)](https://github.com/cppla/ServerStatus)
[![License](https://img.shields.io/badge/license-MIT-4EB1BA.svg?style=flat-square)](https://github.com/cppla/ServerStatus) [![License](https://img.shields.io/badge/license-MIT-4EB1BA.svg?style=flat-square)](https://github.com/cppla/ServerStatus)
[![Version](https://img.shields.io/badge/Version-Build%201.1.9-red)](https://github.com/cppla/ServerStatus)
![Latest Version](http://dl.cpp.la/Archive/serverstatus.png) ![Latest Host Version](https://dl.cpp.la/Archive/serverstatus_1_1_9.webp)
# 目录介绍: `Watchdog 的 `interval` 是最小通知间隔,用于避免频繁报警,并不是探测间隔。`rule` 使用 Exprtk 表达式,当前窄字符解析对中文等 Unicode 字符不友好,规则中建议使用英文、数字和字段名。`
* autodeploy 自动部署.
* clients 客户端文件
* server 服务端文件
* web 网站文件      
# 自动部署: ## 一、服务端
【服务端】:
```bash ```bash
`x86_64`: docker pull cppla/serverstatus:latest # Docker Compose,本地构建加:--build
`arm64`: docker pull cppla/serverstatus:arm ADMIN_TOKEN='your-strong-token' docker compose -f docker-compose-server.yml up -d
wget https://raw.githubusercontent.com/cppla/ServerStatus/master/autodeploy/config.json
docker run -d --restart=always --name=serverstatus -v {$path}/config.json:/ServerStatus/server/config.json -p {$port}:80 -p {$port}:35601 cppla/serverstatus:latest
eg:
docker run -d --restart=always --name=serverstatus -v ~/config.json:/ServerStatus/server/config.json -p 80:80 -p 35601:35601 cppla/serverstatus:latest
``` ```
【客户端】:
```bash ```bash
wget --no-check-certificate -qO client-linux.py 'https://raw.githubusercontent.com/cppla/ServerStatus/master/clients/client-linux.py' && nohup python client-linux.py SERVER={$SERVER} USER={$USER} PASSWORD={$PASSWORD} >/dev/null 2>&1 & # Docker Run
wget -qO ~/serverstatus-config.json \
--header='Accept: application/vnd.github.raw' \
'https://api.github.com/repos/cppla/ServerStatus/contents/server/config.json?ref=master'
mkdir -p ~/serverstatus-monthtraffic
eg: docker run -d --restart=always --name=serverstatus-server \
wget --no-check-certificate -qO client-linux.py 'https://raw.githubusercontent.com/cppla/ServerStatus/master/clients/client-linux.py' && nohup python client-linux.py SERVER=45.79.67.132 USER=s04 >/dev/null 2>&1 & -e ADMIN_TOKEN='your-strong-token' \
-v ~/serverstatus-config.json:/ServerStatus/server/config.json \
-v ~/serverstatus-monthtraffic:/usr/share/nginx/html/json \
-p 8080:80 -p 35601:35601 \
cppla/serverstatus:server
``` ```
# 手动安装教程: 启动后访问:
【克隆代码】: - WebUIhttp://127.0.0.1:8080/
``` - HTTP API 自检:http://127.0.0.1:8080/api/health
git clone https://github.com/cppla/ServerStatus.git - HTTP API 文档:http://127.0.0.1:8080/api/schema
- HTTP 默认端口映射为`8080:80`,客户端连接端口为`35601``ADMIN_TOKEN` 可选:不设置时Web仅可查看监控数据,但web配置页无法修改。
## 二、客户端
```bash
# Docker Compose,本地构建加:--build
SERVER=127.0.0.1 USER=s01 docker compose -f docker-compose-client.yml up -d --force-recreate
``` ```
【服务端配置】(服务端程序在ServerStatus/web下): ```bash
# Docker Run
一、生成服务端程序 docker run -d --restart=always --name=serverstatus-client \
--network=host --pid=host \
-e SERVER=127.0.0.1 \
-e USER=s01 \
cppla/serverstatus:client
``` ```
```bash
# Shell Run
wget -qO client-linux.py --header='Accept: application/vnd.github.raw' 'https://api.github.com/repos/cppla/ServerStatus/contents/clients/client-linux.py?ref=master' && (nohup python3 client-linux.py SERVER=127.0.0.1 USER=s01 >/dev/null 2>&1 &)
```
客户端环境变量和注意事项:
`USER` 是常见的宿主机环境变量名。如果没有显式传递,Compose 可能会把系统里的 `$USER` 解析成本机用户名。推荐优先级:1. 运行命令显式传递 `USER=...`2. 用户修改 `docker-compose-client.yml` 里的 `USER` 默认值
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `SERVER` | `127.0.0.1` | 服务端地址 |
| `USER` | `s01` | 客户端用户名,必须匹配服务端配置 |
| `PORT` | `35601` | 服务端 sergate 端口 |
| `PASSWORD` | `USER_DEFAULT_PASSWORD` | 客户端密码,必须匹配服务端配置 |
| `INTERVAL` | `1` | 上报间隔 |
| `PROBEPORT` | `80` | 探测端口 |
| `PROBE_PROTOCOL_PREFER` | `ipv4` | 探测协议偏好 |
| `PING_PACKET_HISTORY_LEN` | `100` | Ping 历史长度 |
| `CU` | `cu.tz.cloudcpp.com` | 联通探测地址 |
| `CT` | `ct.tz.cloudcpp.com` | 电信探测地址 |
| `CM` | `cm.tz.cloudcpp.com` | 移动探测地址 |
| `CLIENT` | `psutil` | 客户端实现,可选 `psutil``linux` |
## HTTP 管理 API
Docker 服务端镜像已内置 HTTP API,并通过 nginx 暴露在 Web 端口下。源码手动运行时需要单独启动 `server/manage_api.py`;如果要让 WebUI 的「配置」页可用,还需要把 `/api/` 反代到 `manage_api.py`。认证方式:
```bash
Authorization: Bearer your-strong-token
```
无需认证的接口:
```bash
curl http://127.0.0.1:8080/api/health
curl http://127.0.0.1:8080/api/schema
```
读取完整配置:
```bash
curl -H 'Authorization: Bearer your-strong-token' \
http://127.0.0.1:8080/api/config
```
节点 CRUD
```bash
curl -X POST http://127.0.0.1:8080/api/servers \
-H 'Authorization: Bearer your-strong-token' \
-H 'Content-Type: application/json' \
-d '{"username":"s05","name":"node5","type":"kvm","host":"host5","location":"SG","password":"USER_DEFAULT_PASSWORD","monthstart":1}'
curl -X PUT http://127.0.0.1:8080/api/servers/s05 \
-H 'Authorization: Bearer your-strong-token' \
-H 'Content-Type: application/json' \
-d '{"username":"s05","name":"node5-new","type":"kvm","host":"host5","location":"SG","password":"USER_DEFAULT_PASSWORD","monthstart":1}'
curl -X DELETE -H 'Authorization: Bearer your-strong-token' \
http://127.0.0.1:8080/api/servers/s05
```
`monitors``sslcerts``watchdog` 也支持细粒度 CRUD。更新和删除可以用数字 `index`;如果 `name` 唯一,也可以用 URL 编码后的 `name`
```bash
curl -X POST http://127.0.0.1:8080/api/monitors \
-H 'Authorization: Bearer your-strong-token' \
-H 'Content-Type: application/json' \
-d '{"name":"demo","host":"https://example.com","type":"https","interval":600}'
curl -X PUT http://127.0.0.1:8080/api/sslcerts/0 \
-H 'Authorization: Bearer your-strong-token' \
-H 'Content-Type: application/json' \
-d '{"name":"example","domain":"https://example.com","port":443,"interval":7200,"callback":"https://yourSMSurl"}'
curl -X DELETE -H 'Authorization: Bearer your-strong-token' \
http://127.0.0.1:8080/api/watchdog/0
```
重载配置或重启 `sergate`
```bash
curl -X POST -H 'Authorization: Bearer your-strong-token' \
http://127.0.0.1:8080/api/reload
curl -X POST -H 'Authorization: Bearer your-strong-token' \
http://127.0.0.1:8080/api/restart
```
完整端点以 `/api/schema` 输出为准。
## 配置文件
配置文件默认路径:
- Docker 服务端:`/ServerStatus/server/config.json`
- 源码运行:`server/config.json`,或通过 `--config` 指定
基础示例:
```json
{
"servers": [
{
"username": "s01",
"name": "node1",
"type": "kvm",
"host": "host1",
"location": "CN",
"password": "USER_DEFAULT_PASSWORD",
"monthstart": 1
}
],
"monitors": [
{
"name": "example",
"host": "https://example.com",
"interval": 600,
"type": "https"
}
],
"sslcerts": [
{
"name": "example",
"domain": "https://example.com",
"port": 443,
"interval": 7200,
"callback": "https://yourSMSurl"
}
],
"watchdog": [
{
"name": "offline warning",
"rule": "online4=0&online6=0",
"interval": 600,
"callback": "https://yourSMSurl"
},
{
"name": "cpu high warning",
"rule": "cpu>90&load_1>5&username!='s01'",
"interval": 600,
"callback": "https://yourSMSurl"
}
]
}
```
常见 Watchdog 回调:
```text
Telegram: https://api.telegram.org/bot你的密钥/sendMessage?parse_mode=HTML&disable_web_page_preview=true&chat_id=你的标识&text=
Server酱: https://sctapi.ftqq.com/你的密钥.send?title=ServerStatus&desp=
PushDeer: https://api2.pushdeer.com/message/push?pushkey=你的密钥&text=
HttpBasicAuth: https://用户名:密码@你的域名/api/push?message=
```
## 源码编译和运行
服务端依赖:
```bash
# Debian/Ubuntu
apt-get -y install gcc g++ make libcurl4-openssl-dev python3 nginx openssl
# CentOS/RedHat
yum -y install gcc gcc-c++ make libcurl-devel python3 nginx openssl
```
编译并运行 `sergate`
```bash
cd ServerStatus/server cd ServerStatus/server
make make
./sergate mkdir -p ../web/json
``` ./sergate --config=config.json --web-dir=../web &
如果没错误提示,OK,ctrl+c关闭;如果有错误提示,检查35601端口是否被占用 echo $! > /tmp/serverstatus-sergate.pid
二、修改配置文件
修改config.json文件,注意username, password的值需要和客户端对应一致    
```
{"servers":
[
{
"username": "s01",
"name": "Mainserver 1",
"type": "Dedicated Server",
"host": "GenericServerHost123",
"location": "Austria",
"password": "some-hard-to-guess-copy-paste-password"
},
]
}
``` ```
三、拷贝ServerStatus/status到你的网站目录 如果只需要 HTTP API,可以再启动 `manage_api.py`
例如:
``` ```bash
sudo cp -r ServerStatus/web/* /home/wwwroot/default cd ServerStatus/server
ADMIN_TOKEN='your-strong-token' \
CONFIG_PATH="$PWD/config.json" \
SERGATE_PID_FILE=/tmp/serverstatus-sergate.pid \
ADMIN_API_BIND=127.0.0.1 \
ADMIN_API_PORT=35602 \
python3 manage_api.py
``` ```
四、运行服务端: 源码方式直连 API
web-dir参数为上一步设置的网站根目录,务必修改成自己网站的路径
``` ```bash
./sergate --config=config.json --web-dir=/home/wwwroot/default curl http://127.0.0.1:35602/api/health
curl -H 'Authorization: Bearer your-strong-token' \
http://127.0.0.1:35602/api/config
``` ```
【客户端配置】(客户端程序在ServerStatus/clients下): 如果要通过 WebUI 使用「配置」页,需要 nginx 同时提供静态文件并反代 `/api/`。示例配置:
客户端有两个版本,client-linux为普通linuxclient-psutil为跨平台版,普通版不成功,换成跨平台版即可。
一、client-linux版配置: ```nginx
1、vim client-linux.py, 修改SERVER地址,username帐号, password密码 server {
2、python client-linux.py 运行即可。 listen 8080;
server_name _;
二、client-psutil版配置: root /path/to/ServerStatus/web;
1、安装psutil跨平台依赖库 index index.html;
2、vim client-psutil.py, 修改SERVER地址,username帐号, password密码
3、python client-psutil.py 运行即可。 location / {
``` try_files $uri $uri/ =404;
### for Centos }
sudo yum -y install epel-release
sudo yum -y install python-pip location /json/ {
sudo yum clean all add_header Cache-Control "no-store";
sudo yum -y install gcc try_files $uri =404;
sudo yum -y install python-devel }
sudo pip install psutil
### for Ubuntu/Debian: location /api/ {
sudo root proxy_pass http://127.0.0.1:35602;
apt-get -y install python-setuptools python-dev build-essential proxy_http_version 1.1;
apt-get -y install python-pip proxy_set_header Host $host;
pip install psutil proxy_set_header X-Real-IP $remote_addr;
### for Windows: proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
打开网址:https://pypi.python.org/pypi?:action=display&name=psutil#downloads proxy_set_header X-Forwarded-Proto $scheme;
下载psutil for windows程序包 proxy_read_timeout 30s;
安装即可 }
}
``` ```
打开云探针页面,就可以正常的监控。接下来把服务器和客户端脚本自行加入开机启动,或者进程守护,或以后台方式运行即可!例如: nohup python client-linux.py & 启动失败时优先检查端口占用:`35601``sergate` 客户端上报端口,`35602` 是源码方式的管理 API 端口,`8080` 是上面示例的 Web 端口。源码手动运行没有 Docker 入口脚本守护;调用 `/api/restart` 会向 `sergate` 发送 `SIGTERM`,需要你用 systemd、supervisor 或 shell 循环自行拉起。
### 如何快速跟随系统启动呢?其实好多人都搞复杂化了 ## 客户端源码运行
1、chmod 755 /root/client-linux.py
2、vim /etc/crontab,尾部追加 `client-linux.py`
```diff
@reboot root /root/client-linux.py SERVER=$server USER=$user ```bash
wget -qO client-linux.py \
--header='Accept: application/vnd.github.raw' \
'https://api.github.com/repos/cppla/ServerStatus/contents/clients/client-linux.py?ref=master'
nohup python3 client-linux.py SERVER=127.0.0.1 USER=s01 PASSWORD=USER_DEFAULT_PASSWORD \
>/dev/null 2>&1 &
``` ```
# 为什么会有ServerStatus中文版 `client-psutil.py`
* 有些功能确实没用 ```bash
* 原版本部署,英文说明复杂 # Debian/Ubuntu
* 不符合中文版的习惯 apt -y install python3-psutil
* 没有一次又一次的轮子,哪来如此优秀的云探针
# 相关开源项目,感谢: # CentOS/RedHat
yum -y install python3-pip gcc python3-devel
pip3 install psutil
* ServerStatushttps://github.com/BotoX/ServerStatus python3 clients/client-psutil.py SERVER=127.0.0.1 USER=s01
* mojeda: https://github.com/mojeda ```
* mojeda's ServerStatus: https://github.com/mojeda/ServerStatus
* BlueVM's project: http://www.lowendtalk.com/discussion/comment/169690#Comment_169690 后台运行与开机启动:
```bash
nohup python3 client-linux.py SERVER=127.0.0.1 USER=s01 &
# crontab -e
@reboot /usr/bin/python3 /path/to/client-linux.py SERVER=127.0.0.1 USER=s01
```
## 本地构建镜像
```bash
docker build -f Dockerfile.server -t cppla/serverstatus:server .
docker build -f Dockerfile.client -t cppla/serverstatus:client .
```
## Make Better
* BotoXhttps://github.com/BotoX/ServerStatus
* mojedahttps://github.com/mojeda
* mojeda's ServerStatushttps://github.com/mojeda/ServerStatus
* BlueVM's projecthttp://www.lowendtalk.com/discussion/comment/169690#Comment_169690
-24
View File
@@ -1,24 +0,0 @@
FROM debian:latest as builder
MAINTAINER cppla https://cpp.la
RUN apt-get update
RUN apt-get -y install gcc g++ make git
RUN git clone https://github.com/cppla/ServerStatus
WORKDIR /ServerStatus/server
RUN make
RUN pwd && ls -a
# glibc env run
FROM nginx:latest
RUN mkdir -p /ServerStatus/server/
COPY --from=builder /ServerStatus/server /ServerStatus/server/
COPY --from=builder /ServerStatus/web /usr/share/nginx/html/
EXPOSE 80 35601
CMD nohup sh -c '/etc/init.d/nginx start && /ServerStatus/server/sergate --config=/ServerStatus/server/config.json --web-dir=/usr/share/nginx/html'
-37
View File
@@ -1,37 +0,0 @@
{"servers":
[
{
"username": "s01",
"name": "node1",
"type": "xen",
"host": "host1",
"location": "cn",
"password": "USER_DEFAULT_PASSWORD"
},
{
"username": "s02",
"name": "node2",
"type": "vmware",
"host": "host2",
"location": "jp",
"password": "USER_DEFAULT_PASSWORD"
},
{
"disabled": true,
"username": "s03",
"name": "node3",
"type": "Nothing",
"host": "host3",
"location": "fr",
"password": "USER_DEFAULT_PASSWORD"
},
{
"username": "s04",
"name": "ssss",
"type": "ssss",
"host": "ssss",
"location": "ssss",
"password": "USER_DEFAULT_PASSWORD"
}
]
}
-15
View File
@@ -1,15 +0,0 @@
服务端:
docker build -f Dockerfile -t sss .
docker run -d --restart=always --name=sss -v {$path}/config.json:/ServerStatus/server/config.json -p {$port}:80 -p {$port}:35601 sss
客户端:
wget --no-check-certificate -qO client-linux.py 'https://raw.githubusercontent.com/cppla/ServerStatus/master/clients/client-linux.py' && nohup python client-linux.py SERVER={$SERVER} USER={$USER} PASSWORD={$PASSWORD} >/dev/null 2>&1 &
附: docker安装
curl -sSL https://get.docker.com/ | sh
+355 -57
View File
@@ -1,25 +1,23 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
# Update by : https://github.com/cppla/ServerStatus # Update by : https://github.com/cppla/ServerStatus, Update date: 20250902
# 支持Python版本:2.7 to 3.7 # 版本:1.1.0, 支持Python版本:3.6+
# 支持操作系统: Linux, OSX, FreeBSD, OpenBSD and NetBSD, both 32-bit and 64-bit architectures # 支持操作系统: Linux, OSX, FreeBSD, OpenBSD and NetBSD, both 32-bit and 64-bit architectures
# 时间: 20200407
# 说明: 默认情况下修改server和user就可以了。丢包率监测方向可以自定义,例如:CU = "www.facebook.com"。 # 说明: 默认情况下修改server和user就可以了。丢包率监测方向可以自定义,例如:CU = "www.facebook.com"。
SERVER = "127.0.0.1" SERVER = ""
USER = "s01" USER = ""
PORT = 35601
PASSWORD = "USER_DEFAULT_PASSWORD" PASSWORD = "USER_DEFAULT_PASSWORD"
INTERVAL = 1 PORT = 35601
PROBEPORT = 80
PROBE_PROTOCOL_PREFER = "ipv4" # ipv4, ipv6
PING_PACKET_HISTORY_LEN = 100
CU = "cu.tz.cloudcpp.com" CU = "cu.tz.cloudcpp.com"
CT = "ct.tz.cloudcpp.com" CT = "ct.tz.cloudcpp.com"
CM = "cm.tz.cloudcpp.com" CM = "cm.tz.cloudcpp.com"
PROBEPORT = 80
PROBE_PROTOCOL_PREFER = "ipv4" # ipv4, ipv6
PING_PACKET_HISTORY_LEN = 100
INTERVAL = 1
import socket import socket
import time import time
@@ -31,10 +29,36 @@ import json
import errno import errno
import subprocess import subprocess
import threading import threading
try: import platform
from queue import Queue # python3 from queue import Queue
except ImportError:
from Queue import Queue # python2 def _env_str(name, default):
value = os.getenv(name)
if value is None or value == "":
return default
return value
def _env_int(name, default):
value = os.getenv(name)
if value is None or value == "":
return default
try:
return int(value)
except ValueError:
return default
# Allow docker env overrides. 优先级:运行程序传递参数 > 用户修改的USER > Docker/系统
SERVER = _env_str("SERVER", SERVER) if SERVER == "" else SERVER
USER = _env_str("USER", USER) if USER == "" else USER
PASSWORD = _env_str("PASSWORD", PASSWORD)
PORT = _env_int("PORT", PORT)
INTERVAL = _env_int("INTERVAL", INTERVAL)
PROBEPORT = _env_int("PROBEPORT", PROBEPORT)
PROBE_PROTOCOL_PREFER = _env_str("PROBE_PROTOCOL_PREFER", PROBE_PROTOCOL_PREFER)
PING_PACKET_HISTORY_LEN = _env_int("PING_PACKET_HISTORY_LEN", PING_PACKET_HISTORY_LEN)
CU = _env_str("CU", CU)
CT = _env_str("CT", CT)
CM = _env_str("CM", CM)
def get_uptime(): def get_uptime():
with open('/proc/uptime', 'r') as f: with open('/proc/uptime', 'r') as f:
@@ -57,11 +81,34 @@ def get_memory():
return int(MemTotal), int(MemUsed), int(SwapTotal), int(SwapFree) return int(MemTotal), int(MemUsed), int(SwapTotal), int(SwapFree)
def get_hdd(): def get_hdd():
p = subprocess.check_output(['df', '-Tlm', '--total', '-t', 'ext4', '-t', 'ext3', '-t', 'ext2', '-t', 'reiserfs', '-t', 'jfs', '-t', 'ntfs', '-t', 'fat32', '-t', 'btrfs', '-t', 'fuseblk', '-t', 'zfs', '-t', 'simfs', '-t', 'xfs']).decode("Utf-8") valid_fs = {
total = p.splitlines()[-1] "ext4", "ext3", "ext2", "reiserfs", "jfs", "btrfs", "fuseblk",
used = total.split()[3] "zfs", "simfs", "ntfs", "fat32", "exfat", "xfs"
size = total.split()[2] }
return int(size), int(used) disks = {}
size = 0
used = 0
try:
with open("/proc/mounts", "r") as f:
for line in f:
parts = line.split()
if len(parts) < 3:
continue
device = parts[0]
mountpoint = parts[1]
fstype = parts[2].lower()
if fstype not in valid_fs or device in disks:
continue
disks[device] = mountpoint
for mountpoint in disks.values():
st = os.statvfs(mountpoint)
total_bytes = st.f_blocks * st.f_frsize
used_bytes = (st.f_blocks - st.f_bavail) * st.f_frsize
size += total_bytes
used += used_bytes
except Exception:
pass
return int(size / 1024 / 1024), int(used / 1024 / 1024)
def get_time(): def get_time():
with open("/proc/stat", "r") as f: with open("/proc/stat", "r") as f:
@@ -86,12 +133,79 @@ def get_cpu():
result = 100-(t[len(t)-1]*100.00/st) result = 100-(t[len(t)-1]*100.00/st)
return round(result, 1) return round(result, 1)
def get_cpu_cores():
try:
with open('/proc/stat') as f:
cores = sum(1 for line in f if re.match(r'^cpu\d+\s', line))
if cores > 0:
return cores
except Exception:
pass
return os.cpu_count() or 0
def normalize_cpu_model(value):
return re.sub(r'\s+', ' ', str(value or '')).strip()[:160]
def is_generic_cpu_model(value):
v = normalize_cpu_model(value).lower().replace('-', '').replace('_', '').replace(' ', '')
return v in ('', 'unknown', 'x8664', 'amd64', 'i386', 'i686', 'aarch64', 'arm64') or v.startswith('armv')
def get_lscpu_info():
result = {}
try:
output = subprocess.check_output(['lscpu'], stderr=subprocess.DEVNULL, timeout=2).decode(errors='ignore')
for line in output.splitlines():
if ':' not in line:
continue
key, value = line.split(':', 1)
key = key.strip().lower()
value = normalize_cpu_model(value)
if value and key not in result:
result[key] = value
except Exception:
pass
return result
def get_cpuinfo_values():
result = {}
try:
with open('/proc/cpuinfo') as f:
for line in f:
if ':' not in line:
continue
key, value = line.split(':', 1)
key = key.strip().lower()
value = normalize_cpu_model(value)
if value and key not in result:
result[key] = value
except Exception:
pass
return result
def get_cpu_model():
cpuinfo = get_cpuinfo_values()
lscpu = get_lscpu_info()
for value in (
cpuinfo.get('model name'),
lscpu.get('model name'),
cpuinfo.get('hardware'),
cpuinfo.get('processor'),
platform.processor(),
):
value = normalize_cpu_model(value)
if value and not value.isdigit() and not is_generic_cpu_model(value):
return value
vendor = normalize_cpu_model(lscpu.get('vendor id') or cpuinfo.get('vendor_id'))
if vendor:
return vendor
return normalize_cpu_model(lscpu.get('architecture') or platform.machine() or platform.processor())
def liuliang(): def liuliang():
NET_IN = 0 NET_IN = 0
NET_OUT = 0 NET_OUT = 0
with open('/proc/net/dev') as f: with open('/proc/net/dev') as f:
for line in f.readlines(): for line in f.readlines():
netinfo = re.findall('([^\s]+):[\s]{0,}(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)', line) netinfo = re.findall(r'([^\s]+):[\s]{0,}(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)', line)
if netinfo: if netinfo:
if netinfo[0][0] == 'lo' or 'tun' in netinfo[0][0] \ if netinfo[0][0] == 'lo' or 'tun' in netinfo[0][0] \
or 'docker' in netinfo[0][0] or 'veth' in netinfo[0][0] \ or 'docker' in netinfo[0][0] or 'veth' in netinfo[0][0] \
@@ -119,18 +233,6 @@ def tupd():
d = int(s[:-1])-2 d = int(s[:-1])-2
return t,u,p,d return t,u,p,d
def ip_status():
ip_check = 0
for i in [CU, CT, CM]:
try:
socket.create_connection((i, PROBEPORT), timeout=1).close()
except:
ip_check += 1
if ip_check >= 2:
return False
else:
return True
def get_network(ip_version): def get_network(ip_version):
if(ip_version == 4): if(ip_version == 4):
HOST = "ipv4.google.com" HOST = "ipv4.google.com"
@@ -160,22 +262,28 @@ netSpeed = {
'avgrx': 0, 'avgrx': 0,
'avgtx': 0 'avgtx': 0
} }
diskIO = {
'read': 0,
'write': 0
}
monitorServer = {}
def _ping_thread(host, mark, port): def _ping_thread(host, mark, port):
lostPacket = 0 lostPacket = 0
packet_queue = Queue(maxsize=PING_PACKET_HISTORY_LEN) packet_queue = Queue(maxsize=PING_PACKET_HISTORY_LEN)
IP = host while True:
if host.count(':') < 1: # if not plain ipv6 address, means ipv4 address or hostname # flush dns , every time.
try: IP = host
if PROBE_PROTOCOL_PREFER == 'ipv4': if host.count(':') < 1: # if not plain ipv6 address, means ipv4 address or hostname
IP = socket.getaddrinfo(host, None, socket.AF_INET)[0][4][0] try:
else: if PROBE_PROTOCOL_PREFER == 'ipv4':
IP = socket.getaddrinfo(host, None, socket.AF_INET6)[0][4][0] IP = socket.getaddrinfo(host, None, socket.AF_INET)[0][4][0]
except Exception: else:
IP = socket.getaddrinfo(host, None, socket.AF_INET6)[0][4][0]
except Exception:
pass pass
while True:
if packet_queue.full(): if packet_queue.full():
if packet_queue.get() == 0: if packet_queue.get() == 0:
lostPacket -= 1 lostPacket -= 1
@@ -223,7 +331,72 @@ def _net_speed():
netSpeed["avgtx"] = avgtx netSpeed["avgtx"] = avgtx
time.sleep(INTERVAL) time.sleep(INTERVAL)
def get_realtime_date(): def _disk_io():
'''
good luck for opensource! by: cpp.la
磁盘IO:因为IOPS原因,SSD和HDD、包括RAID卡,ZFS等阵列技术。IO对性能的影响还需要结合自身服务器情况来判断。
比如我这里是机械硬盘,大量做随机小文件读写,那么很低的读写也就能造成硬盘长时间的等待。
如果这里做连续性IO,那么普通机械硬盘写入到100Mb/s,那么也能造成硬盘长时间的等待。
磁盘读写有误差:4k8k https://stackoverflow.com/questions/34413926/psutil-vs-dd-monitoring-disk-i-o
:return:
'''
while True:
# pre pid snapshot
snapshot_first = {}
# next pid snapshot
snapshot_second = {}
# read count snapshot
snapshot_read = 0
# write count snapshot
snapshot_write = 0
# process snapshot
pid_snapshot = [str(i) for i in os.listdir("/proc") if i.isdigit() is True]
for pid in pid_snapshot:
try:
with open("/proc/{}/io".format(pid)) as f:
pid_io = {}
for line in f.readlines():
if "read_bytes" in line:
pid_io["read"] = int(line.split("read_bytes:")[-1].strip())
elif "write_bytes" in line and "cancelled_write_bytes" not in line:
pid_io["write"] = int(line.split("write_bytes:")[-1].strip())
pid_io["name"] = open("/proc/{}/comm".format(pid), "r").read().strip()
snapshot_first[pid] = pid_io
except:
if pid in snapshot_first:
snapshot_first.pop(pid)
time.sleep(INTERVAL)
for pid in pid_snapshot:
try:
with open("/proc/{}/io".format(pid)) as f:
pid_io = {}
for line in f.readlines():
if "read_bytes" in line:
pid_io["read"] = int(line.split("read_bytes:")[-1].strip())
elif "write_bytes" in line and "cancelled_write_bytes" not in line:
pid_io["write"] = int(line.split("write_bytes:")[-1].strip())
pid_io["name"] = open("/proc/{}/comm".format(pid), "r").read().strip()
snapshot_second[pid] = pid_io
except:
if pid in snapshot_first:
snapshot_first.pop(pid)
if pid in snapshot_second:
snapshot_second.pop(pid)
for k, v in snapshot_first.items():
if snapshot_first[k]["name"] == snapshot_second[k]["name"] and snapshot_first[k]["name"] != "bash":
snapshot_read += (snapshot_second[k]["read"] - snapshot_first[k]["read"])
snapshot_write += (snapshot_second[k]["write"] - snapshot_first[k]["write"])
diskIO["read"] = snapshot_read
diskIO["write"] = snapshot_write
def get_realtime_data():
'''
real time get system data
:return:
'''
t1 = threading.Thread( t1 = threading.Thread(
target=_ping_thread, target=_ping_thread,
kwargs={ kwargs={
@@ -251,14 +424,76 @@ def get_realtime_date():
t4 = threading.Thread( t4 = threading.Thread(
target=_net_speed, target=_net_speed,
) )
t1.setDaemon(True) t5 = threading.Thread(
t2.setDaemon(True) target=_disk_io,
t3.setDaemon(True) )
t4.setDaemon(True) for ti in [t1, t2, t3, t4, t5]:
t1.start() ti.daemon = True
t2.start() ti.start()
t3.start()
t4.start()
def _monitor_thread(name, host, interval, type):
while True:
if name not in monitorServer.keys():
break
try:
# 1) 解析目标 host 与端口
if type == 'http':
addr = str(host).replace('http://','')
addr = addr.split('/',1)[0]
port = 80
if ':' in addr and not addr.startswith('['):
a, p = addr.rsplit(':',1)
if p.isdigit():
addr, port = a, int(p)
elif type == 'https':
addr = str(host).replace('https://','')
addr = addr.split('/',1)[0]
port = 443
if ':' in addr and not addr.startswith('['):
a, p = addr.rsplit(':',1)
if p.isdigit():
addr, port = a, int(p)
elif type == 'tcp':
addr = str(host)
if addr.startswith('[') and ']' in addr:
a = addr[1:addr.index(']')]
rest = addr[addr.index(']')+1:]
if rest.startswith(':') and rest[1:].isdigit():
addr, port = a, int(rest[1:])
else:
raise Exception('bad tcp target')
else:
a, p = addr.rsplit(':',1)
addr, port = a, int(p)
else:
time.sleep(interval)
continue
# 2) 解析 IP(按偏好族)
IP = addr
if addr.count(':') < 1: # 非纯 IPv6
try:
if PROBE_PROTOCOL_PREFER == 'ipv4':
IP = socket.getaddrinfo(addr, None, socket.AF_INET)[0][4][0]
else:
IP = socket.getaddrinfo(addr, None, socket.AF_INET6)[0][4][0]
except Exception:
pass
# 3) 建连耗时(timeout=1s),ECONNREFUSED 也计入
try:
b = timeit.default_timer()
socket.create_connection((IP, port), timeout=1).close()
monitorServer[name]["latency"] = int((timeit.default_timer() - b) * 1000)
except socket.error as error:
if getattr(error, 'errno', None) == errno.ECONNREFUSED:
monitorServer[name]["latency"] = int((timeit.default_timer() - b) * 1000)
else:
monitorServer[name]["latency"] = 0
except Exception:
monitorServer[name]["latency"] = 0
time.sleep(interval)
def byte_str(object): def byte_str(object):
''' '''
@@ -286,7 +521,7 @@ if __name__ == '__main__':
elif 'INTERVAL' in argc: elif 'INTERVAL' in argc:
INTERVAL = int(argc.split('INTERVAL=')[-1]) INTERVAL = int(argc.split('INTERVAL=')[-1])
socket.setdefaulttimeout(30) socket.setdefaulttimeout(30)
get_realtime_date() get_realtime_data()
while True: while True:
try: try:
print("Connecting...") print("Connecting...")
@@ -306,6 +541,26 @@ if __name__ == '__main__':
if data.find("You are connecting via") < 0: if data.find("You are connecting via") < 0:
data = byte_str(s.recv(1024)) data = byte_str(s.recv(1024))
print(data) print(data)
monitorServer.clear()
for i in data.split('\n'):
if "monitor" in i and "type" in i and "{" in i and "}" in i:
jdata = json.loads(i[i.find("{"):i.find("}")+1])
monitorServer[jdata.get("name")] = {
"type": jdata.get("type"),
"host": jdata.get("host"),
"latency": 0
}
t = threading.Thread(
target=_monitor_thread,
kwargs={
'name': jdata.get("name"),
'host': jdata.get("host"),
'interval': jdata.get("interval"),
'type': jdata.get("type")
}
)
t.daemon = True
t.start()
timer = 0 timer = 0
check_ip = 0 check_ip = 0
@@ -317,6 +572,8 @@ if __name__ == '__main__':
print(data) print(data)
raise socket.error raise socket.error
CPUCores = get_cpu_cores()
CPUModel = get_cpu_model()
while True: while True:
CPU = get_cpu() CPU = get_cpu()
NET_IN, NET_OUT = liuliang() NET_IN, NET_OUT = liuliang()
@@ -324,8 +581,6 @@ if __name__ == '__main__':
Load_1, Load_5, Load_15 = os.getloadavg() Load_1, Load_5, Load_15 = os.getloadavg()
MemoryTotal, MemoryUsed, SwapTotal, SwapFree = get_memory() MemoryTotal, MemoryUsed, SwapTotal, SwapFree = get_memory()
HDDTotal, HDDUsed = get_hdd() HDDTotal, HDDUsed = get_hdd()
IP_STATUS = ip_status()
array = {} array = {}
if not timer: if not timer:
array['online' + str(check_ip)] = get_network(check_ip) array['online' + str(check_ip)] = get_network(check_ip)
@@ -344,11 +599,12 @@ if __name__ == '__main__':
array['hdd_total'] = HDDTotal array['hdd_total'] = HDDTotal
array['hdd_used'] = HDDUsed array['hdd_used'] = HDDUsed
array['cpu'] = CPU array['cpu'] = CPU
array['cpu_cores'] = CPUCores
array['cpu_model'] = CPUModel
array['network_rx'] = netSpeed.get("netrx") array['network_rx'] = netSpeed.get("netrx")
array['network_tx'] = netSpeed.get("nettx") array['network_tx'] = netSpeed.get("nettx")
array['network_in'] = NET_IN array['network_in'] = NET_IN
array['network_out'] = NET_OUT array['network_out'] = NET_OUT
array['ip_status'] = IP_STATUS
array['ping_10010'] = lostRate.get('10010') * 100 array['ping_10010'] = lostRate.get('10010') * 100
array['ping_189'] = lostRate.get('189') * 100 array['ping_189'] = lostRate.get('189') * 100
array['ping_10086'] = lostRate.get('10086') * 100 array['ping_10086'] = lostRate.get('10086') * 100
@@ -356,16 +612,58 @@ if __name__ == '__main__':
array['time_189'] = pingTime.get('189') array['time_189'] = pingTime.get('189')
array['time_10086'] = pingTime.get('10086') array['time_10086'] = pingTime.get('10086')
array['tcp'], array['udp'], array['process'], array['thread'] = tupd() array['tcp'], array['udp'], array['process'], array['thread'] = tupd()
array['io_read'] = diskIO.get("read")
array['io_write'] = diskIO.get("write")
# report OS (normalized)
try:
sysname = platform.system().lower()
if sysname.startswith('linux'):
os_name = 'linux'
# try distro from os-release
try:
with open('/etc/os-release') as f:
for line in f:
if line.startswith('ID='):
val = line.strip().split('=',1)[1].strip().strip('"')
if val: os_name = val
break
except Exception:
pass
elif sysname.startswith('darwin'):
os_name = 'darwin'
elif sysname.startswith('freebsd'):
os_name = 'freebsd'
elif sysname.startswith('openbsd'):
os_name = 'openbsd'
elif sysname.startswith('netbsd'):
os_name = 'netbsd'
else:
os_name = sysname or 'unknown'
except Exception:
os_name = 'unknown'
array['os'] = os_name
items = []
for _n, st in monitorServer.items():
key = str(_n)
try:
ms = int(st.get('latency') or 0)
except Exception:
ms = 0
items.append((key, max(0, ms)))
# 稳定顺序:按 key 排序
items.sort(key=lambda x: x[0])
array['custom'] = ';'.join(f"{k}={v}" for k,v in items)
s.send(byte_str("update " + json.dumps(array) + "\n")) s.send(byte_str("update " + json.dumps(array) + "\n"))
except KeyboardInterrupt: except KeyboardInterrupt:
raise raise
except socket.error: except socket.error:
monitorServer.clear()
print("Disconnected...") print("Disconnected...")
if 's' in locals().keys(): if 's' in locals().keys():
del s del s
time.sleep(3) time.sleep(3)
except Exception as e: except Exception as e:
monitorServer.clear()
print("Caught Exception:", e) print("Caught Exception:", e)
if 's' in locals().keys(): if 's' in locals().keys():
del s del s
+331 -70
View File
@@ -1,40 +1,64 @@
#!/usr/bin/env python #!/usr/bin/env python3
# coding: utf-8 # coding: utf-8
# Update by : https://github.com/cppla/ServerStatus # Update by : https://github.com/cppla/ServerStatus, Update date: 20250902
# 依赖于psutil跨平台库 # 依赖于psutil跨平台库
# 支持Python版本:2.7 to 3.7 # 版本:1.1.0, 支持Python版本:3.6+
# 支持操作系统: Linux, Windows, OSX, Sun Solaris, FreeBSD, OpenBSD and NetBSD, both 32-bit and 64-bit architectures # 支持操作系统: Linux, Windows, OSX, Sun Solaris, FreeBSD, OpenBSD and NetBSD, both 32-bit and 64-bit architectures
# 时间: 20200407
# 说明: 默认情况下修改server和user就可以了。丢包率监测方向可以自定义,例如:CU = "www.facebook.com"。 # 说明: 默认情况下修改server和user就可以了。丢包率监测方向可以自定义,例如:CU = "www.facebook.com"。
SERVER = "127.0.0.1" SERVER = ""
USER = "s01" USER = ""
PORT = 35601
PASSWORD = "USER_DEFAULT_PASSWORD" PASSWORD = "USER_DEFAULT_PASSWORD"
INTERVAL = 1 PORT = 35601
PROBEPORT = 80
PROBE_PROTOCOL_PREFER = "ipv4" # ipv4, ipv6
PING_PACKET_HISTORY_LEN = 100
CU = "cu.tz.cloudcpp.com" CU = "cu.tz.cloudcpp.com"
CT = "ct.tz.cloudcpp.com" CT = "ct.tz.cloudcpp.com"
CM = "cm.tz.cloudcpp.com" CM = "cm.tz.cloudcpp.com"
PROBEPORT = 80
PROBE_PROTOCOL_PREFER = "ipv4" # ipv4, ipv6
PING_PACKET_HISTORY_LEN = 100
INTERVAL = 1
import socket import socket
import time import time
import timeit import timeit
import os import os
import json
import psutil
import sys import sys
import json
import errno
import psutil
import threading import threading
import threading import platform
try: from queue import Queue
from queue import Queue # python3
except ImportError: def _env_str(name, default):
from Queue import Queue # python2 value = os.getenv(name)
if value is None or value == "":
return default
return value
def _env_int(name, default):
value = os.getenv(name)
if value is None or value == "":
return default
try:
return int(value)
except ValueError:
return default
# Allow docker env overrides. 优先级:运行程序传递参数 > 用户修改的USER > Docker/系统
SERVER = _env_str("SERVER", SERVER) if SERVER == "" else SERVER
USER = _env_str("USER", USER) if USER == "" else USER
PASSWORD = _env_str("PASSWORD", PASSWORD)
PORT = _env_int("PORT", PORT)
INTERVAL = _env_int("INTERVAL", INTERVAL)
PROBEPORT = _env_int("PROBEPORT", PROBEPORT)
PROBE_PROTOCOL_PREFER = _env_str("PROBE_PROTOCOL_PREFER", PROBE_PROTOCOL_PREFER)
PING_PACKET_HISTORY_LEN = _env_int("PING_PACKET_HISTORY_LEN", PING_PACKET_HISTORY_LEN)
CU = _env_str("CU", CU)
CT = _env_str("CT", CT)
CM = _env_str("CM", CM)
def get_uptime(): def get_uptime():
return int(time.time() - psutil.boot_time()) return int(time.time() - psutil.boot_time())
@@ -48,22 +72,72 @@ def get_swap():
return int(Mem.total/1024.0), int(Mem.used/1024.0) return int(Mem.total/1024.0), int(Mem.used/1024.0)
def get_hdd(): def get_hdd():
valid_fs = [ "ext4", "ext3", "ext2", "reiserfs", "jfs", "btrfs", "fuseblk", "zfs", "simfs", "ntfs", "fat32", "exfat", "xfs" ] if "darwin" in sys.platform:
disks = dict() return int(psutil.disk_usage("/").total/1024.0/1024.0), int((psutil.disk_usage("/").total-psutil.disk_usage("/").free)/1024.0/1024.0)
size = 0 else:
used = 0 valid_fs = ["ext4", "ext3", "ext2", "reiserfs", "jfs", "btrfs", "fuseblk", "zfs", "simfs", "ntfs", "fat32",
for disk in psutil.disk_partitions(): "exfat", "xfs"]
if not disk.device in disks and disk.fstype.lower() in valid_fs: disks = dict()
disks[disk.device] = disk.mountpoint size = 0
for disk in disks.values(): used = 0
usage = psutil.disk_usage(disk) for disk in psutil.disk_partitions():
size += usage.total if not disk.device in disks and disk.fstype.lower() in valid_fs:
used += usage.used disks[disk.device] = disk.mountpoint
return int(size/1024.0/1024.0), int(used/1024.0/1024.0) for disk in disks.values():
usage = psutil.disk_usage(disk)
size += usage.total
used += usage.used
return int(size/1024.0/1024.0), int(used/1024.0/1024.0)
def get_cpu(): def get_cpu():
return psutil.cpu_percent(interval=INTERVAL) return psutil.cpu_percent(interval=INTERVAL)
def get_cpu_cores():
return psutil.cpu_count(logical=True) or 0
def normalize_cpu_model(value):
return " ".join(str(value or "").split())[:160]
def is_generic_cpu_model(value):
v = normalize_cpu_model(value).lower().replace('-', '').replace('_', '').replace(' ', '')
return v in ('', 'unknown', 'x8664', 'amd64', 'i386', 'i686', 'aarch64', 'arm64') or v.startswith('armv')
def get_platform_cpu_vendor():
values = [
platform.processor(),
getattr(platform.uname(), 'processor', ''),
platform.machine(),
getattr(platform.uname(), 'machine', ''),
platform.platform(),
]
text = " ".join(normalize_cpu_model(v).lower() for v in values)
if 'genuineintel' in text:
return 'GenuineIntel'
if 'authenticamd' in text:
return 'AuthenticAMD'
if 'intel' in text:
return 'Intel'
if 'amd' in text:
return 'AMD'
if sys.platform.startswith('darwin') and platform.machine().lower() in ('arm64', 'aarch64'):
return 'Apple'
if any(token in text for token in ('aarch64', 'arm64', 'armv7', 'armv8', ' arm ')):
return 'ARM'
return ''
def get_platform_cpu_arch():
return normalize_cpu_model(platform.machine() or platform.processor() or platform.architecture()[0])
def get_cpu_model():
for value in (platform.processor(), getattr(platform.uname(), 'processor', '')):
value = normalize_cpu_model(value)
if value and not is_generic_cpu_model(value):
return value
vendor = normalize_cpu_model(get_platform_cpu_vendor())
if vendor:
return vendor
return get_platform_cpu_arch()
def liuliang(): def liuliang():
NET_IN = 0 NET_IN = 0
NET_OUT = 0 NET_OUT = 0
@@ -90,31 +164,29 @@ def tupd():
u = int(os.popen('ss -u|wc -l').read()[:-1])-1 u = int(os.popen('ss -u|wc -l').read()[:-1])-1
p = int(os.popen('ps -ef|wc -l').read()[:-1])-2 p = int(os.popen('ps -ef|wc -l').read()[:-1])-2
d = int(os.popen('ps -eLf|wc -l').read()[:-1])-2 d = int(os.popen('ps -eLf|wc -l').read()[:-1])-2
elif sys.platform.startswith("darwin") is True:
t = int(os.popen('lsof -nP -iTCP | wc -l').read()[:-1]) - 1
u = int(os.popen('lsof -nP -iUDP | wc -l').read()[:-1]) - 1
p = len(psutil.pids())
d = 0
for k in psutil.pids():
try:
d += psutil.Process(k).num_threads()
except:
pass
elif sys.platform.startswith("win") is True: elif sys.platform.startswith("win") is True:
t = int(os.popen('netstat -an|find "TCP" /c').read()[:-1])-1 t = int(os.popen('netstat -an|find "TCP" /c').read()[:-1])-1
u = int(os.popen('netstat -an|find "UDP" /c').read()[:-1])-1 u = int(os.popen('netstat -an|find "UDP" /c').read()[:-1])-1
p = len(psutil.pids()) p = len(psutil.pids())
d = 0 # if you find cpu is high, please set d=0
# cpu is high, default: 0 d = sum([psutil.Process(k).num_threads() for k in psutil.pids()])
# d = sum([psutil.Process(k).num_threads() for k in [x for x in psutil.pids()]])
else: else:
t,u,p,d = 0,0,0,0 t,u,p,d = 0,0,0,0
return t,u,p,d return t,u,p,d
except: except:
return 0,0,0,0 return 0,0,0,0
def ip_status():
ip_check = 0
for i in [CU, CT, CM]:
try:
socket.create_connection((i, PROBEPORT), timeout=1).close()
except:
ip_check += 1
if ip_check >= 2:
return False
else:
return True
def get_network(ip_version): def get_network(ip_version):
if(ip_version == 4): if(ip_version == 4):
HOST = "ipv4.google.com" HOST = "ipv4.google.com"
@@ -144,22 +216,28 @@ netSpeed = {
'avgrx': 0, 'avgrx': 0,
'avgtx': 0 'avgtx': 0
} }
diskIO = {
'read': 0,
'write': 0
}
monitorServer = {}
def _ping_thread(host, mark, port): def _ping_thread(host, mark, port):
lostPacket = 0 lostPacket = 0
packet_queue = Queue(maxsize=PING_PACKET_HISTORY_LEN) packet_queue = Queue(maxsize=PING_PACKET_HISTORY_LEN)
IP = host while True:
if host.count(':') < 1: # if not plain ipv6 address, means ipv4 address or hostname # flush dns, every time.
try: IP = host
if PROBE_PROTOCOL_PREFER == 'ipv4': if host.count(':') < 1: # if not plain ipv6 address, means ipv4 address or hostname
IP = socket.getaddrinfo(host, None, socket.AF_INET)[0][4][0] try:
else: if PROBE_PROTOCOL_PREFER == 'ipv4':
IP = socket.getaddrinfo(host, None, socket.AF_INET6)[0][4][0] IP = socket.getaddrinfo(host, None, socket.AF_INET)[0][4][0]
except Exception: else:
IP = socket.getaddrinfo(host, None, socket.AF_INET6)[0][4][0]
except Exception:
pass pass
while True:
if packet_queue.full(): if packet_queue.full():
if packet_queue.get() == 0: if packet_queue.get() == 0:
lostPacket -= 1 lostPacket -= 1
@@ -203,7 +281,67 @@ def _net_speed():
netSpeed["avgtx"] = avgtx netSpeed["avgtx"] = avgtx
time.sleep(INTERVAL) time.sleep(INTERVAL)
def get_realtime_date(): def _disk_io():
"""
the code is by: https://github.com/giampaolo/psutil/blob/master/scripts/iotop.py
good luck for opensource! modify: cpp.la
Calculate IO usage by comparing IO statics before and
after the interval.
Return a tuple including all currently running processes
sorted by IO activity and total disks I/O activity.
磁盘IO:因为IOPS原因,SSD和HDD、包括RAID卡,ZFS等。IO对性能的影响还需要结合自身服务器情况来判断。
比如我这里是机械硬盘,大量做随机小文件读写,那么很低的读写也就能造成硬盘长时间的等待。
如果这里做连续性IO,那么普通机械硬盘写入到100Mb/s,那么也能造成硬盘长时间的等待。
磁盘读写有误差:4k8k https://stackoverflow.com/questions/34413926/psutil-vs-dd-monitoring-disk-i-o
macos/win,暂不处理。
"""
if "darwin" in sys.platform or "win" in sys.platform:
diskIO["read"] = 0
diskIO["write"] = 0
else:
while True:
# first get a list of all processes and disk io counters
procs = [p for p in psutil.process_iter()]
for p in procs[:]:
try:
p._before = p.io_counters()
except psutil.Error:
procs.remove(p)
continue
disks_before = psutil.disk_io_counters()
# sleep some time, only when INTERVAL==1 , io read/write per_sec.
# when INTERVAL > 1, io read/write per_INTERVAL
time.sleep(INTERVAL)
# then retrieve the same info again
for p in procs[:]:
with p.oneshot():
try:
p._after = p.io_counters()
p._cmdline = ' '.join(p.cmdline())
if not p._cmdline:
p._cmdline = p.name()
p._username = p.username()
except (psutil.NoSuchProcess, psutil.ZombieProcess):
procs.remove(p)
disks_after = psutil.disk_io_counters()
# finally calculate results by comparing data before and
# after the interval
for p in procs:
p._read_per_sec = p._after.read_bytes - p._before.read_bytes
p._write_per_sec = p._after.write_bytes - p._before.write_bytes
p._total = p._read_per_sec + p._write_per_sec
diskIO["read"] = disks_after.read_bytes - disks_before.read_bytes
diskIO["write"] = disks_after.write_bytes - disks_before.write_bytes
def get_realtime_data():
'''
real time get system data
:return:
'''
t1 = threading.Thread( t1 = threading.Thread(
target=_ping_thread, target=_ping_thread,
kwargs={ kwargs={
@@ -231,14 +369,78 @@ def get_realtime_date():
t4 = threading.Thread( t4 = threading.Thread(
target=_net_speed, target=_net_speed,
) )
t1.setDaemon(True) t5 = threading.Thread(
t2.setDaemon(True) target=_disk_io,
t3.setDaemon(True) )
t4.setDaemon(True) for ti in [t1, t2, t3, t4, t5]:
t1.start() ti.daemon = True
t2.start() ti.start()
t3.start()
t4.start() def _monitor_thread(name, host, interval, type):
# 参考 _ping_thread 风格:每轮解析一次目标,按协议族偏好解析 IP,测 TCP 建连耗时
while True:
if name not in monitorServer:
break
try:
# 1) 解析目标 host 与端口
if type == 'http':
addr = str(host).replace('http://','')
addr = addr.split('/',1)[0]
port = 80
if ':' in addr and not addr.startswith('['):
a, p = addr.rsplit(':',1)
if p.isdigit():
addr, port = a, int(p)
elif type == 'https':
addr = str(host).replace('https://','')
addr = addr.split('/',1)[0]
port = 443
if ':' in addr and not addr.startswith('['):
a, p = addr.rsplit(':',1)
if p.isdigit():
addr, port = a, int(p)
elif type == 'tcp':
addr = str(host)
if addr.startswith('[') and ']' in addr:
# [v6]:port
a = addr[1:addr.index(']')]
rest = addr[addr.index(']')+1:]
if rest.startswith(':') and rest[1:].isdigit():
addr, port = a, int(rest[1:])
else:
raise Exception('bad tcp target')
else:
a, p = addr.rsplit(':',1)
addr, port = a, int(p)
else:
time.sleep(interval)
continue
# 2) 解析 IP(按偏好族),与 _ping_thread 保持一致的判定
IP = addr
if addr.count(':') < 1: # 非纯 IPv6,可能是 IPv4 或域名
try:
if PROBE_PROTOCOL_PREFER == 'ipv4':
IP = socket.getaddrinfo(addr, None, socket.AF_INET)[0][4][0]
else:
IP = socket.getaddrinfo(addr, None, socket.AF_INET6)[0][4][0]
except Exception:
pass
# 3) 测 TCP 建连耗时(timeout=1s);ECONNREFUSED 也记为耗时
try:
b = timeit.default_timer()
socket.create_connection((IP, port), timeout=1).close()
monitorServer[name]['latency'] = int((timeit.default_timer() - b) * 1000)
except socket.error as error:
if getattr(error, 'errno', None) == errno.ECONNREFUSED:
monitorServer[name]['latency'] = int((timeit.default_timer() - b) * 1000)
else:
monitorServer[name]['latency'] = 0
except Exception:
monitorServer[name]['latency'] = 0
time.sleep(interval)
def byte_str(object): def byte_str(object):
''' '''
@@ -266,7 +468,7 @@ if __name__ == '__main__':
elif 'INTERVAL' in argc: elif 'INTERVAL' in argc:
INTERVAL = int(argc.split('INTERVAL=')[-1]) INTERVAL = int(argc.split('INTERVAL=')[-1])
socket.setdefaulttimeout(30) socket.setdefaulttimeout(30)
get_realtime_date() get_realtime_data()
while 1: while 1:
try: try:
print("Connecting...") print("Connecting...")
@@ -286,6 +488,25 @@ if __name__ == '__main__':
if data.find("You are connecting via") < 0: if data.find("You are connecting via") < 0:
data = byte_str(s.recv(1024)) data = byte_str(s.recv(1024))
print(data) print(data)
for i in data.split('\n'):
if "monitor" in i and "type" in i and "{" in i and "}" in i:
jdata = json.loads(i[i.find("{"):i.find("}")+1])
monitorServer[jdata.get("name")] = {
"type": jdata.get("type"),
"host": jdata.get("host"),
"latency": 0
}
t = threading.Thread(
target=_monitor_thread,
kwargs={
'name': jdata.get("name"),
'host': jdata.get("host"),
'interval': jdata.get("interval"),
'type': jdata.get("type")
}
)
t.daemon = True
t.start()
timer = 0 timer = 0
check_ip = 0 check_ip = 0
@@ -297,16 +518,16 @@ if __name__ == '__main__':
print(data) print(data)
raise socket.error raise socket.error
CPUCores = get_cpu_cores()
CPUModel = get_cpu_model()
while 1: while 1:
CPU = get_cpu() CPU = get_cpu()
NET_IN, NET_OUT = liuliang() NET_IN, NET_OUT = liuliang()
Uptime = get_uptime() Uptime = get_uptime()
Load_1, Load_5, Load_15 = os.getloadavg() if 'linux' in sys.platform else (0.0, 0.0, 0.0) Load_1, Load_5, Load_15 = os.getloadavg() if 'linux' in sys.platform or 'darwin' in sys.platform else (0.0, 0.0, 0.0)
MemoryTotal, MemoryUsed = get_memory() MemoryTotal, MemoryUsed = get_memory()
SwapTotal, SwapUsed = get_swap() SwapTotal, SwapUsed = get_swap()
HDDTotal, HDDUsed = get_hdd() HDDTotal, HDDUsed = get_hdd()
IP_STATUS = ip_status()
array = {} array = {}
if not timer: if not timer:
array['online' + str(check_ip)] = get_network(check_ip) array['online' + str(check_ip)] = get_network(check_ip)
@@ -325,11 +546,12 @@ if __name__ == '__main__':
array['hdd_total'] = HDDTotal array['hdd_total'] = HDDTotal
array['hdd_used'] = HDDUsed array['hdd_used'] = HDDUsed
array['cpu'] = CPU array['cpu'] = CPU
array['cpu_cores'] = CPUCores
array['cpu_model'] = CPUModel
array['network_rx'] = netSpeed.get("netrx") array['network_rx'] = netSpeed.get("netrx")
array['network_tx'] = netSpeed.get("nettx") array['network_tx'] = netSpeed.get("nettx")
array['network_in'] = NET_IN array['network_in'] = NET_IN
array['network_out'] = NET_OUT array['network_out'] = NET_OUT
array['ip_status'] = IP_STATUS
array['ping_10010'] = lostRate.get('10010') * 100 array['ping_10010'] = lostRate.get('10010') * 100
array['ping_189'] = lostRate.get('189') * 100 array['ping_189'] = lostRate.get('189') * 100
array['ping_10086'] = lostRate.get('10086') * 100 array['ping_10086'] = lostRate.get('10086') * 100
@@ -337,16 +559,55 @@ if __name__ == '__main__':
array['time_189'] = pingTime.get('189') array['time_189'] = pingTime.get('189')
array['time_10086'] = pingTime.get('10086') array['time_10086'] = pingTime.get('10086')
array['tcp'], array['udp'], array['process'], array['thread'] = tupd() array['tcp'], array['udp'], array['process'], array['thread'] = tupd()
array['io_read'] = diskIO.get("read")
array['io_write'] = diskIO.get("write")
# report OS (normalized)
try:
sysname = platform.system().lower()
if sysname.startswith('windows'):
os_name = 'windows'
elif sysname.startswith('darwin') or 'mac' in sysname:
os_name = 'darwin'
elif 'bsd' in sysname:
os_name = 'bsd'
elif sysname.startswith('linux'):
# try distro from os-release
try:
with open('/etc/os-release') as f:
for line in f:
if line.startswith('ID='):
val = line.strip().split('=',1)[1].strip().strip('"')
if val: os_name = val
break
except Exception:
os_name = 'linux'
else:
os_name = sysname or 'unknown'
except Exception:
os_name = 'unknown'
array['os'] = os_name
items = []
for _n, st in monitorServer.items():
key = str(_n)
try:
ms = int(st.get('latency') or 0)
except Exception:
ms = 0
items.append((key, max(0, ms)))
# 稳定顺序:按 key 排序
items.sort(key=lambda x: x[0])
array['custom'] = ';'.join(f"{k}={v}" for k,v in items)
s.send(byte_str("update " + json.dumps(array) + "\n")) s.send(byte_str("update " + json.dumps(array) + "\n"))
except KeyboardInterrupt: except KeyboardInterrupt:
raise raise
except socket.error: except socket.error:
monitorServer.clear()
print("Disconnected...") print("Disconnected...")
if 's' in locals().keys(): if 's' in locals().keys():
del s del s
time.sleep(3) time.sleep(3)
except Exception as e: except Exception as e:
monitorServer.clear()
print("Caught Exception:", e) print("Caught Exception:", e)
if 's' in locals().keys(): if 's' in locals().keys():
del s del s
+20
View File
@@ -0,0 +1,20 @@
#!/bin/sh
set -eu
if [ "$#" -gt 0 ]; then
exec "$@"
fi
case "${CLIENT:-linux}" in
linux|client-linux|client-linux.py)
exec python3 /app/client-linux.py
;;
psutil|client-psutil|client-psutil.py)
exec python3 /app/client-psutil.py
;;
*)
echo "Unknown CLIENT='$CLIENT'. Use 'linux' or 'psutil'." >&2
exit 2
;;
esac
+27
View File
@@ -0,0 +1,27 @@
services:
serverstatus-client:
build:
context: .
dockerfile: Dockerfile.client
image: cppla/serverstatus:client
container_name: serverstatus-client
restart: unless-stopped
network_mode: host
pid: host
environment:
SERVER: "${SERVER:-127.0.0.1}"
# 注意:USER 会被 docker compose 在宿主机环境中先插值。
# 如果运行时没有显式传递,或传递方式不正确,可能会被 Docker/系统环境中的 $USER
# 替换成本机用户名,而不是期望的默认 s01。
# 推荐优先级:运行程序时显式传递 USER > 用户修改这里的 USER 默认值 > Docker/系统环境。
USER: "${USER:-s01}"
PORT: "${PORT:-35601}"
PASSWORD: "${PASSWORD:-USER_DEFAULT_PASSWORD}"
INTERVAL: "${INTERVAL:-1}"
PROBEPORT: "${PROBEPORT:-80}"
PROBE_PROTOCOL_PREFER: "${PROBE_PROTOCOL_PREFER:-ipv4}"
PING_PACKET_HISTORY_LEN: "${PING_PACKET_HISTORY_LEN:-100}"
CU: "${CU:-cu.tz.cloudcpp.com}"
CT: "${CT:-ct.tz.cloudcpp.com}"
CM: "${CM:-cm.tz.cloudcpp.com}"
CLIENT: "${CLIENT:-psutil}"
+30
View File
@@ -0,0 +1,30 @@
services:
serverstatus-server:
build:
context: .
dockerfile: Dockerfile.server
image: cppla/serverstatus:server
healthcheck:
test: ["CMD-SHELL", "python3 -c \"import os,urllib.request; pid=int(open('/tmp/serverstatus-sergate.pid').read().strip()); os.kill(pid,0); urllib.request.urlopen('http://127.0.0.1/',timeout=2).read(1)\""]
interval: 30s
timeout: 10s
retries: 5
container_name: serverstatus-server
restart: unless-stopped
environment:
ADMIN_TOKEN: "${ADMIN_TOKEN:-}"
networks:
serverstatus-network:
ipv4_address: 172.23.0.2
volumes:
- ./server/config.json:/ServerStatus/server/config.json
- ./web/json:/usr/share/nginx/html/json
ports:
- 35601:35601
- 8080:80
networks:
serverstatus-network:
ipam:
config:
- subnet: 172.23.0.0/24
+10
View File
@@ -0,0 +1,10 @@
FROM python:alpine
LABEL maintainer="lidalao"
LABEL version="0.0.1"
LABEL description="Telegram Bot for ServerStatus"
WORKDIR /app
RUN pip install requests
COPY ./bot-telegram.py .
CMD [ "python", "./bot-telegram.py" ]
+73
View File
@@ -0,0 +1,73 @@
#!/usr/bin/env python3
# coding: utf-8
# Create by : https://github.com/lidalao/ServerStatus
# 版本:0.0.1, 支持Python版本:2.7 to 3.9
# 支持操作系统: Linux, OSX, FreeBSD, OpenBSD and NetBSD, both 32-bit and 64-bit architectures
import os
import sys
import requests
import time
import traceback
NODE_STATUS_URL = 'http://serverstatus/json/stats.json'
offs = []
counterOff = {}
counterOn = {}
def _send(text):
chat_id = os.getenv('TG_CHAT_ID')
bot_token = os.environ.get('TG_BOT_TOKEN')
url = f"https://api.telegram.org/bot{bot_token}/sendMessage?parse_mode=HTML&disable_web_page_preview=true&chat_id=" + chat_id + "&text=" + text
try:
requests.get(url)
except Exception as e:
print("catch exception: ", traceback.format_exc())
def send2tg(srv, flag):
if srv not in counterOff:
counterOff[srv] = 0
if srv not in counterOn:
counterOn[srv] = 0
if flag == 1 : # online
if srv in offs:
if counterOn[srv] < 10:
counterOn[srv] += 1
return
#1. Remove srv from offs; 2. Send to tg: I am online
offs.remove(srv)
counterOn[srv] = 0
text = '<b>Server Status</b>' + '\n主机上线: ' + srv
_send(text)
else: #offline
if srv not in offs:
if counterOff[srv] < 10:
counterOff[srv] += 1
return
#1. Append srv to offs; 2. Send to tg: I am offline
offs.append(srv)
counterOff[srv] = 0
text = '<b>Server Status</b>' + '\n主机下线: ' + srv
_send(text)
def sscmd(address):
while True:
r = requests.get(url=address, headers={"User-Agent": "ServerStatus/20211116"})
try:
jsonR = r.json()
except Exception as e:
print('未发现任何节点')
continue
for i in jsonR["servers"]:
if i["online4"] is False and i["online6"] is False:
send2tg(i["name"], 0)
else:
send2tg(i["name"], 1)
time.sleep(3)
if __name__ == '__main__':
sscmd(NODE_STATUS_URL)
+38
View File
@@ -0,0 +1,38 @@
version: "3"
services:
serverstatus:
build:
context: ..
dockerfile: Dockerfile
image: serverstatus_server
container_name: serverstatus
restart: unless-stopped
networks:
serverstatus-network:
ipv4_address: 172.23.0.2
volumes:
- ../server/config.json:/ServerStatus/server/config.json
- ../web/json:/usr/share/nginx/html/json
ports:
- 35601:35601
- 8080:80
bot:
build:
context: .
dockerfile: Dockerfile-telegram
image: serverstatus_bot
container_name: bot4sss
restart: unless-stopped
networks:
serverstatus-network:
ipv4_address: 172.23.0.3
environment:
- TG_CHAT_ID=${TG_CHAT_ID}
- TG_BOT_TOKEN=${TG_BOT_TOKEN}
networks:
serverstatus-network:
name: serverstatus-network
ipam:
config:
- subnet: 172.23.0.0/24
+8 -4
View File
@@ -1,4 +1,5 @@
OUT = sergate OUT = sergate
.DEFAULT_GOAL := $(OUT)
#CC = clang #CC = clang
CC = gcc CC = gcc
@@ -6,7 +7,7 @@ CFLAGS = -Wall -O2
#CXX = clang++ #CXX = clang++
CXX = g++ CXX = g++
CXXFLAGS = -Wall -O2 CXXFLAGS = -Wall -O2 -std=c++11
ODIR = obj ODIR = obj
SDIR = src SDIR = src
@@ -19,14 +20,17 @@ C_OBJS := $(patsubst $(SDIR)/%.c,$(ODIR)/%.o,$(C_SRCS))
CXX_OBJS := $(patsubst $(SDIR)/%.cpp,$(ODIR)/%.o,$(CXX_SRCS)) CXX_OBJS := $(patsubst $(SDIR)/%.cpp,$(ODIR)/%.o,$(CXX_SRCS))
OBJS := $(C_OBJS) $(CXX_OBJS) OBJS := $(C_OBJS) $(CXX_OBJS)
$(ODIR)/%.o: $(SDIR)/%.c $(ODIR):
mkdir -p $(ODIR)
$(ODIR)/%.o: $(SDIR)/%.c | $(ODIR)
$(CC) -c $(INC) $(CFLAGS) $< -o $@ $(CC) -c $(INC) $(CFLAGS) $< -o $@
$(ODIR)/%.o: $(SDIR)/%.cpp $(ODIR)/%.o: $(SDIR)/%.cpp | $(ODIR)
$(CXX) -c $(INC) $(CXXFLAGS) $< -o $@ $(CXX) -c $(INC) $(CXXFLAGS) $< -o $@
$(OUT): $(OBJS) $(OUT): $(OBJS)
$(CXX) $(LIBS) $^ -o $(OUT) $(CXX) $(LIBS) $^ -o $(OUT) -lcurl
.PHONY: clean .PHONY: clean
+104 -13
View File
@@ -1,37 +1,128 @@
{"servers": {
[ "servers": [
{ {
"username": "s01", "username": "s01",
"name": "node1", "name": "node1",
"type": "xen", "type": "xen",
"host": "host1", "host": "host1",
"location": "cn", "location": "🇨🇳",
"password": "USER_DEFAULT_PASSWORD" "password": "USER_DEFAULT_PASSWORD",
"monthstart": 1
}, },
{ {
"username": "s02", "username": "s02",
"name": "node2", "name": "node2",
"type": "vmware", "type": "vmware",
"host": "host2", "host": "host2",
"location": "jp", "location": "🇯🇵",
"password": "USER_DEFAULT_PASSWORD" "password": "USER_DEFAULT_PASSWORD",
"monthstart": 1
}, },
{ {
"disabled": true,
"username": "s03", "username": "s03",
"name": "node3", "name": "node3",
"type": "Nothing", "type": "hyper",
"host": "host3", "host": "host3",
"location": "fr", "location": "🇫🇷",
"password": "USER_DEFAULT_PASSWORD" "password": "USER_DEFAULT_PASSWORD",
"monthstart": 1,
"disabled": false
}, },
{ {
"username": "s04", "username": "s04",
"name": "node4", "name": "node4",
"type": "kvm", "type": "kvm",
"host": "host4", "host": "host4",
"location": "kr", "location": "🇰🇷",
"password": "USER_DEFAULT_PASSWORD" "password": "USER_DEFAULT_PASSWORD",
"monthstart": 1
}
],
"monitors": [
{
"name": "抖音",
"host": "https://www.douyin.com",
"interval": 600,
"type": "https"
},
{
"name": "百度",
"host": "https://www.baidu.com",
"interval": 600,
"type": "https"
}
],
"sslcerts": [
{
"name": "my.cloudcpp.com",
"domain": "https://my.cloudcpp.com",
"port": 443,
"interval": 7200,
"callback": "https://yourSMSurl"
},
{
"name": "tz.cloudcpp.com",
"domain": "https://tz.cloudcpp.com",
"port": 443,
"interval": 7200,
"callback": "https://yourSMSurl"
},
{
"name": "3.0.2.1",
"domain": "https://3.0.2.1",
"port": 443,
"interval": 7200,
"callback": "https://yourSMSurl"
}
],
"watchdog": [
{
"name": "cpu high warning,exclude username s01",
"rule": "cpu>90&load_1>5&username!='s01'",
"interval": 600,
"callback": "https://yourSMSurl"
},
{
"name": "memory high warning, exclude less than 1GB vps",
"rule": "(memory_used/memory_total)*100>90&memory_total>1048576",
"interval": 300,
"callback": "https://yourSMSurl"
},
{
"name": "offline warning",
"rule": "online4=0&online6=0",
"interval": 600,
"callback": "https://yourSMSurl"
},
{
"name": "ddcc attack,limit type Oracle",
"rule": "tcp_count>600&type='Oracle'",
"interval": 300,
"callback": "https://yourSMSurl"
},
{
"name": "month 999GB traffic warning",
"rule": "(network_out-last_network_out)/1024/1024/1024>999",
"interval": 3600,
"callback": "https://yourSMSurl"
},
{
"name": "aliyun china free 18GB traffic warning",
"rule": "(network_out-last_network_out)/1024/1024/1024>18&(username='aliyun1'|username='aliyun2')",
"interval": 3600,
"callback": "https://yourSMSurl"
},
{
"name": "packet loss rate warning",
"rule": "(ping_10010>10|ping_189>10|ping_10086>10)&(host='sgp'|host='qqhk'|host='hk-21-x'|host='hk-31-x')",
"interval": 3600,
"callback": "https://yourSMSurl"
},
{
"name": "you can parse an expression combining any known field",
"rule": "load_5>3",
"interval": 900,
"callback": "https://yourSMSurl"
} }
] ]
} }
+59
View File
@@ -0,0 +1,59 @@
#!/bin/sh
set -eu
: "${CONFIG_PATH:=/ServerStatus/server/config.json}"
: "${WEB_DIR:=/usr/share/nginx/html}"
: "${SERGATE_PID_FILE:=/tmp/serverstatus-sergate.pid}"
: "${ADMIN_API_BIND:=127.0.0.1}"
: "${ADMIN_API_PORT:=35602}"
STOPPING=0
SERGATE_PID=""
API_PID=""
stop_all() {
STOPPING=1
if [ -n "$SERGATE_PID" ] && kill -0 "$SERGATE_PID" 2>/dev/null; then
kill -TERM "$SERGATE_PID" 2>/dev/null || true
fi
if [ -n "$API_PID" ] && kill -0 "$API_PID" 2>/dev/null; then
kill -TERM "$API_PID" 2>/dev/null || true
fi
nginx -s quit 2>/dev/null || true
}
trap 'stop_all; exit 0' INT TERM QUIT
mkdir -p "$WEB_DIR/json"
nginx
CONFIG_PATH="$CONFIG_PATH" \
STATS_PATH="${STATS_PATH:-$WEB_DIR/json/stats.json}" \
SERGATE_PID_FILE="$SERGATE_PID_FILE" \
ADMIN_API_BIND="$ADMIN_API_BIND" \
ADMIN_API_PORT="$ADMIN_API_PORT" \
ADMIN_TOKEN="${ADMIN_TOKEN:-}" \
ADMIN_CORS_ORIGIN="${ADMIN_CORS_ORIGIN:-}" \
python3 /ServerStatus/server/manage_api.py &
API_PID="$!"
if [ -n "${ADMIN_TOKEN:-}" ]; then
echo "management API enabled on ${ADMIN_API_BIND}:${ADMIN_API_PORT}"
else
echo "management API running in read-only discovery mode; set ADMIN_TOKEN to enable writes"
fi
while [ "$STOPPING" -eq 0 ]; do
/ServerStatus/server/sergate --config="$CONFIG_PATH" --web-dir="$WEB_DIR" &
SERGATE_PID="$!"
echo "$SERGATE_PID" > "$SERGATE_PID_FILE"
set +e
wait "$SERGATE_PID"
STATUS="$?"
set -e
rm -f "$SERGATE_PID_FILE"
if [ "$STOPPING" -eq 1 ]; then
exit "$STATUS"
fi
echo "sergate exited with status ${STATUS}; restarting in 1s"
sleep 1
done
+621
View File
@@ -0,0 +1,621 @@
#!/usr/bin/env python3
import json
import os
import shutil
import signal
import tempfile
import time
import errno
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import unquote, urlparse
CONFIG_PATH = os.environ.get("CONFIG_PATH", "/ServerStatus/server/config.json")
STATS_PATH = os.environ.get("STATS_PATH", "/usr/share/nginx/html/json/stats.json")
SERGATE_PID_FILE = os.environ.get("SERGATE_PID_FILE", "/tmp/serverstatus-sergate.pid")
ADMIN_TOKEN = os.environ.get("ADMIN_TOKEN", "")
API_BIND = os.environ.get("ADMIN_API_BIND", "127.0.0.1")
API_PORT = int(os.environ.get("ADMIN_API_PORT", "35602"))
CORS_ORIGIN = os.environ.get("ADMIN_CORS_ORIGIN", "")
class ApiError(Exception):
def __init__(self, status, message, details=None):
super().__init__(message)
self.status = status
self.message = message
self.details = details
def load_config():
with open(CONFIG_PATH, "r", encoding="utf-8") as f:
return json.load(f)
def load_json_file(path):
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
def load_stats_file():
try:
return load_json_file(STATS_PATH)
except FileNotFoundError:
return load_json_file(f"{STATS_PATH}~")
def write_json_file(path, data):
directory = os.path.dirname(path) or "."
os.makedirs(directory, exist_ok=True)
mode = os.stat(path).st_mode if os.path.exists(path) else 0o644
payload = json.dumps(data, ensure_ascii=False, indent="\t") + "\n"
fd, tmp_path = tempfile.mkstemp(prefix=".json.", suffix=".tmp", dir=directory)
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(payload)
f.flush()
os.fsync(f.fileno())
os.chmod(tmp_path, mode)
os.replace(tmp_path, path)
finally:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
def normalize_required_strings(item, required, kind, index=None):
missing = [field for field in required if item.get(field) is None or not str(item.get(field, "")).strip()]
if missing:
raise ApiError(400, f"{kind} has missing required fields", {"missing": missing, "index": index})
normalized = dict(item)
for field in required:
normalized[field] = str(normalized[field]).strip()
return normalized
def normalize_optional_string(item, field):
value = item.get(field, "")
item[field] = "" if value is None else str(value).strip()
def normalize_int(value, field, index=None, default=None, min_value=None, max_value=None):
if value in (None, ""):
value = default
try:
value = int(value)
except (TypeError, ValueError):
raise ApiError(400, f"{field} must be an integer", {"index": index})
if min_value is not None:
value = max(min_value, value)
if max_value is not None:
value = min(max_value, value)
return value
def validate_server(server, index=None):
if not isinstance(server, dict):
raise ApiError(400, "server must be an object", {"index": index})
required = ["username", "name", "type", "host", "location", "password"]
normalized = normalize_required_strings(server, required, "server", index=index)
normalized["monthstart"] = normalize_int(normalized.get("monthstart"), "monthstart", index=index, default=1, min_value=1, max_value=28)
if "disabled" in normalized:
normalized["disabled"] = bool(normalized["disabled"])
return normalized
def validate_monitor(monitor, index=None):
if not isinstance(monitor, dict):
raise ApiError(400, "monitor must be an object", {"index": index})
normalized = normalize_required_strings(monitor, ["name", "host", "type"], "monitor", index=index)
normalized["interval"] = normalize_int(normalized.get("interval"), "interval", index=index, default=600, min_value=1)
return normalized
def validate_sslcert(sslcert, index=None):
if not isinstance(sslcert, dict):
raise ApiError(400, "sslcert must be an object", {"index": index})
normalized = normalize_required_strings(sslcert, ["name", "domain"], "sslcert", index=index)
normalized["port"] = normalize_int(normalized.get("port"), "port", index=index, default=443, min_value=1, max_value=65535)
normalized["interval"] = normalize_int(normalized.get("interval"), "interval", index=index, default=7200, min_value=1)
normalize_optional_string(normalized, "callback")
return normalized
def validate_watchdog(watchdog, index=None):
if not isinstance(watchdog, dict):
raise ApiError(400, "watchdog must be an object", {"index": index})
normalized = normalize_required_strings(watchdog, ["name", "rule"], "watchdog", index=index)
normalized["interval"] = normalize_int(normalized.get("interval"), "interval", index=index, default=600, min_value=1)
normalize_optional_string(normalized, "callback")
return normalized
COLLECTIONS = {
"servers": {
"item": "server",
"id_field": "username",
"validator": validate_server,
"required": ["username", "name", "type", "host", "location", "password"],
"optional": ["monthstart", "disabled"],
},
"monitors": {
"item": "monitor",
"id_field": "name",
"validator": validate_monitor,
"required": ["name", "host", "type"],
"optional": ["interval"],
},
"sslcerts": {
"item": "sslcert",
"id_field": "name",
"validator": validate_sslcert,
"required": ["name", "domain"],
"optional": ["port", "interval", "callback"],
},
"watchdog": {
"item": "watchdog",
"id_field": "name",
"validator": validate_watchdog,
"required": ["name", "rule"],
"optional": ["interval", "callback"],
},
}
def validate_config(config):
if not isinstance(config, dict):
raise ApiError(400, "config must be a JSON object")
config = dict(config)
for key, meta in COLLECTIONS.items():
items = config.get(key, [])
if not isinstance(items, list):
raise ApiError(400, f"{key} must be an array")
normalized = []
seen = set()
for index, item in enumerate(items):
entry = meta["validator"](item, index=index)
if key == "servers":
username = entry["username"]
if username in seen:
raise ApiError(409, "duplicate server username", {"username": username})
seen.add(username)
normalized.append(entry)
config[key] = normalized
return config
def write_config(config):
config = validate_config(config)
directory = os.path.dirname(CONFIG_PATH) or "."
os.makedirs(directory, exist_ok=True)
timestamp = time.strftime("%Y%m%d-%H%M%S")
if os.path.exists(CONFIG_PATH):
shutil.copy2(CONFIG_PATH, f"{CONFIG_PATH}.bak-{timestamp}")
mode = os.stat(CONFIG_PATH).st_mode
else:
mode = 0o644
data = json.dumps(config, ensure_ascii=False, indent="\t") + "\n"
fd, tmp_path = tempfile.mkstemp(prefix=".config.", suffix=".tmp", dir=directory)
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
f.write(data)
f.flush()
os.fsync(f.fileno())
os.chmod(tmp_path, mode)
try:
os.replace(tmp_path, CONFIG_PATH)
except OSError as exc:
if exc.errno != errno.EBUSY:
raise
# A single-file Docker bind mount cannot be atomically replaced.
# Keep the backup above, then rewrite the mounted file in place.
with open(CONFIG_PATH, "w", encoding="utf-8") as f:
f.write(data)
f.flush()
os.fsync(f.fileno())
finally:
if os.path.exists(tmp_path):
os.unlink(tmp_path)
return config
def write_and_reload(config):
config = write_config(config)
pid = signal_sergate(signal.SIGHUP)
return config, pid
def read_body(handler):
length = int(handler.headers.get("Content-Length", "0") or "0")
if length <= 0:
return None
if length > 1024 * 1024:
raise ApiError(413, "request body is too large")
raw = handler.rfile.read(length).decode("utf-8")
try:
return json.loads(raw)
except json.JSONDecodeError as exc:
raise ApiError(400, "invalid JSON body", {"error": str(exc)})
def get_sergate_pid():
try:
with open(SERGATE_PID_FILE, "r", encoding="utf-8") as f:
pid = int(f.read().strip())
os.kill(pid, 0)
return pid
except Exception:
pass
proc_dir = "/proc"
if not os.path.isdir(proc_dir):
return None
for name in os.listdir(proc_dir):
if not name.isdigit():
continue
try:
with open(os.path.join(proc_dir, name, "cmdline"), "rb") as f:
cmdline = f.read().replace(b"\x00", b" ").decode("utf-8", "ignore")
except Exception:
continue
if "sergate" in cmdline:
return int(name)
return None
def signal_sergate(sig):
pid = get_sergate_pid()
if not pid:
raise ApiError(503, "sergate process was not found")
os.kill(pid, sig)
return pid
def wait_for_pid_exit(pid, timeout=2.5):
deadline = time.time() + timeout
while time.time() < deadline:
try:
os.kill(pid, 0)
except OSError:
return True
time.sleep(0.05)
return False
def wait_for_sergate_pid(previous_pid=None, timeout=4.0):
deadline = time.time() + timeout
while time.time() < deadline:
pid = get_sergate_pid()
if pid and pid != previous_pid:
return pid
time.sleep(0.1)
return get_sergate_pid()
def find_server(config, username):
servers = config.get("servers", [])
for index, server in enumerate(servers):
if server.get("username") == username:
return index, server
return -1, None
def find_collection_item(config, key, item_id):
items = config.get(key, [])
if item_id.isdigit():
index = int(item_id)
if 0 <= index < len(items):
return index, items[index]
return -1, None
id_field = COLLECTIONS[key]["id_field"]
matches = [(index, item) for index, item in enumerate(items) if str(item.get(id_field, "")) == item_id]
if len(matches) > 1:
raise ApiError(409, f"{key} has duplicate {id_field}; use numeric index instead", {"id": item_id})
if matches:
return matches[0]
return -1, None
def stats_server_matches(config_server, stats_server):
return all(str(stats_server.get(field, "")) == str(config_server.get(field, "")) for field in ["name", "type", "host", "location"])
def find_stats_server(stats, config_server):
servers = stats.get("servers", [])
if not isinstance(servers, list):
raise ApiError(500, "stats.json has invalid servers data")
for index, stats_server in enumerate(servers):
if isinstance(stats_server, dict) and stats_server_matches(config_server, stats_server):
return index, stats_server
return -1, None
def as_counter(value, field):
try:
return int(value)
except (TypeError, ValueError):
raise ApiError(409, f"{field} is missing or invalid in stats.json")
def require_resettable_stats(stats, server, username):
stats_index, stats_server = find_stats_server(stats, server)
if stats_index < 0:
raise ApiError(404, "server stats were not found", {"username": username})
if "network_in" not in stats_server or "network_out" not in stats_server:
raise ApiError(409, "server has no current traffic counters; it may be offline", {"username": username})
return stats_index, stats_server
def reset_server_month_traffic(username):
config = load_config()
_, server = find_server(config, username)
if server is None:
raise ApiError(404, "server was not found", {"username": username})
require_resettable_stats(load_stats_file(), server, username)
old_pid = signal_sergate(signal.SIGTERM)
wait_for_pid_exit(old_pid)
stats = load_stats_file()
stats_index, stats_server = require_resettable_stats(stats, server, username)
network_in = as_counter(stats_server.get("network_in"), "network_in")
network_out = as_counter(stats_server.get("network_out"), "network_out")
previous_last_in = as_counter(stats_server.get("last_network_in", 0), "last_network_in")
previous_last_out = as_counter(stats_server.get("last_network_out", 0), "last_network_out")
stats["servers"][stats_index]["last_network_in"] = network_in
stats["servers"][stats_index]["last_network_out"] = network_out
stats["updated"] = str(int(time.time()))
write_json_file(STATS_PATH, stats)
new_pid = wait_for_sergate_pid(previous_pid=old_pid)
if new_pid:
os.kill(new_pid, signal.SIGHUP)
return {
"server": server,
"stats": {
"network_in": network_in,
"network_out": network_out,
"previous_last_network_in": previous_last_in,
"previous_last_network_out": previous_last_out,
"last_network_in": network_in,
"last_network_out": network_out,
"month_in_before": max(0, network_in - previous_last_in),
"month_out_before": max(0, network_out - previous_last_out),
},
"oldPid": old_pid,
"pid": new_pid,
}
def collection_routes():
endpoints = []
for key in ["monitors", "sslcerts", "watchdog"]:
endpoints.extend([
{"method": "GET", "path": f"/api/{key}", "auth": True},
{"method": "POST", "path": f"/api/{key}", "auth": True, "body": f"{COLLECTIONS[key]['item']} JSON"},
{"method": "PUT", "path": f"/api/{key}/{{index-or-name}}", "auth": True, "body": f"{COLLECTIONS[key]['item']} JSON"},
{"method": "DELETE", "path": f"/api/{key}/{{index-or-name}}", "auth": True},
])
return endpoints
def api_schema():
return {
"auth": {
"type": "bearer",
"header": "Authorization: Bearer <ADMIN_TOKEN>",
"enabled": bool(ADMIN_TOKEN),
},
"endpoints": [
{"method": "GET", "path": "/api/health", "auth": False},
{"method": "GET", "path": "/api/schema", "auth": False},
{"method": "GET", "path": "/api/config", "auth": True},
{"method": "PUT", "path": "/api/config", "auth": True, "body": "full config JSON"},
{"method": "GET", "path": "/api/servers", "auth": True},
{"method": "POST", "path": "/api/servers", "auth": True, "body": "server JSON"},
{"method": "PUT", "path": "/api/servers/{username}", "auth": True, "body": "server JSON"},
{"method": "DELETE", "path": "/api/servers/{username}", "auth": True},
{"method": "POST", "path": "/api/servers/{username}/reset-traffic", "auth": True},
*collection_routes(),
{"method": "POST", "path": "/api/reload", "auth": True},
{"method": "POST", "path": "/api/restart", "auth": True},
],
"collections": {
key: {
"item": meta["item"],
"idField": meta["id_field"],
"required": meta["required"],
"optional": meta["optional"],
}
for key, meta in COLLECTIONS.items()
},
}
class Handler(BaseHTTPRequestHandler):
server_version = "ServerStatusManageAPI/1.0"
def log_message(self, fmt, *args):
print("%s - %s" % (self.address_string(), fmt % args), flush=True)
def end_headers(self):
self.send_header("Cache-Control", "no-store")
if CORS_ORIGIN:
self.send_header("Access-Control-Allow-Origin", CORS_ORIGIN)
self.send_header("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Admin-Token")
self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
super().end_headers()
def send_json(self, status, payload):
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
self.send_response(status)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.end_headers()
self.wfile.write(data)
def send_error_json(self, err):
payload = {"ok": False, "error": err.message}
if err.details is not None:
payload["details"] = err.details
self.send_json(err.status, payload)
def route(self):
parsed = urlparse(self.path)
path = parsed.path.rstrip("/") or "/"
method = self.command.upper()
if method == "OPTIONS":
self.send_response(204)
self.end_headers()
return
try:
if path == "/api/health" and method == "GET":
pid = get_sergate_pid()
self.send_json(200, {
"ok": True,
"enabled": bool(ADMIN_TOKEN),
"sergate": {"running": bool(pid), "pid": pid},
"configPath": CONFIG_PATH,
})
return
if path == "/api/schema" and method == "GET":
self.send_json(200, {"ok": True, "schema": api_schema()})
return
self.require_auth()
if path == "/api/config":
if method == "GET":
self.send_json(200, {"ok": True, "config": load_config()})
return
if method == "PUT":
config = read_body(self)
config, pid = write_and_reload(config)
self.send_json(200, {"ok": True, "reloaded": True, "pid": pid, "config": config})
return
if path == "/api/servers":
if method == "GET":
config = load_config()
self.send_json(200, {"ok": True, "servers": config.get("servers", [])})
return
if method == "POST":
server = validate_server(read_body(self))
config = load_config()
if find_server(config, server["username"])[1] is not None:
raise ApiError(409, "server username already exists", {"username": server["username"]})
config.setdefault("servers", []).append(server)
config, pid = write_and_reload(config)
self.send_json(201, {"ok": True, "server": server, "reloaded": True, "pid": pid, "config": config})
return
if path.startswith("/api/servers/") and path.endswith("/reset-traffic"):
username = unquote(path[len("/api/servers/"):-len("/reset-traffic")].rstrip("/"))
if not username:
raise ApiError(400, "username is required")
if method == "POST":
result = reset_server_month_traffic(username)
self.send_json(200, {"ok": True, "operation": "reset-traffic", **result})
return
if path.startswith("/api/servers/"):
username = unquote(path[len("/api/servers/"):])
if not username:
raise ApiError(400, "username is required")
config = load_config()
index, existing = find_server(config, username)
if index < 0:
raise ApiError(404, "server was not found", {"username": username})
if method == "PUT":
server = validate_server(read_body(self))
if server["username"] != username and find_server(config, server["username"])[1] is not None:
raise ApiError(409, "server username already exists", {"username": server["username"]})
config["servers"][index] = server
config, pid = write_and_reload(config)
self.send_json(200, {"ok": True, "server": server, "reloaded": True, "pid": pid, "config": config})
return
if method == "DELETE":
removed = config["servers"].pop(index)
config, pid = write_and_reload(config)
self.send_json(200, {"ok": True, "removed": removed, "reloaded": True, "pid": pid, "config": config})
return
for key in ["monitors", "sslcerts", "watchdog"]:
base = f"/api/{key}"
meta = COLLECTIONS[key]
if path == base:
if method == "GET":
config = load_config()
self.send_json(200, {"ok": True, key: config.get(key, [])})
return
if method == "POST":
item = meta["validator"](read_body(self))
config = load_config()
config.setdefault(key, []).append(item)
config, pid = write_and_reload(config)
self.send_json(201, {"ok": True, meta["item"]: item, "reloaded": True, "pid": pid, "config": config})
return
if path.startswith(base + "/"):
item_id = unquote(path[len(base) + 1:])
if not item_id:
raise ApiError(400, "item id is required")
config = load_config()
index, existing = find_collection_item(config, key, item_id)
if index < 0:
raise ApiError(404, f"{meta['item']} was not found", {"id": item_id})
if method == "PUT":
item = meta["validator"](read_body(self))
config[key][index] = item
config, pid = write_and_reload(config)
self.send_json(200, {"ok": True, meta["item"]: item, "reloaded": True, "pid": pid, "config": config})
return
if method == "DELETE":
removed = config[key].pop(index)
config, pid = write_and_reload(config)
self.send_json(200, {"ok": True, "removed": removed, "reloaded": True, "pid": pid, "config": config})
return
if path == "/api/reload" and method == "POST":
pid = signal_sergate(signal.SIGHUP)
self.send_json(200, {"ok": True, "operation": "reload", "pid": pid})
return
if path == "/api/restart" and method == "POST":
pid = signal_sergate(signal.SIGTERM)
self.send_json(202, {"ok": True, "operation": "restart", "pid": pid})
return
raise ApiError(404, "endpoint was not found")
except ApiError as err:
self.send_error_json(err)
except Exception as exc:
self.send_error_json(ApiError(500, "internal server error", {"error": str(exc)}))
def require_auth(self):
if not ADMIN_TOKEN:
raise ApiError(503, "management API is disabled; set ADMIN_TOKEN to enable it")
auth = self.headers.get("Authorization", "")
token = ""
if auth.lower().startswith("bearer "):
token = auth[7:].strip()
token = token or self.headers.get("X-Admin-Token", "").strip()
if token != ADMIN_TOKEN:
raise ApiError(401, "invalid or missing admin token")
def do_GET(self):
self.route()
def do_POST(self):
self.route()
def do_PUT(self):
self.route()
def do_DELETE(self):
self.route()
def do_OPTIONS(self):
self.route()
def main():
httpd = ThreadingHTTPServer((API_BIND, API_PORT), Handler)
print(f"manage-api listening on {API_BIND}:{API_PORT}", flush=True)
httpd.serve_forever()
if __name__ == "__main__":
main()
+26
View File
@@ -0,0 +1,26 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ =404;
}
location /json/ {
add_header Cache-Control "no-store";
try_files $uri =404;
}
location /api/ {
proxy_pass http://127.0.0.1:35602;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 30s;
}
}
+46066
View File
File diff suppressed because it is too large Load Diff
+17 -13
View File
@@ -131,13 +131,15 @@ static int new_value
values_size = sizeof (*value->u.object.values) * value->u.object.length; values_size = sizeof (*value->u.object.values) * value->u.object.length;
if (! ((*(void **) &value->u.object.values) = json_alloc void *tmp_alloc = json_alloc(state, values_size + ((unsigned long) value->u.object.values), 0);
(state, values_size + ((unsigned long) value->u.object.values), 0)) ) if (!tmp_alloc)
{ {
return 0; return 0;
} }
/* 避免违反严格别名:通过中间变量复制 */
value->_reserved.object_mem = (*(char **) &value->u.object.values) + values_size; memcpy(&value->u.object.values, &tmp_alloc, sizeof(void*));
char *obj_mem = (char*)value->u.object.values + values_size;
memcpy(&value->_reserved.object_mem, &obj_mem, sizeof(char*));
value->u.object.length = 0; value->u.object.length = 0;
break; break;
@@ -361,16 +363,18 @@ json_value * json_parse_ex (json_settings * settings,
case json_object: case json_object:
if (state.first_pass) if (state.first_pass)
(*(json_char **) &top->u.object.values) += string_length + 1; {
json_char *adv = (json_char*)top->u.object.values;
adv += string_length + 1;
memcpy(&top->u.object.values, &adv, sizeof(json_char*));
}
else else
{ {
top->u.object.values [top->u.object.length].name top->u.object.values[top->u.object.length].name = (json_char *)top->_reserved.object_mem;
= (json_char *) top->_reserved.object_mem; top->u.object.values[top->u.object.length].name_length = string_length;
json_char *adv2 = (json_char*)top->_reserved.object_mem;
top->u.object.values [top->u.object.length].name_length adv2 += string_length + 1;
= string_length; memcpy(&top->_reserved.object_mem, &adv2, sizeof(json_char*));
(*(json_char **) &top->_reserved.object_mem) += string_length + 1;
} }
flags |= flag_seek_value | flag_need_colon; flags |= flag_seek_value | flag_need_colon;
+717 -29
View File
@@ -7,6 +7,231 @@
#include <json.h> #include <json.h>
#include "server.h" #include "server.h"
#include "main.h" #include "main.h"
#include "exprtk.hpp"
#include "curl/curl.h"
#include <stdio.h>
#include <stdlib.h>
#include <string>
// 全局运行标志(需在 SSLCheckThread 定义前初始化)
static volatile int gs_Running = 1;
static volatile int gs_ReloadConfig = 0;
static void JsonEscape(const char *pSrc, char *pDst, int DstSize)
{
if(!pDst || DstSize <= 0)
return;
int Out = 0;
if(!pSrc)
{
pDst[0] = 0;
return;
}
for(const unsigned char *p = (const unsigned char *)pSrc; *p && Out < DstSize - 1; ++p)
{
const char *pEsc = 0;
switch(*p)
{
case '"': pEsc = "\\\""; break;
case '\\': pEsc = "\\\\"; break;
case '\b': pEsc = "\\b"; break;
case '\f': pEsc = "\\f"; break;
case '\n': pEsc = "\\n"; break;
case '\r': pEsc = "\\r"; break;
case '\t': pEsc = "\\t"; break;
default: break;
}
if(pEsc)
{
for(const char *q = pEsc; *q && Out < DstSize - 1; ++q)
pDst[Out++] = *q;
}
else if(*p < 0x20)
{
if(Out < DstSize - 6)
Out += snprintf(pDst + Out, DstSize - Out, "\\u%04x", *p);
else
break;
}
else
pDst[Out++] = *p;
}
pDst[Out] = 0;
}
static int64_t ParseOpenSSLEnddate(const char *line)
{
// line format: notAfter=Aug 12 23:59:59 2025 GMT
const char *p = strstr(line, "notAfter=");
if(!p) return 0;
p += 9;
struct tm tmv; memset(&tmv,0,sizeof(tmv));
char month[4]={0};
int day, hour, min, sec, year;
if(sscanf(p, "%3s %d %d:%d:%d %d GMT", month, &day, &hour, &min, &sec, &year)!=6) return 0;
const char *months="JanFebMarAprMayJunJulAugSepOctNovDec";
const char *mpos = strstr(months, month);
if(!mpos) return 0;
int mon = (int)((mpos - months)/3);
tmv.tm_year = year - 1900;
tmv.tm_mon = mon;
tmv.tm_mday = day;
tmv.tm_hour = hour; tmv.tm_min = min; tmv.tm_sec = sec;
time_t t = timegm(&tmv);
return (int64_t)t;
}
struct SSLCheckThreadData { CMain *pMain; };
static void SSLCheckThread(void *pUser)
{
SSLCheckThreadData *pData = (SSLCheckThreadData*)pUser;
while(gs_Running){
for(int i=0;i<NET_MAX_CLIENTS;i++){
if(!pData->pMain->SSLCert(i) || !strcmp(pData->pMain->SSLCert(i)->m_aName, "NULL")) break;
CMain::CSSLCerts *cert = pData->pMain->SSLCert(i);
time_t nowt = time(0);
if(cert->m_aLastCheck !=0 && (nowt - cert->m_aLastCheck) < cert->m_aInterval) continue;
cert->m_aLastCheck = nowt;
char cmd[1024];
// 说明: 通过 s_client 获取证书,再用 x509 解析到期时间;统一屏蔽 stderr 以防握手失败/非 TLS 端口时刷屏。
// 若配置中写成 https://domain/path 则需要清洗。
char cleanHost[256];
str_copy(cleanHost, cert->m_aDomain, sizeof(cleanHost));
// 去协议
if(!strncasecmp(cleanHost, "https://", 8)) memmove(cleanHost, cleanHost+8, strlen(cleanHost+8)+1);
else if(!strncasecmp(cleanHost, "http://", 7)) memmove(cleanHost, cleanHost+7, strlen(cleanHost+7)+1);
// 去路径
char *slash = strchr(cleanHost, '/'); if(slash) *slash='\0';
// 若含 :port 再截取主机部分(端口由配置提供)
char *colon = strchr(cleanHost, ':'); if(colon) *colon='\0';
int n = snprintf(cmd,sizeof(cmd),"echo | openssl s_client -servername %s -connect %s:%d </dev/null 2>/dev/null | openssl x509 -noout -enddate -text 2>/dev/null", cleanHost, cleanHost, cert->m_aPort);
if(n <= 0 || n >= (int)sizeof(cmd)) continue; // 避免截断执行
FILE *fp = popen(cmd, "r");
if(!fp) continue;
char line[1024]={0};
int foundEnddate=0;
int mismatch = 1; // 默认视为不匹配,发现任一匹配域名再置0
int haveNames = 0;
// 将目标域名转为小写
char target[256]; str_copy(target, cleanHost, sizeof(target));
for(char *p=target; *p; ++p) *p=tolower(*p);
while(fgets(line,sizeof(line),fp)){
if(!foundEnddate){
int64_t expire = ParseOpenSSLEnddate(line);
if(expire>0){ cert->m_aExpireTS = expire; foundEnddate=1; }
}
// 解析 subjectAltName
// 解析 Subject 中的 CN(备用)
char *subj = strstr(line, "Subject:");
if(subj){
char *cn = strstr(subj, " CN=");
if(cn){
cn += 4; // 跳过 ' CN='
char name[256]={0}; int ni=0;
while(*cn && *cn!='/' && *cn!=',' && *cn!='\n' && ni<(int)sizeof(name)-1){ name[ni++]=*cn++; }
name[ni]='\0';
while(ni>0 && (name[ni-1]==' '||name[ni-1]=='\r'||name[ni-1]=='\t')){ name[--ni]='\0'; }
for(char *q=name; *q; ++q) *q=tolower(*q);
if(ni>0){
haveNames=1;
int match=0;
if(name[0]=='*' && name[1]=='.'){
const char *sub = strchr(target,'.');
if(sub && !strcmp(sub+1, name+2)) match=1;
}else if(!strcmp(name,target)) match=1;
if(match){ mismatch=0; }
}
}
}
if(strstr(line, "DNS:")){
char *p = line;
while((p = strstr(p, "DNS:"))){
p += 4; while(*p==' '){p++;}
char name[256]={0}; int ni=0;
while(*p && *p!=',' && *p!='\n' && ni<(int)sizeof(name)-1){ name[ni++]=*p++; }
name[ni]='\0';
// 去空白
while(ni>0 && (name[ni-1]==' '||name[ni-1]=='\r'||name[ni-1]=='\t')){ name[--ni]='\0'; }
for(char *q=name; *q; ++q) *q=tolower(*q);
haveNames=1;
// 通配符匹配 *.example.com
int match=0;
if(name[0]=='*' && name[1]=='.'){
const char *sub = strchr(target,'.');
if(sub && !strcmp(sub+1, name+2)) match=1;
}else if(!strcmp(name,target)) match=1;
if(match){ mismatch=0; goto names_done; }
}
}
}
names_done:
pclose(fp);
if(haveNames){ cert->m_aHostnameMismatch = mismatch ? 1 : 0; }
else { /* 未能提取任何域名,保留原状态,不触发误报 */ }
// 告警: 仅在不匹配且 24h 冷却
if(cert->m_aHostnameMismatch==1){
if(cert->m_aLastAlarmMismatch==0 || nowt - cert->m_aLastAlarmMismatch > 24*3600){
if(strlen(cert->m_aCallback)>0){
CURL *curl = curl_easy_init();
if(curl){
char msg[1024];
snprintf(msg,sizeof(msg),"【SSL证书域名不匹配】%s(%s) 证书域名与配置不一致", cert->m_aName, cert->m_aDomain);
char *enc = curl_easy_escape(curl,msg,0);
char url[1500]; snprintf(url,sizeof(url),"%s%s", cert->m_aCallback, enc?enc:"");
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "signature=ServerStatusSSL");
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 6L);
curl_easy_perform(curl);
if(enc) curl_free(enc);
curl_easy_cleanup(curl);
}
}
cert->m_aLastAlarmMismatch = nowt;
}
}
// alarm logic
if(cert->m_aExpireTS>0){
// 剩余天数: 向下取整 (floor) —— 与 JSON expire_days 保持一致,用于阈值分桶和消息显示
int64_t secsLeft = cert->m_aExpireTS - nowt;
int days = (int)(secsLeft/86400);
int64_t *lastAlarm = NULL; int need=0;
if(days <=7 && days >3){ lastAlarm=&cert->m_aLastAlarm7; }
else if(days <=3 && days >1){ lastAlarm=&cert->m_aLastAlarm3; }
else if(days <=1){ lastAlarm=&cert->m_aLastAlarm1; }
if(lastAlarm && (*lastAlarm==0 || nowt - *lastAlarm > 20*3600)) need=1; // avoid spam, 20h
if(need && strlen(cert->m_aCallback)>0){
CURL *curl = curl_easy_init();
if(curl){
char msg[1024];
char timebuf[32];
time_t expt = (time_t)cert->m_aExpireTS;
strftime(timebuf,sizeof(timebuf),"%Y-%m-%d %H:%M:%S", gmtime(&expt));
// 使用 floor(days)
snprintf(msg,sizeof(msg),"【SSL证书提醒】%s(%s) 将在 %d 天后(%s UTC) 到期", cert->m_aName, cert->m_aDomain, days, timebuf);
char *enc = curl_easy_escape(curl,msg,0);
char url[1500]; snprintf(url,sizeof(url),"%s%s", cert->m_aCallback, enc?enc:"");
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_URL, url);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, "signature=ServerStatusSSL");
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 6L);
curl_easy_perform(curl);
if(enc) curl_free(enc);
curl_easy_cleanup(curl);
}
*lastAlarm = nowt;
}
}
}
thread_sleep(5000);
}
}
#if defined(CONF_FAMILY_UNIX) #if defined(CONF_FAMILY_UNIX)
#include <signal.h> #include <signal.h>
@@ -16,9 +241,6 @@
#define PRId64 "I64d" #define PRId64 "I64d"
#endif #endif
static volatile int gs_Running = 1;
static volatile int gs_ReloadConfig = 0;
static void ExitFunc(int Signal) static void ExitFunc(int Signal)
{ {
printf("[EXIT] Caught signal %d\n", Signal); printf("[EXIT] Caught signal %d\n", Signal);
@@ -90,6 +312,18 @@ void CMain::OnNewClient(int ClientNetID, int ClientID)
Client(ClientID)->m_Stats.m_Online4 = true; Client(ClientID)->m_Stats.m_Online4 = true;
else if(Client(ClientID)->m_ClientNetType == NETTYPE_IPV6) else if(Client(ClientID)->m_ClientNetType == NETTYPE_IPV6)
Client(ClientID)->m_Stats.m_Online6 = true; Client(ClientID)->m_Stats.m_Online6 = true;
// Send monitor to client
// support by cpp.la
int ID = 0;
char monitorBuffer[2048];
while (strcmp(Monitors(ID)->m_aName, "NULL"))
{
memset(monitorBuffer, 0, sizeof(monitorBuffer));
sprintf(monitorBuffer, "{\"name\":\"%s\",\"host\":\"%s\",\"interval\":%d,\"type\":\"%s\",\"monitor\":%d}", Monitors(ID)->m_aName, Monitors(ID)->m_aHost, Monitors(ID)->m_aInterval, Monitors(ID)->m_aType, ID);
m_Server.Network()->Send(ClientNetID, monitorBuffer);
ID++;
}
} }
void CMain::OnDelClient(int ClientNetID) void CMain::OnDelClient(int ClientNetID)
@@ -103,6 +337,10 @@ void CMain::OnDelClient(int ClientNetID)
Client(ClientID)->m_ClientNetType = NETTYPE_INVALID; Client(ClientID)->m_ClientNetType = NETTYPE_INVALID;
mem_zero(&Client(ClientID)->m_Stats, sizeof(CClient::CStats)); mem_zero(&Client(ClientID)->m_Stats, sizeof(CClient::CStats));
} }
m_OfflineAlarmThreadData.pClients = m_aClients;
m_OfflineAlarmThreadData.pWatchDogs = m_aCWatchDogs;
m_OfflineAlarmThreadData.m_ReloadRequired = ClientID;
thread_create(offlineAlarmThread, &m_OfflineAlarmThreadData);
} }
int CMain::HandleMessage(int ClientNetID, char *pMessage) int CMain::HandleMessage(int ClientNetID, char *pMessage)
@@ -178,36 +416,56 @@ int CMain::HandleMessage(int ClientNetID, char *pMessage)
pClient->m_Stats.m_HDDTotal = rStart["hdd_total"].u.integer; pClient->m_Stats.m_HDDTotal = rStart["hdd_total"].u.integer;
if(rStart["hdd_used"].type) if(rStart["hdd_used"].type)
pClient->m_Stats.m_HDDUsed = rStart["hdd_used"].u.integer; pClient->m_Stats.m_HDDUsed = rStart["hdd_used"].u.integer;
if(rStart["io_read"].type)
pClient->m_Stats.m_IORead = rStart["io_read"].u.integer;
if(rStart["io_write"].type)
pClient->m_Stats.m_IOWrite = rStart["io_write"].u.integer;
if(rStart["cpu"].type) if(rStart["cpu"].type)
pClient->m_Stats.m_CPU = rStart["cpu"].u.dbl; pClient->m_Stats.m_CPU = rStart["cpu"].u.dbl;
if(rStart["cpu_cores"].type)
pClient->m_Stats.m_CPUCores = rStart["cpu_cores"].u.integer;
if(rStart["cpu_model"].type == json_string)
str_copy(pClient->m_Stats.m_aCPUModel, rStart["cpu_model"].u.string.ptr, sizeof(pClient->m_Stats.m_aCPUModel));
if(rStart["online4"].type && pClient->m_ClientNetType == NETTYPE_IPV6) if(rStart["online4"].type && pClient->m_ClientNetType == NETTYPE_IPV6)
pClient->m_Stats.m_Online4 = rStart["online4"].u.boolean; pClient->m_Stats.m_Online4 = rStart["online4"].u.boolean;
if(rStart["online6"].type && pClient->m_ClientNetType == NETTYPE_IPV4) if(rStart["online6"].type && pClient->m_ClientNetType == NETTYPE_IPV4)
pClient->m_Stats.m_Online6 = rStart["online6"].u.boolean; pClient->m_Stats.m_Online6 = rStart["online6"].u.boolean;
if(rStart["ip_status"].type)
pClient->m_Stats.m_IpStatus = rStart["ip_status"].u.boolean;
if(rStart["custom"].type == json_string) if(rStart["custom"].type == json_string)
str_copy(pClient->m_Stats.m_aCustom, rStart["custom"].u.string.ptr, sizeof(pClient->m_Stats.m_aCustom)); str_copy(pClient->m_Stats.m_aCustom, rStart["custom"].u.string.ptr, sizeof(pClient->m_Stats.m_aCustom));
// optional OS field from clients
if(rStart["os"].type == json_string)
str_copy(pClient->m_Stats.m_aOS, rStart["os"].u.string.ptr, sizeof(pClient->m_Stats.m_aOS));
//copy message for watchdog to analysis
WatchdogMessage(ClientNetID,
pClient->m_Stats.m_Load_1, pClient->m_Stats.m_Load_5, pClient->m_Stats.m_Load_15,
pClient->m_Stats.m_ping_10010, pClient->m_Stats.m_ping_189, pClient->m_Stats.m_ping_10086,
pClient->m_Stats.m_time_10010, pClient->m_Stats.m_time_189, pClient->m_Stats.m_time_10086,
pClient->m_Stats.m_tcpCount, pClient->m_Stats.m_udpCount, pClient->m_Stats.m_processCount,
pClient->m_Stats.m_threadCount, pClient->m_Stats.m_NetworkRx, pClient->m_Stats.m_NetworkTx,
pClient->m_Stats.m_NetworkIN, pClient->m_Stats.m_NetworkOUT,
pClient->m_LastNetworkIN, pClient->m_LastNetworkOUT, pClient->m_Stats.m_MemTotal,
pClient->m_Stats.m_MemUsed, pClient->m_Stats.m_SwapTotal, pClient->m_Stats.m_SwapUsed,
pClient->m_Stats.m_HDDTotal, pClient->m_Stats.m_HDDUsed, pClient->m_Stats.m_IORead,
pClient->m_Stats.m_IOWrite, pClient->m_Stats.m_CPU, pClient->m_Stats.m_Online4,
pClient->m_Stats.m_Online6);
if(m_Config.m_Verbose) if(m_Config.m_Verbose)
{ {
if(rStart["online4"].type) if(rStart["online4"].type)
dbg_msg("main", "Online4: %s\nUptime: %" PRId64 "\nIpStatus: %s\nLoad_1: %f\nLoad_5: %f\nLoad_15: %f\nPing_10010: %f\nPing_189: %f\nPing_10086: %f\nTime_10010: %" PRId64 "\nTime_189: %" PRId64 "\nTime_10086: %" PRId64 "\nTcp_count: %" PRId64 "\nUdp_count: %" PRId64 "\nprocess_count: %" PRId64 "\nthread_count: %" PRId64 "\nNetworkRx: %" PRId64 "\nNetworkTx: %" PRId64 "\nNetworkIN: %" PRId64 "\nNetworkOUT: %" PRId64 "\nMemTotal: %" PRId64 "\nMemUsed: %" PRId64 "\nSwapTotal: %" PRId64 "\nSwapUsed: %" PRId64 "\nHDDTotal: %" PRId64 "\nHDDUsed: %" PRId64 "\nCPU: %f\n", dbg_msg("main", "Online4: %s\nUptime: %" PRId64 "\nLoad_1: %f\nLoad_5: %f\nLoad_15: %f\nPing_10010: %f\nPing_189: %f\nPing_10086: %f\nTime_10010: %" PRId64 "\nTime_189: %" PRId64 "\nTime_10086: %" PRId64 "\nTcp_count: %" PRId64 "\nUdp_count: %" PRId64 "\nprocess_count: %" PRId64 "\nthread_count: %" PRId64 "\nNetworkRx: %" PRId64 "\nNetworkTx: %" PRId64 "\nNetworkIN: %" PRId64 "\nNetworkOUT: %" PRId64 "\nMemTotal: %" PRId64 "\nMemUsed: %" PRId64 "\nSwapTotal: %" PRId64 "\nSwapUsed: %" PRId64 "\nHDDTotal: %" PRId64 "\nHDDUsed: %" PRId64 "\nCPU: %f\nIORead: %" PRId64 "\nIOWrite: %" PRId64 "\n",
rStart["online4"].u.boolean ? "true" : "false", rStart["online4"].u.boolean ? "true" : "false",
pClient->m_Stats.m_Uptime, pClient->m_Stats.m_Uptime,
pClient->m_Stats.m_IpStatus ? "true" : "false", pClient->m_Stats.m_Load_1, pClient->m_Stats.m_Load_5, pClient->m_Stats.m_Load_15, pClient->m_Stats.m_ping_10010, pClient->m_Stats.m_ping_189, pClient->m_Stats.m_ping_10086, pClient->m_Stats.m_time_10010, pClient->m_Stats.m_time_189, pClient->m_Stats.m_time_10086,pClient->m_Stats.m_tcpCount,pClient->m_Stats.m_udpCount,pClient->m_Stats.m_processCount,pClient->m_Stats.m_threadCount,pClient->m_Stats.m_NetworkRx, pClient->m_Stats.m_NetworkTx, pClient->m_Stats.m_NetworkIN, pClient->m_Stats.m_NetworkOUT, pClient->m_Stats.m_MemTotal, pClient->m_Stats.m_MemUsed, pClient->m_Stats.m_SwapTotal, pClient->m_Stats.m_SwapUsed, pClient->m_Stats.m_HDDTotal, pClient->m_Stats.m_HDDUsed, pClient->m_Stats.m_CPU, pClient->m_Stats.m_IORead, pClient->m_Stats.m_IOWrite);
pClient->m_Stats.m_Load_1, pClient->m_Stats.m_Load_5, pClient->m_Stats.m_Load_15, pClient->m_Stats.m_ping_10010, pClient->m_Stats.m_ping_189, pClient->m_Stats.m_ping_10086, pClient->m_Stats.m_time_10010, pClient->m_Stats.m_time_189, pClient->m_Stats.m_time_10086,pClient->m_Stats.m_tcpCount,pClient->m_Stats.m_udpCount,pClient->m_Stats.m_processCount,pClient->m_Stats.m_threadCount,pClient->m_Stats.m_NetworkRx, pClient->m_Stats.m_NetworkTx, pClient->m_Stats.m_NetworkIN, pClient->m_Stats.m_NetworkOUT, pClient->m_Stats.m_MemTotal, pClient->m_Stats.m_MemUsed, pClient->m_Stats.m_SwapTotal, pClient->m_Stats.m_SwapUsed, pClient->m_Stats.m_HDDTotal, pClient->m_Stats.m_HDDUsed, pClient->m_Stats.m_CPU);
else if(rStart["online6"].type) else if(rStart["online6"].type)
dbg_msg("main", "Online6: %s\nUptime: %" PRId64 "\nIpStatus: %s\nLoad_1: %f\nLoad_5: %f\nLoad_15: %f\nPing_10010: %f\nPing_189: %f\nPing_10086: %f\nTime_10010: %" PRId64 "\nTime_189: %" PRId64 "\nTime_10086: %" PRId64 "\nTcp_count: %" PRId64 "\nUdp_count: %" PRId64 "\nprocess_count: %" PRId64 "\nthread_count: %" PRId64 "\nNetworkRx: %" PRId64 "\nNetworkTx: %" PRId64 "\nNetworkIN: %" PRId64 "\nNetworkOUT: %" PRId64 "\nMemTotal: %" PRId64 "\nMemUsed: %" PRId64 "\nSwapTotal: %" PRId64 "\nSwapUsed: %" PRId64 "\nHDDTotal: %" PRId64 "\nHDDUsed: %" PRId64 "\nCPU: %f\n", dbg_msg("main", "Online6: %s\nUptime: %" PRId64 "\nLoad_1: %f\nLoad_5: %f\nLoad_15: %f\nPing_10010: %f\nPing_189: %f\nPing_10086: %f\nTime_10010: %" PRId64 "\nTime_189: %" PRId64 "\nTime_10086: %" PRId64 "\nTcp_count: %" PRId64 "\nUdp_count: %" PRId64 "\nprocess_count: %" PRId64 "\nthread_count: %" PRId64 "\nNetworkRx: %" PRId64 "\nNetworkTx: %" PRId64 "\nNetworkIN: %" PRId64 "\nNetworkOUT: %" PRId64 "\nMemTotal: %" PRId64 "\nMemUsed: %" PRId64 "\nSwapTotal: %" PRId64 "\nSwapUsed: %" PRId64 "\nHDDTotal: %" PRId64 "\nHDDUsed: %" PRId64 "\nCPU: %f\nIORead: %" PRId64 "\nIOWrite: %" PRId64 "\n",
rStart["online6"].u.boolean ? "true" : "false", rStart["online6"].u.boolean ? "true" : "false",
pClient->m_Stats.m_Uptime, pClient->m_Stats.m_Uptime,
pClient->m_Stats.m_IpStatus ? "true" : "false", pClient->m_Stats.m_Load_1, pClient->m_Stats.m_Load_5, pClient->m_Stats.m_Load_15, pClient->m_Stats.m_ping_10010, pClient->m_Stats.m_ping_189, pClient->m_Stats.m_ping_10086, pClient->m_Stats.m_time_10010, pClient->m_Stats.m_time_189, pClient->m_Stats.m_time_10086,pClient->m_Stats.m_tcpCount,pClient->m_Stats.m_udpCount,pClient->m_Stats.m_processCount,pClient->m_Stats.m_threadCount,pClient->m_Stats.m_NetworkRx, pClient->m_Stats.m_NetworkTx, pClient->m_Stats.m_NetworkIN, pClient->m_Stats.m_NetworkOUT, pClient->m_Stats.m_MemTotal, pClient->m_Stats.m_MemUsed, pClient->m_Stats.m_SwapTotal, pClient->m_Stats.m_SwapUsed, pClient->m_Stats.m_HDDTotal, pClient->m_Stats.m_HDDUsed, pClient->m_Stats.m_CPU, pClient->m_Stats.m_IORead, pClient->m_Stats.m_IOWrite);
pClient->m_Stats.m_Load_1, pClient->m_Stats.m_Load_5, pClient->m_Stats.m_Load_15, pClient->m_Stats.m_ping_10010, pClient->m_Stats.m_ping_189, pClient->m_Stats.m_ping_10086, pClient->m_Stats.m_time_10010, pClient->m_Stats.m_time_189, pClient->m_Stats.m_time_10086,pClient->m_Stats.m_tcpCount,pClient->m_Stats.m_udpCount,pClient->m_Stats.m_processCount,pClient->m_Stats.m_threadCount,pClient->m_Stats.m_NetworkRx, pClient->m_Stats.m_NetworkTx, pClient->m_Stats.m_NetworkIN, pClient->m_Stats.m_NetworkOUT, pClient->m_Stats.m_MemTotal, pClient->m_Stats.m_MemUsed, pClient->m_Stats.m_SwapTotal, pClient->m_Stats.m_SwapUsed, pClient->m_Stats.m_HDDTotal, pClient->m_Stats.m_HDDUsed, pClient->m_Stats.m_CPU);
else else
dbg_msg("main", "Uptime: %" PRId64 "\nIpStatus: %s\nLoad_1: %f\nLoad_5: %f\nLoad_15: %f\nPing_10010: %f\nPing_189: %f\nPing_10086: %f\nTime_10010: %" PRId64 "\nTime_189: %" PRId64 "\nTime_10086: %" PRId64 "\nTcp_count: %" PRId64 "\nUdp_count: %" PRId64 "\nprocess_count: %" PRId64 "\nthread_count: %" PRId64 "\nNetworkRx: %" PRId64 "\nNetworkTx: %" PRId64 "\nNetworkIN: %" PRId64 "\nNetworkOUT: %" PRId64 "\nMemTotal: %" PRId64 "\nMemUsed: %" PRId64 "\nSwapTotal: %" PRId64 "\nSwapUsed: %" PRId64 "\nHDDTotal: %" PRId64 "\nHDDUsed: %" PRId64 "\nCPU: %f\n", dbg_msg("main", "Uptime: %" PRId64 "\nLoad_1: %f\nLoad_5: %f\nLoad_15: %f\nPing_10010: %f\nPing_189: %f\nPing_10086: %f\nTime_10010: %" PRId64 "\nTime_189: %" PRId64 "\nTime_10086: %" PRId64 "\nTcp_count: %" PRId64 "\nUdp_count: %" PRId64 "\nprocess_count: %" PRId64 "\nthread_count: %" PRId64 "\nNetworkRx: %" PRId64 "\nNetworkTx: %" PRId64 "\nNetworkIN: %" PRId64 "\nNetworkOUT: %" PRId64 "\nMemTotal: %" PRId64 "\nMemUsed: %" PRId64 "\nSwapTotal: %" PRId64 "\nSwapUsed: %" PRId64 "\nHDDTotal: %" PRId64 "\nHDDUsed: %" PRId64 "\nCPU: %f\nIORead: %" PRId64 "\nIOWrite: %" PRId64 "\n",
pClient->m_Stats.m_Uptime, pClient->m_Stats.m_Uptime,
pClient->m_Stats.m_IpStatus ? "true" : "false", pClient->m_Stats.m_Load_1, pClient->m_Stats.m_Load_5, pClient->m_Stats.m_Load_15, pClient->m_Stats.m_ping_10010, pClient->m_Stats.m_ping_189, pClient->m_Stats.m_ping_10086, pClient->m_Stats.m_time_10010, pClient->m_Stats.m_time_189, pClient->m_Stats.m_time_10086,pClient->m_Stats.m_tcpCount,pClient->m_Stats.m_udpCount,pClient->m_Stats.m_processCount,pClient->m_Stats.m_threadCount,pClient->m_Stats.m_NetworkRx, pClient->m_Stats.m_NetworkTx, pClient->m_Stats.m_NetworkIN, pClient->m_Stats.m_NetworkOUT, pClient->m_Stats.m_MemTotal, pClient->m_Stats.m_MemUsed, pClient->m_Stats.m_SwapTotal, pClient->m_Stats.m_SwapUsed, pClient->m_Stats.m_HDDTotal, pClient->m_Stats.m_HDDUsed, pClient->m_Stats.m_CPU, pClient->m_Stats.m_IORead, pClient->m_Stats.m_IOWrite);
pClient->m_Stats.m_Load_1, pClient->m_Stats.m_Load_5, pClient->m_Stats.m_Load_15, pClient->m_Stats.m_ping_10010, pClient->m_Stats.m_ping_189, pClient->m_Stats.m_ping_10086, pClient->m_Stats.m_time_10010, pClient->m_Stats.m_time_189, pClient->m_Stats.m_time_10086,pClient->m_Stats.m_tcpCount,pClient->m_Stats.m_udpCount,pClient->m_Stats.m_processCount,pClient->m_Stats.m_threadCount,pClient->m_Stats.m_NetworkRx, pClient->m_Stats.m_NetworkTx, pClient->m_Stats.m_NetworkIN, pClient->m_Stats.m_NetworkOUT, pClient->m_Stats.m_MemTotal, pClient->m_Stats.m_MemUsed, pClient->m_Stats.m_SwapTotal, pClient->m_Stats.m_SwapUsed, pClient->m_Stats.m_HDDTotal, pClient->m_Stats.m_HDDUsed, pClient->m_Stats.m_CPU);
} }
// clean up // clean up
@@ -235,6 +493,151 @@ int CMain::HandleMessage(int ClientNetID, char *pMessage)
return 1; return 1;
} }
void CMain::WatchdogMessage(int ClientNetID, double load_1, double load_5, double load_15, double ping_10010, double ping_189, double ping_10086,
double time_10010, double time_189, double time_10086, double tcp_count, double udp_count, double process_count, double thread_count,
double network_rx, double network_tx, double network_in, double network_out, double last_network_in, double last_network_out, double memory_total, double memory_used,
double swap_total, double swap_used, double hdd_total, double hdd_used, double io_read, double io_write, double cpu,
double online4, double online6)
{
int ID = 0;
while (strcmp(Watchdog(ID)->m_aName, "NULL"))
{
// Exprtk库默认使用窄字符类型,但可能会出现中文等Unicode字符无法正确解析的问题。
// todo: 为解决此问题,可以使用宽字符类型替换Exprtk库中默认的窄字符类型。
// #include <string>
// #include <vector>
// #include <exprtk.hpp>
// typedef exprtk::expression<wchar_t> expression_type;
// typedef exprtk::parser<wchar_t> parser_type;
// int main()
// {
// std::wstring expression_string = L"sin(x)";
// expression_type expression;
// parser_type parser;
// parser.compile(expression_string, expression);
// double x = 3.14;
// double result = expression.value();
// return 0;
// }
typedef exprtk::symbol_table<double> symbol_table_t;
typedef exprtk::expression<double> expression_t;
typedef exprtk::parser<double> parser_t;
const std::string expression_string = Watchdog(ID)->m_aRule;
int ClientID = ClientNetToClient(ClientNetID);
if(ClientID < 0 || ClientID >= NET_MAX_CLIENTS) {
ID++;
continue; // 无效客户端,跳过当前 watchdog 规则
}
std::string username = Client(ClientID)->m_aUsername;
std::string name = Client(ClientID)->m_aName;
std::string type = Client(ClientID)->m_aType;
std::string host = Client(ClientID)->m_aHost;
std::string location = Client(ClientID)->m_aLocation;
symbol_table_t symbol_table;
symbol_table.add_stringvar("username", username);
symbol_table.add_stringvar("name", name);
symbol_table.add_stringvar("type", type);
symbol_table.add_stringvar("host", host);
symbol_table.add_stringvar("location", location);
symbol_table.add_variable("load_1",load_1);
symbol_table.add_variable("load_5",load_5);
symbol_table.add_variable("load_15",load_15);
symbol_table.add_variable("ping_10010",ping_10010);
symbol_table.add_variable("ping_189",ping_189);
symbol_table.add_variable("ping_10086",ping_10086);
symbol_table.add_variable("time_10010",time_10010);
symbol_table.add_variable("time_189",time_189);
symbol_table.add_variable("time_10086",time_10086);
symbol_table.add_variable("tcp_count",tcp_count);
symbol_table.add_variable("udp_count",udp_count);
symbol_table.add_variable("process_count",process_count);
symbol_table.add_variable("thread_count",thread_count);
symbol_table.add_variable("network_rx",network_rx);
symbol_table.add_variable("network_tx",network_tx);
symbol_table.add_variable("network_in",network_in);
symbol_table.add_variable("network_out",network_out);
symbol_table.add_variable("last_network_in",last_network_in);
symbol_table.add_variable("last_network_out",last_network_out);
symbol_table.add_variable("memory_total",memory_total);
symbol_table.add_variable("memory_used",memory_used);
symbol_table.add_variable("swap_total",swap_total);
symbol_table.add_variable("swap_used",swap_used);
symbol_table.add_variable("hdd_total",hdd_total);
symbol_table.add_variable("hdd_used",hdd_used);
symbol_table.add_variable("io_read",io_read);
symbol_table.add_variable("io_write",io_write);
symbol_table.add_variable("cpu",cpu);
symbol_table.add_variable("online4",online4);
symbol_table.add_variable("online6",online6);
symbol_table.add_constants();
expression_t expression;
expression.register_symbol_table(symbol_table);
parser_t parser;
parser.compile(expression_string,expression);
if (expression.value() > 0)
{
time_t currentStamp = (long long)time(/*ago*/0);
if ((currentStamp-Client(ClientID)->m_AlarmLastTime) > Watchdog(ID)->m_aInterval)
{
if (!Client(ClientID)->m_Stats.m_Online4 && !Client(ClientID)->m_Stats.m_Online6)
{
//休眠5分钟如果5分钟后状态发生了变更,消息不发出。
printf("download\n");
}
Client(ClientID)->m_AlarmLastTime = currentStamp;
CURL *curl;
CURLcode res;
curl_global_init(CURL_GLOBAL_ALL);
curl = curl_easy_init();
if(curl) {
//standard time
char standardTime[32]= { 0 };
strftime(standardTime, sizeof(standardTime), "%Y-%m-%d %H:%M:%S",localtime(&currentStamp));
//url encode, Rules conflict with url special characterseg&, del rules, by https://cpp.la, 2023-10-09
char encodeBuffer[2048] = { 0 };
sprintf(encodeBuffer, "【告警名称】 %s \n\n【告警时间】 %s \n\n【用户名】 %s \n\n【节点名】 %s \n\n【虚拟化】 %s \n\n【主机名】 %s \n\n【位 置】 %s",
Watchdog(ID)->m_aName,
standardTime,
username.c_str(),
name.c_str(),
type.c_str(),
host.c_str(),
location.c_str());
char *encodeUrl = curl_easy_escape(curl, encodeBuffer, strlen(encodeBuffer));
//standard url
char urlBuffer[2048] = { 0 };
sprintf(urlBuffer, "%s%s",Watchdog(ID)->m_aCallback, encodeUrl);
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_URL, urlBuffer);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS,"signature=ServerStatus");
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 6L);
res = curl_easy_perform(curl);
if(res != CURLE_OK)
fprintf(stderr, "watchdog failed: %s\n", curl_easy_strerror(res));
if(encodeUrl)
curl_free(encodeUrl);
curl_easy_cleanup(curl);
}
curl_global_cleanup();
}
}
ID++;
}
}
void CMain::JSONUpdateThread(void *pUser) void CMain::JSONUpdateThread(void *pUser)
{ {
CJSONUpdateThreadData *m_pJSONUpdateThreadData = (CJSONUpdateThreadData *)pUser; CJSONUpdateThreadData *m_pJSONUpdateThreadData = (CJSONUpdateThreadData *)pUser;
@@ -269,27 +672,67 @@ void CMain::JSONUpdateThread(void *pUser)
else else
str_format(aUptime, sizeof(aUptime), "%02d:%02d:%02d", (int)(pClients[i].m_Stats.m_Uptime/60.0/60.0), (int)((pClients[i].m_Stats.m_Uptime/60)%60), (int)((pClients[i].m_Stats.m_Uptime)%60)); str_format(aUptime, sizeof(aUptime), "%02d:%02d:%02d", (int)(pClients[i].m_Stats.m_Uptime/60.0/60.0), (int)((pClients[i].m_Stats.m_Uptime/60)%60), (int)((pClients[i].m_Stats.m_Uptime)%60));
// track month network traffic, diff: 2021-10-01 00:05, 5minutes
// last_network_in/out is last record flag.
time_t currentStamp = (long long)time(/*ago*/0);
if(0 == pClients[i].m_LastNetworkIN || (0 != pClients[i].m_Stats.m_NetworkIN && pClients[i].m_LastNetworkIN > pClients[i].m_Stats.m_NetworkIN) || (localtime(&currentStamp)->tm_mday == pClients[i].m_aMonthStart && localtime(&currentStamp)->tm_hour == 0 && localtime(&currentStamp)->tm_min < 5))
{
pClients[i].m_LastNetworkIN = pClients[i].m_Stats.m_NetworkIN;
pClients[i].m_LastNetworkOUT = pClients[i].m_Stats.m_NetworkOUT;
}
char aCustomEsc[2048] = { 0 };
char aOSEsc[128] = { 0 };
char aCPUModelEsc[384] = { 0 };
JsonEscape(pClients[i].m_Stats.m_aCustom, aCustomEsc, sizeof(aCustomEsc));
JsonEscape(pClients[i].m_Stats.m_aOS[0] ? pClients[i].m_Stats.m_aOS : "", aOSEsc, sizeof(aOSEsc));
JsonEscape(pClients[i].m_Stats.m_aCPUModel[0] ? pClients[i].m_Stats.m_aCPUModel : "", aCPUModelEsc, sizeof(aCPUModelEsc));
str_format(pBuf, sizeof(aFileBuf) - (pBuf - aFileBuf), str_format(pBuf, sizeof(aFileBuf) - (pBuf - aFileBuf),
"{ \"name\": \"%s\",\"type\": \"%s\",\"host\": \"%s\",\"location\": \"%s\",\"online4\": %s, \"online6\": %s,\"ip_status\": %s,\"uptime\": \"%s\",\"load_1\": %.2f, \"load_5\": %.2f, \"load_15\": %.2f,\"ping_10010\": %.2f, \"ping_189\": %.2f, \"ping_10086\": %.2f,\"time_10010\": %" PRId64 ", \"time_189\": %" PRId64 ", \"time_10086\": %" PRId64 ", \"tcp_count\": %" PRId64 ", \"udp_count\": %" PRId64 ", \"process_count\": %" PRId64 ", \"thread_count\": %" PRId64 ", \"network_rx\": %" PRId64 ", \"network_tx\": %" PRId64 ", \"network_in\": %" PRId64 ", \"network_out\": %" PRId64 ", \"cpu\": %d, \"memory_total\": %" PRId64 ", \"memory_used\": %" PRId64 ", \"swap_total\": %" PRId64 ", \"swap_used\": %" PRId64 ", \"hdd_total\": %" PRId64 ", \"hdd_used\": %" PRId64 ", \"custom\": \"%s\" },\n", "{ \"name\": \"%s\",\"type\": \"%s\",\"host\": \"%s\",\"location\": \"%s\",\"online4\": %s, \"online6\": %s, \"uptime\": \"%s\",\"load_1\": %.2f, \"load_5\": %.2f, \"load_15\": %.2f,\"ping_10010\": %.2f, \"ping_189\": %.2f, \"ping_10086\": %.2f,\"time_10010\": %" PRId64 ", \"time_189\": %" PRId64 ", \"time_10086\": %" PRId64 ", \"tcp_count\": %" PRId64 ", \"udp_count\": %" PRId64 ", \"process_count\": %" PRId64 ", \"thread_count\": %" PRId64 ", \"network_rx\": %" PRId64 ", \"network_tx\": %" PRId64 ", \"network_in\": %" PRId64 ", \"network_out\": %" PRId64 ", \"cpu\": %d, \"cpu_cores\": %" PRId64 ", \"cpu_model\": \"%s\", \"memory_total\": %" PRId64 ", \"memory_used\": %" PRId64 ", \"swap_total\": %" PRId64 ", \"swap_used\": %" PRId64 ", \"hdd_total\": %" PRId64 ", \"hdd_used\": %" PRId64 ", \"last_network_in\": %" PRId64 ", \"last_network_out\": %" PRId64 ",\"io_read\": %" PRId64 ", \"io_write\": %" PRId64 ",\"custom\": \"%s\", \"os\": \"%s\" },\n",
pClients[i].m_aName,pClients[i].m_aType,pClients[i].m_aHost,pClients[i].m_aLocation, pClients[i].m_aName,pClients[i].m_aType,pClients[i].m_aHost,pClients[i].m_aLocation,
pClients[i].m_Stats.m_Online4 ? "true" : "false",pClients[i].m_Stats.m_Online6 ? "true" : "false",pClients[i].m_Stats.m_IpStatus ? "true": "false", pClients[i].m_Stats.m_Online4 ? "true" : "false",pClients[i].m_Stats.m_Online6 ? "true" : "false",
aUptime, pClients[i].m_Stats.m_Load_1, pClients[i].m_Stats.m_Load_5, pClients[i].m_Stats.m_Load_15, pClients[i].m_Stats.m_ping_10010, pClients[i].m_Stats.m_ping_189, pClients[i].m_Stats.m_ping_10086, pClients[i].m_Stats.m_time_10010, pClients[i].m_Stats.m_time_189, pClients[i].m_Stats.m_time_10086,pClients[i].m_Stats.m_tcpCount,pClients[i].m_Stats.m_udpCount,pClients[i].m_Stats.m_processCount,pClients[i].m_Stats.m_threadCount,pClients[i].m_Stats.m_NetworkRx, pClients[i].m_Stats.m_NetworkTx, pClients[i].m_Stats.m_NetworkIN, pClients[i].m_Stats.m_NetworkOUT, (int)pClients[i].m_Stats.m_CPU, pClients[i].m_Stats.m_MemTotal, pClients[i].m_Stats.m_MemUsed, pClients[i].m_Stats.m_SwapTotal, pClients[i].m_Stats.m_SwapUsed, pClients[i].m_Stats.m_HDDTotal, pClients[i].m_Stats.m_HDDUsed, pClients[i].m_Stats.m_aCustom); aUptime, pClients[i].m_Stats.m_Load_1, pClients[i].m_Stats.m_Load_5, pClients[i].m_Stats.m_Load_15, pClients[i].m_Stats.m_ping_10010, pClients[i].m_Stats.m_ping_189, pClients[i].m_Stats.m_ping_10086,
pClients[i].m_Stats.m_time_10010, pClients[i].m_Stats.m_time_189, pClients[i].m_Stats.m_time_10086,pClients[i].m_Stats.m_tcpCount,pClients[i].m_Stats.m_udpCount,pClients[i].m_Stats.m_processCount,pClients[i].m_Stats.m_threadCount,
pClients[i].m_Stats.m_NetworkRx, pClients[i].m_Stats.m_NetworkTx, pClients[i].m_Stats.m_NetworkIN, pClients[i].m_Stats.m_NetworkOUT, (int)pClients[i].m_Stats.m_CPU, pClients[i].m_Stats.m_CPUCores, aCPUModelEsc, pClients[i].m_Stats.m_MemTotal, pClients[i].m_Stats.m_MemUsed,
pClients[i].m_Stats.m_SwapTotal, pClients[i].m_Stats.m_SwapUsed, pClients[i].m_Stats.m_HDDTotal, pClients[i].m_Stats.m_HDDUsed,
pClients[i].m_Stats.m_NetworkIN == 0 || pClients[i].m_LastNetworkIN == 0 ? pClients[i].m_Stats.m_NetworkIN : pClients[i].m_LastNetworkIN,
pClients[i].m_Stats.m_NetworkOUT == 0 || pClients[i].m_LastNetworkOUT == 0 ? pClients[i].m_Stats.m_NetworkOUT : pClients[i].m_LastNetworkOUT,
pClients[i].m_Stats.m_IORead, pClients[i].m_Stats.m_IOWrite,
aCustomEsc,
aOSEsc);
pBuf += strlen(pBuf); pBuf += strlen(pBuf);
} }
else else
{ {
str_format(pBuf, sizeof(aFileBuf) - (pBuf - aFileBuf), "{ \"name\": \"%s\", \"type\": \"%s\", \"host\": \"%s\", \"location\": \"%s\", \"online4\": false, \"online6\": false },\n", // sava network traffic record to json when close client
pClients[i].m_aName, pClients[i].m_aType, pClients[i].m_aHost, pClients[i].m_aLocation); // last_network_in == last network in record, last_network_out == last network out record
char aOSEsc[128] = { 0 };
char aCPUModelEsc[384] = { 0 };
JsonEscape(pClients[i].m_Stats.m_aOS[0] ? pClients[i].m_Stats.m_aOS : "", aOSEsc, sizeof(aOSEsc));
JsonEscape(pClients[i].m_Stats.m_aCPUModel[0] ? pClients[i].m_Stats.m_aCPUModel : "", aCPUModelEsc, sizeof(aCPUModelEsc));
str_format(pBuf, sizeof(aFileBuf) - (pBuf - aFileBuf), "{ \"name\": \"%s\", \"type\": \"%s\", \"host\": \"%s\", \"location\": \"%s\", \"online4\": false, \"online6\": false, \"last_network_in\": %" PRId64 ", \"last_network_out\": %" PRId64 ", \"os\": \"%s\", \"cpu_model\": \"%s\" },\n",
pClients[i].m_aName, pClients[i].m_aType, pClients[i].m_aHost, pClients[i].m_aLocation, pClients[i].m_LastNetworkIN, pClients[i].m_LastNetworkOUT,
aOSEsc, aCPUModelEsc);
pBuf += strlen(pBuf); pBuf += strlen(pBuf);
} }
} }
if(!m_pJSONUpdateThreadData->m_ReloadRequired) // append ssl certs data
str_format(pBuf - 2, sizeof(aFileBuf) - (pBuf - aFileBuf), "\n],\n\"updated\": \"%lld\"\n}", (long long)time(/*ago*/0)); str_format(pBuf - 2, sizeof(aFileBuf) - (pBuf - aFileBuf), "\n],\n\"sslcerts\": [\n");
else pBuf += strlen(pBuf);
for(int si = 0; si < NET_MAX_CLIENTS; si++)
{ {
str_format(pBuf - 2, sizeof(aFileBuf) - (pBuf - aFileBuf), "\n],\n\"updated\": \"%lld\",\n\"reload\": true\n}", (long long)time(/*ago*/0)); if(!m_pJSONUpdateThreadData->pMain->SSLCert(si) || !strcmp(m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aName, "NULL")) break;
m_pJSONUpdateThreadData->m_ReloadRequired--; int64_t expire_ts = m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aExpireTS;
int expire_days = 0;
if(expire_ts>0){
int64_t nowts = (long long)time(/*ago*/0);
expire_days = (int)((expire_ts - nowts)/86400);
}
str_format(pBuf, sizeof(aFileBuf) - (pBuf - aFileBuf), "{ \"name\": \"%s\", \"domain\": \"%s\", \"port\": %d, \"expire_ts\": %lld, \"expire_days\": %d, \"mismatch\": %s },\n", m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aName, m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aDomain, m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aPort, (long long)expire_ts, expire_days, m_pJSONUpdateThreadData->pMain->SSLCert(si)->m_aHostnameMismatch?"true":"false");
pBuf += strlen(pBuf);
} }
if(pBuf - aFileBuf >= 2) str_format(pBuf - 2, sizeof(aFileBuf) - (pBuf - aFileBuf), "\n],\n\"updated\": \"%lld\"%s\n}", (long long)time(/*ago*/0), m_pJSONUpdateThreadData->m_ReloadRequired?",\n\"reload\": true":"");
if(m_pJSONUpdateThreadData->m_ReloadRequired) m_pJSONUpdateThreadData->m_ReloadRequired--;
pBuf += strlen(pBuf); pBuf += strlen(pBuf);
char aJSONFileTmp[1024]; char aJSONFileTmp[1024];
@@ -306,7 +749,114 @@ void CMain::JSONUpdateThread(void *pUser)
fs_rename(aJSONFileTmp, pConfig->m_aJSONFile); fs_rename(aJSONFileTmp, pConfig->m_aJSONFile);
thread_sleep(1000); thread_sleep(1000);
} }
fs_remove(pConfig->m_aJSONFile); // support by: https://cpp.la. don't remove month traffic record, storage as "stats.json~", remark: 2021-10-18
// fs_remove(pConfig->m_aJSONFile);
char aJSONFileTmp[1024];
str_format(aJSONFileTmp, sizeof(aJSONFileTmp), "%s~", pConfig->m_aJSONFile);
fs_rename(pConfig->m_aJSONFile, aJSONFileTmp);
}
void CMain::offlineAlarmThread(void *pUser)
{
CJSONUpdateThreadData *m_OfflineAlarmThreadData = (CJSONUpdateThreadData *)pUser;
CClient *pClients = m_OfflineAlarmThreadData->pClients;
CWatchDog *pWatchDogs = m_OfflineAlarmThreadData->pWatchDogs;
volatile short ClientID = m_OfflineAlarmThreadData->m_ReloadRequired;
thread_sleep(25000);
if(!pClients[ClientID].m_Connected)
{
int ID = 0;
while (strcmp(pWatchDogs[ID].m_aName, "NULL"))
{
typedef exprtk::symbol_table<double> symbol_table_t;
typedef exprtk::expression<double> expression_t;
typedef exprtk::parser<double> parser_t;
const std::string expression_string = pWatchDogs[ID].m_aRule;
std::string username = pClients[ClientID].m_aUsername;
std::string name = pClients[ClientID].m_aName;
std::string type = pClients[ClientID].m_aType;
std::string host = pClients[ClientID].m_aHost;
std::string location = pClients[ClientID].m_aLocation;
std::double_t online4 = pClients[ClientID].m_Stats.m_Online4;
std::double_t online6 = pClients[ClientID].m_Stats.m_Online6;
symbol_table_t symbol_table;
symbol_table.add_stringvar("username", username);
symbol_table.add_stringvar("name", name);
symbol_table.add_stringvar("type", type);
symbol_table.add_stringvar("host", host);
symbol_table.add_stringvar("location", location);
symbol_table.add_variable("online4",online4);
symbol_table.add_variable("online6",online6);
symbol_table.add_constants();
expression_t expression;
expression.register_symbol_table(symbol_table);
parser_t parser;
parser.compile(expression_string,expression);
if (expression.value() > 0)
{
time_t currentStamp = (long long)time(/*ago*/0);
if ((currentStamp-pClients[ClientID].m_AlarmLastTime) > pWatchDogs[ID].m_aInterval)
{
printf("客户端下线且超过阈值, Client disconnects and sends alert information\n");
pClients[ClientID].m_AlarmLastTime = currentStamp;
CURL *curl;
CURLcode res;
curl_global_init(CURL_GLOBAL_ALL);
curl = curl_easy_init();
if(curl) {
//standard time
char standardTime[32]= { 0 };
strftime(standardTime, sizeof(standardTime), "%Y-%m-%d %H:%M:%S",localtime(&currentStamp));
//url encode, Rules conflict with url special characterseg&, del rules, by https://cpp.la, 2023-10-09
char encodeBuffer[2048] = { 0 };
sprintf(encodeBuffer, "【告警名称】 %s \n\n【告警时间】 %s \n\n【用户名】 %s \n\n【节点名】 %s \n\n【虚拟化】 %s \n\n【主机名】 %s \n\n【位 置】 %s",
pWatchDogs[ID].m_aName,
standardTime,
pClients[ClientID].m_aUsername,
pClients[ClientID].m_aName,
pClients[ClientID].m_aType,
pClients[ClientID].m_aHost,
pClients[ClientID].m_aLocation);
char *encodeUrl = curl_easy_escape(curl, encodeBuffer, strlen(encodeBuffer));
//standard url
char urlBuffer[2048] = { 0 };
sprintf(urlBuffer, "%s%s",pWatchDogs[ID].m_aCallback, encodeUrl);
curl_easy_setopt(curl, CURLOPT_POST, 1L);
curl_easy_setopt(curl, CURLOPT_URL, urlBuffer);
curl_easy_setopt(curl, CURLOPT_POSTFIELDS,"signature=ServerStatus");
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
curl_easy_setopt(curl, CURLOPT_CONNECTTIMEOUT, 3L);
curl_easy_setopt(curl, CURLOPT_TIMEOUT, 6L);
res = curl_easy_perform(curl);
if(res != CURLE_OK)
fprintf(stderr, "watchdog failed: %s\n", curl_easy_strerror(res));
if(encodeUrl)
curl_free(encodeUrl);
curl_easy_cleanup(curl);
}
curl_global_cleanup();
}
else
printf("客户端下线但未超过阈值,No alarm if the threshold is not exceeded\n");
}
ID++;
}
}
else
{
printf("网络波动,No alarm information is sent due to network fluctuations\n");
}
fflush(stdout);
} }
int CMain::ReadConfig() int CMain::ReadConfig()
@@ -367,21 +917,155 @@ int CMain::ReadConfig()
str_copy(Client(ID)->m_aHost, rStart[i]["host"].u.string.ptr, sizeof(Client(ID)->m_aHost)); str_copy(Client(ID)->m_aHost, rStart[i]["host"].u.string.ptr, sizeof(Client(ID)->m_aHost));
str_copy(Client(ID)->m_aLocation, rStart[i]["location"].u.string.ptr, sizeof(Client(ID)->m_aLocation)); str_copy(Client(ID)->m_aLocation, rStart[i]["location"].u.string.ptr, sizeof(Client(ID)->m_aLocation));
str_copy(Client(ID)->m_aPassword, rStart[i]["password"].u.string.ptr, sizeof(Client(ID)->m_aPassword)); str_copy(Client(ID)->m_aPassword, rStart[i]["password"].u.string.ptr, sizeof(Client(ID)->m_aPassword));
//if month start day > 28, diff: 3days(29,30,31)
Client(ID)->m_aMonthStart = rStart[i]["monthstart"].u.integer;
if(Client(ID)->m_aMonthStart > 28)
{
Client(ID)->m_aMonthStart = 28;
}
Client(ID)->m_LastNetworkIN = 0;
Client(ID)->m_LastNetworkOUT = 0;
if(m_Config.m_Verbose) if(m_Config.m_Verbose)
{ {
if(Client(ID)->m_Disabled) if(Client(ID)->m_Disabled)
dbg_msg("main", "[#%d: Name: \"%s\", Username: \"%s\", Type: \"%s\", Host: \"%s\", Location: \"%s\", Password: \"%s\"]", dbg_msg("main", "[#%d: Name: \"%s\", Username: \"%s\", Type: \"%s\", Host: \"%s\", Location: \"%s\", Password: \"%s\", MonthStart: %\" PRId64 \"]",
ID, Client(ID)->m_aName, Client(ID)->m_aUsername, Client(ID)->m_aType, Client(ID)->m_aHost, Client(ID)->m_aLocation, Client(ID)->m_aPassword); ID, Client(ID)->m_aName, Client(ID)->m_aUsername, Client(ID)->m_aType, Client(ID)->m_aHost, Client(ID)->m_aLocation, Client(ID)->m_aPassword, Client(ID)->m_aMonthStart);
else else
dbg_msg("main", "#%d: Name: \"%s\", Username: \"%s\", Type: \"%s\", Host: \"%s\", Location: \"%s\", Password: \"%s\"", dbg_msg("main", "#%d: Name: \"%s\", Username: \"%s\", Type: \"%s\", Host: \"%s\", Location: \"%s\", Password: \"%s\", MonthStart: %\" PRId64 \"",
ID, Client(ID)->m_aName, Client(ID)->m_aUsername, Client(ID)->m_aType, Client(ID)->m_aHost, Client(ID)->m_aLocation, Client(ID)->m_aPassword); ID, Client(ID)->m_aName, Client(ID)->m_aUsername, Client(ID)->m_aType, Client(ID)->m_aHost, Client(ID)->m_aLocation, Client(ID)->m_aPassword, Client(ID)->m_aMonthStart);
} }
ID++; ID++;
} }
} }
// watch dog
// support by: https://cpp.la
ID = 0;
const json_value &jStart = (*pJsonData)["watchdog"];
if(jStart.type == json_array)
{
for(unsigned i = 0; i < jStart.u.array.length; i++)
{
if(ID < 0 || ID >= NET_MAX_CLIENTS)
continue;
str_copy(Watchdog(ID)->m_aName, jStart[i]["name"].u.string.ptr, sizeof(Watchdog(ID)->m_aName));
str_copy(Watchdog(ID)->m_aRule, jStart[i]["rule"].u.string.ptr, sizeof(Watchdog(ID)->m_aRule));
Watchdog(ID)->m_aInterval = jStart[i]["interval"].u.integer;
str_copy(Watchdog(ID)->m_aCallback, jStart[i]["callback"].u.string.ptr, sizeof(Watchdog(ID)->m_aCallback));
ID++;
}
str_copy(Watchdog(ID)->m_aName, "NULL", sizeof(Watchdog(ID)->m_aName));
}
else
{
str_copy(Watchdog(ID)->m_aName, "NULL", sizeof(Watchdog(ID)->m_aName));
}
// monitor
// support by: https://cpp.la
ID = 0;
const json_value &mStart = (*pJsonData)["monitors"];
if(mStart.type == json_array)
{
for(unsigned i = 0; i < mStart.u.array.length; i++)
{
if(ID < 0 || ID >= NET_MAX_CLIENTS)
continue;
str_copy(Monitors(ID)->m_aName, mStart[i]["name"].u.string.ptr, sizeof(Monitors(ID)->m_aName));
str_copy(Monitors(ID)->m_aHost, mStart[i]["host"].u.string.ptr, sizeof(Monitors(ID)->m_aHost));
Monitors(ID)->m_aInterval = mStart[i]["interval"].u.integer;
str_copy(Monitors(ID)->m_aType, mStart[i]["type"].u.string.ptr, sizeof(Monitors(ID)->m_aType));
ID++;
}
str_copy(Monitors(ID)->m_aName, "NULL", sizeof(Monitors(ID)->m_aName));
}
else
{
str_copy(Monitors(ID)->m_aName, "NULL", sizeof(Monitors(ID)->m_aName));
}
// sslcerts
ID = 0;
const json_value &sStart = (*pJsonData)["sslcerts"];
if(sStart.type == json_array)
{
for(unsigned i = 0; i < sStart.u.array.length; i++)
{
if(ID < 0 || ID >= NET_MAX_CLIENTS)
continue;
str_copy(SSLCert(ID)->m_aName, sStart[i]["name"].u.string.ptr, sizeof(SSLCert(ID)->m_aName));
str_copy(SSLCert(ID)->m_aDomain, sStart[i]["domain"].u.string.ptr, sizeof(SSLCert(ID)->m_aDomain));
SSLCert(ID)->m_aPort = sStart[i]["port"].u.integer;
SSLCert(ID)->m_aInterval = sStart[i]["interval"].u.integer;
str_copy(SSLCert(ID)->m_aCallback, sStart[i]["callback"].u.string.ptr, sizeof(SSLCert(ID)->m_aCallback));
SSLCert(ID)->m_aExpireTS = 0; // reset
SSLCert(ID)->m_aLastCheck = 0;
SSLCert(ID)->m_aLastAlarm7 = 0;
SSLCert(ID)->m_aLastAlarm3 = 0;
SSLCert(ID)->m_aLastAlarm1 = 0;
ID++;
}
str_copy(SSLCert(ID)->m_aName, "NULL", sizeof(SSLCert(ID)->m_aName));
}else
str_copy(SSLCert(ID)->m_aName, "NULL", sizeof(SSLCert(ID)->m_aName));
// if file exists, read last network traffic recordreset m_LastNetworkIN and m_LastNetworkOUT
// support by: https://cpp.la
IOHANDLE nFile = io_open(m_Config.m_aJSONFile, IOFLAG_READ);
if(!nFile)
{
char aJSONFileTmp[1024];
str_format(aJSONFileTmp, sizeof(aJSONFileTmp), "%s~", m_Config.m_aJSONFile);
nFile = io_open(aJSONFileTmp, IOFLAG_READ);
}
if(nFile)
{
int nFileSize = (int)io_length(nFile);
char *pNFileData = (char *)mem_alloc(nFileSize + 1, 1);
io_read(nFile, pNFileData, nFileSize);
pNFileData[nFileSize] = 0;
io_close(nFile);
json_settings nJsonSettings;
mem_zero(&nJsonSettings, sizeof(nJsonSettings));
json_value *pNJsonData = json_parse_ex(&nJsonSettings, pNFileData, strlen(pNFileData), aError);
if(pNJsonData)
{
const json_value &cStart = (*pNJsonData)["servers"];
if(rStart.type == json_array)
{
int ID = 0;
for(unsigned i = 0; i < rStart.u.array.length; i++)
{
if(ID < 0 || ID >= NET_MAX_CLIENTS)
continue;
for(unsigned j = 0; j < cStart.u.array.length; j++)
{
if(strcmp(Client(ID)->m_aName, cStart[j]["name"].u.string.ptr)==0 &&
strcmp(Client(ID)->m_aType, cStart[j]["type"].u.string.ptr)==0 &&
strcmp(Client(ID)->m_aHost, cStart[j]["host"].u.string.ptr)==0 &&
strcmp(Client(ID)->m_aLocation, cStart[j]["location"].u.string.ptr)==0)
{
Client(ID)->m_LastNetworkIN = cStart[j]["last_network_in"].u.integer;
Client(ID)->m_LastNetworkOUT = cStart[j]["last_network_out"].u.integer;
break;
}
}
ID++;
}
}
json_value_free(pNJsonData);
}
mem_free(pNFileData);
}
// clean up // clean up
json_value_free(pJsonData); json_value_free(pJsonData);
mem_free(pFileData); mem_free(pFileData);
@@ -404,7 +1088,11 @@ int CMain::Run()
m_JSONUpdateThreadData.m_ReloadRequired = 2; m_JSONUpdateThreadData.m_ReloadRequired = 2;
m_JSONUpdateThreadData.pClients = m_aClients; m_JSONUpdateThreadData.pClients = m_aClients;
m_JSONUpdateThreadData.pConfig = &m_Config; m_JSONUpdateThreadData.pConfig = &m_Config;
m_JSONUpdateThreadData.pWatchDogs = m_aCWatchDogs;
m_JSONUpdateThreadData.pMain = this;
void *LoadThread = thread_create(JSONUpdateThread, &m_JSONUpdateThreadData); void *LoadThread = thread_create(JSONUpdateThread, &m_JSONUpdateThreadData);
// Start SSL check thread
static SSLCheckThreadData sslData; sslData.pMain = this; thread_create(SSLCheckThread, &sslData);
//thread_detach(LoadThread); //thread_detach(LoadThread);
while(gs_Running) while(gs_Running)
+59 -5
View File
@@ -36,15 +36,20 @@ class CMain
char m_aHost[128]; char m_aHost[128];
char m_aLocation[128]; char m_aLocation[128];
char m_aPassword[128]; char m_aPassword[128];
int m_aMonthStart; //track month network traffic. by: https://cpp.la
int64 m_TimeConnected; int64_t m_LastNetworkIN; //restore month traffic info.
int64 m_LastUpdate; int64_t m_LastNetworkOUT; //restore month traffic info.
int64_t m_TimeConnected;
int64_t m_LastUpdate;
int64_t m_AlarmLastTime; //record last alarm time.
struct CStats struct CStats
{ {
bool m_Online4; bool m_Online4;
bool m_Online6; bool m_Online6;
bool m_IpStatus; //mh361 or mh370, mourn mh370, 2014-03-08 01:20 lost from all over the world. by:cpp.la // bool m_IpStatus delete ip_status check, Duplicate packet loss rate detection
// mh361 or mh370, mourn mh370, 2014-03-08 01:20 lost from all over the world. by:https://cpp.la
int64_t m_Uptime; int64_t m_Uptime;
double m_Load_1; double m_Load_1;
double m_Load_5; double m_Load_5;
@@ -69,21 +74,59 @@ class CMain
int64_t m_udpCount; int64_t m_udpCount;
int64_t m_processCount; int64_t m_processCount;
int64_t m_threadCount; int64_t m_threadCount;
int64_t m_IORead;
int64_t m_IOWrite;
int64_t m_CPUCores;
double m_CPU; double m_CPU;
char m_aCustom[512]; char m_aCustom[1024];
char m_aCPUModel[192];
// OS name reported by client (e.g. linux/windows/darwin/freebsd)
char m_aOS[64];
// Options // Options
bool m_Pong; bool m_Pong;
} m_Stats; } m_Stats;
} m_aClients[NET_MAX_CLIENTS]; } m_aClients[NET_MAX_CLIENTS];
struct CWatchDog{
char m_aName[128];
char m_aRule[128];
int m_aInterval;
char m_aCallback[1024];
} m_aCWatchDogs[NET_MAX_CLIENTS];
struct CMonitors{
char m_aName[128];
char m_aHost[128];
int m_aInterval;
char m_aType[128];
} m_aCMonitors[NET_MAX_CLIENTS];
public:
struct CSSLCerts{
char m_aName[128];
char m_aDomain[256];
int m_aPort;
int m_aInterval; // seconds
char m_aCallback[1024];
int64_t m_aExpireTS; // epoch seconds cache
int64_t m_aLastCheck; // last check time
int64_t m_aLastAlarm7;
int64_t m_aLastAlarm3;
int64_t m_aLastAlarm1;
int m_aHostnameMismatch; // 1: 域名与证书不匹配
int64_t m_aLastAlarmMismatch; // 上次不匹配告警时间
} m_aCSSLCerts[NET_MAX_CLIENTS];
struct CJSONUpdateThreadData struct CJSONUpdateThreadData
{ {
CClient *pClients; CClient *pClients;
CConfig *pConfig; CConfig *pConfig;
CWatchDog *pWatchDogs;
CMain *pMain;
volatile short m_ReloadRequired; volatile short m_ReloadRequired;
} m_JSONUpdateThreadData; } m_JSONUpdateThreadData, m_OfflineAlarmThreadData;
static void JSONUpdateThread(void *pUser); static void JSONUpdateThread(void *pUser);
static void offlineAlarmThread(void *pUser);
public: public:
CMain(CConfig Config); CMain(CConfig Config);
@@ -93,6 +136,17 @@ public:
int ReadConfig(); int ReadConfig();
int Run(); int Run();
CWatchDog *Watchdog(int ruleID) { return &m_aCWatchDogs[ruleID]; }
CMonitors *Monitors(int ruleID) { return &m_aCMonitors[ruleID]; }
CSSLCerts *SSLCert(int ruleID) { return &m_aCSSLCerts[ruleID]; }
void WatchdogMessage(int ClientNetID,
double load_1, double load_5, double load_15, double ping_10010, double ping_189, double ping_10086,
double time_10010, double time_189, double time_10086, double tcp_count, double udp_count, double process_count, double thread_count,
double network_rx, double network_tx, double network_in, double network_out, double last_network_in, double last_network_out,
double memory_total, double memory_used,double swap_total, double swap_used, double hdd_total,
double hdd_used, double io_read, double io_write, double cpu,double online4, double online6);
CClient *Client(int ClientID) { return &m_aClients[ClientID]; } CClient *Client(int ClientID) { return &m_aClients[ClientID]; }
CClient *ClientNet(int ClientNetID); CClient *ClientNet(int ClientNetID);
const CConfig *Config() const { return &m_Config; } const CConfig *Config() const { return &m_Config; }
+1 -1
View File
@@ -10,7 +10,7 @@ enum
NET_CONNSTATE_ERROR=4, NET_CONNSTATE_ERROR=4,
NET_MAX_PACKETSIZE = 1400, NET_MAX_PACKETSIZE = 1400,
NET_MAX_CLIENTS = 128 NET_MAX_CLIENTS = 512
}; };
typedef int (*NETFUNC_DELCLIENT)(int ClientID, const char* pReason, void *pUser); typedef int (*NETFUNC_DELCLIENT)(int ClientID, const char* pReason, void *pUser);
+9
View File
@@ -0,0 +1,9 @@
[Unit]
Description=ServerStatus-Client
After=network.target
[Service]
ExecStart=/usr/bin/python3 /usr/local/ServerStatus/clients/client-linux.py
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
[Install]
WantedBy=multi-user.target
+9
View File
@@ -0,0 +1,9 @@
[Unit]
Description=ServerStatus-Server
After=network.target
[Service]
ExecStart=/usr/local/ServerStatus/server/sergate --config=/usr/local/ServerStatus/server/config.json --web-dir=/usr/local/ServerStatus/web
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
[Install]
WantedBy=multi-user.target
+1041
View File
File diff suppressed because it is too large Load Diff
+406
View File
@@ -0,0 +1,406 @@
:root{color-scheme:dark;--bg:#0d1117;--bg-alt:#171d27;--surface:#1d2530;--surface-soft:#151b24;--border:#334155;--text:#f1f5f9;--text-dim:#a7b4c8;--accent:#3b82f6;--accent-glow:#7db5ff;--danger:#f05252;--warn:#fbbf24;--ok:#10b981;--radius:10px;--radius-sm:4px;--shadow:0 8px 22px -10px rgba(0,0,0,.75);--font:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,'Noto Sans SC',sans-serif;--trans:.25s cubic-bezier(.4,0,.2,1);--logo-start:#3b82f6;--logo-end:#2563eb;--logo-accent-grad-start:#8bc1ff;--logo-accent-grad-end:#d4e8ff}
body.light{color-scheme:light;--bg:#f6f7f9;--bg-alt:#ffffff;--surface:#ffffff;--surface-soft:#f8fafc;--border:#e2e8f0;--text:#1e293b;--text-dim:#64748b;--accent:#2563eb;--accent-glow:#3b82f6;--shadow:0 4px 20px -4px rgba(0,0,0,.08);--logo-start:#2563eb;--logo-end:#1d4ed8;--logo-accent-grad-start:#1d4ed8;--logo-accent-grad-end:#60a5fa}
*{box-sizing:border-box}
html,body{height:100%;margin:0;padding:0;font-family:var(--font);background:var(--bg);color:var(--text);-webkit-font-smoothing:antialiased}
html.light{background:var(--bg);color-scheme:light}
body,button{font-size:14px;line-height:1.35}
a{color:var(--accent);text-decoration:none}
a:hover{color:var(--accent-glow)}
.topbar{position:sticky;top:0;z-index:20;display:flex;align-items:center;gap:1rem;padding:.75rem 1.25rem;background:var(--bg-alt);border-bottom:1px solid var(--border)}
.brand{font-weight:600;letter-spacing:.5px;font-size:15px}
.nav{display:flex;gap:.5rem}
.nav button{background:var(--surface-soft);border:1px solid var(--border);color:var(--text-dim);padding:.45rem .9rem;border-radius:var(--radius-sm);cursor:pointer;display:flex;align-items:center;gap:.35rem;transition:var(--trans);font-weight:600}
.nav button.active,.nav button:hover{color:#fff;background:var(--accent);border-color:var(--accent);box-shadow:0 0 0 1px var(--accent-glow),0 4px 10px -2px rgba(0,0,0,.5)}
.actions{margin-left:auto;display:flex;align-items:center;gap:.75rem}
.actions button{background:var(--bg);border:1px solid var(--border);color:var(--text-dim);height:32px;width:38px;border-radius:8px;cursor:pointer;display:grid;place-items:center;transition:var(--trans)}
.actions button:hover{color:#fff;border-color:var(--accent);background:var(--accent)}
.wrapper{max-width:1680px;margin:1.2rem auto;padding:0 1.2rem;display:flex;flex-direction:column;gap:1.25rem}
.notice{padding:.9rem 1rem;border:1px solid var(--border);background:linear-gradient(145deg,var(--bg-alt),var(--bg));border-radius:var(--radius);display:flex;align-items:center;gap:.75rem;font-size:13px}
.notice.info:before{content:"";width:8px;height:8px;border-radius:50%;background:var(--accent);box-shadow:0 0 0 4px color-mix(in srgb,var(--accent) 20%,transparent)}
.panel{display:none;flex-direction:column;gap:.75rem;animation:fade .4s ease}
.panel.active{display:flex}
.table-wrap{overflow:auto;border:1px solid var(--border);border-radius:var(--radius);background:var(--bg-alt);box-shadow:var(--shadow)}
table.data{width:100%;border-collapse:separate;border-spacing:0;min-width:960px}
table.data thead th{position:sticky;top:0;background:var(--surface-soft);font-weight:700;text-align:left;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:var(--text-dim);padding:.7rem .75rem;border-bottom:1px solid var(--border);white-space:nowrap}
table.data tbody td{padding:.55rem .75rem;border-bottom:1px solid var(--border);font-size:13px;vertical-align:middle;white-space:nowrap}
/* 防止数值变化导致列抖动 */
table.data th,table.data td{font-variant-numeric:tabular-nums}
/* 月流量胶囊 */
.caps-traffic{display:inline-flex;align-items:center;gap:6px;background:linear-gradient(145deg,var(--bg),var(--bg-alt));border:1px solid var(--border);padding:3px 12px 3px 10px;border-radius:999px;font-size:12px;line-height:1;font-weight:500;position:relative;box-shadow:0 2px 4px -2px rgba(0,0,0,.35),0 0 0 1px rgba(255,255,255,.03);}
.caps-traffic:before{content:"";position:absolute;inset:0;border-radius:inherit;background:radial-gradient(circle at 20% 20%,rgba(255,255,255,.06),transparent 70%);pointer-events:none;}
.caps-traffic.sm{padding:2px 8px 2px 7px;font-size:11px;gap:5px;}
/* 双色胶囊 */
.caps-traffic.duo{background:none;border:0;gap:0;padding:0;box-shadow:none;position:relative;border-radius:999px;overflow:hidden;font-size:12px;}
/* 宽度按内容自适应,每半边仅为其文本 + padding */
.caps-traffic.duo .half{flex:0 0 auto;display:flex;align-items:center;justify-content:center;padding:2px 4px;font-variant-numeric:tabular-nums;font-weight:600;line-height:1.25; /* 与 .pill 保持一致高度 */ letter-spacing:.25px;color:#fff;font-size:12px;white-space:nowrap;}
.caps-traffic.duo.normal .half.in{background:#d1fae5;color:#065f46;} /* emerald-100 / text-emerald-800 */
.caps-traffic.duo.normal .half.out{background:#bfdbfe;color:#1e3a8a;} /* blue-200 / text-blue-900 */
.caps-traffic.duo.heavy .half.in{background:#fde68a;color:#78350f;}
.caps-traffic.duo.heavy .half.out{background:#fbbf24;color:#111827;}
/* 半之间分隔线 */
.caps-traffic.duo .half + .half{border-left:1px solid rgba(0,0,0,.18);}
body.light .caps-traffic.duo .half + .half{border-left:1px solid rgba(0,0,0,.08);}
.caps-traffic.duo.sm .half{padding:1px 4px;font-size:10px;min-width:0;}
table.data tbody tr:last-child td{border-bottom:none}
table.data tbody tr:hover{background:rgba(59,130,246,.045)}
.badge{display:inline-block;padding:2px 6px;font-size:11px;border-radius:12px;font-weight:500;line-height:1.2;background:var(--bg);border:1px solid var(--border);color:var(--text-dim)}
.badge.ok{background:rgba(16,185,129,.15);color:var(--ok);border-color:rgba(16,185,129,.3)}
.badge.warn{background:rgba(245,158,11,.15);color:var(--warn);border-color:rgba(245,158,11,.4)}
.badge.err{background:rgba(239,68,68,.15);color:var(--danger);border-color:rgba(239,68,68,.4)}
.footer{margin:2rem 0 2.5rem;display:flex;align-items:center;justify-content:center;gap:.5rem;font-size:12px;color:var(--text-dim)}
.footer a{color:var(--text-dim)}
.footer a:hover{color:var(--accent)}
.muted{color:var(--text-dim)}
/* 旧状态文字样式已不再使用(采用 pill) */
@media (max-width:1100px){.nav{flex-wrap:wrap}.table-wrap{border-radius:8px}}
@keyframes fade{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:translateY(0)}}
/* modal styles */
.modal-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.55);display:flex;align-items:flex-start;justify-content:center;padding:5vh 1rem;z-index:50;backdrop-filter:blur(4px)}
.modal-box{position:relative;width:100%;max-width:560px;background:var(--bg-alt);border:1px solid var(--border);border-radius:16px;box-shadow:0 8px 30px -6px rgba(0,0,0,.6);padding:1.25rem 1.35rem;display:flex;flex-direction:column;gap:.9rem;animation:fade .25s ease}
.modal-title{margin:0;font-size:16px;font-weight:600;letter-spacing:.5px}
.modal-close{position:absolute;top:10px;right:12px;background:transparent;border:0;color:var(--text-dim);font-size:20px;line-height:1;cursor:pointer;padding:4px;border-radius:8px;transition:var(--trans)}
.modal-close:hover{color:var(--text);background:var(--bg)}
.modal-content{font-size:13px;line-height:1.5;display:grid;gap:.6rem}
.kv{display:flex;justify-content:space-between;gap:1rem;padding:.5rem .75rem;background:linear-gradient(145deg,var(--bg),var(--bg-alt));border:1px solid var(--border);border-radius:10px}
.kv span{white-space:nowrap}
.mono{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:12px}
/* 半圆仪表盘 */
.gauge-half{--p:0;width:60px;height:34px;position:relative;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;font-family:ui-monospace,monospace;font-size:10px;font-weight:600;gap:0;color:var(--text);}
.gauge-half svg{width:100%;height:28px;overflow:visible;}
.gauge-half path{fill:none;stroke-linecap:round;}
/* 基础颜色 (暗色/亮色自动过渡) */
.gauge-half path.track{stroke:color-mix(in srgb,var(--text-dim) 18%,transparent);stroke-width:6;}
.gauge-half path.arc{stroke:var(--gauge-base,#3b82f6);stroke-width:8;stroke-dasharray:126;stroke-dashoffset:calc(126*(1 - var(--p)));transition:stroke-dashoffset .8s cubic-bezier(.4,0,.2,1),stroke .35s;filter:drop-shadow(0 1px 2px rgba(0,0,0,.45));}
.gauge-half[data-type=mem] path.arc{--gauge-base:#10b981}
.gauge-half[data-type=hdd] path.arc{--gauge-base:#06b6d4}
/* 阈值颜色:CPU>=75%、内存>=80%、硬盘>=85% 警告黄,>=90% 危险红 */
.gauge-half[data-warn] path.arc{stroke:var(--warn)}
.gauge-half[data-bad] path.arc{stroke:var(--danger)}
.gauge-half span{line-height:1;position:relative;top:-6px;font-size:12px;}
/* 亮色模式细化对比度 */
body.light .gauge-half path.track{stroke:color-mix(in srgb,var(--text-dim) 28%,transparent)}
body.light .gauge-half path.arc{filter:none}
/* status pill */
.pill{display:inline-block;padding:2px 8px;font-size:12px;font-weight:600;border-radius:999px;letter-spacing:.45px;min-width:48px;text-align:center;line-height:1.25;border:0;box-shadow:0 2px 4px -1px rgba(0,0,0,.4),0 0 0 1px rgba(255,255,255,.04);transition:var(--trans);color:#fff}
.pill.on{background:var(--ok)}
.pill.off{background:var(--danger)}
.pill.on:hover{filter:brightness(1.1)}
.pill.off:hover{filter:brightness(1.1)}
.proto-signal{display:inline-grid;grid-template-columns:1fr 1fr;width:18px;height:18px;border-radius:50%;overflow:hidden;vertical-align:middle;border:1px solid color-mix(in srgb,var(--border) 80%,transparent);box-shadow:0 2px 5px -2px rgba(0,0,0,.55),0 0 0 1px rgba(255,255,255,.04)}
.proto-signal i{display:block;min-width:0;background:color-mix(in srgb,var(--text-dim) 42%,var(--bg-alt));}
.proto-signal[data-v4="1"] .v4,.proto-signal[data-v6="1"] .v6{background:#22c55e}
.proto-signal[data-state="ipv4"] .v4{background:#22c55e}
.proto-signal[data-state="ipv6"] .v6{background:#22c55e}
.proto-signal[data-state="offline"] .v4,.proto-signal[data-state="offline"] .v6{background:var(--danger)}
body.light .proto-signal{border-color:color-mix(in srgb,var(--border) 92%,#94a3b8);box-shadow:0 1px 3px rgba(15,23,42,.12)}
body.light .proto-signal i{background:#cbd5e1}
.virt-pill{--virt:var(--accent);display:inline-flex;align-items:center;justify-content:center;min-width:50px;padding:2px 9px;border-radius:999px;border:1px solid color-mix(in srgb,var(--virt) 42%,var(--border));background:color-mix(in srgb,var(--virt) 14%,var(--bg-alt));color:color-mix(in srgb,var(--virt) 72%,#fff);font-size:12px;font-weight:650;line-height:1.2;letter-spacing:.25px;white-space:nowrap}
body.light .virt-pill{background:color-mix(in srgb,var(--virt) 12%,#fff);color:color-mix(in srgb,var(--virt) 68%,#111827)}
.node-name{display:inline-block;max-width:160px;overflow:hidden;text-overflow:ellipsis;vertical-align:middle;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:13px;font-weight:750;letter-spacing:.15px;color:var(--text)}
.row-server[data-online="0"] .node-name{color:color-mix(in srgb,var(--text) 72%,var(--text-dim));font-weight:650}
.row-server:hover .node-name{color:var(--accent)}
.load-with-cores{display:inline-flex;align-items:flex-start;gap:.28rem;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-weight:700;line-height:1;white-space:nowrap}
.load-value{display:inline-block;min-width:2.55rem}
.load-core-bubble{display:inline-flex;align-items:center;justify-content:center;height:14px;min-width:24px;padding:0 5px;margin-top:-6px;border-radius:999px;border:1px solid color-mix(in srgb,var(--accent) 34%,var(--border));background:color-mix(in srgb,var(--accent) 13%,var(--bg-alt));color:color-mix(in srgb,var(--accent) 74%,#fff);font-size:9px;font-weight:800;line-height:1;letter-spacing:0;box-shadow:0 1px 3px rgba(0,0,0,.22)}
body.light .load-core-bubble{background:color-mix(in srgb,var(--accent) 8%,#fff);color:color-mix(in srgb,var(--accent) 78%,#111827);box-shadow:0 1px 2px rgba(15,23,42,.08)}
/* buckets CU/CT/CM (simple version) */
.buckets{display:flex;align-items:flex-end;gap:8px;min-width:140px}
.bucket{position:relative;width:26px;height:34px;background:linear-gradient(145deg,var(--bg),var(--bg-alt));border:1px solid var(--border);border-radius:8px;padding:4px 4px 16px;box-sizing:border-box;display:flex;justify-content:flex-end}
.bucket span{display:block;width:100%;background:var(--accent);border-radius:4px 4px 6px 6px;height:var(--h);align-self:flex-end;transition:height .8s cubic-bezier(.4,0,.2,1),background .3s}
.bucket[data-lv=warn] span{background:var(--warn)}
.bucket[data-lv=bad] span{background:var(--danger)}
.bucket label{position:absolute;left:0;right:0;bottom:2px;font-size:10px;text-align:center;color:var(--text-dim);pointer-events:none}
.bucket:hover label{color:var(--text)}
/* 调整“总流量”表头(第9列)padding 使标题文字居中,不受正文额外右 padding 影响 */
#serversTable thead th:nth-child(9){padding-left:.75rem;padding-right:.75rem;}
.buckets{justify-content:center}
/* 响应式隐藏非关键列,保持可读性 */
@media (max-width:1100px){
#serversTable{min-width:100%;}
#serversTable thead th:nth-child(3),
#serversTable tbody td:nth-child(3), /* 节点 */
#serversTable thead th:nth-child(4),
#serversTable tbody td:nth-child(4), /* 虚拟化 */
#serversTable thead th:nth-child(8),
#serversTable tbody td:nth-child(8) /* 当前网络 */ {display:none}
}
@media (max-width:820px){
#serversTable thead th:nth-child(5),
#serversTable tbody td:nth-child(5), /* 在线 */
#serversTable thead th:nth-child(6),
#serversTable tbody td:nth-child(6) /* 负载 */ {display:none}
.buckets{gap:6px;min-width:100px}
}
@media (max-width:640px){
#serversTable thead th:nth-child(4),
#serversTable tbody td:nth-child(4) /* 位置 */ {display:none}
.topbar{flex-wrap:wrap;padding:.6rem .8rem}
.actions{width:100%;justify-content:flex-end;margin-top:.4rem}
.wrapper{padding:0 .7rem}
#panel-servers .table-wrap{display:none;}
#serversCards{display:grid!important;grid-template-columns:1fr;gap:.75rem;margin-top:.5rem;}
/* 服务与证书移动端卡片 */
#panel-monitors .table-wrap, #panel-ssl .table-wrap{display:none;}
#monitorsCards,#sslCards{display:grid!important;grid-template-columns:1fr;gap:.75rem;margin-top:.5rem;}
.modal-box{max-width:100%;border-radius:14px;padding:1rem .95rem;}
.modal-content{max-height:65vh;overflow:auto;}
}
/* 三个数据表使用自动宽度,按关键列单独约束 */
#serversTable,#monitorsTable,#sslTable{min-width:0;}
#serversTable thead th,#serversTable tbody td,
#monitorsTable thead th,#monitorsTable tbody td,
#sslTable thead th,#sslTable tbody td{
width:auto!important;min-width:0!important;max-width:none!important;
padding:.55rem .7rem;
}
#sslTable th:nth-child(2),#sslTable td:nth-child(2){max-width:none;white-space:normal;overflow-wrap:anywhere;}
#serversTable thead th:nth-child(13),#serversTable tbody td:nth-child(13){width:112px!important;min-width:112px!important;max-width:112px!important;padding-left:0!important;padding-right:0!important;text-align:center;}
#serversTable thead th:nth-child(8),#serversTable tbody td:nth-child(8),
#serversTable thead th:nth-child(9),#serversTable tbody td:nth-child(9){
min-width:20ch !important; /* 保留自动宽度,但不小于此值 */
text-align:center;
font-variant-numeric:tabular-nums;
}
#serversTable thead th:nth-child(2),#serversTable tbody td:nth-child(2){
width:134px!important;min-width:134px!important;max-width:134px!important;text-align:center;padding-left:.35rem!important;padding-right:.35rem!important;font-variant-numeric:tabular-nums;
}
#serversTable thead th:nth-child(1),#serversTable tbody td:nth-child(1){
width:46px!important;min-width:46px!important;max-width:46px!important;text-align:center;padding-left:0!important;padding-right:0!important;
}
#serversTable thead th:nth-child(3),#serversTable tbody td:nth-child(3){
text-align:left;padding-left:18px!important;
}
#serversTable tbody td:nth-child(13) .buckets{
min-width:112px;
gap:6px;
margin:0 auto;
justify-content:center;
}
.cards .card{border:1px solid var(--border);border-radius:12px;padding:.75rem .85rem;background:linear-gradient(145deg,var(--bg),var(--bg-alt));display:flex;flex-direction:column;gap:.45rem;position:relative;}
.cards .card.offline{filter:saturate(.92);}
.cards .card.alert-critical{border-color:rgba(239,68,68,.6);background:linear-gradient(180deg, rgba(239,68,68,.22), rgba(239,68,68,.12)), var(--bg-alt);box-shadow:0 0 0 1px rgba(239,68,68,.48),0 6px 18px -6px rgba(239,68,68,.28);}
.cards .card.alert-warning{border-color:rgba(245,158,11,.55);background:linear-gradient(180deg, rgba(245,158,11,.16), rgba(245,158,11,.08)), var(--bg-alt);box-shadow:0 0 0 1px rgba(245,158,11,.32),0 6px 18px -8px rgba(245,158,11,.22);}
table.data tbody tr.alert-critical{background:rgba(239,68,68,.18) !important;}
table.data tbody tr.alert-critical:hover{background:rgba(239,68,68,.21) !important;}
table.data tbody tr.alert-warning{background:rgba(245,158,11,.12) !important;}
table.data tbody tr.alert-warning:hover{background:rgba(245,158,11,.14) !important;}
/* SSL 域名告警底色:与高负载相同 */
#sslTable td.alert-domain{background:rgba(239,68,68,.18) !important;}
#sslTable tr:hover td.alert-domain{background:rgba(239,68,68,.26) !important;}
/* OS 着色更明显
1) 为各 OS 类定义 --os-color 变量
2) 行左侧使用 inset box-shadow 4px 彩条
3) 行背景叠加轻度渐变以提示 OS
*/
table.data tbody tr[class*="os-"]{box-shadow:inset 4px 0 0 0 var(--os-color, transparent);background:linear-gradient(180deg, color-mix(in srgb, var(--os-color, transparent) 10%, transparent), transparent 60%);}
table.data tbody tr[class*="os-"]:hover{background:linear-gradient(180deg, color-mix(in srgb, var(--os-color, transparent) 12%, transparent), transparent 65%);}
.cards .card[class*="os-"]{border-color:color-mix(in srgb, var(--os-color, var(--accent)) 60%, transparent);box-shadow:0 0 0 1px color-mix(in srgb, var(--os-color, var(--accent)) 40%, transparent),0 4px 16px -6px color-mix(in srgb, var(--os-color, #000) 35%, transparent);}
/* 弹窗 OS 着色:左侧彩条 + 渐变与卡片一致 */
/* 取消弹窗背景着色,改为仅在标题展示系统胶囊 */
.os-chip{display:inline-flex;align-items:center;padding:2px 8px;margin-left:.5rem;border-radius:999px;font-size:12px;font-weight:600;line-height:1.2;background:var(--os-color, var(--border));color:#fff;border:0;white-space:nowrap;}
.spec-chip{display:inline-flex;align-items:center;padding:2px 8px;margin-left:.4rem;border-radius:999px;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;font-weight:750;line-height:1.2;background:color-mix(in srgb,var(--accent) 16%,var(--bg-alt));color:color-mix(in srgb,var(--accent) 72%,#fff);border:1px solid color-mix(in srgb,var(--accent) 38%,var(--border));white-space:nowrap;}
body.light .spec-chip{background:color-mix(in srgb,var(--accent) 10%,#fff);color:color-mix(in srgb,var(--accent) 78%,#111827)}
.cpu-model-chip{display:inline-block;max-width:min(240px,58vw);overflow:hidden;text-overflow:ellipsis;vertical-align:middle;padding:2px 8px;margin-left:.4rem;border-radius:999px;border:1px solid color-mix(in srgb,var(--text-dim) 34%,var(--border));background:color-mix(in srgb,var(--text-dim) 12%,var(--bg-alt));color:color-mix(in srgb,var(--text) 84%,var(--text-dim));font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:12px;font-weight:700;line-height:1.2;white-space:nowrap;}
body.light .cpu-model-chip{background:color-mix(in srgb,var(--text-dim) 9%,#fff);color:color-mix(in srgb,var(--text) 82%,#111827)}
/* 为常见系统赋色 */
.os-linux{--os-color: rgba(16,185,129,.85);} /* 绿色 (通用 Linux,保持不变) */
.os-ubuntu{--os-color: rgba(221,72,20,.9);} /* Ubuntu 橙 (#dd4814) */
.os-debian{--os-color: rgba(215,10,83,.9);} /* Debian 品红 (#d70a53) */
.os-centos{--os-color: rgba(102,0,153,.9);} /* CentOS 紫 (#660099) */
.os-rocky{--os-color: rgba(1,122,66,.9);} /* Rocky Linux 绿 (#017a42) */
.os-almalinux{--os-color: rgba(0,92,170,.9);} /* AlmaLinux 蓝 (#005caa) */
.os-rhel{--os-color: rgba(204,0,0,.9);} /* Red Hat 红 (#cc0000) */
.os-arch{--os-color: rgba(23,147,209,.9);} /* Arch Linux 蓝 (#1793d1) */
.os-alpine{--os-color: rgba(14,87,123,.9);} /* Alpine Linux 蓝 (#0e577b) */
.os-fedora{--os-color: rgba(60,110,180,.9);} /* Fedora 蓝 (#3c6eb4) */
.os-amazon{--os-color: rgba(255,153,0,.9);} /* Amazon Linux 橙 (#ff9900) */
.os-suse{--os-color: rgba(0,150,0,.9);} /* openSUSE 绿 (#009600) */
.os-freebsd{--os-color: rgba(166,31,47,.9);} /* FreeBSD 红 (#a61f2f) */
.os-openbsd{--os-color: rgba(255,204,0,.9);} /* OpenBSD 黄 (#ffcc00) */
.os-bsd{--os-color: rgba(166,31,47,.9);} /* BSD 系统一般跟 FreeBSD 接近 */
.os-darwin{--os-color: rgba(29,29,31,.95);} /* macOS 深空灰 (#1d1d1f) */
.os-windows{--os-color: rgba(0,120,215,.95);} /* Windows 蓝 (#0078d7) */
/* 旧进度条相关样式已清理 */
.cards .card-header{display:flex;align-items:center;justify-content:space-between;gap:.5rem;}
.cards .card-title{display:flex;align-items:center;flex-wrap:wrap;gap:.24rem .34rem;min-width:0;font-weight:600;font-size:.95rem;}
.cards .card-spec-chip{display:inline-flex;align-items:center;justify-content:center;height:16px;padding:0 6px;border-radius:999px;border:1px solid color-mix(in srgb,var(--accent) 34%,var(--border));background:color-mix(in srgb,var(--accent) 13%,var(--bg-alt));color:color-mix(in srgb,var(--accent) 74%,#fff);font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;font-size:10px;font-weight:800;line-height:1;letter-spacing:0;white-space:nowrap}
body.light .cards .card-spec-chip{background:color-mix(in srgb,var(--accent) 8%,#fff);color:color-mix(in srgb,var(--accent) 78%,#111827)}
.cards .tag{font-size:.65rem;padding:.15rem .4rem;border-radius:4px;background:var(--surface);color:var(--text);letter-spacing:.5px;}
.cards .status-pill{font-size:.6rem;padding:.2rem .45rem;border-radius:999px;font-weight:500;}
.cards .status-pill.on{background:var(--ok);color:#fff;}
.cards .status-pill.off{background:var(--danger);color:#fff;}
.cards .kvlist{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.35rem .75rem;font-size:.7rem;line-height:1.25;}
.cards .kvlist div{display:flex;flex-direction:column;}
.cards .kvlist span.key{opacity:.6;}
.cards .buckets{margin-top:.25rem;}
/* 证书卡片:域名告警底色(与高负载卡片风格一致) */
.cards .kvlist .alert-domain{background:rgba(239,68,68,.18);border:1px solid rgba(239,68,68,.35);border-radius:8px;padding:.4rem .5rem;}
.cards .kvlist .alert-domain .key{opacity:.85}
/* 移除移动端卡片展开箭头与展开区域(已按需简化交互) */
/* 旧移动端 latency spark 样式移除 */
/* 简易信号格,用于服务连通性延迟展示 */
.sig{display:inline-flex;gap:2px;vertical-align:baseline;margin:0 4px 0 6px;align-items:flex-end;line-height:1}
.sig .b{width:3px;background:color-mix(in srgb,var(--text-dim) 35%,transparent);border-radius:2px;display:inline-block}
.sig .b:nth-child(1){height:7px}
.sig .b:nth-child(2){height:9px}
.sig .b:nth-child(3){height:11px}
.sig .b:nth-child(4){height:13px}
.sig .b:nth-child(5){height:15px}
.sig .b.on{background:var(--ok)}
.sig .b.off{opacity:.35}
/* 服务监测项:不同组竖排,同一组横排;不考虑自动换行 */
.mon-items{display:flex;flex-direction:column;gap:6px;align-items:flex-start}
.mon-item{display:inline-flex;align-items:center;white-space:nowrap;line-height:1}
.mon-item .name{margin-right:6px}
.mon-item .ms{margin-left:6px;font-variant-numeric:tabular-nums}
.mon-item .sig{margin:0 6px;transform:translateY(-1px)}
/* 新 Logo 样式 */
.brand{display:flex;align-items:center;gap:.55rem;font-weight:600;letter-spacing:.5px;font-size:16px;position:relative}
.brand .logo-mark{display:inline-flex;width:34px;height:34px;border-radius:10px;background:linear-gradient(145deg,var(--logo-start) 0%,var(--logo-end) 90%);color:#fff;align-items:center;justify-content:center;box-shadow:0 4px 12px -2px rgba(0,0,0,.45),0 0 0 1px rgba(255,255,255,.08);transition:var(--trans)}
.brand .logo-mark svg{display:block}
.brand .logo-text{font-size:17px;font-weight:700;line-height:1;display:flex;align-items:baseline;gap:2px;letter-spacing:1px}
.brand .logo-text .logo-accent{background:linear-gradient(90deg,var(--logo-accent-grad-start),var(--logo-accent-grad-end));-webkit-background-clip:text;background-clip:text;color:transparent;filter:drop-shadow(0 2px 4px rgba(0,0,0,.3))}
.brand:hover .logo-mark{transform:translateY(-2px) scale(1.05)}
.brand:hover .logo-text{color:var(--text)}
@media (max-width:640px){.brand .logo-text{font-size:15px}.brand .logo-mark{width:30px;height:30px}}
/* 运维控制台增强 */
.ops-overview{display:grid;grid-template-columns:repeat(5,minmax(140px,1fr));gap:.75rem}
.overview-card{border:1px solid var(--border);background:linear-gradient(180deg,var(--surface-soft),var(--bg-alt));border-radius:8px;padding:.8rem .9rem;display:flex;flex-direction:column;gap:.25rem;min-height:82px}
.overview-card .label{font-size:12px;color:var(--text-dim)}
.overview-card .value{font-size:24px;line-height:1;font-weight:700;font-variant-numeric:tabular-nums}
.overview-card .hint{font-size:12px;color:var(--text-dim);line-height:1.3}
.overview-card.ok{border-color:rgba(16,185,129,.35)}
.overview-card.warn{border-color:rgba(245,158,11,.45)}
.overview-card.err{border-color:rgba(239,68,68,.5)}
.overview-card.traffic-down{border-color:rgba(16,185,129,.38)}
.overview-card.traffic-up{border-color:rgba(59,130,246,.38)}
.ops-toolbar{display:flex;align-items:end;gap:.75rem;flex-wrap:wrap;border:1px solid var(--border);background:var(--bg-alt);border-radius:8px;padding:.75rem}
.search-field,.select-field{display:flex;flex-direction:column;gap:.3rem;color:var(--text-dim);font-size:12px}
.search-field input,.select-field select,.config-form input,.config-form textarea{border:1px solid var(--border);background:var(--bg);color:var(--text);border-radius:6px;padding:0 .65rem;outline:none;transition:var(--trans)}
.search-field input::placeholder,.config-form input::placeholder,.config-form textarea::placeholder{color:color-mix(in srgb,var(--text-dim) 78%,transparent);opacity:1}
.search-field input,.select-field select,.config-form input{height:36px}
.config-form textarea{min-height:92px;resize:vertical;padding:.55rem .65rem;line-height:1.45;font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
.search-field input:focus,.select-field select:focus,.config-form input:focus,.config-form textarea:focus{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent) 18%,transparent)}
.search-field input{min-width:280px}
.segmented{display:inline-flex;border:1px solid var(--border);border-radius:7px;overflow:hidden;background:var(--surface-soft)}
.segmented button{height:36px;border:0;border-right:1px solid var(--border);background:transparent;color:var(--text-dim);padding:0 .8rem;cursor:pointer}
.segmented button:last-child{border-right:0}
.segmented button.active,.segmented button:hover{background:var(--accent);color:#fff}
.icon-text,.primary-btn,.warn-btn,.danger-btn{height:36px;border:1px solid var(--border);border-radius:6px;background:var(--bg);color:var(--text);padding:0 .75rem;cursor:pointer;transition:var(--trans);font-weight:600}
.icon-text:hover{border-color:var(--accent);color:var(--accent)}
.primary-btn{background:var(--accent);border-color:var(--accent);color:#fff}
.primary-btn:hover{filter:brightness(1.08)}
.warn-btn{background:rgba(245,158,11,.14);border-color:rgba(245,158,11,.42);color:var(--warn)}
.warn-btn:hover{background:var(--warn);border-color:var(--warn);color:#111827}
.danger-btn{background:rgba(239,68,68,.14);border-color:rgba(239,68,68,.38);color:var(--danger)}
.danger-btn:hover{background:var(--danger);color:#fff}
th[data-sort]{cursor:pointer;user-select:none}
th[data-sort]:after{content:"";display:inline-block;margin-left:6px;border:4px solid transparent;border-top-color:color-mix(in srgb,var(--text-dim) 70%,transparent);transform:translateY(2px);opacity:.45}
th[data-sort].sorted-asc:after{border-top-color:transparent;border-bottom-color:var(--accent);transform:translateY(-2px)}
th[data-sort].sorted-desc:after{border-top-color:var(--accent);opacity:1}
.section-head h2{margin:0;font-size:18px}
.section-head p{margin:.25rem 0 0}
.empty-state{border:1px dashed var(--border);border-radius:8px;background:var(--bg-alt);padding:1.25rem;color:var(--text-dim);text-align:center}
/* 右侧详情抽屉 */
.drawer-backdrop{align-items:stretch;justify-content:flex-end;padding:0;background:rgba(0,0,0,.48)}
.detail-drawer{height:100dvh;max-height:100dvh;width:min(620px,100vw);max-width:min(620px,100vw);border-radius:0;border-top:0;border-right:0;border-bottom:0;padding:1.1rem;overflow:auto;animation:slideIn .24s ease}
.detail-drawer.alert-critical{border-color:rgba(239,68,68,.6);background:linear-gradient(180deg, rgba(239,68,68,.16), rgba(239,68,68,.08)), var(--bg-alt);box-shadow:0 0 0 1px rgba(239,68,68,.38),0 10px 28px -10px rgba(239,68,68,.28)}
.detail-drawer.alert-warning{border-color:rgba(245,158,11,.55);background:linear-gradient(180deg, rgba(245,158,11,.14), rgba(245,158,11,.06)), var(--bg-alt);box-shadow:0 0 0 1px rgba(245,158,11,.28),0 10px 28px -10px rgba(245,158,11,.2)}
.modal-title{padding-right:2.2rem;display:flex;align-items:center;flex-wrap:wrap;gap:.32rem .4rem}
.modal-title .os-chip,.modal-title .spec-chip,.modal-title .cpu-model-chip{margin-left:0}
.detail-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.55rem}
.detail-section{border:1px solid var(--border);border-radius:8px;background:linear-gradient(145deg,var(--bg),var(--bg-alt));padding:.75rem;display:flex;flex-direction:column;gap:.65rem}
.detail-section h4{margin:0;font-size:13px;color:var(--text-dim);font-weight:700;letter-spacing:.4px}
.detail-section .kv{display:grid;grid-template-columns:max-content minmax(0,1fr);align-items:flex-start;gap:.6rem;min-width:0}
.detail-section .kv span{min-width:0;white-space:normal}
.detail-section .kv>span:last-child{text-align:right;overflow-wrap:anywhere;word-break:break-word}
.detail-section .mono{display:block;white-space:normal;overflow-wrap:anywhere;word-break:break-word;line-height:1.45}
.detail-inline{display:inline-flex;align-items:center;justify-content:flex-end;gap:.45rem;white-space:nowrap!important}
.detail-inline .mono{display:inline!important;white-space:nowrap!important;line-height:1}
.detail-values{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:.15rem .55rem}
.detail-values span{white-space:nowrap!important}
.resource-section{gap:.5rem}
.resource-meter{display:flex;flex-direction:column;gap:.26rem;min-width:0}
.resource-meter-head,.resource-mini{display:flex;align-items:center;justify-content:space-between;gap:.55rem;font-size:12px;min-width:0}
.resource-meter-head span,.resource-mini span{color:var(--text-dim);white-space:nowrap}
.resource-meter-head strong,.resource-mini strong{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:11px;font-weight:700;text-align:right;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.resource-track{height:7px;border-radius:999px;background:color-mix(in srgb,var(--text-dim) 16%,transparent);overflow:hidden;border:1px solid color-mix(in srgb,var(--border) 75%,transparent)}
.resource-track i{display:block;height:100%;width:var(--p);border-radius:inherit;background:var(--accent);transition:width .5s ease}
.resource-meter[data-kind=mem] .resource-track i{background:var(--ok)}
.resource-meter[data-kind=swap] .resource-track i{background:#14b8a6}
.resource-meter[data-kind=hdd] .resource-track i{background:#06b6d4}
.resource-meter[data-level=warn] .resource-track i{background:var(--warn)}
.resource-meter[data-level=bad] .resource-track i{background:var(--danger)}
.resource-mini{border-top:1px solid var(--border);padding-top:.45rem}
.chart-section{gap:.55rem}
.chart-head{display:flex;align-items:center;justify-content:space-between;gap:.75rem;flex-wrap:wrap}
.chart-head h4{margin:0}
.chart-legend{display:inline-flex;align-items:center;justify-content:flex-end;gap:.7rem;flex-wrap:wrap;font-size:11px;color:var(--text-dim)}
.chart-legend span{display:inline-flex;align-items:center;gap:.28rem;white-space:nowrap}
.chart-legend i{display:inline-block;width:10px;height:3px;border-radius:999px;box-shadow:0 0 0 1px rgba(0,0,0,.16)}
.detail-chart{width:100%;border:1px solid var(--border);border-radius:8px;background:var(--bg)}
@keyframes slideIn{from{transform:translateX(22px);opacity:.7}to{transform:translateX(0);opacity:1}}
/* 管理配置 */
.config-grid{display:grid;grid-template-columns:minmax(320px,.95fr) minmax(320px,1.05fr);gap:.9rem;align-items:start}
.admin-card{border:1px solid var(--border);border-radius:8px;background:var(--bg-alt);padding:1rem;display:flex;flex-direction:column;gap:.85rem}
.section-head{display:flex;align-items:flex-start;justify-content:space-between;gap:1rem}
.section-actions{display:flex;align-items:center;gap:.5rem;flex-wrap:wrap;justify-content:flex-end}
.admin-status{border:1px solid var(--border);border-radius:8px;background:var(--bg);padding:.75rem;font-size:13px;color:var(--text-dim)}
.admin-status.ok{border-color:rgba(16,185,129,.35);color:var(--ok)}
.admin-status.err{border-color:rgba(239,68,68,.45);color:var(--danger)}
.admin-login,.config-actions,.form-actions{display:flex;align-items:end;gap:.65rem;flex-wrap:wrap}
.config-tabs{align-self:flex-start;max-width:100%}
.config-list{display:flex;flex-direction:column;gap:.5rem}
.config-row{display:grid;grid-template-columns:1fr auto;gap:.75rem;align-items:center;border:1px solid var(--border);border-radius:8px;background:var(--bg);padding:.7rem;cursor:pointer}
.config-row:hover,.config-row.active{border-color:var(--accent)}
.config-row .name{font-weight:700}
.config-row .meta{font-size:12px;color:var(--text-dim);margin-top:.2rem;word-break:break-all}
.config-form{display:flex;flex-direction:column;gap:.75rem}
.config-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem}
.config-form label{display:flex;flex-direction:column;gap:.3rem;color:var(--text-dim);font-size:12px}
.config-form label.wide{grid-column:1/-1}
.config-form .check-row{flex-direction:row;align-items:center;gap:.5rem}
.config-form .check-row input{height:auto}
.config-form .form-actions{grid-column:1/-1}
.config-form input[name=password],.config-form textarea[name=rule]{font-family:ui-monospace,SFMono-Regular,Menlo,monospace}
.config-form button:disabled{opacity:.5;cursor:not-allowed}
@media (max-width:980px){
.ops-overview{grid-template-columns:repeat(2,minmax(0,1fr))}
.config-grid{grid-template-columns:1fr}
.search-field input{min-width:220px}
}
@media (max-width:700px){
.ops-overview{display:none}
}
@media (max-width:640px){
.ops-toolbar{align-items:stretch}
.search-field,.select-field,.search-field input,.select-field select,.segmented,.icon-text,.primary-btn,.warn-btn,.danger-btn{width:100%}
.segmented button{flex:1;padding:0 .35rem}
.section-head{flex-direction:column}
.section-actions{width:100%;justify-content:stretch}
.detail-grid,.config-fields{grid-template-columns:1fr}
.detail-drawer{width:100vw;height:100dvh;max-height:100dvh;padding:calc(.95rem + env(safe-area-inset-top)) .95rem calc(.95rem + env(safe-area-inset-bottom));}
.detail-drawer .modal-content{max-height:none;overflow:visible;}
}
File diff suppressed because one or more lines are too long
-6
View File
File diff suppressed because one or more lines are too long
-54
View File
@@ -1,54 +0,0 @@
body { background: #222 url('../img/dark.png'); color: #fff; }
.navbar { min-height: 40px; }
.navbar-brand { color: #FFF !important; padding: 10px; font-size: 20px; }
.dropdown .dropdown-toggle { padding-bottom: 10px; padding-top: 10px; }
.dropdown-menu > li > a { color: #FFF !important; background-color: #222222 !important; }
.dropdown-menu > li > a:hover { color: #FFF !important; background: #000 !important; }
.dropdown-menu { background: #222 !important; background-color: #222222 !important; }
.navbar-inverse .navbar-inner { background-color:#1B1B1B; background-image:-moz-linear-gradient(top, #222222, #111111); background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#222222), to(#111111)); background-image:-webkit-linear-gradient(top, #222222, #111111); background-image:-o-linear-gradient(top, #222222, #111111); background-image:linear-gradient(to bottom, #222222, #111111); background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff222222', endColorstr='#ff111111', GradientType=0); border-color: #252525; }
.content { background: #222; padding: 20px; border-radius: 5px; border: 1px #000 solid; -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, .1); -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, .1); box-shadow: 0 1px 10px rgba(0, 0, 0, .1); margin-bottom: 20px; }
.table { background: #000; margin-bottom: 0; border-collapse: collapse; border-radius: 3px; }
.table th { text-align: center; }
.table-striped tbody > tr.even > td, .table-striped tbody > tr.even > th { background-color: #2F2F2F; }
.table-striped tbody > tr.odd > td, .table-striped tbody > tr.odd > th { background-color: #000; }
.table td { text-align: center; border-color: #2F2F2F; }
.progress { margin-bottom: 0; background: #222; }
.table-hover > tbody > tr:hover > td { background: #414141; }
tr.even.expandRow > :hover { background: #2F2F2F !important; }
tr.odd.expandRow > :hover { background: #000 !important; }
.expandRow > td { padding: 0 !important; border-top: 0px !important; }
#cpu, #ram, #hdd, #network { min-width: 55px; max-width: 100px; }
#ping { min-width: 55px; max-width: 130px; }
@media only screen and (max-width: 992px) {
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
}
@media only screen and (max-width: 720px) {
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
}
@media only screen and (max-width: 600px) {
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
#uptime, tr td:nth-child(6) { display:none; visibility:hidden; }
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
}
@media only screen and (max-width: 533px) {
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
#uptime, tr td:nth-child(6) { display:none; visibility:hidden; }
#network, tr td:nth-child(8) { display:none; visibility:hidden; }
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
}
@media only screen and (max-width: 450px) {
body { font-size: 10px; }
.content { padding: 0; }
#name, tr td:nth-child(3) { min-width: 55px; max-width: 85px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
#uptime, tr td:nth-child(6) { display:none; visibility:hidden; }
#network, tr td:nth-child(8) { display:none; visibility:hidden; }
#cpu, #ram, #hdd { min-width: 25px; max-width: 50px; }
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
}
-51
View File
@@ -1,51 +0,0 @@
body { background: #ebebeb url('../img/light.png'); }
.navbar { min-height: 40px; }
.navbar-brand { color: #fff; padding: 10px; font-size: 20px; }
.dropdown .dropdown-toggle { padding-bottom: 10px; padding-top: 10px; }
.navbar-inverse .navbar-brand { color: #fff; padding: 10px; font-size: 20px; }
.content { background: #ffffff; padding: 20px; border-radius: 5px; border: 1px #cecece solid; -webkit-box-shadow: 0 1px 10px rgba(0, 0, 0, .1); -moz-box-shadow: 0 1px 10px rgba(0, 0, 0, .1); box-shadow: 0 1px 10px rgba(0, 0, 0, .1); margin-bottom: 20px; }
.table { background: #ffffff; margin-bottom: 0; border-collapse: collapse; border-radius: 3px; }
.table th, .table td { text-align: center; }
.table-striped tbody > tr.even > td, .table-striped tbody > tr.even > th { background-color: #F9F9F9; }
.table-striped tbody > tr.odd > td, .table-striped tbody > tr.odd > th { background-color: #FFF; }
.progress { margin-bottom: 0; }
.progress-bar { color: #000; }
.table-hover > tbody > tr:hover > td { background: #E6E6E6; }
tr.even.expandRow > :hover { background: #F9F9F9 !important; }
tr.odd.expandRow > :hover { background: #FFF !important; }
.expandRow > td { padding: 0 !important; border-top: 0px !important; }
#cpu, #ram, #hdd, #network { min-width: 55px; max-width: 100px; }
#ping { min-width: 55px; max-width: 130px; }
@media only screen and (max-width: 992px) {
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
}
@media only screen and (max-width: 720px) {
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
}
@media only screen and (max-width: 600px) {
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
#uptime, tr td:nth-child(6) { display:none; visibility:hidden; }
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
}
@media only screen and (max-width: 533px) {
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
#uptime, tr td:nth-child(6) { display:none; visibility:hidden; }
#network, tr td:nth-child(8) { display:none; visibility:hidden; }
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
}
@media only screen and (max-width: 450px) {
body { font-size: 10px; }
.content { padding: 0; }
#name, tr td:nth-child(3) { min-width: 55px; max-width: 85px; text-overflow: ellipsis; white-space: nowrap; overflow: hidden; }
#type, tr td:nth-child(4) { display:none; visibility:hidden; }
#location, tr td:nth-child(5) { display:none; visibility:hidden; }
#uptime, tr td:nth-child(6) { display:none; visibility:hidden; }
#network, tr td:nth-child(8) { display:none; visibility:hidden; }
#cpu, #ram, #hdd { min-width: 25px; max-width: 50px; }
#ping, tr td:nth-child(13) { display:none; visibility:hidden; }
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

+7
View File
@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 86" fill="none">
<path d="M4 46H28L38 34L55 82L65 4L74 47H88M100 47H110M122 47H126"
stroke="#111827"
stroke-width="8"
stroke-linecap="round"
stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 266 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 600 B

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

+201 -107
View File
@@ -1,111 +1,205 @@
<!DOCTYPE html> <!DOCTYPE html>
<!-- <html lang="zh-CN">
json字段保持完整, 后期更新会向下兼容 <head>
可以自定义前端展示 <meta charset="utf-8" />
ლ(•̀ _ •́ ლ) <meta name="viewport" content="width=device-width,initial-scale=1" />
ლ(•̀ _ •́ ლ)ლ(•̀ _ •́ ლ) <meta name="description" content="云监控,ServerStatus中文版,ServerStatus,ServerStatus cppla" />
ლ(•̀ _ •́ ლ)ლ(•̀ _ •́ ლ)ლ(•̀ _ •́ ლ) <title>云监控</title>
ლ(•̀ _ •́ ლ)ლ(•̀ _ •́ ლ) <link rel="icon" type="image/svg+xml" href="favicon.svg" />
ლ(•̀ _ •́ ლ) <link rel="stylesheet" href="css/app.css?v=20260709-19" />
by:https://www.cpp.la </head>
--> <body>
<html> <header class="topbar">
<head> <div class="brand" title="云监控">
<title>云监控</title> <span class="logo-mark" aria-hidden="true">
<meta charset="utf-8"> <svg viewBox="0 0 128 86" width="24" height="18" fill="none" stroke="currentColor" stroke-width="8" stroke-linecap="round" stroke-linejoin="round">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <path d="M4 46H28L38 34L55 82L65 4L74 47H88M100 47H110M122 47H126" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> </svg>
<meta name="description" content="云监控"> </span>
<meta name="author" content="BotoX"> <span class="logo-text"><span class="logo-accent"></span>监控</span>
<link rel="stylesheet" href="css/bootstrap.min.css"> </div>
<link rel="stylesheet" href="css/bootstrap-theme.min.css"> <nav class="nav" id="navTabs">
<link rel="stylesheet" href="css/dark.css" title="dark"> <button data-tab="servers" class="active">主机</button>
<link rel="stylesheet" href="css/light.css" title="light"> <button data-tab="monitors">服务</button>
<style> <button data-tab="ssl">证书</button>
body { <button data-tab="config">配置</button>
padding-top: 70px; </nav>
padding-bottom: 30px; <div class="actions">
} <button id="themeToggle" title="切换主题 (当前: 自动或手动)" aria-label="切换主题">🌓</button>
</style> <span id="lastUpdate" class="muted">更新中...</span>
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> </div>
<!--[if lt IE 9]> </header>
<script src="js/html5shiv.js"></script>
<script src="js/respond.min.js"></script>
<![endif]-->
</head>
<body>
<div role="navigation" class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<div class="navbar-header">
<button data-target=".navbar-collapse" data-toggle="collapse" class="navbar-toggle" type="button">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a href="#" class="navbar-brand">云监控</a>
</div>
<div class="navbar-collapse collapse">
<ul class="nav navbar-nav">
<li class="dropdown">
<a data-toggle="dropdown" class="dropdown-toggle" href="#">风格<b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#" onclick="setActiveStyleSheet('dark')">黑夜</a></li>
<li><a href="#" onclick="setActiveStyleSheet('light')">白天</a></li>
</ul>
</li>
</ul>
</div><!--/.nav-collapse -->
</div>
</div>
</div>
<div class="container content"> <main class="wrapper">
<div id="loading-notice"> <div id="notice" class="notice info">初始化中...</div>
<noscript>
<div class="alert alert-danger" style="text-align: center;">
<strong>Enable JavaScript</strong> , please do it.
</div>
</noscript>
<div style="text-align: center;">
警告:如果出现此消息,请确保您已启用Javascript! <br />否则云监控主服务没启动或已关闭.
</div>
<p></p>
</div>
<table class="table table-striped table-condensed table-hover">
<thead>
<tr>
<th id="status4" style="text-align: center;">协议</th>
<th id="ipstatus" style="text-align: center;">Flight</th>
<th id="name">节点名</th>
<th id="type">虚拟化</th>
<th id="location">位置</th>
<th id="uptime">在线时间</th>
<th id="load">负载</th>
<th id="network">网络 ↓|↑</th>
<th id="traffic">流量 ↓|↑</th>
<th id="cpu">处理器</th>
<th id="ram">内存</th>
<th id="hdd">硬盘</th>
<th id="ping">丢包率(CU|CT|CM)</th>
</tr>
</thead>
<tbody id="servers">
<!-- Servers here \o/ -->
</tbody>
</table>
<br />
<div id="updated">Updating...</div>
</div>
<div class="container"> <section class="ops-overview" id="overviewCards" aria-label="运行概览"></section>
<p style="text-align: center; font-size: 10px;">
<a href="https://github.com/cppla/ServerStatus">ServerStatus中文版</a> <section class="ops-toolbar" id="serversToolbar" aria-label="主机筛选" style="display:none;">
</p> <label class="search-field">
</div> <span>搜索</span>
<script src="js/jquery.min.js"></script> <input id="serverSearch" type="search" autocomplete="off" placeholder="节点 / 地区 / 系统 / 主机名" />
<script src="js/bootstrap.min.js"></script> </label>
<script src="js/serverstatus.js"></script> <div class="segmented" id="statusFilter" role="group" aria-label="状态筛选">
</body> <button data-filter="all" class="active">全部</button>
<button data-filter="online">在线</button>
<button data-filter="offline">离线</button>
<button data-filter="alert" title="只显示在线但资源、丢包或流量异常的节点">异常</button>
</div>
<label class="select-field">
<span>系统</span>
<select id="osFilter">
<option value="all">全部系统</option>
</select>
</label>
<label class="select-field">
<span>排序</span>
<select id="sortSelect">
<option value="name">节点</option>
<option value="status">在线状态</option>
<option value="load">负载</option>
<option value="cpu">CPU</option>
<option value="memory">内存</option>
<option value="hdd">硬盘</option>
<option value="traffic">月流量</option>
<option value="loss">丢包</option>
</select>
</label>
<button id="sortDirection" class="icon-text" title="切换排序方向">降序</button>
</section>
<section id="panel-servers" class="panel active" aria-labelledby="主机">
<div class="table-wrap">
<table class="data" id="serversTable">
<thead>
<tr>
<th data-sort="status">协议</th>
<th data-sort="traffic">月流量 ↓|↑</th>
<th data-sort="name">节点</th>
<th data-sort="type">虚拟化</th>
<th data-sort="location">位置</th>
<th>在线</th>
<th data-sort="load">负载</th>
<th>当前网络 ↓|↑</th>
<th>总流量 ↓|↑</th>
<th data-sort="cpu">CPU</th>
<th data-sort="memory">内存</th>
<th data-sort="hdd">硬盘</th>
<th data-sort="loss" style="text-align:center;">联通|电信|移动</th>
</tr>
</thead>
<tbody id="serversBody"></tbody>
</table>
</div>
<!-- 移动端卡片布局 -->
<div id="serversCards" class="cards" style="display:none;"></div>
</section>
<section id="panel-monitors" class="panel" aria-labelledby="服务">
<div class="table-wrap">
<table class="data" id="monitorsTable">
<thead>
<tr>
<th>协议</th>
<th>监测节点</th>
<th>监测位置</th>
<th>监测内容</th>
</tr>
</thead>
<tbody id="monitorsBody"></tbody>
</table>
</div>
<!-- 移动端卡片布局 (服务) -->
<div id="monitorsCards" class="cards" style="display:none;"></div>
</section>
<section id="panel-ssl" class="panel" aria-labelledby="证书">
<div class="table-wrap">
<table class="data" id="sslTable">
<thead>
<tr>
<th>名称</th>
<th>域名</th>
<th>端口</th>
<th>剩余(天)</th>
<th>到期(UTC)</th>
<th>状态</th>
</tr>
</thead>
<tbody id="sslBody"></tbody>
</table>
</div>
<!-- 移动端卡片布局 (证书) -->
<div id="sslCards" class="cards" style="display:none;"></div>
</section>
<section id="panel-config" class="panel" aria-labelledby="配置">
<div class="config-grid">
<section class="admin-card">
<div class="section-head">
<div>
<h2>配置管理</h2>
<p class="muted">可编辑节点、服务监测、证书与告警规则,保存后自动重载。</p>
</div>
<div class="section-actions">
<button id="adminReload" class="icon-text">重载配置</button>
<button id="adminRestart" class="danger-btn">重启服务</button>
</div>
</div>
<div id="adminStatus" class="admin-status">正在检测管理 API...</div>
<form id="adminTokenForm" class="admin-login">
<label class="search-field">
<span>管理令牌</span>
<input id="adminToken" type="password" autocomplete="current-password" placeholder="ADMIN_TOKEN" />
</label>
<button type="submit" class="primary-btn">连接</button>
</form>
<div class="segmented config-tabs" id="configTypeTabs" role="group" aria-label="配置类型">
<button type="button" data-type="servers" class="active">节点</button>
<button type="button" data-type="monitors">监测</button>
<button type="button" data-type="sslcerts">证书</button>
<button type="button" data-type="watchdog">告警</button>
</div>
<div class="config-actions">
<button id="addConfigItemBtn" class="primary-btn">新增节点</button>
<button id="refreshConfigBtn" class="icon-text">刷新配置</button>
</div>
<div id="configItemList" class="config-list"></div>
</section>
<section class="admin-card">
<div class="section-head">
<div>
<h2 id="configEditorTitle">新增节点</h2>
<p class="muted" id="configEditorHint">保存后会写入 config.json,并向 sergate 发送 SIGHUP 重载。</p>
</div>
</div>
<form id="configForm" class="config-form">
<div id="configFields" class="config-fields"></div>
<div class="form-actions">
<button type="submit" class="primary-btn">保存配置</button>
<button type="button" id="resetTrafficBtn" class="warn-btn" style="display:none;">重置月流量</button>
<button type="button" id="deleteConfigItemBtn" class="danger-btn">删除配置</button>
<button type="button" id="resetConfigFormBtn" class="icon-text">清空</button>
</div>
</form>
</section>
</div>
</section>
</main>
<!-- 详情抽屉 -->
<div id="detailModal" class="modal-backdrop drawer-backdrop" style="display:none;">
<aside class="modal-box detail-drawer" role="dialog" aria-modal="true" aria-labelledby="detailTitle">
<button class="modal-close" id="detailClose" aria-label="关闭">×</button>
<h3 id="detailTitle" class="modal-title">节点详情</h3>
<div id="detailContent" class="modal-content">加载中...</div>
</aside>
</div>
<footer class="footer">
<a href="https://github.com/cppla/ServerStatus" target="_blank" rel="noopener">ServerStatus中文版</a>
</footer>
<script src="js/app.js?v=20260709-19" defer></script>
</body>
</html> </html>
+1097
View File
File diff suppressed because it is too large Load Diff
-7
View File
File diff suppressed because one or more lines are too long
-326
View File
@@ -1,326 +0,0 @@
/**
* @preserve HTML5 Shiv 3.7.3 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed
*/
;(function(window, document) {
/*jshint evil:true */
/** version */
var version = '3.7.3';
/** Preset options */
var options = window.html5 || {};
/** Used to skip problem elements */
var reSkip = /^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i;
/** Not all elements can be cloned in IE **/
var saveClones = /^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i;
/** Detect whether the browser supports default html5 styles */
var supportsHtml5Styles;
/** Name of the expando, to work with multiple documents or to re-shiv one document */
var expando = '_html5shiv';
/** The id for the the documents expando */
var expanID = 0;
/** Cached data for each document */
var expandoData = {};
/** Detect whether the browser supports unknown elements */
var supportsUnknownElements;
(function() {
try {
var a = document.createElement('a');
a.innerHTML = '<xyz></xyz>';
//if the hidden property is implemented we can assume, that the browser supports basic HTML5 Styles
supportsHtml5Styles = ('hidden' in a);
supportsUnknownElements = a.childNodes.length == 1 || (function() {
// assign a false positive if unable to shiv
(document.createElement)('a');
var frag = document.createDocumentFragment();
return (
typeof frag.cloneNode == 'undefined' ||
typeof frag.createDocumentFragment == 'undefined' ||
typeof frag.createElement == 'undefined'
);
}());
} catch(e) {
// assign a false positive if detection fails => unable to shiv
supportsHtml5Styles = true;
supportsUnknownElements = true;
}
}());
/*--------------------------------------------------------------------------*/
/**
* Creates a style sheet with the given CSS text and adds it to the document.
* @private
* @param {Document} ownerDocument The document.
* @param {String} cssText The CSS text.
* @returns {StyleSheet} The style element.
*/
function addStyleSheet(ownerDocument, cssText) {
var p = ownerDocument.createElement('p'),
parent = ownerDocument.getElementsByTagName('head')[0] || ownerDocument.documentElement;
p.innerHTML = 'x<style>' + cssText + '</style>';
return parent.insertBefore(p.lastChild, parent.firstChild);
}
/**
* Returns the value of `html5.elements` as an array.
* @private
* @returns {Array} An array of shived element node names.
*/
function getElements() {
var elements = html5.elements;
return typeof elements == 'string' ? elements.split(' ') : elements;
}
/**
* Extends the built-in list of html5 elements
* @memberOf html5
* @param {String|Array} newElements whitespace separated list or array of new element names to shiv
* @param {Document} ownerDocument The context document.
*/
function addElements(newElements, ownerDocument) {
var elements = html5.elements;
if(typeof elements != 'string'){
elements = elements.join(' ');
}
if(typeof newElements != 'string'){
newElements = newElements.join(' ');
}
html5.elements = elements +' '+ newElements;
shivDocument(ownerDocument);
}
/**
* Returns the data associated to the given document
* @private
* @param {Document} ownerDocument The document.
* @returns {Object} An object of data.
*/
function getExpandoData(ownerDocument) {
var data = expandoData[ownerDocument[expando]];
if (!data) {
data = {};
expanID++;
ownerDocument[expando] = expanID;
expandoData[expanID] = data;
}
return data;
}
/**
* returns a shived element for the given nodeName and document
* @memberOf html5
* @param {String} nodeName name of the element
* @param {Document|DocumentFragment} ownerDocument The context document.
* @returns {Object} The shived element.
*/
function createElement(nodeName, ownerDocument, data){
if (!ownerDocument) {
ownerDocument = document;
}
if(supportsUnknownElements){
return ownerDocument.createElement(nodeName);
}
if (!data) {
data = getExpandoData(ownerDocument);
}
var node;
if (data.cache[nodeName]) {
node = data.cache[nodeName].cloneNode();
} else if (saveClones.test(nodeName)) {
node = (data.cache[nodeName] = data.createElem(nodeName)).cloneNode();
} else {
node = data.createElem(nodeName);
}
// Avoid adding some elements to fragments in IE < 9 because
// * Attributes like `name` or `type` cannot be set/changed once an element
// is inserted into a document/fragment
// * Link elements with `src` attributes that are inaccessible, as with
// a 403 response, will cause the tab/window to crash
// * Script elements appended to fragments will execute when their `src`
// or `text` property is set
return node.canHaveChildren && !reSkip.test(nodeName) && !node.tagUrn ? data.frag.appendChild(node) : node;
}
/**
* returns a shived DocumentFragment for the given document
* @memberOf html5
* @param {Document} ownerDocument The context document.
* @returns {Object} The shived DocumentFragment.
*/
function createDocumentFragment(ownerDocument, data){
if (!ownerDocument) {
ownerDocument = document;
}
if(supportsUnknownElements){
return ownerDocument.createDocumentFragment();
}
data = data || getExpandoData(ownerDocument);
var clone = data.frag.cloneNode(),
i = 0,
elems = getElements(),
l = elems.length;
for(;i<l;i++){
clone.createElement(elems[i]);
}
return clone;
}
/**
* Shivs the `createElement` and `createDocumentFragment` methods of the document.
* @private
* @param {Document|DocumentFragment} ownerDocument The document.
* @param {Object} data of the document.
*/
function shivMethods(ownerDocument, data) {
if (!data.cache) {
data.cache = {};
data.createElem = ownerDocument.createElement;
data.createFrag = ownerDocument.createDocumentFragment;
data.frag = data.createFrag();
}
ownerDocument.createElement = function(nodeName) {
//abort shiv
if (!html5.shivMethods) {
return data.createElem(nodeName);
}
return createElement(nodeName, ownerDocument, data);
};
ownerDocument.createDocumentFragment = Function('h,f', 'return function(){' +
'var n=f.cloneNode(),c=n.createElement;' +
'h.shivMethods&&(' +
// unroll the `createElement` calls
getElements().join().replace(/[\w\-:]+/g, function(nodeName) {
data.createElem(nodeName);
data.frag.createElement(nodeName);
return 'c("' + nodeName + '")';
}) +
');return n}'
)(html5, data.frag);
}
/*--------------------------------------------------------------------------*/
/**
* Shivs the given document.
* @memberOf html5
* @param {Document} ownerDocument The document to shiv.
* @returns {Document} The shived document.
*/
function shivDocument(ownerDocument) {
if (!ownerDocument) {
ownerDocument = document;
}
var data = getExpandoData(ownerDocument);
if (html5.shivCSS && !supportsHtml5Styles && !data.hasCSS) {
data.hasCSS = !!addStyleSheet(ownerDocument,
// corrects block display not defined in IE6/7/8/9
'article,aside,dialog,figcaption,figure,footer,header,hgroup,main,nav,section{display:block}' +
// adds styling not present in IE6/7/8/9
'mark{background:#FF0;color:#000}' +
// hides non-rendered elements
'template{display:none}'
);
}
if (!supportsUnknownElements) {
shivMethods(ownerDocument, data);
}
return ownerDocument;
}
/*--------------------------------------------------------------------------*/
/**
* The `html5` object is exposed so that more elements can be shived and
* existing shiving can be detected on iframes.
* @type Object
* @example
*
* // options can be changed before the script is included
* html5 = { 'elements': 'mark section', 'shivCSS': false, 'shivMethods': false };
*/
var html5 = {
/**
* An array or space separated string of node names of the elements to shiv.
* @memberOf html5
* @type Array|String
*/
'elements': options.elements || 'abbr article aside audio bdi canvas data datalist details dialog figcaption figure footer header hgroup main mark meter nav output picture progress section summary template time video',
/**
* current version of html5shiv
*/
'version': version,
/**
* A flag to indicate that the HTML5 style sheet should be inserted.
* @memberOf html5
* @type Boolean
*/
'shivCSS': (options.shivCSS !== false),
/**
* Is equal to true if a browser supports creating unknown/HTML5 elements
* @memberOf html5
* @type boolean
*/
'supportsUnknownElements': supportsUnknownElements,
/**
* A flag to indicate that the document's `createElement` and `createDocumentFragment`
* methods should be overwritten.
* @memberOf html5
* @type Boolean
*/
'shivMethods': (options.shivMethods !== false),
/**
* A string to describe the type of `html5` object ("default" or "default print").
* @memberOf html5
* @type String
*/
'type': 'default',
// shivs the document according to the specified `html5` object options
'shivDocument': shivDocument,
//creates a shived element
createElement: createElement,
//creates a shived documentFragment
createDocumentFragment: createDocumentFragment,
//extends list of elements
addElements: addElements
};
/*--------------------------------------------------------------------------*/
// expose html5
window.html5 = html5;
// shiv the document
shivDocument(document);
if(typeof module == 'object' && module.exports){
module.exports = html5;
}
}(typeof window !== "undefined" ? window : this, document));
-2
View File
File diff suppressed because one or more lines are too long
-5
View File
@@ -1,5 +0,0 @@
/*! Respond.js v1.4.2: min/max-width media query polyfill * Copyright 2013 Scott Jehl
* Licensed under https://github.com/scottjehl/Respond/blob/master/LICENSE-MIT
* */
!function(a){"use strict";a.matchMedia=a.matchMedia||function(a){var b,c=a.documentElement,d=c.firstElementChild||c.firstChild,e=a.createElement("body"),f=a.createElement("div");return f.id="mq-test-1",f.style.cssText="position:absolute;top:-100em",e.style.background="none",e.appendChild(f),function(a){return f.innerHTML='&shy;<style media="'+a+'"> #mq-test-1 { width: 42px; }</style>',c.insertBefore(e,d),b=42===f.offsetWidth,c.removeChild(e),{matches:b,media:a}}}(a.document)}(this),function(a){"use strict";function b(){u(!0)}var c={};a.respond=c,c.update=function(){};var d=[],e=function(){var b=!1;try{b=new a.XMLHttpRequest}catch(c){b=new a.ActiveXObject("Microsoft.XMLHTTP")}return function(){return b}}(),f=function(a,b){var c=e();c&&(c.open("GET",a,!0),c.onreadystatechange=function(){4!==c.readyState||200!==c.status&&304!==c.status||b(c.responseText)},4!==c.readyState&&c.send(null))};if(c.ajax=f,c.queue=d,c.regex={media:/@media[^\{]+\{([^\{\}]*\{[^\}\{]*\})+/gi,keyframes:/@(?:\-(?:o|moz|webkit)\-)?keyframes[^\{]+\{(?:[^\{\}]*\{[^\}\{]*\})+[^\}]*\}/gi,urls:/(url\()['"]?([^\/\)'"][^:\)'"]+)['"]?(\))/g,findStyles:/@media *([^\{]+)\{([\S\s]+?)$/,only:/(only\s+)?([a-zA-Z]+)\s?/,minw:/\([\s]*min\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/,maxw:/\([\s]*max\-width\s*:[\s]*([\s]*[0-9\.]+)(px|em)[\s]*\)/},c.mediaQueriesSupported=a.matchMedia&&null!==a.matchMedia("only all")&&a.matchMedia("only all").matches,!c.mediaQueriesSupported){var g,h,i,j=a.document,k=j.documentElement,l=[],m=[],n=[],o={},p=30,q=j.getElementsByTagName("head")[0]||k,r=j.getElementsByTagName("base")[0],s=q.getElementsByTagName("link"),t=function(){var a,b=j.createElement("div"),c=j.body,d=k.style.fontSize,e=c&&c.style.fontSize,f=!1;return b.style.cssText="position:absolute;font-size:1em;width:1em",c||(c=f=j.createElement("body"),c.style.background="none"),k.style.fontSize="100%",c.style.fontSize="100%",c.appendChild(b),f&&k.insertBefore(c,k.firstChild),a=b.offsetWidth,f?k.removeChild(c):c.removeChild(b),k.style.fontSize=d,e&&(c.style.fontSize=e),a=i=parseFloat(a)},u=function(b){var c="clientWidth",d=k[c],e="CSS1Compat"===j.compatMode&&d||j.body[c]||d,f={},o=s[s.length-1],r=(new Date).getTime();if(b&&g&&p>r-g)return a.clearTimeout(h),h=a.setTimeout(u,p),void 0;g=r;for(var v in l)if(l.hasOwnProperty(v)){var w=l[v],x=w.minw,y=w.maxw,z=null===x,A=null===y,B="em";x&&(x=parseFloat(x)*(x.indexOf(B)>-1?i||t():1)),y&&(y=parseFloat(y)*(y.indexOf(B)>-1?i||t():1)),w.hasquery&&(z&&A||!(z||e>=x)||!(A||y>=e))||(f[w.media]||(f[w.media]=[]),f[w.media].push(m[w.rules]))}for(var C in n)n.hasOwnProperty(C)&&n[C]&&n[C].parentNode===q&&q.removeChild(n[C]);n.length=0;for(var D in f)if(f.hasOwnProperty(D)){var E=j.createElement("style"),F=f[D].join("\n");E.type="text/css",E.media=D,q.insertBefore(E,o.nextSibling),E.styleSheet?E.styleSheet.cssText=F:E.appendChild(j.createTextNode(F)),n.push(E)}},v=function(a,b,d){var e=a.replace(c.regex.keyframes,"").match(c.regex.media),f=e&&e.length||0;b=b.substring(0,b.lastIndexOf("/"));var g=function(a){return a.replace(c.regex.urls,"$1"+b+"$2$3")},h=!f&&d;b.length&&(b+="/"),h&&(f=1);for(var i=0;f>i;i++){var j,k,n,o;h?(j=d,m.push(g(a))):(j=e[i].match(c.regex.findStyles)&&RegExp.$1,m.push(RegExp.$2&&g(RegExp.$2))),n=j.split(","),o=n.length;for(var p=0;o>p;p++)k=n[p],l.push({media:k.split("(")[0].match(c.regex.only)&&RegExp.$2||"all",rules:m.length-1,hasquery:k.indexOf("(")>-1,minw:k.match(c.regex.minw)&&parseFloat(RegExp.$1)+(RegExp.$2||""),maxw:k.match(c.regex.maxw)&&parseFloat(RegExp.$1)+(RegExp.$2||"")})}u()},w=function(){if(d.length){var b=d.shift();f(b.href,function(c){v(c,b.href,b.media),o[b.href]=!0,a.setTimeout(function(){w()},0)})}},x=function(){for(var b=0;b<s.length;b++){var c=s[b],e=c.href,f=c.media,g=c.rel&&"stylesheet"===c.rel.toLowerCase();e&&g&&!o[e]&&(c.styleSheet&&c.styleSheet.rawCssText?(v(c.styleSheet.rawCssText,e,f),o[e]=!0):(!/^([a-zA-Z:]*\/\/)/.test(e)&&!r||e.replace(RegExp.$1,"").split("/")[0]===a.location.host)&&("//"===e.substring(0,2)&&(e=a.location.protocol+e),d.push({href:e,media:f})))}w()};x(),c.update=x,c.getEmValue=t,a.addEventListener?a.addEventListener("resize",b,!1):a.attachEvent&&a.attachEvent("onresize",b)}}(this);
-427
View File
@@ -1,427 +0,0 @@
// serverstatus.js
var error = 0;
var d = 0;
var server_status = new Array();
function timeSince(date) {
if(date == 0)
return "从未.";
var seconds = Math.floor((new Date() - date) / 1000);
var interval = Math.floor(seconds / 31536000);
if (interval > 1)
return interval + " 年前.";
interval = Math.floor(seconds / 2592000);
if (interval > 1)
return interval + " 月前.";
interval = Math.floor(seconds / 86400);
if (interval > 1)
return interval + " 日前.";
interval = Math.floor(seconds / 3600);
if (interval > 1)
return interval + " 小时前.";
interval = Math.floor(seconds / 60);
if (interval > 1)
return interval + " 分钟前.";
/*if(Math.floor(seconds) >= 5)
return Math.floor(seconds) + " seconds";*/
else
return "几秒前.";
}
function bytesToSize(bytes, precision, si)
{
var ret;
si = typeof si !== 'undefined' ? si : 0;
if(si != 0) {
var kilobyte = 1000;
var megabyte = kilobyte * 1000;
var gigabyte = megabyte * 1000;
var terabyte = gigabyte * 1000;
} else {
var kilobyte = 1024;
var megabyte = kilobyte * 1024;
var gigabyte = megabyte * 1024;
var terabyte = gigabyte * 1024;
}
if ((bytes >= 0) && (bytes < kilobyte)) {
return bytes + ' B';
} else if ((bytes >= kilobyte) && (bytes < megabyte)) {
ret = (bytes / kilobyte).toFixed(precision) + ' K';
} else if ((bytes >= megabyte) && (bytes < gigabyte)) {
ret = (bytes / megabyte).toFixed(precision) + ' M';
} else if ((bytes >= gigabyte) && (bytes < terabyte)) {
ret = (bytes / gigabyte).toFixed(precision) + ' G';
} else if (bytes >= terabyte) {
ret = (bytes / terabyte).toFixed(precision) + ' T';
} else {
return bytes + ' B';
}
if(si != 0) {
return ret + 'B';
} else {
return ret + 'iB';
}
}
function uptime() {
$.getJSON("json/stats.json", function(result) {
$("#loading-notice").remove();
if(result.reload)
setTimeout(function() { location.reload(true) }, 1000);
for (var i = 0, rlen=result.servers.length; i < rlen; i++) {
var TableRow = $("#servers tr#r" + i);
var ExpandRow = $("#servers #rt" + i);
var hack; // fuck CSS for making me do this
if(i%2) hack="odd"; else hack="even";
if (!TableRow.length) {
$("#servers").append(
"<tr id=\"r" + i + "\" data-toggle=\"collapse\" data-target=\"#rt" + i + "\" class=\"accordion-toggle " + hack + "\">" +
"<td id=\"online4\"><div class=\"progress\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-warning\"><small>加载中</small></div></div></td>" +
"<td id=\"ip_status\"><div class=\"progress\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-warning\"><small>加载中</small></div></div></td>" +
"<td id=\"name\">加载中</td>" +
"<td id=\"type\">加载中</td>" +
"<td id=\"location\">加载中</td>" +
"<td id=\"uptime\">加载中</td>" +
"<td id=\"load\">加载中</td>" +
"<td id=\"network\">加载中</td>" +
"<td id=\"traffic\">加载中</td>" +
"<td id=\"cpu\"><div class=\"progress\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-warning\"><small>加载中</small></div></div></td>" +
"<td id=\"memory\"><div class=\"progress\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-warning\"><small>加载中</small></div></div></td>" +
"<td id=\"hdd\"><div class=\"progress\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-warning\"><small>加载中</small></div></div></td>" +
"<td id=\"ping\"><div class=\"progress\"><div style=\"width: 100%;\" class=\"progress-bar progress-bar-warning\"><small>加载中</small></div></div></td>" +
"</tr>" +
"<tr class=\"expandRow " + hack + "\"><td colspan=\"16\"><div class=\"accordian-body collapse\" id=\"rt" + i + "\">" +
"<div id=\"expand_mem\">加载中</div>" +
"<div id=\"expand_swap\">加载中</div>" +
"<div id=\"expand_hdd\">加载中</div>" +
"<div id=\"expand_tupd\">加载中</div>" +
"<div id=\"expand_ping\">加载中</div>" +
"<div id=\"expand_custom\">加载中</div>" +
"</div></td></tr>"
);
TableRow = $("#servers tr#r" + i);
ExpandRow = $("#servers #rt" + i);
server_status[i] = true;
}
TableRow = TableRow[0];
if(error) {
TableRow.setAttribute("data-target", "#rt" + i);
server_status[i] = true;
}
// Online4
if (result.servers[i].online4 && !result.servers[i].online6) {
TableRow.children["online4"].children[0].children[0].className = "progress-bar progress-bar-success";
TableRow.children["online4"].children[0].children[0].innerHTML = "<small>IPv4</small>";
} else if (result.servers[i].online4 && result.servers[i].online6) {
TableRow.children["online4"].children[0].children[0].className = "progress-bar progress-bar-success";
TableRow.children["online4"].children[0].children[0].innerHTML = "<small>双栈</small>";
} else if (!result.servers[i].online4 && result.servers[i].online6) {
TableRow.children["online4"].children[0].children[0].className = "progress-bar progress-bar-success";
TableRow.children["online4"].children[0].children[0].innerHTML = "<small>IPv6</small>";
} else {
TableRow.children["online4"].children[0].children[0].className = "progress-bar progress-bar-danger";
TableRow.children["online4"].children[0].children[0].innerHTML = "<small>关闭</small>";
}
// Online6
//if (result.servers[i].online6) {
// TableRow.children["online6"].children[0].children[0].className = "progress-bar progress-bar-success";
// TableRow.children["online6"].children[0].children[0].innerHTML = "<small>开启</small>";
//} else {
// TableRow.children["online6"].children[0].children[0].className = "progress-bar progress-bar-danger";
// TableRow.children["online6"].children[0].children[0].innerHTML = "<small>关闭</small>";
//}
// Ipstatus
// mh361 or mh370, mourn mh370, 2014-03-08 01:20 lost from all over the world.
if (result.servers[i].ip_status) {
TableRow.children["ip_status"].children[0].children[0].className = "progress-bar progress-bar-success";
TableRow.children["ip_status"].children[0].children[0].innerHTML = "<small>MH361</small>";
} else {
TableRow.children["ip_status"].children[0].children[0].className = "progress-bar progress-bar-danger";
TableRow.children["ip_status"].children[0].children[0].innerHTML = "<small>MH370</small>";
}
// Name
TableRow.children["name"].innerHTML = result.servers[i].name;
// Type
TableRow.children["type"].innerHTML = result.servers[i].type;
// Location
TableRow.children["location"].innerHTML = result.servers[i].location;
if (!result.servers[i].online4 && !result.servers[i].online6) {
if (server_status[i]) {
TableRow.children["uptime"].innerHTML = "";
TableRow.children["load"].innerHTML = "";
TableRow.children["network"].innerHTML = "";
TableRow.children["traffic"].innerHTML = "";
TableRow.children["cpu"].children[0].children[0].className = "progress-bar progress-bar-danger";
TableRow.children["cpu"].children[0].children[0].style.width = "100%";
TableRow.children["cpu"].children[0].children[0].innerHTML = "<small>关闭</small>";
TableRow.children["memory"].children[0].children[0].className = "progress-bar progress-bar-danger";
TableRow.children["memory"].children[0].children[0].style.width = "100%";
TableRow.children["memory"].children[0].children[0].innerHTML = "<small>关闭</small>";
TableRow.children["hdd"].children[0].children[0].className = "progress-bar progress-bar-danger";
TableRow.children["hdd"].children[0].children[0].style.width = "100%";
TableRow.children["hdd"].children[0].children[0].innerHTML = "<small>关闭</small>";
TableRow.children["ping"].children[0].children[0].className = "progress-bar progress-bar-danger";
TableRow.children["ping"].children[0].children[0].style.width = "100%";
TableRow.children["ping"].children[0].children[0].innerHTML = "<small>关闭</small>";
if(ExpandRow.hasClass("in")) {
ExpandRow.collapse("hide");
}
TableRow.setAttribute("data-target", "");
server_status[i] = false;
}
} else {
if (!server_status[i]) {
TableRow.setAttribute("data-target", "#rt" + i);
server_status[i] = true;
}
// Uptime
TableRow.children["uptime"].innerHTML = result.servers[i].uptime;
// Load: default load_1, you can change show: load_1, load_5, load_15
if(result.servers[i].load == -1) {
TableRow.children["load"].innerHTML = "";
} else {
TableRow.children["load"].innerHTML = result.servers[i].load_1.toFixed(2);
}
// Network
var netstr = "";
if(result.servers[i].network_rx < 1024)
netstr += result.servers[i].network_rx.toFixed(0) + "B";
else if(result.servers[i].network_rx < 1024*1024)
netstr += (result.servers[i].network_rx/1024).toFixed(0) + "K";
else
netstr += (result.servers[i].network_rx/1024/1024).toFixed(1) + "M";
netstr += " | "
if(result.servers[i].network_tx < 1024)
netstr += result.servers[i].network_tx.toFixed(0) + "B";
else if(result.servers[i].network_tx < 1024*1024)
netstr += (result.servers[i].network_tx/1024).toFixed(0) + "K";
else
netstr += (result.servers[i].network_tx/1024/1024).toFixed(1) + "M";
TableRow.children["network"].innerHTML = netstr;
//Traffic
var trafficstr = "";
if(result.servers[i].network_in < 1024)
trafficstr += result.servers[i].network_in.toFixed(0) + "B";
else if(result.servers[i].network_in < 1024*1024)
trafficstr += (result.servers[i].network_in/1024).toFixed(0) + "K";
else if(result.servers[i].network_in < 1024*1024*1024)
trafficstr += (result.servers[i].network_in/1024/1024).toFixed(1) + "M";
else if(result.servers[i].network_in < 1024*1024*1024*1024)
trafficstr += (result.servers[i].network_in/1024/1024/1024).toFixed(2) + "G";
else
trafficstr += (result.servers[i].network_in/1024/1024/1024/1024).toFixed(2) + "T";
trafficstr += " | "
if(result.servers[i].network_out < 1024)
trafficstr += result.servers[i].network_out.toFixed(0) + "B";
else if(result.servers[i].network_out < 1024*1024)
trafficstr += (result.servers[i].network_out/1024).toFixed(0) + "K";
else if(result.servers[i].network_out < 1024*1024*1024)
trafficstr += (result.servers[i].network_out/1024/1024).toFixed(1) + "M";
else if(result.servers[i].network_out < 1024*1024*1024*1024)
trafficstr += (result.servers[i].network_out/1024/1024/1024).toFixed(2) + "G";
else
trafficstr += (result.servers[i].network_out/1024/1024/1024/1024).toFixed(2) + "T";
TableRow.children["traffic"].innerHTML = trafficstr;
// CPU
if (result.servers[i].cpu >= 90)
TableRow.children["cpu"].children[0].children[0].className = "progress-bar progress-bar-danger";
else if (result.servers[i].cpu >= 80)
TableRow.children["cpu"].children[0].children[0].className = "progress-bar progress-bar-warning";
else
TableRow.children["cpu"].children[0].children[0].className = "progress-bar progress-bar-success";
TableRow.children["cpu"].children[0].children[0].style.width = result.servers[i].cpu + "%";
TableRow.children["cpu"].children[0].children[0].innerHTML = result.servers[i].cpu + "%";
// Memory
var Mem = ((result.servers[i].memory_used/result.servers[i].memory_total)*100.0).toFixed(0);
if (Mem >= 90)
TableRow.children["memory"].children[0].children[0].className = "progress-bar progress-bar-danger";
else if (Mem >= 80)
TableRow.children["memory"].children[0].children[0].className = "progress-bar progress-bar-warning";
else
TableRow.children["memory"].children[0].children[0].className = "progress-bar progress-bar-success";
TableRow.children["memory"].children[0].children[0].style.width = Mem + "%";
TableRow.children["memory"].children[0].children[0].innerHTML = Mem + "%";
ExpandRow[0].children["expand_mem"].innerHTML = "内存: " + bytesToSize(result.servers[i].memory_used*1024, 2) + " / " + bytesToSize(result.servers[i].memory_total*1024, 2);
// Swap
ExpandRow[0].children["expand_swap"].innerHTML = "交换分区: " + bytesToSize(result.servers[i].swap_used*1024, 2) + " / " + bytesToSize(result.servers[i].swap_total*1024, 2);
// HDD
var HDD = ((result.servers[i].hdd_used/result.servers[i].hdd_total)*100.0).toFixed(0);
if (HDD >= 90)
TableRow.children["hdd"].children[0].children[0].className = "progress-bar progress-bar-danger";
else if (HDD >= 80)
TableRow.children["hdd"].children[0].children[0].className = "progress-bar progress-bar-warning";
else
TableRow.children["hdd"].children[0].children[0].className = "progress-bar progress-bar-success";
TableRow.children["hdd"].children[0].children[0].style.width = HDD + "%";
TableRow.children["hdd"].children[0].children[0].innerHTML = HDD + "%";
ExpandRow[0].children["expand_hdd"].innerHTML = "硬盘: " + bytesToSize(result.servers[i].hdd_used*1024*1024, 2) + " / " + bytesToSize(result.servers[i].hdd_total*1024*1024, 2);
// delay time
// tcp, udp, process, thread count
ExpandRow[0].children["expand_tupd"].innerHTML = "TCP/UDP/进/线: " + result.servers[i].tcp_count + " / " + result.servers[i].udp_count + " / " + result.servers[i].process_count+ " / " + result.servers[i].thread_count;
ExpandRow[0].children["expand_ping"].innerHTML = "联通/电信/移动: " + result.servers[i].time_10010 + "ms / " + result.servers[i].time_189 + "ms / " + result.servers[i].time_10086 + "ms"
// ping
var PING_10010 = result.servers[i].ping_10010.toFixed(0);
var PING_189 = result.servers[i].ping_189.toFixed(0);
var PING_10086 = result.servers[i].ping_10086.toFixed(0);
if (PING_10010 >= 20 || PING_189 >= 20 || PING_10086 >= 20)
TableRow.children["ping"].children[0].children[0].className = "progress-bar progress-bar-danger";
else
TableRow.children["ping"].children[0].children[0].className = "progress-bar progress-bar-success";
TableRow.children["ping"].children[0].children[0].innerHTML = PING_10010 + "%💻" + PING_189 + "%💻" + PING_10086 + "%";
// Custom
if (result.servers[i].custom) {
ExpandRow[0].children["expand_custom"].innerHTML = result.servers[i].custom
} else {
ExpandRow[0].children["expand_custom"].innerHTML = ""
}
}
};
d = new Date(result.updated*1000);
error = 0;
}).fail(function(update_error) {
if (!error) {
$("#servers > tr.accordion-toggle").each(function(i) {
var TableRow = $("#servers tr#r" + i)[0];
var ExpandRow = $("#servers #rt" + i);
TableRow.children["online4"].children[0].children[0].className = "progress-bar progress-bar-error";
TableRow.children["online4"].children[0].children[0].innerHTML = "<small>错误</small>";
//TableRow.children["online6"].children[0].children[0].className = "progress-bar progress-bar-error";
//TableRow.children["online6"].children[0].children[0].innerHTML = "<small>错误</small>";
TableRow.children["ip_status"].children[0].children[0].className = "progress-bar progress-bar-error";
TableRow.children["ip_status"].children[0].children[0].innerHTML = "<small>错误</small>";
TableRow.children["uptime"].children[0].children[0].className = "progress-bar progress-bar-error";
TableRow.children["uptime"].children[0].children[0].innerHTML = "<small>错误</small>";
TableRow.children["load"].children[0].children[0].className = "progress-bar progress-bar-error";
TableRow.children["load"].children[0].children[0].innerHTML = "<small>错误</small>";
TableRow.children["network"].children[0].children[0].className = "progress-bar progress-bar-error";
TableRow.children["network"].children[0].children[0].innerHTML = "<small>错误</small>";
TableRow.children["traffic"].children[0].children[0].className = "progress-bar progress-bar-error";
TableRow.children["traffic"].children[0].children[0].innerHTML = "<small>错误</small>";
TableRow.children["cpu"].children[0].children[0].className = "progress-bar progress-bar-error";
TableRow.children["cpu"].children[0].children[0].style.width = "100%";
TableRow.children["cpu"].children[0].children[0].innerHTML = "<small>错误</small>";
TableRow.children["memory"].children[0].children[0].className = "progress-bar progress-bar-error";
TableRow.children["memory"].children[0].children[0].style.width = "100%";
TableRow.children["memory"].children[0].children[0].innerHTML = "<small>错误</small>";
TableRow.children["hdd"].children[0].children[0].className = "progress-bar progress-bar-error";
TableRow.children["hdd"].children[0].children[0].style.width = "100%";
TableRow.children["hdd"].children[0].children[0].innerHTML = "<small>错误</small>";
TableRow.children["ping"].children[0].children[0].className = "progress-bar progress-bar-error";
TableRow.children["ping"].children[0].children[0].style.width = "100%";
TableRow.children["ping"].children[0].children[0].innerHTML = "<small>错误</small>";
if(ExpandRow.hasClass("in")) {
ExpandRow.collapse("hide");
}
TableRow.setAttribute("data-target", "");
server_status[i] = false;
});
}
error = 1;
$("#updated").html("更新错误.");
});
}
function updateTime() {
if (!error)
$("#updated").html("最后更新: " + timeSince(d));
}
uptime();
updateTime();
setInterval(uptime, 2000);
setInterval(updateTime, 2000);
// styleswitcher.js
function setActiveStyleSheet(title) {
var i, a, main;
for(i=0; (a = document.getElementsByTagName("link")[i]); i++) {
if(a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title")) {
a.disabled = true;
if(a.getAttribute("title") == title) a.disabled = false;
}
}
}
function getActiveStyleSheet() {
var i, a;
for(i=0; (a = document.getElementsByTagName("link")[i]); i++) {
if(a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title") && !a.disabled)
return a.getAttribute("title");
}
return null;
}
function getPreferredStyleSheet() {
var i, a;
for(i=0; (a = document.getElementsByTagName("link")[i]); i++) {
if(a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("rel").indexOf("alt") == -1 && a.getAttribute("title"))
return a.getAttribute("title");
}
return null;
}
function createCookie(name,value,days) {
if (days) {
var date = new Date();
date.setTime(date.getTime()+(days*24*60*60*1000));
var expires = "; expires="+date.toGMTString();
}
else expires = "";
document.cookie = name+"="+value+expires+"; path=/";
}
function readCookie(name) {
var nameEQ = name + "=";
var ca = document.cookie.split(';');
for(var i=0;i < ca.length;i++) {
var c = ca[i];
while (c.charAt(0)==' ')
c = c.substring(1,c.length);
if (c.indexOf(nameEQ) == 0)
return c.substring(nameEQ.length,c.length);
}
return null;
}
window.onload = function(e) {
var cookie = readCookie("style");
var title = cookie ? cookie : getPreferredStyleSheet();
setActiveStyleSheet(title);
}
window.onunload = function(e) {
var title = getActiveStyleSheet();
createCookie("style", title, 365);
}
var cookie = readCookie("style");
var title = cookie ? cookie : getPreferredStyleSheet();
setActiveStyleSheet(title);
-70
View File
@@ -1,70 +0,0 @@
#!/usr/bin/env python
# coding: utf-8
# Update by : https://github.com/cppla/ServerStatus
# 支持Python版本:2.7 to 3.5; requirements.txt: requests, PrettyTable
# 时间: 20180828
'''
maybe better by youself
'''
import os
import sys
import requests
import time
from prettytable import PrettyTable
# todo: 程序在非gui环境下目前有闪屏的bug
scroll = True
clear = lambda: os.system('clear' if 'linux' in sys.platform else 'cls')
def sscmd(address):
while True:
r = requests.get(
url=address,
headers={
"User-Agent": "ServerStatus/20181203",
}
)
jsonR = r.json()
ss = PrettyTable(
[
"Flight",
"节点名",
# "虚拟化",
"位置",
"在线时间",
"负载",
"网络",
"流量",
"处理器",
"内存",
"硬盘"
],
)
for i in jsonR["servers"]:
ss.add_row(
[
"%s" % 'MH361' if i["ip_status"] is True else 'MH370',
"%s" % i["name"],
# "%s" % i["type"],
"%s" % i["location"],
"%s" % i["uptime"],
"%s" % (i["load_1"]),
"%.2fM|%.2fM" % (float(i["network_rx"]) / 1000 / 1000, float(i["network_tx"]) / 1000 / 1000),
"%.2fG|%.2fG" % (
float(i["network_in"]) / 1024 / 1024 / 1024, float(i["network_out"]) / 1024 / 1024 / 1024),
"%d%%" % (i["cpu"]),
"%d%%" % (float(i["memory_used"]) / i["memory_total"] * 100),
"%d%%" % (float(i["hdd_used"]) / i["hdd_total"] * 100),
]
)
if scroll is True:
clear()
print(ss)
time.sleep(1)
if __name__ == '__main__':
default = 'https://tz.cloudcpp.com/json/stats.json'
ads = sys.argv[1] if len(sys.argv)==2 else default
sscmd(ads)