Compare commits

...
389 Commits
Author SHA1 Message Date
cppla 1512d2999b fix client command-line argument parsing 2026-07-10 18:09:36 +08:00
cpplaandGitHub 27e85c9bfd Merge pull request #287 from cppla/dependabot/go_modules/server/golang.org/x/net-0.55.0
Bump golang.org/x/net from 0.54.0 to 0.55.0 in /server
2026-07-10 17:59:15 +08:00
cpplaandGitHub 8f8fcb2ff0 Merge pull request #286 from cppla/dependabot/go_modules/server/github.com/quic-go/quic-go-0.59.1
Bump github.com/quic-go/quic-go from 0.59.0 to 0.59.1 in /server
2026-07-10 17:11:54 +08:00
cppla 1483f9063a fix CI after removing telegram plugin 2026-07-10 17:07:23 +08:00
dependabot[bot]andGitHub 0d2c12a034 Bump golang.org/x/net from 0.54.0 to 0.55.0 in /server
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.54.0 to 0.55.0.
- [Commits](https://github.com/golang/net/compare/v0.54.0...v0.55.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-version: 0.55.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 09:01:40 +00:00
dependabot[bot]andGitHub 888af9bf12 Bump github.com/quic-go/quic-go from 0.59.0 to 0.59.1 in /server
Bumps [github.com/quic-go/quic-go](https://github.com/quic-go/quic-go) from 0.59.0 to 0.59.1.
- [Release notes](https://github.com/quic-go/quic-go/releases)
- [Commits](https://github.com/quic-go/quic-go/compare/v0.59.0...v0.59.1)

---
updated-dependencies:
- dependency-name: github.com/quic-go/quic-go
  dependency-version: 0.59.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 09:01:32 +00:00
cpplaandGitHub 2e13110c3b Merge pull request #285 from cppla/dependabot/go_modules/server/golang.org/x/crypto-0.52.0
Bump golang.org/x/crypto from 0.48.0 to 0.52.0 in /server
2026-07-10 16:59:50 +08:00
cppla 950c4d4a9a update 2.0.0 2026-07-10 16:53:46 +08:00
dependabot[bot]andGitHub bdf5e45c5b Bump golang.org/x/crypto from 0.48.0 to 0.52.0 in /server
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.48.0 to 0.52.0.
- [Commits](https://github.com/golang/crypto/compare/v0.48.0...v0.52.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.52.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-10 07:37:18 +00:00
cppla e4ca0558f7 1.2.0 2026-07-10 15:35:10 +08:00
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
cpplaandGitHub 6c57044935 Merge pull request #100 from kings-way/master
更准确的网络丢包数据以及更快的数据收敛
2021-07-05 11:08:54 +08:00
King's Way fdf5f03a13 take 'Connection Refused' as a successful ping probe 2021-07-03 15:26:39 +08:00
King's Way 05f9784b1e ping_thread, prefer ipv4 first 2021-06-18 19:18:51 +08:00
King's Way 270c45aead More accurate packet loss rate and faster convergence 2021-06-18 18:40:29 +08:00
cpplaandGitHub 2d0bdd0ab8 Update README.md
增加arm64 版本的镜像
2021-05-28 17:17:40 +08:00
cpplaandGitHub 7e866f70cc Update README.md
增加arm64版本的镜像
2021-05-28 17:16:37 +08:00
cpplaandGitHub 032189e257 Update README.md
update title strong
2020-10-22 17:42:32 +08:00
cpplaandGitHub b05f73bb88 增加快速跟随系统启动说明
增加快速跟随系统启动说明
2020-10-22 17:39:23 +08:00
71 changed files with 7301 additions and 8478 deletions
+10
View File
@@ -0,0 +1,10 @@
.git
.github
.idea
*.sublime-workspace
server/serverstatus
web/json/stats.json
web/json/stats.json~
*.bak-*
__pycache__
*.pyc
+42
View File
@@ -0,0 +1,42 @@
name: CI
on:
push:
pull_request:
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version-file: server/go.mod
cache-dependency-path: server/go.sum
- name: Test Go server
working-directory: server
run: |
test -z "$(gofmt -l .)"
go vet ./...
go test -race ./...
CGO_ENABLED=0 go build -trimpath -o /tmp/serverstatus .
- name: Check clients, WebUI and shell scripts
run: |
python3 -m py_compile clients/client-linux.py clients/client-psutil.py
python3 -m unittest clients/test_client_args.py
sh -n clients/entrypoint.sh
bash -n 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 .
+28
View File
@@ -0,0 +1,28 @@
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 \
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 golang:1.25-alpine AS builder
WORKDIR /src/server
COPY server/go.mod server/go.sum ./
RUN go mod download
COPY server/*.go ./
ARG VERSION=2.0.0
ARG COMMIT=none
ARG BUILD_TIME=unknown
RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags="-s -w -X main.version=${VERSION} -X main.commit=${COMMIT} -X main.buildTime=${BUILD_TIME}" \
-o /out/serverstatus .
FROM alpine:3.22
LABEL maintainer="cppla <https://cpp.la>"
RUN apk add --no-cache ca-certificates tzdata \
&& mkdir -p /app/config /app/data /app/web
COPY --from=builder /out/serverstatus /usr/local/bin/serverstatus
COPY server/config.json /app/config/config.json
COPY web /app/web/
ENV TZ=Asia/Shanghai \
CONFIG_PATH=/app/config/config.json \
STATS_PATH=/app/data/stats.json \
WEB_DIR=/app/web \
HTTP_ADDR=:80 \
AGENT_ADDR=:35601
EXPOSE 80 35601
HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
CMD wget -q -O /dev/null http://127.0.0.1/api/health || exit 1
ENTRYPOINT ["/usr/local/bin/serverstatus"]
+302 -103
View File
@@ -1,141 +1,340 @@
# ServerStatus中文版 # ServerStatus 中文版
* ServerStatus中文版是一个酷炫高逼格的云探针云监控、服务器云监控、多服务器探针~ ServerStatus 是一个轻量的服务器探针云监控面板,支持多节点在线状态、资源占用、三网延迟、服务监测、SSL 证书检查、Watchdog 告警、HTTP API 和 Web 配置管理
* 在线演示:https://tz.cloudcpp.com
[![Build Status](https://img.shields.io/travis/otale/tale.svg?style=flat-square)](https://github.com/cppla/ServerStatus) 在线演示:https://tz.cloudcpp.com
[![Python Support](https://img.shields.io/badge/python-2.7%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)
[![License](https://img.shields.io/badge/license-MIT-4EB1BA.svg?style=flat-square)](https://github.com/cppla/ServerStatus)
![Latest Version](http://dl.cpp.la/Archive/serverstatus.png) [![Go](https://img.shields.io/badge/Go-1.25%2B-00ADD8?logo=go&logoColor=white)](https://go.dev/)
[![CI](https://github.com/cppla/ServerStatus/actions/workflows/ci.yml/badge.svg)](https://github.com/cppla/ServerStatus/actions/workflows/ci.yml)
[![License](https://img.shields.io/badge/license-MIT-4EB1BA.svg)](LICENSE)
[![Version](https://img.shields.io/badge/Version-2.0.0-red)](https://github.com/cppla/ServerStatus)
# 目录介绍: ![ServerStatus WebUI](https://dl.cpp.la/Archive/serverstatus_2_0_0.webp)
* autodeploy 自动部署.
* clients 客户端文件
* server 服务端文件
* web 网站文件
# 更新说明: ## 一、服务端
* 20200629, 优化IPv6,优化前端。注意docker默认是不支持IPv6的, 如使用docker需要手动开启ipv6
* 20200407, 网速计算不严谨,fixed
* 20190129, 降低CPU占用
* 20181221, 增加实时到三网的延迟
* 20181126, add tupd(tcp, udp, process ,thread) count for view ddcc attack
* 20180829, 网络情况:主机到三网(CU,CT,CM)每小时丢包率的检测
* 20180726, 一切皆容器额,查看自动部署或autodeploy/readme
* 20180312, 加入失联(被照顾)检测【正常:MH361, 屏蔽:MH370】,校准虚拟化流量统计异常      
* 20170807, 更新平均1,5,15负载, 增加服务器总流量监控        
# 自动部署:
【服务端】:
```bash ```bash
wget https://raw.githubusercontent.com/cppla/ServerStatus/master/autodeploy/config.json # Docker Compose,本地构建加:--build
docker run -d --restart=always --name=serverstatus -v {$path}/config.json:/ServerStatus/server/config.json -p {$port}:80 -p {$port}:35601 cppla/serverstatus ADMIN_TOKEN='your-strong-token' docker compose -f docker-compose-server.yml up -d --build
eg:
docker run -d --restart=always --name=serverstatus -v ~/config.json:/ServerStatus/server/config.json -p 80:80 -p 35601:35601 cppla/serverstatus
``` ```
【客户端】:
```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-data
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:/app/config/config.json \
-v ~/serverstatus-data:/app/data \
-p 8080:80 -p 35601:35601 \
cppla/serverstatus:server
``` ```
# 手动安装教程: 启动后访问:
【克隆代码】: - WebUIhttp://127.0.0.1:8080/
``` - 健康检查:http://127.0.0.1:8080/api/health
git clone https://github.com/cppla/ServerStatus.git - API 描述:http://127.0.0.1:8080/api/schema
- OpenAPI 3.1http://127.0.0.1:8080/api/openapi.json
- 客户端上报端口:`35601/tcp`
`ADMIN_TOKEN` 不设置时,监控页面仍可读取,管理 API 返回 `503`,WebUI 的“配置”页不能修改数据。
## 二、客户端
```bash
# Docker Compose,本地构建加:--build
SERVER=127.0.0.1 USER=s01 PASSWORD=USER_DEFAULT_PASSWORD \
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 \
-e PASSWORD=USER_DEFAULT_PASSWORD \
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 PASSWORD=USER_DEFAULT_PASSWORD >/dev/null 2>&1 &
``` ```
cd ServerStatus/server
make
./sergate
```
如果没错误提示,OK,ctrl+c关闭;如果有错误提示,检查35601端口是否被占用
二、修改配置文件 `USER` 是常见的宿主机环境变量名。如果没有显式传递或传递方式错误,Compose 可能会把系统中的 `$USER` 解析成本机用户名,而不是默认的 `s01`。推荐优先级:
修改config.json文件,注意username, password的值需要和客户端对应一致    
1. 运行命令显式传递 `USER=s01`
2. 修改 `docker-compose-client.yml` 中的 `USER` 默认值
3. Docker 或系统环境中的 `USER`
| 变量 | 默认值 | 说明 |
| --- | --- | --- |
| `SERVER` | `127.0.0.1` | Go 服务端地址 |
| `USER` | `s01` | 客户端用户名,必须匹配服务端配置 |
| `PORT` | `35601` | Agent TCP 上报端口 |
| `PASSWORD` | `USER_DEFAULT_PASSWORD` | 客户端密码,必须匹配服务端配置 |
| `INTERVAL` | `1` | 状态上报间隔,单位秒 |
| `PROBEPORT` | `80` | 三网 TCP 探测端口 |
| `PROBE_PROTOCOL_PREFER` | `ipv4` | 探测协议偏好,可选 `ipv4``ipv6` |
| `PING_PACKET_HISTORY_LEN` | `100` | 丢包历史窗口 |
| `CU` | `cu.tz.cloudcpp.com` | 联通探测地址 |
| `CT` | `ct.tz.cloudcpp.com` | 电信探测地址 |
| `CM` | `cm.tz.cloudcpp.com` | 移动探测地址 |
| `CLIENT` | `psutil` | 客户端实现,可选 `psutil``linux` |
## 服务端参数
Docker 镜像中的默认路径为:
| 环境变量 | 默认值 | 说明 |
| --- | --- | --- |
| `CONFIG_PATH` | `/app/config/config.json` | 主配置文件 |
| `STATS_PATH` | `/app/data/stats.json` | 月流量与状态持久化文件 |
| `WEB_DIR` | `/app/web` | WebUI 静态文件目录 |
| `HTTP_ADDR` | `:80` | WebUI 与 HTTP API 监听地址 |
| `AGENT_ADDR` | `:35601` | 客户端 TCP 上报监听地址 |
| `ADMIN_TOKEN` | 空 | 管理 API Bearer Token;为空时禁用管理接口 |
| `ADMIN_CORS_ORIGIN` | 空 | 可选的 API CORS Origin |
| `INSECURE_CALLBACK_TLS` | `false` | 是否允许 Watchdog/证书回调使用不可信 TLS 证书 |
| `VERBOSE` | `false` | 输出 Gin HTTP 请求日志 |
| `TZ` | `Asia/Shanghai` | 容器时区 |
对应命令行参数:
```text
--config, -c config.json 路径
--stats stats.json 路径
--web-dir, -d WebUI 目录
--http HTTP 监听地址
--agent Agent TCP 监听地址
--verbose, -v 详细请求日志
--version 输出构建版本
``` ```
{"servers":
[ 旧参数 `--bind/-b``--port/-p` 仍可用于设置 Agent TCP 监听地址。
## HTTP 管理 API
管理接口使用 Bearer Token
```http
Authorization: Bearer <ADMIN_TOKEN>
```
无需认证:
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| `GET` | `/api/health` | 进程、Agent TCP、版本和配置路径状态 |
| `GET` | `/api/schema` | 机器可读的端点与配置集合描述 |
| `GET` | `/api/openapi.json` | 可供 AI Agent 直接导入的 OpenAPI 3.1 文档 |
| `GET` | `/json/stats.json` | WebUI 使用的实时状态快照 |
需要认证:
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| `GET/PUT` | `/api/config` | 读取或整体替换配置 |
| `GET/POST` | `/api/servers` | 查询或新增节点 |
| `PUT/DELETE` | `/api/servers/{username}` | 修改或删除节点 |
| `POST` | `/api/servers/{username}/reset-traffic` | 将当前流量设为本月基线 |
| `GET/POST` | `/api/monitors` | 查询或新增服务监测 |
| `PUT/DELETE` | `/api/monitors/{index-or-name}` | 修改或删除服务监测 |
| `GET/POST` | `/api/sslcerts` | 查询或新增证书监测 |
| `PUT/DELETE` | `/api/sslcerts/{index-or-name}` | 修改或删除证书监测 |
| `GET/POST` | `/api/watchdog` | 查询或新增 Watchdog |
| `PUT/DELETE` | `/api/watchdog/{index-or-name}` | 修改或删除 Watchdog |
| `POST` | `/api/reload` | 从磁盘重新读取配置 |
| `POST` | `/api/restart` | 在进程内重启采集运行时 |
配置修改采用“校验 → 备份 → 持久化 → 原子切换”的顺序。成功后现有 Agent 连接会被关闭,Python 客户端约 3 秒后自动重连并获取新的 `monitors``/api/restart` 不退出 Go 进程,因此 Docker 和手动运行方式具有一致语义。
常用调用:
```bash
TOKEN='请替换为 ADMIN_TOKEN'
curl http://127.0.0.1:8080/api/health
curl -H "Authorization: Bearer ${TOKEN}" \
http://127.0.0.1:8080/api/config
curl -X POST http://127.0.0.1:8080/api/servers \
-H "Authorization: Bearer ${TOKEN}" \
-H 'Content-Type: application/json' \
-d '{"username":"s05","name":"node5","type":"kvm","host":"host5","location":"SG","password":"change-me","monthstart":1}'
curl -X DELETE \
-H "Authorization: Bearer ${TOKEN}" \
http://127.0.0.1:8080/api/servers/s05
```
请求体最大为 `1 MiB`。AI Agent 可直接导入 `/api/openapi.json`;轻量客户端也可以先读取 `/api/schema`,再根据返回的集合字段调用 CRUD 接口。
## 配置文件
```json
{
"servers": [
{ {
"username": "s01", "username": "s01",
"name": "Mainserver 1", "name": "node1",
"type": "Dedicated Server", "type": "kvm",
"host": "GenericServerHost123", "host": "host1",
"location": "Austria", "location": "CN",
"password": "some-hard-to-guess-copy-paste-password" "password": "USER_DEFAULT_PASSWORD",
}, "monthstart": 1,
"disabled": false
}
],
"monitors": [
{
"name": "example",
"host": "https://example.com",
"interval": 600,
"type": "https"
}
],
"sslcerts": [
{
"name": "example",
"domain": "https://example.com",
"port": 443,
"interval": 7200,
"callback": "https://example.net/push?message="
}
],
"watchdog": [
{
"name": "offline warning",
"rule": "online4=0&online6=0",
"interval": 600,
"callback": "https://example.net/push?message="
}
] ]
} }
``` ```
三、拷贝ServerStatus/status到你的网站目录 约束:
例如:
``` - `servers.username` 必须唯一。
sudo cp -r ServerStatus/web/* /home/wwwroot/default - `monthstart` 自动限制在 `1-28`
- `port` 自动限制在 `1-65535`
- `interval` 最小为 1 秒;Watchdog 中表示通知冷却时间,不是客户端采集间隔。
- 配置写入前会创建 `config.json.bak-*`,最多保留 10 份。
- Docker 单文件 bind mount 无法被 `rename` 覆盖时,服务端会在完成备份后安全地写回原 inode。
使用 Docker 单文件挂载时,配置备份位于容器 `/app/config` 的可写层;如需长期保留历史版本,建议同时在宿主机备份 `server/config.json`
### Watchdog 表达式
`rule` 由 Go `expr` 引擎执行,并兼容旧版 Exprtk 的常用写法。Go 服务会把字符串外的单个操作符自动转换:
```text
& -> &&
| -> ||
= -> ==
``` ```
四、运行服务端: 例如以下两种写法等价:
web-dir参数为上一步设置的网站根目录,务必修改成自己网站的路径
``` ```text
./sergate --config=config.json --web-dir=/home/wwwroot/default cpu>90&load_1>5&username!='s01'
cpu>90 && load_1>5 && username!='s01'
``` ```
【客户端配置】(客户端程序在ServerStatus/clients下): 字符串值支持中文、Emoji 和其他 Unicode 字符,例如:
客户端有两个版本,client-linux为普通linuxclient-psutil为跨平台版,普通版不成功,换成跨平台版即可。
一、client-linux版配置: ```text
1、vim client-linux.py, 修改SERVER地址,username帐号, password密码 username='节点一号'&name='上海节点'&location='中国 🇨🇳'&type='云主机'
2、python client-linux.py 运行即可。
二、client-psutil版配置:
1、安装psutil跨平台依赖库
2、vim client-psutil.py, 修改SERVER地址,username帐号, password密码
3、python client-psutil.py 运行即可。
```
### for Centos
sudo yum -y install epel-release
sudo yum -y install python-pip
sudo yum clean all
sudo yum -y install gcc
sudo yum -y install python-devel
sudo pip install psutil
### for Ubuntu/Debian:
sudo root
apt-get -y install python-setuptools python-dev build-essential
apt-get -y install python-pip
pip install psutil
### for Windows:
打开网址:https://pypi.python.org/pypi?:action=display&name=psutil#downloads
下载psutil for windows程序包
安装即可
``` ```
打开云探针页面,就可以正常的监控。接下来把服务器和客户端脚本自行加入开机启动,或者进程守护,或以后台方式运行即可!例如: nohup python client-linux.py & 字段名必须使用系统定义的英文名称。可用字段包括:`username``name``type``host``location``load_1``load_5``load_15``cpu``memory_total``memory_used``swap_total``swap_used``hdd_total``hdd_used``network_rx``network_tx``network_in``network_out``last_network_in``last_network_out``ping_10010``ping_189``ping_10086``time_10010``time_189``time_10086``tcp_count``udp_count``process_count``thread_count``io_read``io_write``online4``online6`
# 为什么会有ServerStatus中文版: 客户端断开 25 秒后仍未重连,服务端才计算离线规则,避免短暂网络波动触发告警。每个节点、每条规则分别记录冷却时间。
* 有些功能确实没用 ### SSL 证书
* 原版本部署,英文说明复杂
* 不符合中文版的习惯
* 没有一次又一次的轮子,哪来如此优秀的云探针
# 相关开源项目,感谢: 证书检查使用 Go `crypto/tls`,不再调用外部 `openssl`。服务端记录到期时间、剩余天数和域名匹配状态,并保留原来的 7/3/1 天通知档位与冷却时间。
* ServerStatushttps://github.com/BotoX/ServerStatus 回调默认校验 HTTPS 证书。仅在必须兼容自签名回调服务时设置:
* mojeda: https://github.com/mojeda
* mojeda's ServerStatus: https://github.com/mojeda/ServerStatus ```bash
* BlueVM's project: http://www.lowendtalk.com/discussion/comment/169690#Comment_169690 INSECURE_CALLBACK_TLS=true
```
## 源码编译和运行
需要 Go `1.25` 或更高版本:
```bash
cd server
go mod download
go test ./...
go build -trimpath -ldflags='-s -w' -o serverstatus .
```
`server/` 目录启动:
```bash
ADMIN_TOKEN='请替换为高强度随机字符串' \
./serverstatus \
--config=config.json \
--stats=../web/json/stats.json \
--web-dir=../web \
--http=:8080 \
--agent=:35601
```
访问 http://127.0.0.1:8080/。发送 `SIGHUP` 可以重新读取配置:
```bash
kill -HUP "$(pgrep -x serverstatus)"
```
Systemd 示例位于 `service/status-server.service`。一键脚本 `status.sh` 也已切换到 Go 构建,但 Docker 仍是推荐部署方式。
## 构建和测试
```bash
# Go 单元、协议、API、TLS 和回调测试
cd server
go test ./...
go test -race ./...
go vet ./...
# Docker 镜像
cd ..
docker build -f Dockerfile.server -t cppla/serverstatus:server .
docker build -f Dockerfile.client -t cppla/serverstatus:client .
# Compose 配置
docker compose -f docker-compose-server.yml config
docker compose -f docker-compose-client.yml config
```
CI 还会检查 Go 格式、Python 客户端、Shell 脚本、WebUI JavaScript、服务端/客户端 Compose 文件和两个 Docker 镜像。
## 从旧服务端迁移
1. 备份原来的 `config.json``web/json/stats.json`
2. 原配置结构和客户端账号无需转换。
3. Docker 挂载目标改为 `/app/config/config.json``/app/data`
4. 删除旧的 nginx、`manage_api.py``sergate` 启动或监督配置。
5. 启动 Go 服务端后检查 `/api/health`,再观察客户端自动重连。
`stats.json` 会按节点的 `name/type/host/location` 恢复月流量基线。修改这些身份字段会被视为新节点。
## 致谢
- BotoXhttps://github.com/BotoX/ServerStatus
- mojedahttps://github.com/mojeda/ServerStatus
-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
+390 -71
View File
@@ -1,23 +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
PORBEPORT = 80
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
@@ -26,8 +26,47 @@ import re
import os import os
import sys import sys
import json import json
import errno
import subprocess import subprocess
import threading import threading
import platform
from queue import Queue
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 parse_cli_args(arguments):
overrides = {}
for argument in arguments:
key, separator, value = argument.partition('=')
if separator and key in {'SERVER', 'PORT', 'USER', 'PASSWORD', 'INTERVAL'}:
overrides[key] = value
return overrides
def get_uptime(): def get_uptime():
with open('/proc/uptime', 'r') as f: with open('/proc/uptime', 'r') as f:
@@ -50,11 +89,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:
@@ -79,12 +141,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] \
@@ -112,18 +241,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, PORBEPORT), 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"
@@ -153,30 +270,47 @@ 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
allPacket = 0 packet_queue = Queue(maxsize=PING_PACKET_HISTORY_LEN)
startTime = time.time()
while True: while True:
# flush dns , every time.
IP = host
if host.count(':') < 1: # if not plain ipv6 address, means ipv4 address or hostname
try:
if PROBE_PROTOCOL_PREFER == 'ipv4':
IP = socket.getaddrinfo(host, None, socket.AF_INET)[0][4][0]
else:
IP = socket.getaddrinfo(host, None, socket.AF_INET6)[0][4][0]
except Exception:
pass
if packet_queue.full():
if packet_queue.get() == 0:
lostPacket -= 1
try: try:
b = timeit.default_timer() b = timeit.default_timer()
socket.create_connection((host, port), timeout=1).close() socket.create_connection((IP, port), timeout=1).close()
pingTime[mark] = int((timeit.default_timer()-b)*1000) pingTime[mark] = int((timeit.default_timer() - b) * 1000)
except: packet_queue.put(1)
except socket.error as error:
if error.errno == errno.ECONNREFUSED:
pingTime[mark] = int((timeit.default_timer() - b) * 1000)
packet_queue.put(1)
#elif error.errno == errno.ETIMEDOUT:
else:
lostPacket += 1 lostPacket += 1
finally: packet_queue.put(0)
allPacket += 1
if allPacket > 100: if packet_queue.qsize() > 30:
lostRate[mark] = float(lostPacket) / allPacket lostRate[mark] = float(lostPacket) / packet_queue.qsize()
endTime = time.time()
if endTime - startTime > 3600:
lostPacket = 0
allPacket = 0
startTime = endTime
time.sleep(INTERVAL) time.sleep(INTERVAL)
@@ -205,13 +339,78 @@ 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={
'host': CU, 'host': CU,
'mark': '10010', 'mark': '10010',
'port': PORBEPORT 'port': PROBEPORT
} }
) )
t2 = threading.Thread( t2 = threading.Thread(
@@ -219,7 +418,7 @@ def get_realtime_date():
kwargs={ kwargs={
'host': CT, 'host': CT,
'mark': '189', 'mark': '189',
'port': PORBEPORT 'port': PROBEPORT
} }
) )
t3 = threading.Thread( t3 = threading.Thread(
@@ -227,20 +426,82 @@ def get_realtime_date():
kwargs={ kwargs={
'host': CM, 'host': CM,
'mark': '10086', 'mark': '10086',
'port': PORBEPORT 'port': PROBEPORT
} }
) )
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):
''' '''
@@ -256,19 +517,14 @@ def byte_str(object):
print(type(object)) print(type(object))
if __name__ == '__main__': if __name__ == '__main__':
for argc in sys.argv: cli_args = parse_cli_args(sys.argv[1:])
if 'SERVER' in argc: SERVER = cli_args.get('SERVER', SERVER)
SERVER = argc.split('SERVER=')[-1] PORT = int(cli_args.get('PORT', PORT))
elif 'PORT' in argc: USER = cli_args.get('USER', USER)
PORT = int(argc.split('PORT=')[-1]) PASSWORD = cli_args.get('PASSWORD', PASSWORD)
elif 'USER' in argc: INTERVAL = int(cli_args.get('INTERVAL', INTERVAL))
USER = argc.split('USER=')[-1]
elif 'PASSWORD' in argc:
PASSWORD = argc.split('PASSWORD=')[-1]
elif 'INTERVAL' in argc:
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...")
@@ -288,6 +544,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
@@ -299,6 +575,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()
@@ -306,8 +584,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)
@@ -326,11 +602,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
@@ -338,16 +615,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
+353 -71
View File
@@ -1,33 +1,72 @@
#!/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
PORBEPORT = 80
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 platform
from queue import Queue
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 parse_cli_args(arguments):
overrides = {}
for argument in arguments:
key, separator, value = argument.partition('=')
if separator and key in {'SERVER', 'PORT', 'USER', 'PASSWORD', 'INTERVAL'}:
overrides[key] = value
return overrides
def get_uptime(): def get_uptime():
return int(time.time() - psutil.boot_time()) return int(time.time() - psutil.boot_time())
@@ -41,7 +80,11 @@ 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:
return int(psutil.disk_usage("/").total/1024.0/1024.0), int((psutil.disk_usage("/").total-psutil.disk_usage("/").free)/1024.0/1024.0)
else:
valid_fs = ["ext4", "ext3", "ext2", "reiserfs", "jfs", "btrfs", "fuseblk", "zfs", "simfs", "ntfs", "fat32",
"exfat", "xfs"]
disks = dict() disks = dict()
size = 0 size = 0
used = 0 used = 0
@@ -57,6 +100,52 @@ def get_hdd():
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
@@ -83,31 +172,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, PORBEPORT), 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"
@@ -137,30 +224,47 @@ 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
allPacket = 0 packet_queue = Queue(maxsize=PING_PACKET_HISTORY_LEN)
startTime = time.time()
while True: while True:
# flush dns, every time.
IP = host
if host.count(':') < 1: # if not plain ipv6 address, means ipv4 address or hostname
try:
if PROBE_PROTOCOL_PREFER == 'ipv4':
IP = socket.getaddrinfo(host, None, socket.AF_INET)[0][4][0]
else:
IP = socket.getaddrinfo(host, None, socket.AF_INET6)[0][4][0]
except Exception:
pass
if packet_queue.full():
if packet_queue.get() == 0:
lostPacket -= 1
try: try:
b = timeit.default_timer() b = timeit.default_timer()
socket.create_connection((host, port), timeout=1).close() socket.create_connection((IP, port), timeout=1).close()
pingTime[mark] = int((timeit.default_timer() - b) * 1000) pingTime[mark] = int((timeit.default_timer() - b) * 1000)
except: packet_queue.put(1)
except socket.error as error:
if error.errno == errno.ECONNREFUSED:
pingTime[mark] = int((timeit.default_timer() - b) * 1000)
packet_queue.put(1)
#elif error.errno == errno.ETIMEDOUT:
else:
lostPacket += 1 lostPacket += 1
finally: packet_queue.put(0)
allPacket += 1
if allPacket > 100: if packet_queue.qsize() > 30:
lostRate[mark] = float(lostPacket) / allPacket lostRate[mark] = float(lostPacket) / packet_queue.qsize()
endTime = time.time()
if endTime - startTime > 3600:
lostPacket = 0
allPacket = 0
startTime = endTime
time.sleep(INTERVAL) time.sleep(INTERVAL)
@@ -185,13 +289,73 @@ 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={
'host': CU, 'host': CU,
'mark': '10010', 'mark': '10010',
'port': PORBEPORT 'port': PROBEPORT
} }
) )
t2 = threading.Thread( t2 = threading.Thread(
@@ -199,7 +363,7 @@ def get_realtime_date():
kwargs={ kwargs={
'host': CT, 'host': CT,
'mark': '189', 'mark': '189',
'port': PORBEPORT 'port': PROBEPORT
} }
) )
t3 = threading.Thread( t3 = threading.Thread(
@@ -207,20 +371,84 @@ def get_realtime_date():
kwargs={ kwargs={
'host': CM, 'host': CM,
'mark': '10086', 'mark': '10086',
'port': PORBEPORT 'port': PROBEPORT
} }
) )
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):
''' '''
@@ -236,19 +464,14 @@ def byte_str(object):
print(type(object)) print(type(object))
if __name__ == '__main__': if __name__ == '__main__':
for argc in sys.argv: cli_args = parse_cli_args(sys.argv[1:])
if 'SERVER' in argc: SERVER = cli_args.get('SERVER', SERVER)
SERVER = argc.split('SERVER=')[-1] PORT = int(cli_args.get('PORT', PORT))
elif 'PORT' in argc: USER = cli_args.get('USER', USER)
PORT = int(argc.split('PORT=')[-1]) PASSWORD = cli_args.get('PASSWORD', PASSWORD)
elif 'USER' in argc: INTERVAL = int(cli_args.get('INTERVAL', INTERVAL))
USER = argc.split('USER=')[-1]
elif 'PASSWORD' in argc:
PASSWORD = argc.split('PASSWORD=')[-1]
elif 'INTERVAL' in argc:
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...")
@@ -268,6 +491,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
@@ -279,16 +521,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)
@@ -307,11 +549,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
@@ -319,16 +562,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
+40
View File
@@ -0,0 +1,40 @@
import importlib.util
import runpy
import sys
import types
import unittest
from pathlib import Path
CLIENT_DIR = Path(__file__).resolve().parent
class ClientArgumentTests(unittest.TestCase):
def test_password_with_user_text_does_not_replace_username(self):
if importlib.util.find_spec("psutil") is None:
sys.modules["psutil"] = types.ModuleType("psutil")
arguments = [
"SERVER=127.0.0.1",
"PORT=35601",
"USER=s01",
"PASSWORD=USER_DEFAULT_PASSWORD",
"INTERVAL=2",
"NOTUSER=ignored",
]
expected = {
"SERVER": "127.0.0.1",
"PORT": "35601",
"USER": "s01",
"PASSWORD": "USER_DEFAULT_PASSWORD",
"INTERVAL": "2",
}
for filename in ("client-linux.py", "client-psutil.py"):
with self.subTest(client=filename):
namespace = runpy.run_path(str(CLIENT_DIR / filename))
self.assertEqual(namespace["parse_cli_args"](arguments), expected)
if __name__ == "__main__":
unittest.main()
+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}"
+32
View File
@@ -0,0 +1,32 @@
services:
serverstatus-server:
build:
context: .
dockerfile: Dockerfile.server
image: cppla/serverstatus:server
healthcheck:
test: ["CMD", "wget", "-q", "-O", "/dev/null", "http://127.0.0.1/api/health"]
interval: 30s
timeout: 10s
retries: 5
container_name: serverstatus-server
restart: unless-stopped
environment:
ADMIN_TOKEN: "${ADMIN_TOKEN:-}"
HTTP_ADDR: ":80"
AGENT_ADDR: ":35601"
networks:
serverstatus-network:
ipv4_address: 172.23.0.2
volumes:
- ./server/config.json:/app/config/config.json
- ./web/json:/app/data
ports:
- 35601:35601
- 8080:80
networks:
serverstatus-network:
ipam:
config:
- subnet: 172.23.0.0/24
+3 -1
View File
@@ -1,2 +1,4 @@
sergate serverstatus
*.test
coverage.out
.tags* .tags*
-34
View File
@@ -1,34 +0,0 @@
OUT = sergate
#CC = clang
CC = gcc
CFLAGS = -Wall -O2
#CXX = clang++
CXX = g++
CXXFLAGS = -Wall -O2
ODIR = obj
SDIR = src
LIBS = -pthread -lm
INC = -Iinclude
C_SRCS := $(wildcard $(SDIR)/*.c)
CXX_SRCS := $(wildcard $(SDIR)/*.cpp)
C_OBJS := $(patsubst $(SDIR)/%.c,$(ODIR)/%.o,$(C_SRCS))
CXX_OBJS := $(patsubst $(SDIR)/%.cpp,$(ODIR)/%.o,$(CXX_SRCS))
OBJS := $(C_OBJS) $(CXX_OBJS)
$(ODIR)/%.o: $(SDIR)/%.c
$(CC) -c $(INC) $(CFLAGS) $< -o $@
$(ODIR)/%.o: $(SDIR)/%.cpp
$(CXX) -c $(INC) $(CXXFLAGS) $< -o $@
$(OUT): $(OBJS)
$(CXX) $(LIBS) $^ -o $(OUT)
.PHONY: clean
clean:
rm -f $(ODIR)/*.o $(OUT)
+472
View File
@@ -0,0 +1,472 @@
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"math"
"net"
"os"
"strconv"
"sync"
"sync/atomic"
"time"
)
type Options struct {
ConfigPath string
StatsPath string
WebDir string
HTTPAddr string
AgentAddr string
AdminToken string
CORSOrigin string
InsecureCallbackTLS bool
Verbose bool
}
type NodeState struct {
Config ServerConfig
Connected bool
Connection net.Conn
ConnectionID uint64
Family int
Online4 bool
Online6 bool
Stats AgentStats
HasUpdate bool
LastNetworkIn int64
LastNetworkOut int64
LastUpdate time.Time
AlarmLast map[string]time.Time
Pong bool
}
type App struct {
opts Options
startedAt time.Time
ctx context.Context
cancel context.CancelFunc
mutationMu sync.Mutex
configMu sync.RWMutex
document ConfigDocument
runtime RuntimeConfig
nodeMu sync.RWMutex
nodes map[string]*NodeState
connectionID atomic.Uint64
generation atomic.Uint64
agentRunning atomic.Bool
reloadWrites atomic.Int32
certMu sync.RWMutex
certs map[string]*CertState
statsWake chan struct{}
persistMu sync.Mutex
logger *log.Logger
}
func NewApp(opts Options) (*App, error) {
doc, runtime, err := readConfig(opts.ConfigPath)
if err != nil {
return nil, err
}
ctx, cancel := context.WithCancel(context.Background())
app := &App{
opts: opts,
startedAt: time.Now(),
ctx: ctx,
cancel: cancel,
nodes: make(map[string]*NodeState),
certs: make(map[string]*CertState),
statsWake: make(chan struct{}, 1),
logger: log.New(os.Stdout, "serverstatus ", log.LstdFlags|log.Lmicroseconds),
}
app.applyValidatedConfig(doc, runtime, false)
app.restorePersistentState()
return app, nil
}
func (a *App) StartBackground() {
go a.statsLoop()
go a.sslLoop()
a.wakeStatsWriter()
}
func (a *App) Close() {
a.cancel()
a.disconnectAll("Server shutting down...")
_ = a.PersistStats()
}
func (a *App) ConfigSnapshot() ConfigDocument {
a.configMu.RLock()
defer a.configMu.RUnlock()
clone, err := cloneDocument(a.document)
if err != nil {
panic(err)
}
return clone
}
func (a *App) RuntimeSnapshot() RuntimeConfig {
a.configMu.RLock()
defer a.configMu.RUnlock()
result := a.runtime
result.Servers = append([]ServerConfig(nil), a.runtime.Servers...)
result.Monitors = append([]MonitorConfig(nil), a.runtime.Monitors...)
result.SSLCerts = append([]SSLCertConfig(nil), a.runtime.SSLCerts...)
result.Watchdogs = append([]CompiledWatchdog(nil), a.runtime.Watchdogs...)
return result
}
func (a *App) ReplaceConfig(input ConfigDocument) (ConfigDocument, *APIError) {
a.mutationMu.Lock()
defer a.mutationMu.Unlock()
normalized, runtime, apiErr := normalizeConfig(input)
if apiErr != nil {
return nil, apiErr
}
if err := writeConfig(a.opts.ConfigPath, normalized); err != nil {
return nil, &APIError{Status: 500, Message: "config could not be written", Details: map[string]any{"error": err.Error()}}
}
a.applyValidatedConfig(normalized, runtime, true)
return a.ConfigSnapshot(), nil
}
func (a *App) MutateConfig(mutate func(ConfigDocument) *APIError) (ConfigDocument, *APIError) {
a.mutationMu.Lock()
defer a.mutationMu.Unlock()
doc := a.ConfigSnapshot()
if apiErr := mutate(doc); apiErr != nil {
return nil, apiErr
}
normalized, runtime, apiErr := normalizeConfig(doc)
if apiErr != nil {
return nil, apiErr
}
if err := writeConfig(a.opts.ConfigPath, normalized); err != nil {
return nil, &APIError{Status: 500, Message: "config could not be written", Details: map[string]any{"error": err.Error()}}
}
a.applyValidatedConfig(normalized, runtime, true)
return a.ConfigSnapshot(), nil
}
func (a *App) ReloadConfig() *APIError {
a.mutationMu.Lock()
defer a.mutationMu.Unlock()
doc, runtime, err := readConfig(a.opts.ConfigPath)
if err != nil {
if apiErr, ok := err.(*APIError); ok {
return apiErr
}
return &APIError{Status: 500, Message: "config could not be reloaded", Details: map[string]any{"error": err.Error()}}
}
a.applyValidatedConfig(doc, runtime, true)
return nil
}
func (a *App) applyValidatedConfig(doc ConfigDocument, runtime RuntimeConfig, disconnect bool) {
a.configMu.Lock()
a.nodeMu.Lock()
oldNodes := a.nodes
newNodes := make(map[string]*NodeState, len(runtime.Servers))
connections := make([]net.Conn, 0)
for _, server := range runtime.Servers {
node := &NodeState{Config: server, AlarmLast: make(map[string]time.Time)}
if old := oldNodes[server.Username]; old != nil && sameServerIdentity(old.Config, server) {
node.LastNetworkIn = old.LastNetworkIn
node.LastNetworkOut = old.LastNetworkOut
node.Stats = old.Stats
node.HasUpdate = old.HasUpdate
node.AlarmLast = old.AlarmLast
if !disconnect {
node.Connected = old.Connected
node.Connection = old.Connection
node.ConnectionID = old.ConnectionID
node.Family = old.Family
node.Online4 = old.Online4
node.Online6 = old.Online6
}
}
newNodes[server.Username] = node
}
if disconnect {
for _, node := range oldNodes {
if node.Connection != nil {
connections = append(connections, node.Connection)
}
}
}
a.document = doc
a.runtime = runtime
a.nodes = newNodes
a.generation.Add(1)
a.nodeMu.Unlock()
a.configMu.Unlock()
a.reconcileCerts(runtime.SSLCerts)
if disconnect {
for _, conn := range connections {
_, _ = conn.Write([]byte("Server reloading...\n"))
_ = conn.Close()
}
}
a.reloadWrites.Store(2)
a.wakeStatsWriter()
}
func sameServerIdentity(left, right ServerConfig) bool {
return left.Username == right.Username && left.Name == right.Name && left.Type == right.Type && left.Host == right.Host && left.Location == right.Location
}
func (a *App) disconnectAll(reason string) {
a.nodeMu.Lock()
connections := make([]net.Conn, 0)
for _, node := range a.nodes {
if node.Connection != nil {
connections = append(connections, node.Connection)
node.Connection = nil
node.Connected = false
node.Online4 = false
node.Online6 = false
}
}
a.nodeMu.Unlock()
for _, conn := range connections {
if reason != "" {
_, _ = conn.Write([]byte(reason + "\n"))
}
_ = conn.Close()
}
}
func (a *App) statsLoop() {
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-a.ctx.Done():
return
case <-ticker.C:
case <-a.statsWake:
}
if err := a.PersistStats(); err != nil {
a.logger.Printf("write stats: %v", err)
}
}
}
func (a *App) wakeStatsWriter() {
select {
case a.statsWake <- struct{}{}:
default:
}
}
func (a *App) SnapshotStats() map[string]any {
return a.snapshotStats(false)
}
func (a *App) snapshotStats(consumeReload bool) map[string]any {
runtime := a.RuntimeSnapshot()
now := time.Now()
servers := make([]any, 0, len(runtime.Servers))
a.nodeMu.Lock()
for _, server := range runtime.Servers {
if server.Disabled {
continue
}
node := a.nodes[server.Username]
if node == nil {
continue
}
base := map[string]any{
"name": server.Name, "type": server.Type, "host": server.Host, "location": server.Location,
"online4": false, "online6": false,
}
if node.Connected && node.HasUpdate {
s := node.Stats
updateTrafficBaselines(node, s.NetworkIn, s.NetworkOut, monthResetWindow(now, server.MonthStart))
base["online4"] = node.Online4
base["online6"] = node.Online6
base["uptime"] = formatUptime(s.Uptime)
base["load_1"], base["load_5"], base["load_15"] = round2(s.Load1), round2(s.Load5), round2(s.Load15)
base["ping_10010"], base["ping_189"], base["ping_10086"] = round2(s.Ping10010), round2(s.Ping189), round2(s.Ping10086)
base["time_10010"], base["time_189"], base["time_10086"] = s.Time10010, s.Time189, s.Time10086
base["tcp_count"], base["udp_count"] = s.TCPCount, s.UDPCount
base["process_count"], base["thread_count"] = s.ProcessCount, s.ThreadCount
base["network_rx"], base["network_tx"] = s.NetworkRX, s.NetworkTX
base["network_in"], base["network_out"] = s.NetworkIn, s.NetworkOut
base["cpu"], base["cpu_cores"], base["cpu_model"] = int(s.CPU), s.CPUCores, s.CPUModel
base["memory_total"], base["memory_used"] = s.MemoryTotal, s.MemoryUsed
base["swap_total"], base["swap_used"] = s.SwapTotal, s.SwapUsed
base["hdd_total"], base["hdd_used"] = s.HDDTotal, s.HDDUsed
base["last_network_in"] = trafficBaseline(s.NetworkIn, node.LastNetworkIn)
base["last_network_out"] = trafficBaseline(s.NetworkOut, node.LastNetworkOut)
base["io_read"], base["io_write"] = s.IORead, s.IOWrite
base["custom"], base["os"] = s.Custom, s.OS
} else {
base["last_network_in"] = node.LastNetworkIn
base["last_network_out"] = node.LastNetworkOut
base["os"] = node.Stats.OS
base["cpu_model"] = node.Stats.CPUModel
}
servers = append(servers, base)
}
a.nodeMu.Unlock()
result := map[string]any{
"servers": servers,
"sslcerts": a.sslSnapshot(runtime.SSLCerts, now),
"updated": strconv.FormatInt(now.Unix(), 10),
}
if a.reloadWrites.Load() > 0 {
result["reload"] = true
if consumeReload {
a.reloadWrites.Add(-1)
}
}
return result
}
func (a *App) PersistStats() error {
a.persistMu.Lock()
defer a.persistMu.Unlock()
return writeStatsFile(a.opts.StatsPath, a.snapshotStats(true))
}
func monthResetWindow(now time.Time, monthStart int) bool {
return now.Day() == clamp(monthStart, 1, 28) && now.Hour() == 0 && now.Minute() < 5
}
func trafficBaseline(current, baseline int64) int64 {
if current == 0 || baseline == 0 {
return current
}
return baseline
}
func updateTrafficBaselines(node *NodeState, currentIn, currentOut int64, reset bool) {
if reset {
node.LastNetworkIn = currentIn
node.LastNetworkOut = currentOut
return
}
if node.LastNetworkIn == 0 || (currentIn != 0 && node.LastNetworkIn > currentIn) {
node.LastNetworkIn = currentIn
}
if node.LastNetworkOut == 0 || (currentOut != 0 && node.LastNetworkOut > currentOut) {
node.LastNetworkOut = currentOut
}
}
func round2(value float64) float64 {
return math.Round(value*100) / 100
}
func formatUptime(seconds int64) string {
days := seconds / 86400
if days > 0 {
return fmt.Sprintf("%d 天", days)
}
return fmt.Sprintf("%02d:%02d:%02d", seconds/3600, (seconds/60)%60, seconds%60)
}
func (a *App) restorePersistentState() {
data, err := os.ReadFile(a.opts.StatsPath)
if err != nil {
data, err = os.ReadFile(a.opts.StatsPath + "~")
}
if err != nil {
return
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
var previous struct {
Servers []map[string]any `json:"servers"`
}
if err := decoder.Decode(&previous); err != nil {
a.logger.Printf("read previous stats: %v", err)
return
}
a.nodeMu.Lock()
defer a.nodeMu.Unlock()
for _, node := range a.nodes {
for _, saved := range previous.Servers {
if fmt.Sprint(saved["name"]) != node.Config.Name || fmt.Sprint(saved["type"]) != node.Config.Type || fmt.Sprint(saved["host"]) != node.Config.Host || fmt.Sprint(saved["location"]) != node.Config.Location {
continue
}
node.LastNetworkIn = anyInt64(saved["last_network_in"])
node.LastNetworkOut = anyInt64(saved["last_network_out"])
node.Stats.OS = anyString(saved["os"])
node.Stats.CPUModel = anyString(saved["cpu_model"])
break
}
}
}
func anyString(value any) string {
if value == nil {
return ""
}
return fmt.Sprint(value)
}
func anyInt64(value any) int64 {
switch number := value.(type) {
case json.Number:
parsed, _ := number.Int64()
return parsed
case float64:
return int64(number)
case int64:
return number
case int:
return int64(number)
case string:
parsed, _ := strconv.ParseInt(number, 10, 64)
return parsed
default:
return 0
}
}
func (a *App) ResetTraffic(username string) (map[string]any, *APIError) {
a.nodeMu.Lock()
node := a.nodes[username]
if node == nil {
a.nodeMu.Unlock()
return nil, &APIError{Status: 404, Message: "server was not found", Details: map[string]any{"username": username}}
}
if !node.Connected || !node.HasUpdate {
a.nodeMu.Unlock()
return nil, &APIError{Status: 409, Message: "server has no current traffic counters; it may be offline", Details: map[string]any{"username": username}}
}
previousIn, previousOut := node.LastNetworkIn, node.LastNetworkOut
networkIn, networkOut := node.Stats.NetworkIn, node.Stats.NetworkOut
node.LastNetworkIn, node.LastNetworkOut = networkIn, networkOut
server := node.Config
a.nodeMu.Unlock()
a.wakeStatsWriter()
return map[string]any{
"server": server,
"stats": map[string]any{
"network_in": networkIn, "network_out": networkOut,
"previous_last_network_in": previousIn, "previous_last_network_out": previousOut,
"last_network_in": networkIn, "last_network_out": networkOut,
"month_in_before": max64(0, networkIn-previousIn), "month_out_before": max64(0, networkOut-previousOut),
},
}, nil
}
func max64(left, right int64) int64 {
if left > right {
return left
}
return right
}
+50
View File
@@ -0,0 +1,50 @@
package main
import (
"net"
"testing"
)
func TestTrafficBaselinesResetIndependently(t *testing.T) {
node := &NodeState{LastNetworkIn: 100, LastNetworkOut: 0}
updateTrafficBaselines(node, 150, 500, false)
if node.LastNetworkIn != 100 || node.LastNetworkOut != 500 {
t.Fatalf("missing outbound baseline was not initialized independently: %#v", node)
}
node.LastNetworkOut = 700
updateTrafficBaselines(node, 200, 50, false)
if node.LastNetworkIn != 100 || node.LastNetworkOut != 50 {
t.Fatalf("outbound counter reset changed the wrong baseline: %#v", node)
}
updateTrafficBaselines(node, 900, 800, true)
if node.LastNetworkIn != 900 || node.LastNetworkOut != 800 {
t.Fatalf("monthly reset did not reset both baselines: %#v", node)
}
}
func TestDisconnectPreservesOfflineDisplayMetadata(t *testing.T) {
app := newTestApp(t, minimalTestConfig())
client, server := net.Pipe()
defer client.Close()
defer server.Close()
app.nodeMu.Lock()
node := app.nodes["s01"]
node.Connected = true
node.Connection = server
node.ConnectionID = 42
node.HasUpdate = true
node.Stats = AgentStats{OS: "linux", CPUModel: "Test CPU"}
app.nodeMu.Unlock()
app.disconnectAgent("s01", server, 42)
serverStats := app.SnapshotStats()["servers"].([]any)[0].(map[string]any)
if serverStats["online4"] != false || serverStats["online6"] != false {
t.Fatalf("disconnected node remained online: %#v", serverStats)
}
if serverStats["os"] != "linux" || serverStats["cpu_model"] != "Test CPU" {
t.Fatalf("offline display metadata was discarded: %#v", serverStats)
}
}
+103 -12
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"
} }
] ]
} }
+161
View File
@@ -0,0 +1,161 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"syscall"
"time"
)
func readConfig(path string) (ConfigDocument, RuntimeConfig, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, RuntimeConfig{}, err
}
doc, err := decodeDocument(data)
if err != nil {
return nil, RuntimeConfig{}, fmt.Errorf("parse %s: %w", path, err)
}
normalized, runtime, apiErr := normalizeConfig(doc)
if apiErr != nil {
return nil, RuntimeConfig{}, apiErr
}
return normalized, runtime, nil
}
func marshalIndented(value any) ([]byte, error) {
data, err := jsonMarshalIndent(value)
if err != nil {
return nil, err
}
return append(data, '\n'), nil
}
// jsonMarshalIndent is a variable so file-writing failure paths can be tested.
var jsonMarshalIndent = func(value any) ([]byte, error) {
return json.MarshalIndent(value, "", "\t")
}
func writeConfig(path string, doc ConfigDocument) error {
data, err := marshalIndented(doc)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
mode := os.FileMode(0o644)
if info, statErr := os.Stat(path); statErr == nil {
mode = info.Mode().Perm()
backup := fmt.Sprintf("%s.bak-%s", path, time.Now().Format("20060102-150405.000000000"))
if err := copyFile(path, backup, mode); err != nil {
return fmt.Errorf("backup config: %w", err)
}
pruneBackups(path, 10)
} else if !errors.Is(statErr, os.ErrNotExist) {
return statErr
}
return atomicWrite(path, data, mode, true)
}
func writeStatsFile(path string, value any) error {
data, err := marshalIndented(value)
if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
return atomicWrite(path, data, 0o644, false)
}
func atomicWrite(path string, data []byte, mode os.FileMode, allowBusyFallback bool) error {
directory := filepath.Dir(path)
tmp, err := os.CreateTemp(directory, ".serverstatus-*.tmp")
if err != nil {
return err
}
tmpPath := tmp.Name()
defer os.Remove(tmpPath)
if err := tmp.Chmod(mode); err != nil {
tmp.Close()
return err
}
if _, err := tmp.Write(data); err != nil {
tmp.Close()
return err
}
if err := tmp.Sync(); err != nil {
tmp.Close()
return err
}
if err := tmp.Close(); err != nil {
return err
}
if err := os.Rename(tmpPath, path); err == nil {
return syncDirectory(directory)
} else if !allowBusyFallback || !errors.Is(err, syscall.EBUSY) {
return err
}
// Docker cannot rename over a single-file bind mount. The backup above is
// already durable, so truncate and sync the mounted inode as a fallback.
file, err := os.OpenFile(path, os.O_WRONLY|os.O_TRUNC, mode)
if err != nil {
return err
}
if _, err = file.Write(data); err == nil {
err = file.Sync()
}
closeErr := file.Close()
if err != nil {
return err
}
return closeErr
}
func copyFile(source, destination string, mode os.FileMode) error {
in, err := os.Open(source)
if err != nil {
return err
}
defer in.Close()
out, err := os.OpenFile(destination, os.O_CREATE|os.O_EXCL|os.O_WRONLY, mode)
if err != nil {
return err
}
if _, err = io.Copy(out, in); err == nil {
err = out.Sync()
}
closeErr := out.Close()
if err != nil {
return err
}
return closeErr
}
func pruneBackups(configPath string, keep int) {
matches, err := filepath.Glob(configPath + ".bak-*")
if err != nil || len(matches) <= keep {
return
}
sort.Strings(matches)
for _, path := range matches[:len(matches)-keep] {
_ = os.Remove(path)
}
}
func syncDirectory(directory string) error {
dir, err := os.Open(directory)
if err != nil {
return err
}
defer dir.Close()
return dir.Sync()
}
+40
View File
@@ -0,0 +1,40 @@
module github.com/cppla/serverstatus/server
go 1.25.0
require (
github.com/expr-lang/expr v1.17.8
github.com/gin-gonic/gin v1.12.0
)
require (
github.com/bytedance/gopkg v0.1.3 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic/loader v0.5.0 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
github.com/gin-contrib/sse v1.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.30.1 // indirect
github.com/goccy/go-json v0.10.5 // indirect
github.com/goccy/go-yaml v1.19.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/quic-go/qpack v0.6.0 // indirect
github.com/quic-go/quic-go v0.59.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.3.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
golang.org/x/arch v0.22.0 // indirect
golang.org/x/crypto v0.52.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
google.golang.org/protobuf v1.36.10 // indirect
)
+91
View File
@@ -0,0 +1,91 @@
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/expr-lang/expr v1.17.8 h1:W1loDTT+0PQf5YteHSTpju2qfUfNoBt4yw9+wOEU9VM=
github.com/expr-lang/expr v1.17.8/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
github.com/quic-go/quic-go v0.59.1 h1:0Gmua0HW1Tv7ANR7hUYwRyD0MG5OJfgvYSZasGZzBic=
github.com/quic-go/quic-go v0.59.1/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+397
View File
@@ -0,0 +1,397 @@
package main
import (
"crypto/subtle"
"encoding/json"
"errors"
"fmt"
"io"
"mime"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
)
var (
version = "2.0.0"
commit = "none"
buildTime = "unknown"
)
func (a *App) HTTPServer() *http.Server {
if !a.opts.Verbose {
gin.SetMode(gin.ReleaseMode)
}
return &http.Server{
Addr: a.opts.HTTPAddr,
Handler: a.router(),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20,
}
}
func (a *App) router() *gin.Engine {
router := gin.New()
router.Use(gin.Recovery(), a.securityHeaders(), a.corsMiddleware())
if a.opts.Verbose {
router.Use(gin.Logger())
}
router.GET("/api/health", a.healthHandler)
router.GET("/api/schema", a.schemaHandler)
router.GET("/api/openapi.json", func(c *gin.Context) {
c.JSON(http.StatusOK, openAPISpec())
})
router.GET("/json/stats.json", func(c *gin.Context) {
c.Header("Cache-Control", "no-store")
c.JSON(http.StatusOK, a.SnapshotStats())
})
api := router.Group("/api", a.authMiddleware())
api.GET("/config", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true, "config": a.ConfigSnapshot()})
})
api.PUT("/config", func(c *gin.Context) {
body, apiErr := decodeRequestObject(c)
if apiErr != nil {
a.writeAPIError(c, apiErr)
return
}
doc, apiErr := a.ReplaceConfig(ConfigDocument(body))
if apiErr != nil {
a.writeAPIError(c, apiErr)
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "reloaded": true, "pid": os.Getpid(), "generation": a.generation.Load(), "config": doc})
})
for _, collection := range []string{"servers", "monitors", "sslcerts", "watchdog"} {
key := collection
api.GET("/"+key, func(c *gin.Context) { a.getCollectionHandler(c, key) })
api.POST("/"+key, func(c *gin.Context) { a.createCollectionHandler(c, key) })
api.PUT("/"+key+"/:id", func(c *gin.Context) { a.updateCollectionHandler(c, key) })
api.DELETE("/"+key+"/:id", func(c *gin.Context) { a.deleteCollectionHandler(c, key) })
}
api.POST("/servers/:id/reset-traffic", a.resetTrafficHandler)
api.POST("/reload", func(c *gin.Context) {
if apiErr := a.ReloadConfig(); apiErr != nil {
a.writeAPIError(c, apiErr)
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "operation": "reload", "pid": os.Getpid(), "generation": a.generation.Load()})
})
api.POST("/restart", func(c *gin.Context) {
if apiErr := a.ReloadConfig(); apiErr != nil {
a.writeAPIError(c, apiErr)
return
}
c.JSON(http.StatusAccepted, gin.H{"ok": true, "operation": "restart", "mode": "in-process", "pid": os.Getpid(), "generation": a.generation.Load()})
})
router.NoRoute(a.staticHandler)
return router
}
func (a *App) securityHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
c.Header("X-Content-Type-Options", "nosniff")
c.Header("X-Frame-Options", "SAMEORIGIN")
c.Header("Referrer-Policy", "same-origin")
if strings.HasPrefix(c.Request.URL.Path, "/api/") {
c.Header("Cache-Control", "no-store")
}
c.Next()
}
}
func (a *App) corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if a.opts.CORSOrigin != "" {
c.Header("Access-Control-Allow-Origin", a.opts.CORSOrigin)
c.Header("Access-Control-Allow-Headers", "Authorization, Content-Type, X-Admin-Token")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Vary", "Origin")
}
if c.Request.Method == http.MethodOptions && strings.HasPrefix(c.Request.URL.Path, "/api/") {
c.AbortWithStatus(http.StatusNoContent)
return
}
c.Next()
}
}
func (a *App) authMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
if a.opts.AdminToken == "" {
a.writeAPIError(c, &APIError{Status: 503, Message: "management API is disabled; set ADMIN_TOKEN to enable it"})
c.Abort()
return
}
token := ""
authorization := c.GetHeader("Authorization")
if len(authorization) >= 7 && strings.EqualFold(authorization[:7], "bearer ") {
token = strings.TrimSpace(authorization[7:])
}
if token == "" {
token = strings.TrimSpace(c.GetHeader("X-Admin-Token"))
}
if len(token) != len(a.opts.AdminToken) || subtle.ConstantTimeCompare([]byte(token), []byte(a.opts.AdminToken)) != 1 {
a.writeAPIError(c, &APIError{Status: 401, Message: "invalid or missing admin token"})
c.Abort()
return
}
c.Next()
}
}
func (a *App) healthHandler(c *gin.Context) {
running := a.agentRunning.Load()
c.JSON(http.StatusOK, gin.H{
"ok": true, "enabled": a.opts.AdminToken != "",
"service": gin.H{"running": true, "pid": os.Getpid(), "version": version, "uptime": int64(time.Since(a.startedAt).Seconds()), "generation": a.generation.Load()},
"agent": gin.H{"running": running, "address": a.opts.AgentAddr},
"configPath": a.opts.ConfigPath,
})
}
func (a *App) schemaHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"ok": true, "schema": apiSchema(a.opts.AdminToken != "")})
}
func apiSchema(enabled bool) map[string]any {
endpoints := []any{
map[string]any{"method": "GET", "path": "/api/health", "auth": false},
map[string]any{"method": "GET", "path": "/api/schema", "auth": false},
map[string]any{"method": "GET", "path": "/api/openapi.json", "auth": false},
map[string]any{"method": "GET", "path": "/api/config", "auth": true},
map[string]any{"method": "PUT", "path": "/api/config", "auth": true, "body": "full config JSON"},
}
for _, key := range []string{"servers", "monitors", "sslcerts", "watchdog"} {
spec := collectionSpecs[key]
endpoints = append(endpoints,
map[string]any{"method": "GET", "path": "/api/" + key, "auth": true},
map[string]any{"method": "POST", "path": "/api/" + key, "auth": true, "body": spec.itemName + " JSON"},
map[string]any{"method": "PUT", "path": "/api/" + key + "/{id}", "auth": true, "body": spec.itemName + " JSON"},
map[string]any{"method": "DELETE", "path": "/api/" + key + "/{id}", "auth": true},
)
}
endpoints = append(endpoints,
map[string]any{"method": "POST", "path": "/api/servers/{username}/reset-traffic", "auth": true},
map[string]any{"method": "POST", "path": "/api/reload", "auth": true},
map[string]any{"method": "POST", "path": "/api/restart", "auth": true},
)
collections := make(map[string]any)
for key, spec := range collectionSpecs {
collections[key] = map[string]any{"item": spec.itemName, "idField": spec.idField, "required": spec.required, "optional": spec.optional}
}
return map[string]any{
"version": version,
"auth": map[string]any{"type": "bearer", "header": "Authorization: Bearer <ADMIN_TOKEN>", "enabled": enabled},
"endpoints": endpoints, "collections": collections,
}
}
func decodeRequestObject(c *gin.Context) (map[string]any, *APIError) {
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxRequestBody)
decoder := json.NewDecoder(c.Request.Body)
decoder.UseNumber()
var object map[string]any
if err := decoder.Decode(&object); err != nil {
status := http.StatusBadRequest
if strings.Contains(err.Error(), "request body too large") {
status = http.StatusRequestEntityTooLarge
}
return nil, &APIError{Status: status, Message: "invalid JSON body", Details: map[string]any{"error": err.Error()}}
}
if object == nil {
return nil, &APIError{Status: 400, Message: "request body must be an object"}
}
var trailing any
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
return nil, &APIError{Status: 400, Message: "request body must contain one JSON value"}
}
return object, nil
}
func (a *App) getCollectionHandler(c *gin.Context, key string) {
doc := a.ConfigSnapshot()
items, _ := doc[key].([]any)
c.JSON(http.StatusOK, gin.H{"ok": true, key: items})
}
func (a *App) createCollectionHandler(c *gin.Context, key string) {
item, apiErr := decodeRequestObject(c)
if apiErr != nil {
a.writeAPIError(c, apiErr)
return
}
doc, apiErr := a.MutateConfig(func(doc ConfigDocument) *APIError {
items, _ := doc[key].([]any)
doc[key] = append(items, item)
return nil
})
if apiErr != nil {
a.writeAPIError(c, apiErr)
return
}
items, _ := doc[key].([]any)
created := items[len(items)-1]
c.JSON(http.StatusCreated, gin.H{"ok": true, collectionSpecs[key].itemName: created, "reloaded": true, "pid": os.Getpid(), "config": doc})
}
func (a *App) updateCollectionHandler(c *gin.Context, key string) {
item, apiErr := decodeRequestObject(c)
if apiErr != nil {
a.writeAPIError(c, apiErr)
return
}
id := c.Param("id")
if id == "" {
a.writeAPIError(c, &APIError{Status: 400, Message: "item id is required"})
return
}
var updated any
doc, apiErr := a.MutateConfig(func(doc ConfigDocument) *APIError {
items, _ := doc[key].([]any)
index, _, findErr := findCollectionItem(items, collectionSpecs[key].idField, id, key != "servers")
if findErr != nil {
return findErr
}
if index < 0 {
return &APIError{Status: 404, Message: collectionSpecs[key].itemName + " was not found", Details: map[string]any{"id": id}}
}
items[index] = item
doc[key] = items
updated = item
return nil
})
if apiErr != nil {
a.writeAPIError(c, apiErr)
return
}
// Return the normalized item from the resulting document.
items, _ := doc[key].([]any)
if itemMap, ok := updated.(map[string]any); ok {
idValue := fmt.Sprint(itemMap[collectionSpecs[key].idField])
if index, normalized, _ := findCollectionItem(items, collectionSpecs[key].idField, idValue, key != "servers"); index >= 0 {
updated = normalized
}
}
c.JSON(http.StatusOK, gin.H{"ok": true, collectionSpecs[key].itemName: updated, "reloaded": true, "pid": os.Getpid(), "config": doc})
}
func (a *App) deleteCollectionHandler(c *gin.Context, key string) {
id := c.Param("id")
if id == "" {
a.writeAPIError(c, &APIError{Status: 400, Message: "item id is required"})
return
}
var removed any
doc, apiErr := a.MutateConfig(func(doc ConfigDocument) *APIError {
items, _ := doc[key].([]any)
index, item, findErr := findCollectionItem(items, collectionSpecs[key].idField, id, key != "servers")
if findErr != nil {
return findErr
}
if index < 0 {
return &APIError{Status: 404, Message: collectionSpecs[key].itemName + " was not found", Details: map[string]any{"id": id}}
}
removed = item
doc[key] = append(items[:index], items[index+1:]...)
return nil
})
if apiErr != nil {
a.writeAPIError(c, apiErr)
return
}
c.JSON(http.StatusOK, gin.H{"ok": true, "removed": removed, "reloaded": true, "pid": os.Getpid(), "config": doc})
}
func findCollectionItem(items []any, idField, id string, allowIndex bool) (int, any, *APIError) {
if numeric, err := strconv.Atoi(id); allowIndex && err == nil {
if numeric >= 0 && numeric < len(items) {
return numeric, items[numeric], nil
}
return -1, nil, nil
}
matches := make([]int, 0, 1)
for index, raw := range items {
item, ok := raw.(map[string]any)
if ok && fmt.Sprint(item[idField]) == id {
matches = append(matches, index)
}
}
if len(matches) > 1 {
return -1, nil, &APIError{Status: 409, Message: "collection has duplicate " + idField + "; use numeric index instead", Details: map[string]any{"id": id}}
}
if len(matches) == 1 {
return matches[0], items[matches[0]], nil
}
return -1, nil, nil
}
func (a *App) resetTrafficHandler(c *gin.Context) {
username := c.Param("id")
if username == "" {
a.writeAPIError(c, &APIError{Status: 400, Message: "username is required"})
return
}
result, apiErr := a.ResetTraffic(username)
if apiErr != nil {
a.writeAPIError(c, apiErr)
return
}
result["ok"] = true
result["operation"] = "reset-traffic"
result["pid"] = os.Getpid()
c.JSON(http.StatusOK, result)
}
func (a *App) writeAPIError(c *gin.Context, apiErr *APIError) {
payload := gin.H{"ok": false, "error": apiErr.Message}
if apiErr.Details != nil {
payload["details"] = apiErr.Details
}
c.JSON(apiErr.Status, payload)
}
func (a *App) staticHandler(c *gin.Context) {
if strings.HasPrefix(c.Request.URL.Path, "/api/") || strings.HasPrefix(c.Request.URL.Path, "/json/") {
c.JSON(http.StatusNotFound, gin.H{"ok": false, "error": "endpoint was not found"})
return
}
requestPath := filepath.Clean("/" + c.Request.URL.Path)
if requestPath == "/" {
requestPath = "/index.html"
}
root, err := filepath.Abs(a.opts.WebDir)
if err != nil {
c.Status(http.StatusInternalServerError)
return
}
filePath := filepath.Join(root, strings.TrimPrefix(requestPath, "/"))
if filePath != root && !strings.HasPrefix(filePath, root+string(os.PathSeparator)) {
c.Status(http.StatusNotFound)
return
}
info, err := os.Stat(filePath)
if err != nil || info.IsDir() {
c.Status(http.StatusNotFound)
return
}
if contentType := mime.TypeByExtension(filepath.Ext(filePath)); contentType != "" {
c.Header("Content-Type", contentType)
}
if filepath.Base(filePath) == "index.html" {
c.Header("Cache-Control", "no-cache")
}
http.ServeFile(c.Writer, c.Request, filePath)
}
+158
View File
@@ -0,0 +1,158 @@
package main
import (
"encoding/json"
"net/http"
"net/url"
"os"
"strings"
"testing"
)
func TestHTTPAPIAndStaticUI(t *testing.T) {
app := newTestApp(t, minimalTestConfig())
router := app.router()
response := performRequest(router, http.MethodGet, "/", "", "")
if response.Code != 200 || !strings.Contains(response.Body.String(), "test-ui") {
t.Fatalf("static UI: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodGet, "/api/health", "", "")
if response.Code != 200 || !strings.Contains(response.Body.String(), `"enabled":true`) {
t.Fatalf("health: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodGet, "/api/openapi.json", "", "")
if response.Code != 200 {
t.Fatalf("openapi: status=%d body=%s", response.Code, response.Body.String())
}
var openapi map[string]any
if err := json.Unmarshal(response.Body.Bytes(), &openapi); err != nil {
t.Fatal(err)
}
paths := openapi["paths"].(map[string]any)
if openapi["openapi"] != "3.1.0" || paths["/api/servers/{username}"] == nil || paths["/api/watchdog/{id}"] == nil {
t.Fatalf("OpenAPI document is incomplete: %#v", openapi)
}
monitorOperations := paths["/api/monitors"].(map[string]any)
createResponses := monitorOperations["post"].(map[string]any)["responses"].(map[string]any)
if createResponses["201"] == nil || createResponses["200"] != nil {
t.Fatalf("OpenAPI create response must describe HTTP 201: %#v", createResponses)
}
listResponses := monitorOperations["get"].(map[string]any)["responses"].(map[string]any)
listSchema := listResponses["200"].(map[string]any)["content"].(map[string]any)["application/json"].(map[string]any)["schema"].(map[string]any)
if listSchema["properties"].(map[string]any)["monitors"] == nil {
t.Fatalf("OpenAPI list response does not expose monitors: %#v", listSchema)
}
restartResponses := paths["/api/restart"].(map[string]any)["post"].(map[string]any)["responses"].(map[string]any)
if restartResponses["202"] == nil {
t.Fatalf("OpenAPI restart response must describe HTTP 202: %#v", restartResponses)
}
response = performRequest(router, http.MethodGet, "/api/config", "", "wrong")
if response.Code != http.StatusUnauthorized {
t.Fatalf("expected 401, got %d", response.Code)
}
response = performRequest(router, http.MethodGet, "/api/config", "", "test-token")
if response.Code != 200 {
t.Fatalf("config: status=%d body=%s", response.Code, response.Body.String())
}
monitor := `{"name":"新增服务","host":"tcp://127.0.0.1:80","type":"tcp","interval":"30"}`
response = performRequest(router, http.MethodPost, "/api/monitors", monitor, "test-token")
if response.Code != http.StatusCreated || !strings.Contains(response.Body.String(), "新增服务") {
t.Fatalf("create monitor: status=%d body=%s", response.Code, response.Body.String())
}
path := "/api/monitors/" + url.PathEscape("新增服务")
monitor = `{"name":"更新服务","host":"https://example.org","type":"https","interval":45}`
response = performRequest(router, http.MethodPut, path, monitor, "test-token")
if response.Code != 200 || !strings.Contains(response.Body.String(), "更新服务") {
t.Fatalf("update monitor: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodDelete, "/api/monitors/"+url.PathEscape("更新服务"), "", "test-token")
if response.Code != 200 {
t.Fatalf("delete monitor: status=%d body=%s", response.Code, response.Body.String())
}
percentMonitor := `{"name":"rate%check","host":"https://example.com","type":"https","interval":30}`
response = performRequest(router, http.MethodPost, "/api/monitors", percentMonitor, "test-token")
if response.Code != http.StatusCreated {
t.Fatalf("create percent monitor: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodDelete, "/api/monitors/"+url.PathEscape("rate%check"), "", "test-token")
if response.Code != 200 {
t.Fatalf("delete percent monitor: status=%d body=%s", response.Code, response.Body.String())
}
server := `{"username":"s02","name":"node2","type":"kvm","host":"host2","location":"JP","password":"secret","monthstart":31}`
response = performRequest(router, http.MethodPost, "/api/servers", server, "test-token")
if response.Code != http.StatusCreated || !strings.Contains(response.Body.String(), `"monthstart":28`) {
t.Fatalf("create server: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodDelete, "/api/servers/s02", "", "test-token")
if response.Code != 200 {
t.Fatalf("delete server: status=%d body=%s", response.Code, response.Body.String())
}
numericServer := `{"username":"0","name":"numeric","type":"kvm","host":"host0","location":"US","password":"secret","monthstart":1}`
response = performRequest(router, http.MethodPost, "/api/servers", numericServer, "test-token")
if response.Code != http.StatusCreated {
t.Fatalf("create numeric server: status=%d body=%s", response.Code, response.Body.String())
}
numericServer = `{"username":"0","name":"numeric-updated","type":"kvm","host":"host0","location":"US","password":"secret","monthstart":1}`
response = performRequest(router, http.MethodPut, "/api/servers/0", numericServer, "test-token")
if response.Code != 200 || !strings.Contains(response.Body.String(), "numeric-updated") {
t.Fatalf("update numeric server: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodDelete, "/api/servers/0", "", "test-token")
if response.Code != 200 || !strings.Contains(response.Body.String(), "numeric-updated") {
t.Fatalf("delete numeric server: status=%d body=%s", response.Code, response.Body.String())
}
if app.RuntimeSnapshot().Servers[0].Username != "s01" {
t.Fatal("numeric username operation modified the server at numeric index")
}
response = performRequest(router, http.MethodPost, "/api/servers/s01/reset-traffic", "", "test-token")
if response.Code != http.StatusConflict {
t.Fatalf("offline reset should conflict: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodPost, "/api/reload", "", "test-token")
if response.Code != 200 {
t.Fatalf("reload: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodPost, "/api/restart", "", "test-token")
if response.Code != http.StatusAccepted || !strings.Contains(response.Body.String(), "in-process") {
t.Fatalf("restart: status=%d body=%s", response.Code, response.Body.String())
}
data, err := os.ReadFile(app.opts.ConfigPath)
if err != nil {
t.Fatal(err)
}
var persisted map[string]any
if err := json.Unmarshal(data, &persisted); err != nil {
t.Fatalf("persisted config is invalid: %v", err)
}
}
func TestHTTPRejectsInvalidAndOversizedBodies(t *testing.T) {
app := newTestApp(t, minimalTestConfig())
router := app.router()
response := performRequest(router, http.MethodPost, "/api/servers", `{"name":`, "test-token")
if response.Code != 400 {
t.Fatalf("invalid JSON: status=%d body=%s", response.Code, response.Body.String())
}
response = performRequest(router, http.MethodPost, "/api/servers", `{"name":"x"}`, "test-token")
if response.Code != 400 {
t.Fatalf("missing fields: status=%d body=%s", response.Code, response.Body.String())
}
oversized := `{"name":"` + strings.Repeat("x", maxRequestBody) + `"}`
response = performRequest(router, http.MethodPost, "/api/servers", oversized, "test-token")
if response.Code != http.StatusRequestEntityTooLarge {
t.Fatalf("oversized body: status=%d body=%s", response.Code, response.Body.String())
}
doc := app.ConfigSnapshot()
doc["watchdog"] = []any{map[string]any{"name": "broken", "rule": "cpu >", "interval": 10}}
data, _ := json.Marshal(doc)
response = performRequest(router, http.MethodPut, "/api/config", string(data), "test-token")
if response.Code != 400 {
t.Fatalf("invalid watchdog: status=%d body=%s", response.Code, response.Body.String())
}
}
-139
View File
@@ -1,139 +0,0 @@
#ifndef ARGPARSE_H
#define ARGPARSE_H
/**
* Command-line arguments parsing library.
*
* This module is inspired by parse-options.c (git) and python's argparse
* module.
*
* Arguments parsing is common task in cli program, but traditional `getopt`
* libraries are not easy to use. This library provides high-level arguments
* parsing solutions.
*
* The program defines what arguments it requires, and `argparse` will figure
* out how to parse those out of `argc` and `argv`, it also automatically
* generates help and usage messages and issues errors when users give the
* program invalid arguments.
*
* Reserved namespaces:
* argparse
* OPT
* Author: Yecheng Fu <cofyc.jackson@gmail.com>
*/
#include <assert.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#ifdef __cplusplus
extern "C" {
#endif
struct argparse;
struct argparse_option;
typedef int argparse_callback(struct argparse *this_,
const struct argparse_option *option);
enum argparse_flag {
ARGPARSE_STOP_AT_NON_OPTION = 1,
};
enum argparse_option_type {
/* special */
ARGPARSE_OPT_END,
/* options with no arguments */
ARGPARSE_OPT_BOOLEAN,
ARGPARSE_OPT_BIT,
/* options with arguments (optional or required) */
ARGPARSE_OPT_INTEGER,
ARGPARSE_OPT_STRING,
};
enum argparse_option_flags {
OPT_NONEG = 1, /* Negation disabled. */
};
/*
* Argparse option struct.
*
* `type`:
* holds the type of the option, you must have an ARGPARSE_OPT_END last in your
* array.
*
* `short_name`:
* the character to use as a short option name, '\0' if none.
*
* `long_name`:
* the long option name, without the leading dash, NULL if none.
*
* `value`:
* stores pointer to the value to be filled.
*
* `help`:
* the short help message associated to what the option does.
* Must never be NULL (except for ARGPARSE_OPT_END).
*
* `callback`:
* function is called when corresponding argument is parsed.
*
* `data`:
* associated data. Callbacks can use it like they want.
*
* `flags`:
* option flags.
*
*/
struct argparse_option {
enum argparse_option_type type;
const char short_name;
const char *long_name;
void *value;
const char *help;
argparse_callback *callback;
intptr_t data;
int flags;
};
/*
* argpparse
*/
struct argparse {
// user supplied
const struct argparse_option *options;
const char *usage;
int flags;
// internal context
int argc;
const char **argv;
const char **out;
int cpidx;
const char *optvalue; // current option value
};
// builtin callbacks
int argparse_help_cb(struct argparse *this_,
const struct argparse_option *option);
// builtin option macros
#define OPT_END() { ARGPARSE_OPT_END, 0 }
#define OPT_BOOLEAN(...) { ARGPARSE_OPT_BOOLEAN, __VA_ARGS__ }
#define OPT_BIT(...) { ARGPARSE_OPT_BIT, __VA_ARGS__ }
#define OPT_INTEGER(...) { ARGPARSE_OPT_INTEGER, __VA_ARGS__ }
#define OPT_STRING(...) { ARGPARSE_OPT_STRING, __VA_ARGS__ }
#define OPT_HELP() OPT_BOOLEAN('h', "help", 0, "Show this help message and exit", argparse_help_cb)
int argparse_init(struct argparse *this_, struct argparse_option *options,
const char *usage, int flags);
int argparse_parse(struct argparse *this_, int argc, const char **argv);
void argparse_usage(struct argparse *this_);
#ifdef __cplusplus
}
#endif
#endif
-149
View File
@@ -1,149 +0,0 @@
/* (c) Magnus Auvinen. See licence.txt in the root of the distribution for more information. */
/* If you are missing that file, acquire a complete release at teeworlds.com. */
#ifndef BASE_DETECT_H
#define BASE_DETECT_H
/*
this file detected the family, platform and architecture
to compile for.
*/
/* platforms */
/* windows Family */
#if defined(WIN64) || defined(_WIN64)
/* Hmm, is this IA64 or x86-64? */
#define CONF_FAMILY_WINDOWS 1
#define CONF_FAMILY_STRING "windows"
#define CONF_PLATFORM_WIN64 1
#define CONF_PLATFORM_STRING "win64"
#elif defined(WIN32) || defined(_WIN32) || defined(__CYGWIN32__) || defined(__MINGW32__)
#define CONF_FAMILY_WINDOWS 1
#define CONF_FAMILY_STRING "windows"
#define CONF_PLATFORM_WIN32 1
#define CONF_PLATFORM_STRING "win32"
#endif
/* unix family */
#if defined(__FreeBSD__) || defined(__FreeBSD_kernel__)
#define CONF_FAMILY_UNIX 1
#define CONF_FAMILY_STRING "unix"
#define CONF_PLATFORM_FREEBSD 1
#define CONF_PLATFORM_STRING "freebsd"
#endif
#if defined(__OpenBSD__)
#define CONF_FAMILY_UNIX 1
#define CONF_FAMILY_STRING "unix"
#define CONF_PLATFORM_OPENBSD 1
#define CONF_PLATFORM_STRING "openbsd"
#endif
#if defined(__LINUX__) || defined(__linux__)
#define CONF_FAMILY_UNIX 1
#define CONF_FAMILY_STRING "unix"
#define CONF_PLATFORM_LINUX 1
#define CONF_PLATFORM_STRING "linux"
#endif
#if defined(__GNU__) || defined(__gnu__)
#define CONF_FAMILY_UNIX 1
#define CONF_FAMILY_STRING "unix"
#define CONF_PLATFORM_HURD 1
#define CONF_PLATFORM_STRING "gnu"
#endif
#if defined(MACOSX) || defined(__APPLE__) || defined(__DARWIN__)
#define CONF_FAMILY_UNIX 1
#define CONF_FAMILY_STRING "unix"
#define CONF_PLATFORM_MACOSX 1
#define CONF_PLATFORM_STRING "macosx"
#endif
#if defined(__sun)
#define CONF_FAMILY_UNIX 1
#define CONF_FAMILY_STRING "unix"
#define CONF_PLATFORM_SOLARIS 1
#define CONF_PLATFORM_STRING "solaris"
#endif
/* beos family */
#if defined(__BeOS) || defined(__BEOS__)
#define CONF_FAMILY_BEOS 1
#define CONF_FAMILY_STRING "beos"
#define CONF_PLATFORM_BEOS 1
#define CONF_PLATFORM_STRING "beos"
#endif
/* use gcc endianness definitions when available */
#if defined(__GNUC__) && !defined(__APPLE__) && !defined(__MINGW32__) && !defined(__sun)
#if defined(__FreeBSD__) || defined(__OpenBSD__)
#include <sys/endian.h>
#else
#include <endian.h>
#endif
#if __BYTE_ORDER == __LITTLE_ENDIAN
#define CONF_ARCH_ENDIAN_LITTLE 1
#elif __BYTE_ORDER == __BIG_ENDIAN
#define CONF_ARCH_ENDIAN_BIG 1
#endif
#endif
/* architectures */
#if defined(i386) || defined(__i386__) || defined(__x86__) || defined(CONF_PLATFORM_WIN32)
#define CONF_ARCH_IA32 1
#define CONF_ARCH_STRING "ia32"
#if !defined(CONF_ARCH_ENDIAN_LITTLE) && !defined(CONF_ARCH_ENDIAN_BIG)
#define CONF_ARCH_ENDIAN_LITTLE 1
#endif
#endif
#if defined(__ia64__) || defined(_M_IA64)
#define CONF_ARCH_IA64 1
#define CONF_ARCH_STRING "ia64"
#if !defined(CONF_ARCH_ENDIAN_LITTLE) && !defined(CONF_ARCH_ENDIAN_BIG)
#define CONF_ARCH_ENDIAN_LITTLE 1
#endif
#endif
#if defined(__amd64__) || defined(__x86_64__) || defined(_M_X64)
#define CONF_ARCH_AMD64 1
#define CONF_ARCH_STRING "amd64"
#if !defined(CONF_ARCH_ENDIAN_LITTLE) && !defined(CONF_ARCH_ENDIAN_BIG)
#define CONF_ARCH_ENDIAN_LITTLE 1
#endif
#endif
#if defined(__powerpc__) || defined(__ppc__)
#define CONF_ARCH_PPC 1
#define CONF_ARCH_STRING "ppc"
#if !defined(CONF_ARCH_ENDIAN_LITTLE) && !defined(CONF_ARCH_ENDIAN_BIG)
#define CONF_ARCH_ENDIAN_BIG 1
#endif
#endif
#if defined(__sparc__)
#define CONF_ARCH_SPARC 1
#define CONF_ARCH_STRING "sparc"
#if !defined(CONF_ARCH_ENDIAN_LITTLE) && !defined(CONF_ARCH_ENDIAN_BIG)
#define CONF_ARCH_ENDIAN_BIG 1
#endif
#endif
#ifndef CONF_FAMILY_STRING
#define CONF_FAMILY_STRING "unknown"
#endif
#ifndef CONF_PLATFORM_STRING
#define CONF_PLATFORM_STRING "unknown"
#endif
#ifndef CONF_ARCH_STRING
#define CONF_ARCH_STRING "unknown"
#endif
#endif
-269
View File
@@ -1,269 +0,0 @@
/* vim: set et ts=3 sw=3 sts=3 ft=c:
*
* Copyright (C) 2012, 2013, 2014 James McLaughlin et al. All rights reserved.
* https://github.com/udp/json-parser
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#ifndef _JSON_H
#define _JSON_H
#ifndef json_char
#define json_char char
#endif
#ifndef json_int_t
#ifndef _MSC_VER
#include <inttypes.h>
#define json_int_t int64_t
#else
#define json_int_t __int64
#endif
#endif
#include <stdlib.h>
#ifdef __cplusplus
#include <string.h>
extern "C"
{
#endif
typedef struct
{
unsigned long max_memory;
int settings;
/* Custom allocator support (leave null to use malloc/free)
*/
void * (* mem_alloc) (size_t, int zero, void * user_data);
void (* mem_free) (void *, void * user_data);
void * user_data; /* will be passed to mem_alloc and mem_free */
} json_settings;
#define json_enable_comments 0x01
typedef enum
{
json_none,
json_object,
json_array,
json_integer,
json_double,
json_string,
json_boolean,
json_null
} json_type;
extern const struct _json_value json_value_none;
typedef struct _json_value
{
struct _json_value * parent;
json_type type;
union
{
int boolean;
json_int_t integer;
double dbl;
struct
{
unsigned int length;
json_char * ptr; /* null terminated */
} string;
struct
{
unsigned int length;
struct
{
json_char * name;
unsigned int name_length;
struct _json_value * value;
} * values;
#if defined(__cplusplus) && __cplusplus >= 201103L
decltype(values) begin () const
{ return values;
}
decltype(values) end () const
{ return values + length;
}
#endif
} object;
struct
{
unsigned int length;
struct _json_value ** values;
#if defined(__cplusplus) && __cplusplus >= 201103L
decltype(values) begin () const
{ return values;
}
decltype(values) end () const
{ return values + length;
}
#endif
} array;
} u;
union
{
struct _json_value * next_alloc;
void * object_mem;
} _reserved;
/* Some C++ operator sugar */
#ifdef __cplusplus
public:
inline _json_value ()
{ memset (this, 0, sizeof (_json_value));
}
inline const struct _json_value &operator [] (int index) const
{
if (type != json_array || index < 0
|| ((unsigned int) index) >= u.array.length)
{
return json_value_none;
}
return *u.array.values [index];
}
inline const struct _json_value &operator [] (const char * index) const
{
if (type != json_object)
return json_value_none;
for (unsigned int i = 0; i < u.object.length; ++ i)
if (!strcmp (u.object.values [i].name, index))
return *u.object.values [i].value;
return json_value_none;
}
inline operator const char * () const
{
switch (type)
{
case json_string:
return u.string.ptr;
default:
return "";
};
}
inline operator json_int_t () const
{
switch (type)
{
case json_integer:
return u.integer;
case json_double:
return (json_int_t) u.dbl;
default:
return 0;
};
}
inline operator bool () const
{
if (type != json_boolean)
return false;
return u.boolean != 0;
}
inline operator double () const
{
switch (type)
{
case json_integer:
return (double) u.integer;
case json_double:
return u.dbl;
default:
return 0;
};
}
#endif
} json_value;
json_value * json_parse (const json_char * json,
size_t length);
#define json_error_max 128
json_value * json_parse_ex (json_settings * settings,
const json_char * json,
size_t length,
char * error);
void json_value_free (json_value *);
/* Not usually necessary, unless you used a custom mem_alloc and now want to
* use a custom mem_free.
*/
void json_value_free_ex (json_settings * settings,
json_value *);
#ifdef __cplusplus
} /* extern "C" */
#endif
#endif
File diff suppressed because it is too large Load Diff
+138
View File
@@ -0,0 +1,138 @@
package main
import (
"context"
"errors"
"flag"
"fmt"
"net"
"net/http"
"os"
"os/signal"
"path/filepath"
"strconv"
"strings"
"syscall"
"time"
)
func main() {
var opts Options
var legacyBind string
var legacyPort int
var printVersion bool
flag.StringVar(&opts.ConfigPath, "config", envOr("CONFIG_PATH", "config.json"), "configuration file")
flag.StringVar(&opts.ConfigPath, "c", envOr("CONFIG_PATH", "config.json"), "configuration file (shorthand)")
flag.StringVar(&opts.StatsPath, "stats", os.Getenv("STATS_PATH"), "persistent stats JSON file")
flag.StringVar(&opts.WebDir, "web-dir", envOr("WEB_DIR", "../web"), "WebUI directory")
flag.StringVar(&opts.WebDir, "d", envOr("WEB_DIR", "../web"), "WebUI directory (shorthand)")
flag.StringVar(&opts.HTTPAddr, "http", envOr("HTTP_ADDR", ":8080"), "HTTP listen address")
flag.StringVar(&opts.AgentAddr, "agent", envOr("AGENT_ADDR", ":35601"), "agent TCP listen address")
flag.StringVar(&legacyBind, "bind", "", "agent bind address (legacy compatibility)")
flag.StringVar(&legacyBind, "b", "", "agent bind address (legacy shorthand)")
flag.IntVar(&legacyPort, "port", 0, "agent TCP port (legacy compatibility)")
flag.IntVar(&legacyPort, "p", 0, "agent TCP port (legacy shorthand)")
flag.BoolVar(&opts.Verbose, "verbose", envBool("VERBOSE", false), "verbose HTTP logging")
flag.BoolVar(&opts.Verbose, "v", envBool("VERBOSE", false), "verbose HTTP logging (shorthand)")
flag.BoolVar(&printVersion, "version", false, "print version and exit")
flag.Parse()
if printVersion {
fmt.Printf("serverstatus %s commit=%s built=%s\n", version, commit, buildTime)
return
}
if opts.StatsPath == "" {
opts.StatsPath = filepath.Join(opts.WebDir, "json", "stats.json")
}
if legacyPort != 0 || legacyBind != "" {
if legacyPort == 0 {
legacyPort = 35601
}
opts.AgentAddr = net.JoinHostPort(legacyBind, strconv.Itoa(legacyPort))
}
opts.AdminToken = os.Getenv("ADMIN_TOKEN")
opts.CORSOrigin = os.Getenv("ADMIN_CORS_ORIGIN")
opts.InsecureCallbackTLS = envBool("INSECURE_CALLBACK_TLS", false)
if err := os.MkdirAll(filepath.Dir(opts.StatsPath), 0o755); err != nil {
fatalf("create stats directory: %v", err)
}
app, err := NewApp(opts)
if err != nil {
fatalf("start: %v", err)
}
app.StartBackground()
httpServer := app.HTTPServer()
agentServer := NewAgentServer(app)
errorsChannel := make(chan error, 2)
go func() {
app.logger.Printf("HTTP listening on %s", opts.HTTPAddr)
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
errorsChannel <- fmt.Errorf("HTTP server: %w", err)
}
}()
go func() {
if err := agentServer.ListenAndServe(); err != nil {
errorsChannel <- fmt.Errorf("agent server: %w", err)
}
}()
reloadSignals := make(chan os.Signal, 1)
stopSignals := make(chan os.Signal, 1)
signal.Notify(reloadSignals, syscall.SIGHUP)
signal.Notify(stopSignals, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
defer signal.Stop(reloadSignals)
defer signal.Stop(stopSignals)
go func() {
for range reloadSignals {
if apiErr := app.ReloadConfig(); apiErr != nil {
app.logger.Printf("reload config: %s", apiErr.Message)
continue
}
app.logger.Printf("configuration reloaded; generation=%d", app.generation.Load())
}
}()
var fatalErr error
select {
case signalValue := <-stopSignals:
app.logger.Printf("received %s; shutting down", signalValue)
case fatalErr = <-errorsChannel:
app.logger.Printf("fatal: %v", fatalErr)
}
app.cancel()
shutdownContext, cancel := context.WithTimeout(context.Background(), 10*time.Second)
_ = httpServer.Shutdown(shutdownContext)
cancel()
app.Close()
if fatalErr != nil {
os.Exit(1)
}
}
func envOr(name, fallback string) string {
if value := strings.TrimSpace(os.Getenv(name)); value != "" {
return value
}
return fallback
}
func envBool(name string, fallback bool) bool {
value := strings.TrimSpace(os.Getenv(name))
if value == "" {
return fallback
}
parsed, err := strconv.ParseBool(value)
if err != nil {
return fallback
}
return parsed
}
func fatalf(format string, args ...any) {
fmt.Fprintf(os.Stderr, "serverstatus: "+format+"\n", args...)
os.Exit(1)
}
+367
View File
@@ -0,0 +1,367 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"math"
"strconv"
"strings"
"time"
"github.com/expr-lang/expr/vm"
)
const maxRequestBody = 1 << 20
type ConfigDocument map[string]any
type ServerConfig struct {
Username string `json:"username"`
Name string `json:"name"`
Type string `json:"type"`
Host string `json:"host"`
Location string `json:"location"`
Password string `json:"password"`
MonthStart int `json:"monthstart"`
Disabled bool `json:"disabled,omitempty"`
}
type MonitorConfig struct {
Name string `json:"name"`
Host string `json:"host"`
Interval int `json:"interval"`
Type string `json:"type"`
}
type SSLCertConfig struct {
Name string `json:"name"`
Domain string `json:"domain"`
Port int `json:"port"`
Interval int `json:"interval"`
Callback string `json:"callback"`
}
type WatchdogConfig struct {
Name string `json:"name"`
Rule string `json:"rule"`
Interval int `json:"interval"`
Callback string `json:"callback"`
}
type CompiledWatchdog struct {
WatchdogConfig
Key string
Normalized string
Program *vm.Program
}
type RuntimeConfig struct {
Servers []ServerConfig
Monitors []MonitorConfig
SSLCerts []SSLCertConfig
Watchdogs []CompiledWatchdog
}
type AgentStats struct {
Uptime int64 `json:"uptime"`
Load1 float64 `json:"load_1"`
Load5 float64 `json:"load_5"`
Load15 float64 `json:"load_15"`
Ping10010 float64 `json:"ping_10010"`
Ping189 float64 `json:"ping_189"`
Ping10086 float64 `json:"ping_10086"`
Time10010 int64 `json:"time_10010"`
Time189 int64 `json:"time_189"`
Time10086 int64 `json:"time_10086"`
TCPCount int64 `json:"tcp"`
UDPCount int64 `json:"udp"`
ProcessCount int64 `json:"process"`
ThreadCount int64 `json:"thread"`
NetworkRX int64 `json:"network_rx"`
NetworkTX int64 `json:"network_tx"`
NetworkIn int64 `json:"network_in"`
NetworkOut int64 `json:"network_out"`
MemoryTotal int64 `json:"memory_total"`
MemoryUsed int64 `json:"memory_used"`
SwapTotal int64 `json:"swap_total"`
SwapUsed int64 `json:"swap_used"`
HDDTotal int64 `json:"hdd_total"`
HDDUsed int64 `json:"hdd_used"`
IORead int64 `json:"io_read"`
IOWrite int64 `json:"io_write"`
CPU float64 `json:"cpu"`
CPUCores int64 `json:"cpu_cores"`
CPUModel string `json:"cpu_model"`
Custom string `json:"custom"`
OS string `json:"os"`
Online4 *bool `json:"online4"`
Online6 *bool `json:"online6"`
}
type APIError struct {
Status int
Message string
Details any
}
func (e *APIError) Error() string {
if e.Details != nil {
return fmt.Sprintf("%s: %v", e.Message, e.Details)
}
return e.Message
}
func decodeDocument(data []byte) (ConfigDocument, error) {
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
var doc ConfigDocument
if err := decoder.Decode(&doc); err != nil {
return nil, err
}
if doc == nil {
return nil, fmt.Errorf("config must be a JSON object")
}
var extra any
if err := decoder.Decode(&extra); !errors.Is(err, io.EOF) {
return nil, fmt.Errorf("config contains more than one JSON value")
}
return doc, nil
}
func cloneDocument(doc ConfigDocument) (ConfigDocument, error) {
data, err := json.Marshal(doc)
if err != nil {
return nil, err
}
return decodeDocument(data)
}
type collectionSpec struct {
itemName string
idField string
required []string
optional []string
defaults map[string]int
}
var collectionSpecs = map[string]collectionSpec{
"servers": {
itemName: "server", idField: "username",
required: []string{"username", "name", "type", "host", "location", "password"},
optional: []string{"monthstart", "disabled"},
defaults: map[string]int{"monthstart": 1},
},
"monitors": {
itemName: "monitor", idField: "name",
required: []string{"name", "host", "type"},
optional: []string{"interval"},
defaults: map[string]int{"interval": 600},
},
"sslcerts": {
itemName: "sslcert", idField: "name",
required: []string{"name", "domain"},
optional: []string{"port", "interval", "callback"},
defaults: map[string]int{"port": 443, "interval": 7200},
},
"watchdog": {
itemName: "watchdog", idField: "name",
required: []string{"name", "rule"},
optional: []string{"interval", "callback"},
defaults: map[string]int{"interval": 600},
},
}
func normalizeConfig(input ConfigDocument) (ConfigDocument, RuntimeConfig, *APIError) {
doc, err := cloneDocument(input)
if err != nil {
return nil, RuntimeConfig{}, &APIError{Status: 400, Message: "config must be a JSON object", Details: map[string]any{"error": err.Error()}}
}
for _, key := range []string{"servers", "monitors", "sslcerts", "watchdog"} {
spec := collectionSpecs[key]
raw, exists := doc[key]
if !exists || raw == nil {
raw = []any{}
}
items, ok := raw.([]any)
if !ok {
return nil, RuntimeConfig{}, &APIError{Status: 400, Message: key + " must be an array"}
}
normalized := make([]any, 0, len(items))
seen := make(map[string]struct{})
for index, rawItem := range items {
item, ok := rawItem.(map[string]any)
if !ok {
return nil, RuntimeConfig{}, &APIError{Status: 400, Message: spec.itemName + " must be an object", Details: map[string]any{"index": index}}
}
missing := make([]string, 0)
for _, field := range spec.required {
value, ok := item[field]
text := ""
if ok && value != nil {
text = strings.TrimSpace(fmt.Sprint(value))
}
if text == "" {
missing = append(missing, field)
} else {
item[field] = text
}
}
if len(missing) > 0 {
return nil, RuntimeConfig{}, &APIError{Status: 400, Message: spec.itemName + " has missing required fields", Details: map[string]any{"missing": missing, "index": index}}
}
for field, fallback := range spec.defaults {
value, apiErr := normalizeInteger(item[field], field, index, fallback)
if apiErr != nil {
return nil, RuntimeConfig{}, apiErr
}
switch {
case key == "servers" && field == "monthstart":
value = clamp(value, 1, 28)
case key == "sslcerts" && field == "port":
value = clamp(value, 1, 65535)
default:
if value < 1 {
value = 1
}
}
item[field] = value
}
if key == "servers" {
if value, exists := item["disabled"]; exists {
disabled, ok := normalizeBool(value)
if !ok {
return nil, RuntimeConfig{}, &APIError{Status: 400, Message: "disabled must be a boolean", Details: map[string]any{"index": index}}
}
item["disabled"] = disabled
}
username := item["username"].(string)
if _, duplicate := seen[username]; duplicate {
return nil, RuntimeConfig{}, &APIError{Status: 409, Message: "duplicate server username", Details: map[string]any{"username": username}}
}
seen[username] = struct{}{}
}
if key == "sslcerts" || key == "watchdog" {
value := item["callback"]
if value == nil {
item["callback"] = ""
} else {
item["callback"] = strings.TrimSpace(fmt.Sprint(value))
}
}
normalized = append(normalized, item)
}
doc[key] = normalized
}
runtime, apiErr := buildRuntimeConfig(doc)
if apiErr != nil {
return nil, RuntimeConfig{}, apiErr
}
return doc, runtime, nil
}
func buildRuntimeConfig(doc ConfigDocument) (RuntimeConfig, *APIError) {
data, err := json.Marshal(doc)
if err != nil {
return RuntimeConfig{}, &APIError{Status: 400, Message: "config could not be encoded", Details: map[string]any{"error": err.Error()}}
}
var raw struct {
Servers []ServerConfig `json:"servers"`
Monitors []MonitorConfig `json:"monitors"`
SSLCerts []SSLCertConfig `json:"sslcerts"`
Watchdogs []WatchdogConfig `json:"watchdog"`
}
if err := json.Unmarshal(data, &raw); err != nil {
return RuntimeConfig{}, &APIError{Status: 400, Message: "config has invalid field types", Details: map[string]any{"error": err.Error()}}
}
runtime := RuntimeConfig{Servers: raw.Servers, Monitors: raw.Monitors, SSLCerts: raw.SSLCerts}
for index, rule := range raw.Watchdogs {
compiled, err := compileWatchdog(rule, index)
if err != nil {
return RuntimeConfig{}, &APIError{Status: 400, Message: "watchdog rule is invalid", Details: map[string]any{"index": index, "name": rule.Name, "error": err.Error()}}
}
runtime.Watchdogs = append(runtime.Watchdogs, compiled)
}
return runtime, nil
}
func normalizeInteger(raw any, field string, index, fallback int) (int, *APIError) {
if raw == nil || raw == "" {
return fallback, nil
}
var value int64
var err error
switch v := raw.(type) {
case json.Number:
value, err = strconv.ParseInt(v.String(), 10, 64)
case float64:
if math.Trunc(v) != v {
err = fmt.Errorf("not an integer")
} else {
value = int64(v)
}
case float32:
if math.Trunc(float64(v)) != float64(v) {
err = fmt.Errorf("not an integer")
} else {
value = int64(v)
}
case int:
value = int64(v)
case int64:
value = v
case string:
value, err = strconv.ParseInt(strings.TrimSpace(v), 10, 64)
default:
err = fmt.Errorf("unsupported value")
}
if err != nil || value > math.MaxInt || value < math.MinInt {
return 0, &APIError{Status: 400, Message: field + " must be an integer", Details: map[string]any{"index": index}}
}
return int(value), nil
}
func normalizeBool(raw any) (bool, bool) {
switch value := raw.(type) {
case bool:
return value, true
case string:
parsed, err := strconv.ParseBool(strings.TrimSpace(value))
return parsed, err == nil
case json.Number:
if value.String() == "0" {
return false, true
}
if value.String() == "1" {
return true, true
}
}
return false, false
}
func clamp(value, minimum, maximum int) int {
if value < minimum {
return minimum
}
if value > maximum {
return maximum
}
return value
}
func secondsDuration(value int) time.Duration {
if value < 1 {
value = 1
}
maxDuration := time.Duration(1<<63 - 1)
if int64(value) > int64(maxDuration/time.Second) {
return maxDuration
}
return time.Duration(value) * time.Second
}
+85
View File
@@ -0,0 +1,85 @@
package main
import (
"encoding/json"
"strings"
"testing"
"time"
"github.com/expr-lang/expr"
)
func TestNormalizeConfigPreservesCompatibility(t *testing.T) {
doc := minimalTestConfig()
doc["future"] = map[string]any{"enabled": true}
doc["watchdog"] = []any{
map[string]any{"name": "legacy", "rule": "cpu>90&load_1>5&username!='s01'", "interval": "600", "callback": nil},
map[string]any{"name": "type", "rule": "tcp_count>600&type='Oracle'", "interval": 60},
}
doc["servers"].([]any)[0].(map[string]any)["future_field"] = "kept"
normalized, runtime, apiErr := normalizeConfig(doc)
if apiErr != nil {
t.Fatal(apiErr)
}
if len(runtime.Watchdogs) != 2 || runtime.Watchdogs[0].Normalized != "cpu>90&&load_1>5&&username!='s01'" {
t.Fatalf("legacy rules were not normalized: %#v", runtime.Watchdogs)
}
server := normalized["servers"].([]any)[0].(map[string]any)
if server["future_field"] != "kept" || normalized["future"] == nil {
t.Fatal("unknown config fields were discarded")
}
if server["monthstart"] != json.Number("1") && server["monthstart"] != 1 {
t.Fatalf("monthstart not normalized: %#v", server["monthstart"])
}
}
func TestWatchdogLegacyRulesEvaluate(t *testing.T) {
rules := []string{
"online4=0&online6=0",
"(memory_used/memory_total)*100>90&memory_total>1048576",
"tcp_count>600&type='Oracle'",
"(network_out-last_network_out)/1024/1024/1024>18&(username='aliyun1'|username='aliyun2')",
}
for index, rule := range rules {
compiled, err := compileWatchdog(WatchdogConfig{Name: "test", Rule: rule, Interval: 1}, index)
if err != nil {
t.Fatalf("rule %q: %v", rule, err)
}
environment := watchdogEnvironment(ServerConfig{Username: "aliyun1", Type: "Oracle"}, AgentStats{MemoryTotal: 2_000_000, MemoryUsed: 1_900_000, TCPCount: 700, NetworkOut: 30 << 30}, false, false, 0, 0)
if _, err := expr.Run(compiled.Program, environment); err != nil {
t.Fatalf("run %q: %v", rule, err)
}
}
}
func TestConfigValidationErrors(t *testing.T) {
doc := minimalTestConfig()
doc["servers"] = append(doc["servers"].([]any), doc["servers"].([]any)[0])
_, _, apiErr := normalizeConfig(doc)
if apiErr == nil || apiErr.Status != 409 {
t.Fatalf("expected duplicate username error, got %#v", apiErr)
}
if _, err := decodeDocument([]byte(`{"servers":[]} {"servers":[]}`)); err == nil || !strings.Contains(err.Error(), "more than one") {
t.Fatalf("expected trailing JSON error, got %v", err)
}
}
func TestFormattingHelpers(t *testing.T) {
if got := formatUptime(90061); got != "1 天" {
t.Fatalf("formatUptime=%q", got)
}
if got := formatUptime(3661); got != "01:01:01" {
t.Fatalf("formatUptime=%q", got)
}
if got, err := certificateHost("https://example.com/path"); err != nil || got != "example.com" {
t.Fatalf("certificateHost=%q", got)
}
if got := secondsDuration(0); got != time.Second {
t.Fatalf("secondsDuration(0)=%s", got)
}
if got := secondsDuration(int(^uint(0) >> 1)); got < 365*24*time.Hour {
t.Fatalf("large interval overflowed or was truncated: %s", got)
}
}
-1
View File
@@ -1 +0,0 @@
*.o
+223
View File
@@ -0,0 +1,223 @@
package main
import "strings"
func openAPISpec() map[string]any {
paths := map[string]any{
"/api/health": map[string]any{
"get": publicOperation("health", "服务健康状态", objectResponse("Go 服务、Agent TCP 与配置状态")),
},
"/api/schema": map[string]any{
"get": publicOperation("schema", "ServerStatus API 简表", objectResponse("端点和集合描述")),
},
"/api/openapi.json": map[string]any{
"get": publicOperation("openapi", "OpenAPI 3.1 文档", objectResponse("OpenAPI document")),
},
"/json/stats.json": map[string]any{
"get": publicOperation("stats", "实时状态快照", objectResponse("WebUI 状态数据")),
},
"/api/config": map[string]any{
"get": protectedOperation("getConfig", "读取完整配置", nil, configResponse("200", "配置文档")),
"put": protectedOperation("replaceConfig", "整体校验并替换配置", requestBody("Config"), configResponse("200", "已保存并热重载的配置")),
},
"/api/reload": map[string]any{
"post": protectedOperation("reloadConfig", "从磁盘热重载配置", nil, objectResponse("重载结果")),
},
"/api/restart": map[string]any{
"post": protectedOperation("restartRuntime", "在进程内重启采集运行时", nil, statusResponse("202", "重启结果", map[string]any{
"operation": map[string]any{"type": "string", "const": "restart"},
"mode": map[string]any{"type": "string", "const": "in-process"},
})),
},
"/api/servers/{username}/reset-traffic": map[string]any{
"post": withParameters(protectedOperation("resetServerTraffic", "重置节点本月流量基线", nil, objectResponse("流量重置结果")), pathParameter("username", "节点用户名")),
},
}
for _, key := range []string{"servers", "monitors", "sslcerts", "watchdog"} {
spec := collectionSpecs[key]
schemaName := map[string]string{"servers": "Server", "monitors": "Monitor", "sslcerts": "SSLCert", "watchdog": "Watchdog"}[key]
operationBase := strings.TrimSuffix(key, "s")
if key == "sslcerts" {
operationBase = "sslcert"
}
paths["/api/"+key] = map[string]any{
"get": protectedOperation("list"+titleWord(key), "查询 "+key, nil, collectionResponse(key, schemaName, "配置集合")),
"post": protectedOperation("create"+titleWord(operationBase), "新增 "+spec.itemName, requestBody(schemaName), itemResponse("201", spec.itemName, schemaName, "创建结果")),
}
parameterName := "id"
parameterDescription := "数字下标或唯一的 " + spec.idField
if key == "servers" {
parameterName = "username"
parameterDescription = "节点用户名"
}
paths["/api/"+key+"/{"+parameterName+"}"] = map[string]any{
"put": withParameters(protectedOperation("update"+titleWord(operationBase), "修改 "+spec.itemName, requestBody(schemaName), itemResponse("200", spec.itemName, schemaName, "修改结果")), pathParameter(parameterName, parameterDescription)),
"delete": withParameters(protectedOperation("delete"+titleWord(operationBase), "删除 "+spec.itemName, nil, itemResponse("200", "removed", schemaName, "删除结果")), pathParameter(parameterName, parameterDescription)),
}
}
return map[string]any{
"openapi": "3.1.0",
"info": map[string]any{
"title": "ServerStatus HTTP API",
"version": version,
"description": "单进程 Go ServerStatus 的配置、运行状态和采集控制 API。",
},
"servers": []any{map[string]any{"url": "/", "description": "当前 ServerStatus 实例"}},
"paths": paths,
"components": map[string]any{
"securitySchemes": map[string]any{
"bearerAuth": map[string]any{"type": "http", "scheme": "bearer", "bearerFormat": "ADMIN_TOKEN"},
},
"schemas": openAPISchemas(),
},
}
}
func openAPISchemas() map[string]any {
stringProperty := func(description string) map[string]any {
return map[string]any{"type": "string", "description": description}
}
integerProperty := func(description string, minimum, maximum int) map[string]any {
property := map[string]any{"type": "integer", "description": description, "minimum": minimum}
if maximum > 0 {
property["maximum"] = maximum
}
return property
}
server := map[string]any{
"type": "object", "required": collectionSpecs["servers"].required,
"properties": map[string]any{
"username": stringProperty("唯一客户端用户名"), "name": stringProperty("节点显示名称"),
"type": stringProperty("虚拟化或节点类型"), "host": stringProperty("主机标识"),
"location": stringProperty("位置"), "password": stringProperty("客户端密码"),
"monthstart": integerProperty("月流量重置日", 1, 28), "disabled": map[string]any{"type": "boolean", "default": false},
},
}
monitor := map[string]any{
"type": "object", "required": collectionSpecs["monitors"].required,
"properties": map[string]any{
"name": stringProperty("监测名称"), "host": stringProperty("HTTP(S) URL 或 TCP 地址"),
"type": stringProperty("https、http 或 tcp"), "interval": integerProperty("客户端探测间隔秒数", 1, 0),
},
}
sslcert := map[string]any{
"type": "object", "required": collectionSpecs["sslcerts"].required,
"properties": map[string]any{
"name": stringProperty("证书名称"), "domain": stringProperty("域名或 URL"),
"port": integerProperty("TLS 端口", 1, 65535), "interval": integerProperty("检查间隔秒数", 1, 0),
"callback": stringProperty("告警回调 URL 前缀"),
},
}
watchdog := map[string]any{
"type": "object", "required": collectionSpecs["watchdog"].required,
"properties": map[string]any{
"name": stringProperty("告警名称"), "rule": stringProperty("兼容 Exprtk 的状态表达式"),
"interval": integerProperty("通知冷却秒数", 1, 0), "callback": stringProperty("告警回调 URL 前缀"),
},
}
return map[string]any{
"Server": server, "Monitor": monitor, "SSLCert": sslcert, "Watchdog": watchdog,
"Config": map[string]any{
"type": "object", "required": []string{"servers", "monitors", "sslcerts", "watchdog"},
"properties": map[string]any{
"servers": arraySchema("Server"), "monitors": arraySchema("Monitor"),
"sslcerts": arraySchema("SSLCert"), "watchdog": arraySchema("Watchdog"),
},
},
"Error": map[string]any{
"type": "object", "required": []string{"ok", "error"},
"properties": map[string]any{"ok": map[string]any{"type": "boolean", "const": false}, "error": map[string]any{"type": "string"}, "details": map[string]any{}},
},
}
}
func titleWord(value string) string {
if value == "" {
return ""
}
return strings.ToUpper(value[:1]) + value[1:]
}
func schemaRef(name string) map[string]any {
return map[string]any{"$ref": "#/components/schemas/" + name}
}
func arraySchema(name string) map[string]any {
return map[string]any{"type": "array", "items": schemaRef(name)}
}
func requestBody(schemaName string) map[string]any {
return map[string]any{
"required": true,
"content": map[string]any{"application/json": map[string]any{"schema": schemaRef(schemaName)}},
}
}
func objectResponse(description string) map[string]any {
return map[string]any{"200": jsonResponse(description, map[string]any{"type": "object"}), "4XX": jsonResponse("请求错误", schemaRef("Error"))}
}
func configResponse(status, description string) map[string]any {
return statusResponse(status, description, map[string]any{"config": schemaRef("Config")})
}
func collectionResponse(key, schemaName, description string) map[string]any {
return statusResponse("200", description, map[string]any{key: arraySchema(schemaName)})
}
func itemResponse(status, key, schemaName, description string) map[string]any {
return statusResponse(status, description, map[string]any{
key: schemaRef(schemaName),
"reloaded": map[string]any{"type": "boolean", "const": true},
"pid": map[string]any{"type": "integer", "minimum": 1},
"config": schemaRef("Config"),
})
}
func statusResponse(status, description string, properties map[string]any) map[string]any {
required := []string{"ok"}
allProperties := map[string]any{"ok": map[string]any{"type": "boolean", "const": true}}
for key, property := range properties {
allProperties[key] = property
required = append(required, key)
}
return map[string]any{
status: jsonResponse(description, map[string]any{
"type": "object", "required": required, "properties": allProperties,
}),
"4XX": jsonResponse("请求错误", schemaRef("Error")),
}
}
func jsonResponse(description string, schema map[string]any) map[string]any {
return map[string]any{"description": description, "content": map[string]any{"application/json": map[string]any{"schema": schema}}}
}
func publicOperation(operationID, summary string, responses map[string]any) map[string]any {
return map[string]any{"operationId": operationID, "summary": summary, "security": []any{}, "responses": responses}
}
func protectedOperation(operationID, summary string, body map[string]any, responses map[string]any) map[string]any {
operation := map[string]any{
"operationId": operationID, "summary": summary,
"security": []any{map[string]any{"bearerAuth": []any{}}}, "responses": responses,
}
if body != nil {
operation["requestBody"] = body
}
return operation
}
func withParameters(operation map[string]any, parameters ...map[string]any) map[string]any {
items := make([]any, 0, len(parameters))
for _, parameter := range parameters {
items = append(items, parameter)
}
operation["parameters"] = items
return operation
}
func pathParameter(name, description string) map[string]any {
return map[string]any{"name": name, "in": "path", "required": true, "description": description, "schema": map[string]any{"type": "string"}}
}
-322
View File
@@ -1,322 +0,0 @@
#include "argparse.h"
#if defined(__cplusplus)
extern "C" {
#endif
#define OPT_UNSET 1
static const char *
prefix_skip(const char *str, const char *prefix)
{
size_t len = strlen(prefix);
return strncmp(str, prefix, len) ? NULL : str + len;
}
int
prefix_cmp(const char *str, const char *prefix)
{
for (;; str++, prefix++)
if (!*prefix)
return 0;
else if (*str != *prefix)
return (unsigned char)*prefix - (unsigned char)*str;
}
static void
argparse_error(struct argparse *this_, const struct argparse_option *opt,
const char *reason)
{
if (!strncmp(this_->argv[0], "--", 2)) {
fprintf(stderr, "error: option `%s` %s\n", opt->long_name, reason);
exit(-1);
} else {
fprintf(stderr, "error: option `%c` %s\n", opt->short_name, reason);
exit(-1);
}
}
static int
argparse_getvalue(struct argparse *this_, const struct argparse_option *opt,
int flags)
{
const char *s = NULL;
if (!opt->value)
goto skipped;
switch (opt->type) {
case ARGPARSE_OPT_BOOLEAN:
if (flags & OPT_UNSET) {
*(int *)opt->value = *(int *)opt->value - 1;
} else {
*(int *)opt->value = *(int *)opt->value + 1;
}
if (*(int *)opt->value < 0) {
*(int *)opt->value = 0;
}
break;
case ARGPARSE_OPT_BIT:
if (flags & OPT_UNSET) {
*(int *)opt->value &= ~opt->data;
} else {
*(int *)opt->value |= opt->data;
}
break;
case ARGPARSE_OPT_STRING:
if (this_->optvalue) {
*(const char **)opt->value = this_->optvalue;
this_->optvalue = NULL;
} else if (this_->argc > 1) {
this_->argc--;
*(const char **)opt->value = *++this_->argv;
} else {
argparse_error(this_, opt, "requires a value");
}
break;
case ARGPARSE_OPT_INTEGER:
if (this_->optvalue) {
*(int *)opt->value = strtol(this_->optvalue, (char **)&s, 0);
this_->optvalue = NULL;
} else if (this_->argc > 1) {
this_->argc--;
*(int *)opt->value = strtol(*++this_->argv, (char **)&s, 0);
} else {
argparse_error(this_, opt, "requires a value");
}
if (*s)
argparse_error(this_, opt, "expects a numerical value");
break;
default:
assert(0);
}
skipped:
if (opt->callback) {
return opt->callback(this_, opt);
}
return 0;
}
static void
argparse_options_check(const struct argparse_option *options)
{
for (; options->type != ARGPARSE_OPT_END; options++) {
switch (options->type) {
case ARGPARSE_OPT_END:
case ARGPARSE_OPT_BOOLEAN:
case ARGPARSE_OPT_BIT:
case ARGPARSE_OPT_INTEGER:
case ARGPARSE_OPT_STRING:
continue;
default:
fprintf(stderr, "wrong option type: %d", options->type);
break;
}
}
}
static int
argparse_short_opt(struct argparse *this_, const struct argparse_option *options)
{
for (; options->type != ARGPARSE_OPT_END; options++) {
if (options->short_name == *this_->optvalue) {
this_->optvalue = this_->optvalue[1] ? this_->optvalue + 1 : NULL;
return argparse_getvalue(this_, options, 0);
}
}
return -2;
}
static int
argparse_long_opt(struct argparse *this_, const struct argparse_option *options)
{
for (; options->type != ARGPARSE_OPT_END; options++) {
const char *rest;
int opt_flags = 0;
if (!options->long_name)
continue;
rest = prefix_skip(this_->argv[0] + 2, options->long_name);
if (!rest) {
// Negation allowed?
if (options->flags & OPT_NONEG) {
continue;
}
// Only boolean/bit allow negation.
if (options->type != ARGPARSE_OPT_BOOLEAN && options->type != ARGPARSE_OPT_BIT) {
continue;
}
if (!prefix_cmp(this_->argv[0] + 2, "no-")) {
rest = prefix_skip(this_->argv[0] + 2 + 3, options->long_name);
if (!rest)
continue;
opt_flags |= OPT_UNSET;
} else {
continue;
}
}
if (*rest) {
if (*rest != '=')
continue;
this_->optvalue = rest + 1;
}
return argparse_getvalue(this_, options, opt_flags);
}
return -2;
}
int
argparse_init(struct argparse *this_, struct argparse_option *options,
const char *usage, int flags)
{
memset(this_, 0, sizeof(*this_));
this_->options = options;
this_->usage = usage;
this_->flags = flags;
return 0;
}
int
argparse_parse(struct argparse *this_, int argc, const char **argv)
{
this_->argc = argc - 1;
this_->argv = argv + 1;
this_->out = argv;
argparse_options_check(this_->options);
for (; this_->argc; this_->argc--, this_->argv++) {
const char *arg = this_->argv[0];
if (arg[0] != '-' || !arg[1]) {
if (this_->flags & ARGPARSE_STOP_AT_NON_OPTION) {
goto end;
}
// if it's not option or is a single char '-', copy verbatimly
this_->out[this_->cpidx++] = this_->argv[0];
continue;
}
// short option
if (arg[1] != '-') {
this_->optvalue = arg + 1;
switch (argparse_short_opt(this_, this_->options)) {
case -1:
break;
case -2:
goto unknown;
}
while (this_->optvalue) {
switch (argparse_short_opt(this_, this_->options)) {
case -1:
break;
case -2:
goto unknown;
}
}
continue;
}
// if '--' presents
if (!arg[2]) {
this_->argc--;
this_->argv++;
break;
}
// long option
switch (argparse_long_opt(this_, this_->options)) {
case -1:
break;
case -2:
goto unknown;
}
continue;
unknown:
fprintf(stderr, "error: unknown option `%s`\n", this_->argv[0]);
argparse_usage(this_);
exit(0);
}
end:
memmove(this_->out + this_->cpidx, this_->argv,
this_->argc * sizeof(*this_->out));
this_->out[this_->cpidx + this_->argc] = NULL;
return this_->cpidx + this_->argc;
}
void
argparse_usage(struct argparse *this_)
{
fprintf(stdout, "Usage: %s\n", this_->usage);
fputc('\n', stdout);
const struct argparse_option *options;
// figure out best width
size_t usage_opts_width = 0;
size_t len;
options = this_->options;
for (; options->type != ARGPARSE_OPT_END; options++) {
len = 0;
if ((options)->short_name) {
len += 2;
}
if ((options)->short_name && (options)->long_name) {
len += 2; // separator ", "
}
if ((options)->long_name) {
len += strlen((options)->long_name) + 2;
}
if (options->type == ARGPARSE_OPT_INTEGER) {
len += strlen("=<int>");
} else if (options->type == ARGPARSE_OPT_STRING) {
len += strlen("=<str>");
}
len = ceil((float)len / 4) * 4;
if (usage_opts_width < len) {
usage_opts_width = len;
}
}
usage_opts_width += 4; // 4 spaces prefix
options = this_->options;
for (; options->type != ARGPARSE_OPT_END; options++) {
size_t pos;
int pad;
pos = fprintf(stdout, " ");
if (options->short_name) {
pos += fprintf(stdout, "-%c", options->short_name);
}
if (options->long_name && options->short_name) {
pos += fprintf(stdout, ", ");
}
if (options->long_name) {
pos += fprintf(stdout, "--%s", options->long_name);
}
if (options->type == ARGPARSE_OPT_INTEGER) {
pos += fprintf(stdout, "=<int>");
} else if (options->type == ARGPARSE_OPT_STRING) {
pos += fprintf(stdout, "=<str>");
}
if (pos <= usage_opts_width) {
pad = usage_opts_width - pos;
} else {
fputc('\n', stdout);
pad = usage_opts_width;
}
fprintf(stdout, "%*s%s\n", pad + 2, "", options->help);
}
}
int
argparse_help_cb(struct argparse *this_, const struct argparse_option *option)
{
(void)option;
argparse_usage(this_);
exit(0);
return 0;
}
#if defined(__cplusplus)
}
#endif
-949
View File
@@ -1,949 +0,0 @@
/* vim: set et ts=3 sw=3 sts=3 ft=c:
*
* Copyright (C) 2012, 2013, 2014 James McLaughlin et al. All rights reserved.
* https://github.com/udp/json-parser
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
*
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
#include "json.h"
#ifdef _MSC_VER
#ifndef _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#endif
#endif
#ifdef __cplusplus
const struct _json_value json_value_none; /* zero-d by ctor */
#else
const struct _json_value json_value_none = { 0 };
#endif
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
typedef unsigned short json_uchar;
static unsigned char hex_value (json_char c)
{
if (isdigit(c))
return c - '0';
switch (c) {
case 'a': case 'A': return 0x0A;
case 'b': case 'B': return 0x0B;
case 'c': case 'C': return 0x0C;
case 'd': case 'D': return 0x0D;
case 'e': case 'E': return 0x0E;
case 'f': case 'F': return 0x0F;
default: return 0xFF;
}
}
typedef struct
{
unsigned long used_memory;
unsigned int uint_max;
unsigned long ulong_max;
json_settings settings;
int first_pass;
} json_state;
static void * default_alloc (size_t size, int zero, void * user_data)
{
return zero ? calloc (1, size) : malloc (size);
}
static void default_free (void * ptr, void * user_data)
{
free (ptr);
}
static void * json_alloc (json_state * state, unsigned long size, int zero)
{
if ((state->ulong_max - state->used_memory) < size)
return 0;
if (state->settings.max_memory
&& (state->used_memory += size) > state->settings.max_memory)
{
return 0;
}
return state->settings.mem_alloc (size, zero, state->settings.user_data);
}
static int new_value
(json_state * state, json_value ** top, json_value ** root, json_value ** alloc, json_type type)
{
json_value * value;
int values_size;
if (!state->first_pass)
{
value = *top = *alloc;
*alloc = (*alloc)->_reserved.next_alloc;
if (!*root)
*root = value;
switch (value->type)
{
case json_array:
if (! (value->u.array.values = (json_value **) json_alloc
(state, value->u.array.length * sizeof (json_value *), 0)) )
{
return 0;
}
value->u.array.length = 0;
break;
case json_object:
values_size = sizeof (*value->u.object.values) * value->u.object.length;
if (! ((*(void **) &value->u.object.values) = json_alloc
(state, values_size + ((unsigned long) value->u.object.values), 0)) )
{
return 0;
}
value->_reserved.object_mem = (*(char **) &value->u.object.values) + values_size;
value->u.object.length = 0;
break;
case json_string:
if (! (value->u.string.ptr = (json_char *) json_alloc
(state, (value->u.string.length + 1) * sizeof (json_char), 0)) )
{
return 0;
}
value->u.string.length = 0;
break;
default:
break;
};
return 1;
}
value = (json_value *) json_alloc (state, sizeof (json_value), 1);
if (!value)
return 0;
if (!*root)
*root = value;
value->type = type;
value->parent = *top;
if (*alloc)
(*alloc)->_reserved.next_alloc = value;
*alloc = *top = value;
return 1;
}
#define e_off \
((int) (i - cur_line_begin))
#define whitespace \
case '\n': ++ cur_line; cur_line_begin = i; \
case ' ': case '\t': case '\r'
#define string_add(b) \
do { if (!state.first_pass) string [string_length] = b; ++ string_length; } while (0);
static const long
flag_next = 1 << 0,
flag_reproc = 1 << 1,
flag_need_comma = 1 << 2,
flag_seek_value = 1 << 3,
flag_escaped = 1 << 4,
flag_string = 1 << 5,
flag_need_colon = 1 << 6,
flag_done = 1 << 7,
flag_num_negative = 1 << 8,
flag_num_zero = 1 << 9,
flag_num_e = 1 << 10,
flag_num_e_got_sign = 1 << 11,
flag_num_e_negative = 1 << 12,
flag_line_comment = 1 << 13,
flag_block_comment = 1 << 14;
json_value * json_parse_ex (json_settings * settings,
const json_char * json,
size_t length,
char * error_buf)
{
json_char error [json_error_max];
unsigned int cur_line;
const json_char * cur_line_begin, * i, * end;
json_value * top, * root, * alloc = 0;
json_state state = { 0 };
long flags;
long num_digits = 0, num_e = 0;
json_int_t num_fraction = 0;
/* Skip UTF-8 BOM
*/
if (length >= 3 && ((unsigned char) json [0]) == 0xEF
&& ((unsigned char) json [1]) == 0xBB
&& ((unsigned char) json [2]) == 0xBF)
{
json += 3;
length -= 3;
}
error[0] = '\0';
end = (json + length);
memcpy (&state.settings, settings, sizeof (json_settings));
if (!state.settings.mem_alloc)
state.settings.mem_alloc = default_alloc;
if (!state.settings.mem_free)
state.settings.mem_free = default_free;
memset (&state.uint_max, 0xFF, sizeof (state.uint_max));
memset (&state.ulong_max, 0xFF, sizeof (state.ulong_max));
state.uint_max -= 8; /* limit of how much can be added before next check */
state.ulong_max -= 8;
for (state.first_pass = 1; state.first_pass >= 0; -- state.first_pass)
{
json_uchar uchar;
unsigned char uc_b1, uc_b2, uc_b3, uc_b4;
json_char * string = 0;
unsigned int string_length = 0;
top = root = 0;
flags = flag_seek_value;
cur_line = 1;
cur_line_begin = json;
for (i = json ;; ++ i)
{
json_char b = (i == end ? 0 : *i);
if (flags & flag_string)
{
if (!b)
{ sprintf (error, "Unexpected EOF in string (at %d:%d)", cur_line, e_off);
goto e_failed;
}
if (string_length > state.uint_max)
goto e_overflow;
if (flags & flag_escaped)
{
flags &= ~ flag_escaped;
switch (b)
{
case 'b': string_add ('\b'); break;
case 'f': string_add ('\f'); break;
case 'n': string_add ('\n'); break;
case 'r': string_add ('\r'); break;
case 't': string_add ('\t'); break;
case 'u':
if (end - i < 4 ||
(uc_b1 = hex_value (*++ i)) == 0xFF || (uc_b2 = hex_value (*++ i)) == 0xFF
|| (uc_b3 = hex_value (*++ i)) == 0xFF || (uc_b4 = hex_value (*++ i)) == 0xFF)
{
sprintf (error, "Invalid character value `%c` (at %d:%d)", b, cur_line, e_off);
goto e_failed;
}
uc_b1 = uc_b1 * 16 + uc_b2;
uc_b2 = uc_b3 * 16 + uc_b4;
uchar = ((json_char) uc_b1) * 256 + uc_b2;
if (sizeof (json_char) >= sizeof (json_uchar) || (uc_b1 == 0 && uc_b2 <= 0x7F))
{
string_add ((json_char) uchar);
break;
}
if (uchar <= 0x7FF)
{
if (state.first_pass)
string_length += 2;
else
{ string [string_length ++] = 0xC0 | ((uc_b2 & 0xC0) >> 6) | ((uc_b1 & 0x7) << 2);
string [string_length ++] = 0x80 | (uc_b2 & 0x3F);
}
break;
}
if (state.first_pass)
string_length += 3;
else
{ string [string_length ++] = 0xE0 | ((uc_b1 & 0xF0) >> 4);
string [string_length ++] = 0x80 | ((uc_b1 & 0xF) << 2) | ((uc_b2 & 0xC0) >> 6);
string [string_length ++] = 0x80 | (uc_b2 & 0x3F);
}
break;
default:
string_add (b);
};
continue;
}
if (b == '\\')
{
flags |= flag_escaped;
continue;
}
if (b == '"')
{
if (!state.first_pass)
string [string_length] = 0;
flags &= ~ flag_string;
string = 0;
switch (top->type)
{
case json_string:
top->u.string.length = string_length;
flags |= flag_next;
break;
case json_object:
if (state.first_pass)
(*(json_char **) &top->u.object.values) += string_length + 1;
else
{
top->u.object.values [top->u.object.length].name
= (json_char *) top->_reserved.object_mem;
top->u.object.values [top->u.object.length].name_length
= string_length;
(*(json_char **) &top->_reserved.object_mem) += string_length + 1;
}
flags |= flag_seek_value | flag_need_colon;
continue;
default:
break;
};
}
else
{
string_add (b);
continue;
}
}
if (state.settings.settings & json_enable_comments)
{
if (flags & (flag_line_comment | flag_block_comment))
{
if (flags & flag_line_comment)
{
if (b == '\r' || b == '\n' || !b)
{
flags &= ~ flag_line_comment;
-- i; /* so null can be reproc'd */
}
continue;
}
if (flags & flag_block_comment)
{
if (!b)
{ sprintf (error, "%d:%d: Unexpected EOF in block comment", cur_line, e_off);
goto e_failed;
}
if (b == '*' && i < (end - 1) && i [1] == '/')
{
flags &= ~ flag_block_comment;
++ i; /* skip closing sequence */
}
continue;
}
}
else if (b == '/')
{
if (! (flags & (flag_seek_value | flag_done)) && top->type != json_object)
{
sprintf (error, "%d:%d: Comment not allowed here", cur_line, e_off);
goto e_failed;
}
if (++ i == end)
{ sprintf (error, "%d:%d: EOF unexpected", cur_line, e_off);
goto e_failed;
}
switch (b = *i)
{
case '/':
flags |= flag_line_comment;
continue;
case '*':
flags |= flag_block_comment;
continue;
default:
sprintf (error, "%d:%d: Unexpected `%c` in comment opening sequence", cur_line, e_off, b);
goto e_failed;
};
}
}
if (flags & flag_done)
{
if (!b)
break;
switch (b)
{
whitespace:
continue;
default:
sprintf (error, "%d:%d: Trailing garbage: `%c`", cur_line, e_off, b);
goto e_failed;
};
}
if (flags & flag_seek_value)
{
switch (b)
{
whitespace:
continue;
case ']':
if (top->type == json_array)
flags = (flags & ~ (flag_need_comma | flag_seek_value)) | flag_next;
else
{ sprintf (error, "%d:%d: Unexpected ]", cur_line, e_off);
goto e_failed;
}
break;
default:
if (flags & flag_need_comma)
{
if (b == ',')
{ flags &= ~ flag_need_comma;
continue;
}
else
{ sprintf (error, "%d:%d: Expected , before %c", cur_line, e_off, b);
goto e_failed;
}
}
if (flags & flag_need_colon)
{
if (b == ':')
{ flags &= ~ flag_need_colon;
continue;
}
else
{ sprintf (error, "%d:%d: Expected : before %c", cur_line, e_off, b);
goto e_failed;
}
}
flags &= ~ flag_seek_value;
switch (b)
{
case '{':
if (!new_value (&state, &top, &root, &alloc, json_object))
goto e_alloc_failure;
continue;
case '[':
if (!new_value (&state, &top, &root, &alloc, json_array))
goto e_alloc_failure;
flags |= flag_seek_value;
continue;
case '"':
if (!new_value (&state, &top, &root, &alloc, json_string))
goto e_alloc_failure;
flags |= flag_string;
string = top->u.string.ptr;
string_length = 0;
continue;
case 't':
if ((end - i) < 3 || *(++ i) != 'r' || *(++ i) != 'u' || *(++ i) != 'e')
goto e_unknown_value;
if (!new_value (&state, &top, &root, &alloc, json_boolean))
goto e_alloc_failure;
top->u.boolean = 1;
flags |= flag_next;
break;
case 'f':
if ((end - i) < 4 || *(++ i) != 'a' || *(++ i) != 'l' || *(++ i) != 's' || *(++ i) != 'e')
goto e_unknown_value;
if (!new_value (&state, &top, &root, &alloc, json_boolean))
goto e_alloc_failure;
flags |= flag_next;
break;
case 'n':
if ((end - i) < 3 || *(++ i) != 'u' || *(++ i) != 'l' || *(++ i) != 'l')
goto e_unknown_value;
if (!new_value (&state, &top, &root, &alloc, json_null))
goto e_alloc_failure;
flags |= flag_next;
break;
default:
if (isdigit (b) || b == '-')
{
if (!new_value (&state, &top, &root, &alloc, json_integer))
goto e_alloc_failure;
if (!state.first_pass)
{
while (isdigit (b) || b == '+' || b == '-'
|| b == 'e' || b == 'E' || b == '.')
{
if ( (++ i) == end)
{
b = 0;
break;
}
b = *i;
}
flags |= flag_next | flag_reproc;
break;
}
flags &= ~ (flag_num_negative | flag_num_e |
flag_num_e_got_sign | flag_num_e_negative |
flag_num_zero);
num_digits = 0;
num_fraction = 0;
num_e = 0;
if (b != '-')
{
flags |= flag_reproc;
break;
}
flags |= flag_num_negative;
continue;
}
else
{ sprintf (error, "%d:%d: Unexpected %c when seeking value", cur_line, e_off, b);
goto e_failed;
}
};
};
}
else
{
switch (top->type)
{
case json_object:
switch (b)
{
whitespace:
continue;
case '"':
if (flags & flag_need_comma)
{
sprintf (error, "%d:%d: Expected , before \"", cur_line, e_off);
goto e_failed;
}
flags |= flag_string;
string = (json_char *) top->_reserved.object_mem;
string_length = 0;
break;
case '}':
flags = (flags & ~ flag_need_comma) | flag_next;
break;
case ',':
if (flags & flag_need_comma)
{
flags &= ~ flag_need_comma;
break;
}
default:
sprintf (error, "%d:%d: Unexpected `%c` in object", cur_line, e_off, b);
goto e_failed;
};
break;
case json_integer:
case json_double:
if (isdigit (b))
{
++ num_digits;
if (top->type == json_integer || flags & flag_num_e)
{
if (! (flags & flag_num_e))
{
if (flags & flag_num_zero)
{ sprintf (error, "%d:%d: Unexpected `0` before `%c`", cur_line, e_off, b);
goto e_failed;
}
if (num_digits == 1 && b == '0')
flags |= flag_num_zero;
}
else
{
flags |= flag_num_e_got_sign;
num_e = (num_e * 10) + (b - '0');
continue;
}
top->u.integer = (top->u.integer * 10) + (b - '0');
continue;
}
num_fraction = (num_fraction * 10) + (b - '0');
continue;
}
if (b == '+' || b == '-')
{
if ( (flags & flag_num_e) && !(flags & flag_num_e_got_sign))
{
flags |= flag_num_e_got_sign;
if (b == '-')
flags |= flag_num_e_negative;
continue;
}
}
else if (b == '.' && top->type == json_integer)
{
if (!num_digits)
{ sprintf (error, "%d:%d: Expected digit before `.`", cur_line, e_off);
goto e_failed;
}
top->type = json_double;
top->u.dbl = (double) top->u.integer;
num_digits = 0;
continue;
}
if (! (flags & flag_num_e))
{
if (top->type == json_double)
{
if (!num_digits)
{ sprintf (error, "%d:%d: Expected digit after `.`", cur_line, e_off);
goto e_failed;
}
top->u.dbl += ((double) num_fraction) / (pow (10, (double) num_digits));
}
if (b == 'e' || b == 'E')
{
flags |= flag_num_e;
if (top->type == json_integer)
{
top->type = json_double;
top->u.dbl = (double) top->u.integer;
}
num_digits = 0;
flags &= ~ flag_num_zero;
continue;
}
}
else
{
if (!num_digits)
{ sprintf (error, "%d:%d: Expected digit after `e`", cur_line, e_off);
goto e_failed;
}
top->u.dbl *= pow (10, (double) (flags & flag_num_e_negative ? - num_e : num_e));
}
if (flags & flag_num_negative)
{
if (top->type == json_integer)
top->u.integer = - top->u.integer;
else
top->u.dbl = - top->u.dbl;
}
flags |= flag_next | flag_reproc;
break;
default:
break;
};
}
if (flags & flag_reproc)
{
flags &= ~ flag_reproc;
-- i;
}
if (flags & flag_next)
{
flags = (flags & ~ flag_next) | flag_need_comma;
if (!top->parent)
{
/* root value done */
flags |= flag_done;
continue;
}
if (top->parent->type == json_array)
flags |= flag_seek_value;
if (!state.first_pass)
{
json_value * parent = top->parent;
switch (parent->type)
{
case json_object:
parent->u.object.values
[parent->u.object.length].value = top;
break;
case json_array:
parent->u.array.values
[parent->u.array.length] = top;
break;
default:
break;
};
}
if ( (++ top->parent->u.array.length) > state.uint_max)
goto e_overflow;
top = top->parent;
continue;
}
}
alloc = root;
}
return root;
e_unknown_value:
sprintf (error, "%d:%d: Unknown value", cur_line, e_off);
goto e_failed;
e_alloc_failure:
strcpy (error, "Memory allocation failure");
goto e_failed;
e_overflow:
sprintf (error, "%d:%d: Too long (caught overflow)", cur_line, e_off);
goto e_failed;
e_failed:
if (error_buf)
{
if (*error)
strcpy (error_buf, error);
else
strcpy (error_buf, "Unknown error");
}
if (state.first_pass)
alloc = root;
while (alloc)
{
top = alloc->_reserved.next_alloc;
state.settings.mem_free (alloc, state.settings.user_data);
alloc = top;
}
if (!state.first_pass)
json_value_free_ex (&state.settings, root);
return 0;
}
json_value * json_parse (const json_char * json, size_t length)
{
json_settings settings = { 0 };
return json_parse_ex (&settings, json, length, 0);
}
void json_value_free_ex (json_settings * settings, json_value * value)
{
json_value * cur_value;
if (!value)
return;
value->parent = 0;
while (value)
{
switch (value->type)
{
case json_array:
if (!value->u.array.length)
{
settings->mem_free (value->u.array.values, settings->user_data);
break;
}
value = value->u.array.values [-- value->u.array.length];
continue;
case json_object:
if (!value->u.object.length)
{
settings->mem_free (value->u.object.values, settings->user_data);
break;
}
value = value->u.object.values [-- value->u.object.length].value;
continue;
case json_string:
settings->mem_free (value->u.string.ptr, settings->user_data);
break;
default:
break;
};
cur_value = value;
value = value->parent;
settings->mem_free (cur_value, settings->user_data);
}
}
void json_value_free (json_value * value)
{
json_settings settings = { 0 };
settings.mem_free = default_free;
json_value_free_ex (&settings, value);
}
-488
View File
@@ -1,488 +0,0 @@
#define __STDC_FORMAT_MACROS
#include <inttypes.h>
#include <time.h>
#include <detect.h>
#include <system.h>
#include <argparse.h>
#include <json.h>
#include "server.h"
#include "main.h"
#if defined(CONF_FAMILY_UNIX)
#include <signal.h>
#endif
#ifndef PRId64
#define PRId64 "I64d"
#endif
static volatile int gs_Running = 1;
static volatile int gs_ReloadConfig = 0;
static void ExitFunc(int Signal)
{
printf("[EXIT] Caught signal %d\n", Signal);
gs_Running = 0;
}
static void ReloadFunc(int Signal)
{
printf("[RELOAD] Caught signal %d\n", Signal);
gs_ReloadConfig = 1;
}
CConfig::CConfig()
{
// Initialize to default values
m_Verbose = false; // -v, --verbose
str_copy(m_aConfigFile, "config.json", sizeof(m_aConfigFile)); // -c, --config
str_copy(m_aWebDir, "../web/", sizeof(m_aJSONFile)); // -d, --web-dir
str_copy(m_aTemplateFile, "template.html", sizeof(m_aTemplateFile));
str_copy(m_aJSONFile, "json/stats.json", sizeof(m_aJSONFile));
str_copy(m_aBindAddr, "", sizeof(m_aBindAddr)); // -b, --bind
m_Port = 35601; // -p, --port
}
CMain::CMain(CConfig Config) : m_Config(Config)
{
mem_zero(m_aClients, sizeof(m_aClients));
for(int i = 0; i < NET_MAX_CLIENTS; i++)
m_aClients[i].m_ClientNetID = -1;
}
CMain::CClient *CMain::ClientNet(int ClientNetID)
{
if(ClientNetID < 0 || ClientNetID >= NET_MAX_CLIENTS)
return 0;
for(int i = 0; i < NET_MAX_CLIENTS; i++)
{
if(Client(i)->m_ClientNetID == ClientNetID)
return Client(i);
}
return 0;
}
int CMain::ClientNetToClient(int ClientNetID)
{
if(ClientNetID < 0 || ClientNetID >= NET_MAX_CLIENTS)
return -1;
for(int i = 0; i < NET_MAX_CLIENTS; i++)
{
if(Client(i)->m_ClientNetID == ClientNetID)
return i;
}
return -1;
}
void CMain::OnNewClient(int ClientNetID, int ClientID)
{
dbg_msg("main", "OnNewClient(ncid=%d, cid=%d)", ClientNetID, ClientID);
Client(ClientID)->m_ClientNetID = ClientNetID;
Client(ClientID)->m_ClientNetType = m_Server.Network()->ClientAddr(ClientNetID)->type;
Client(ClientID)->m_TimeConnected = time_get();
Client(ClientID)->m_Connected = true;
if(Client(ClientID)->m_ClientNetType == NETTYPE_IPV4)
Client(ClientID)->m_Stats.m_Online4 = true;
else if(Client(ClientID)->m_ClientNetType == NETTYPE_IPV6)
Client(ClientID)->m_Stats.m_Online6 = true;
}
void CMain::OnDelClient(int ClientNetID)
{
int ClientID = ClientNetToClient(ClientNetID);
dbg_msg("main", "OnDelClient(ncid=%d, cid=%d)", ClientNetID, ClientID);
if(ClientID >= 0 && ClientID < NET_MAX_CLIENTS)
{
Client(ClientID)->m_Connected = false;
Client(ClientID)->m_ClientNetID = -1;
Client(ClientID)->m_ClientNetType = NETTYPE_INVALID;
mem_zero(&Client(ClientID)->m_Stats, sizeof(CClient::CStats));
}
}
int CMain::HandleMessage(int ClientNetID, char *pMessage)
{
CClient *pClient = ClientNet(ClientNetID);
if(!pClient)
return true;
if(str_comp_num(pMessage, "update", sizeof("update")-1) == 0)
{
char *pData = str_skip_whitespaces(&pMessage[sizeof("update")-1]);
// parse json data
json_settings JsonSettings;
mem_zero(&JsonSettings, sizeof(JsonSettings));
char aError[256];
json_value *pJsonData = json_parse_ex(&JsonSettings, pData, strlen(pData), aError);
if(!pJsonData)
{
dbg_msg("main", "JSON Error: %s", aError);
if(pClient->m_Stats.m_Pong)
m_Server.Network()->Send(ClientNetID, "1");
return 1;
}
// extract data
const json_value &rStart = (*pJsonData);
if(rStart["uptime"].type)
pClient->m_Stats.m_Uptime = rStart["uptime"].u.integer;
if(rStart["load_1"].type)
pClient->m_Stats.m_Load_1 = rStart["load_1"].u.dbl;
if(rStart["load_5"].type)
pClient->m_Stats.m_Load_5 = rStart["load_5"].u.dbl;
if(rStart["load_15"].type)
pClient->m_Stats.m_Load_15 = rStart["load_15"].u.dbl;
if(rStart["ping_10010"].type)
pClient->m_Stats.m_ping_10010 = rStart["ping_10010"].u.dbl;
if(rStart["ping_189"].type)
pClient->m_Stats.m_ping_189 = rStart["ping_189"].u.dbl;
if(rStart["ping_10086"].type)
pClient->m_Stats.m_ping_10086 = rStart["ping_10086"].u.dbl;
if(rStart["time_10010"].type)
pClient->m_Stats.m_time_10010 = rStart["time_10010"].u.integer;
if(rStart["time_189"].type)
pClient->m_Stats.m_time_189 = rStart["time_189"].u.integer;
if(rStart["time_10086"].type)
pClient->m_Stats.m_time_10086 = rStart["time_10086"].u.integer;
if(rStart["tcp"].type)
pClient->m_Stats.m_tcpCount = rStart["tcp"].u.integer;
if(rStart["udp"].type)
pClient->m_Stats.m_udpCount = rStart["udp"].u.integer;
if(rStart["process"].type)
pClient->m_Stats.m_processCount = rStart["process"].u.integer;
if(rStart["thread"].type)
pClient->m_Stats.m_threadCount = rStart["thread"].u.integer;
if(rStart["network_rx"].type)
pClient->m_Stats.m_NetworkRx = rStart["network_rx"].u.integer;
if(rStart["network_tx"].type)
pClient->m_Stats.m_NetworkTx = rStart["network_tx"].u.integer;
if(rStart["network_in"].type)
pClient->m_Stats.m_NetworkIN = rStart["network_in"].u.integer;
if(rStart["network_out"].type)
pClient->m_Stats.m_NetworkOUT = rStart["network_out"].u.integer;
if(rStart["memory_total"].type)
pClient->m_Stats.m_MemTotal = rStart["memory_total"].u.integer;
if(rStart["memory_used"].type)
pClient->m_Stats.m_MemUsed = rStart["memory_used"].u.integer;
if(rStart["swap_total"].type)
pClient->m_Stats.m_SwapTotal = rStart["swap_total"].u.integer;
if(rStart["swap_used"].type)
pClient->m_Stats.m_SwapUsed = rStart["swap_used"].u.integer;
if(rStart["hdd_total"].type)
pClient->m_Stats.m_HDDTotal = rStart["hdd_total"].u.integer;
if(rStart["hdd_used"].type)
pClient->m_Stats.m_HDDUsed = rStart["hdd_used"].u.integer;
if(rStart["cpu"].type)
pClient->m_Stats.m_CPU = rStart["cpu"].u.dbl;
if(rStart["online4"].type && pClient->m_ClientNetType == NETTYPE_IPV6)
pClient->m_Stats.m_Online4 = rStart["online4"].u.boolean;
if(rStart["online6"].type && pClient->m_ClientNetType == NETTYPE_IPV4)
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)
str_copy(pClient->m_Stats.m_aCustom, rStart["custom"].u.string.ptr, sizeof(pClient->m_Stats.m_aCustom));
if(m_Config.m_Verbose)
{
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",
rStart["online4"].u.boolean ? "true" : "false",
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);
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",
rStart["online6"].u.boolean ? "true" : "false",
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);
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",
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);
}
// clean up
json_value_free(pJsonData);
if(pClient->m_Stats.m_Pong)
m_Server.Network()->Send(ClientNetID, "0");
return 0;
}
else if(str_comp_num(pMessage, "pong", sizeof("pong")-1) == 0)
{
char *pData = str_skip_whitespaces(&pMessage[sizeof("pong")-1]);
if(!str_comp(pData, "0") || !str_comp(pData, "off"))
pClient->m_Stats.m_Pong = false;
else if(!str_comp(pData, "1") || !str_comp(pData, "on"))
pClient->m_Stats.m_Pong = true;
return 0;
}
if(pClient->m_Stats.m_Pong)
m_Server.Network()->Send(ClientNetID, "1");
return 1;
}
void CMain::JSONUpdateThread(void *pUser)
{
CJSONUpdateThreadData *m_pJSONUpdateThreadData = (CJSONUpdateThreadData *)pUser;
CClient *pClients = m_pJSONUpdateThreadData->pClients;
CConfig *pConfig = m_pJSONUpdateThreadData->pConfig;
while(gs_Running)
{
char aFileBuf[2048*NET_MAX_CLIENTS];
char *pBuf = aFileBuf;
str_format(pBuf, sizeof(aFileBuf), "{\n\"servers\": [\n");
pBuf += strlen(pBuf);
for(int i = 0; i < NET_MAX_CLIENTS; i++)
{
if(!pClients[i].m_Active || pClients[i].m_Disabled)
continue;
if(pClients[i].m_Connected)
{
// Uptime
char aUptime[16];
int Days = pClients[i].m_Stats.m_Uptime/60.0/60.0/24.0;
if(Days > 0)
{
if(Days > 1)
str_format(aUptime, sizeof(aUptime), "%d 天", Days);
else
str_format(aUptime, sizeof(aUptime), "%d 天", Days);
}
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(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",
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",
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);
pBuf += strlen(pBuf);
}
else
{
str_format(pBuf, sizeof(aFileBuf) - (pBuf - aFileBuf), "{ \"name\": \"%s\", \"type\": \"%s\", \"host\": \"%s\", \"location\": \"%s\", \"online4\": false, \"online6\": false },\n",
pClients[i].m_aName, pClients[i].m_aType, pClients[i].m_aHost, pClients[i].m_aLocation);
pBuf += strlen(pBuf);
}
}
if(!m_pJSONUpdateThreadData->m_ReloadRequired)
str_format(pBuf - 2, sizeof(aFileBuf) - (pBuf - aFileBuf), "\n],\n\"updated\": \"%lld\"\n}", (long long)time(/*ago*/0));
else
{
str_format(pBuf - 2, sizeof(aFileBuf) - (pBuf - aFileBuf), "\n],\n\"updated\": \"%lld\",\n\"reload\": true\n}", (long long)time(/*ago*/0));
m_pJSONUpdateThreadData->m_ReloadRequired--;
}
pBuf += strlen(pBuf);
char aJSONFileTmp[1024];
str_format(aJSONFileTmp, sizeof(aJSONFileTmp), "%s~", pConfig->m_aJSONFile);
IOHANDLE File = io_open(aJSONFileTmp, IOFLAG_WRITE);
if(!File)
{
dbg_msg("main", "Couldn't open %s", aJSONFileTmp);
exit(1);
}
io_write(File, aFileBuf, (pBuf - aFileBuf));
io_flush(File);
io_close(File);
fs_rename(aJSONFileTmp, pConfig->m_aJSONFile);
thread_sleep(1000);
}
fs_remove(pConfig->m_aJSONFile);
}
int CMain::ReadConfig()
{
// read and parse config
IOHANDLE File = io_open(m_Config.m_aConfigFile, IOFLAG_READ);
if(!File)
{
dbg_msg("main", "Couldn't open %s", m_Config.m_aConfigFile);
return 1;
}
int FileSize = (int)io_length(File);
char *pFileData = (char *)mem_alloc(FileSize + 1, 1);
io_read(File, pFileData, FileSize);
pFileData[FileSize] = 0;
io_close(File);
// parse json data
json_settings JsonSettings;
mem_zero(&JsonSettings, sizeof(JsonSettings));
char aError[256];
json_value *pJsonData = json_parse_ex(&JsonSettings, pFileData, strlen(pFileData), aError);
if(!pJsonData)
{
dbg_msg("main", "JSON Error in file %s: %s", m_Config.m_aConfigFile, aError);
mem_free(pFileData);
return 1;
}
// reset clients
for(int i = 0; i < NET_MAX_CLIENTS; i++)
{
if(!Client(i)->m_Active || !Client(i)->m_Connected)
continue;
m_Server.Network()->Drop(Client(i)->m_ClientNetID, "Server reloading...");
}
mem_zero(m_aClients, sizeof(m_aClients));
for(int i = 0; i < NET_MAX_CLIENTS; i++)
m_aClients[i].m_ClientNetID = -1;
// extract data
int ID = 0;
const json_value &rStart = (*pJsonData)["servers"];
if(rStart.type == json_array)
{
for(unsigned i = 0; i < rStart.u.array.length; i++)
{
if(ID < 0 || ID >= NET_MAX_CLIENTS)
continue;
Client(ID)->m_Active = true;
Client(ID)->m_Disabled = rStart[i]["disabled"].u.boolean;
str_copy(Client(ID)->m_aName, rStart[i]["name"].u.string.ptr, sizeof(Client(ID)->m_aName));
str_copy(Client(ID)->m_aUsername, rStart[i]["username"].u.string.ptr, sizeof(Client(ID)->m_aUsername));
str_copy(Client(ID)->m_aType, rStart[i]["type"].u.string.ptr, sizeof(Client(ID)->m_aType));
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_aPassword, rStart[i]["password"].u.string.ptr, sizeof(Client(ID)->m_aPassword));
if(m_Config.m_Verbose)
{
if(Client(ID)->m_Disabled)
dbg_msg("main", "[#%d: Name: \"%s\", Username: \"%s\", Type: \"%s\", Host: \"%s\", Location: \"%s\", Password: \"%s\"]",
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);
else
dbg_msg("main", "#%d: Name: \"%s\", Username: \"%s\", Type: \"%s\", Host: \"%s\", Location: \"%s\", Password: \"%s\"",
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++;
}
}
// clean up
json_value_free(pJsonData);
mem_free(pFileData);
// tell clients to reload the page
m_JSONUpdateThreadData.m_ReloadRequired = 2;
return 0;
}
int CMain::Run()
{
if(m_Server.Init(this, m_Config.m_aBindAddr, m_Config.m_Port))
return 1;
if(ReadConfig())
return 1;
// Start JSON Update Thread
m_JSONUpdateThreadData.m_ReloadRequired = 2;
m_JSONUpdateThreadData.pClients = m_aClients;
m_JSONUpdateThreadData.pConfig = &m_Config;
void *LoadThread = thread_create(JSONUpdateThread, &m_JSONUpdateThreadData);
//thread_detach(LoadThread);
while(gs_Running)
{
if(gs_ReloadConfig)
{
if(ReadConfig())
return 1;
m_Server.NetBan()->UnbanAll();
gs_ReloadConfig = 0;
}
m_Server.Update();
// wait for incomming data
net_socket_read_wait(*m_Server.Network()->Socket(), 10);
}
dbg_msg("server", "Closing.");
m_Server.Network()->Close();
thread_wait(LoadThread);
return 0;
}
int main(int argc, const char *argv[])
{
int RetVal;
dbg_logger_stdout();
#if defined(CONF_FAMILY_UNIX)
signal(SIGINT, ExitFunc);
signal(SIGTERM, ExitFunc);
signal(SIGQUIT, ExitFunc);
signal(SIGHUP, ReloadFunc);
#endif
char aUsage[128];
CConfig Config;
str_format(aUsage, sizeof(aUsage), "%s [options]", argv[0]);
const char *pConfigFile = 0;
const char *pWebDir = 0;
const char *pBindAddr = 0;
struct argparse_option aOptions[] = {
OPT_HELP(),
OPT_BOOLEAN('v', "verbose", &Config.m_Verbose, "Verbose output", 0),
OPT_STRING('c', "config", &pConfigFile, "Config file to use", 0),
OPT_STRING('d', "web-dir", &pWebDir, "Location of the web directory", 0),
OPT_STRING('b', "bind", &pBindAddr, "Bind to address", 0),
OPT_INTEGER('p', "port", &Config.m_Port, "Listen on port", 0),
OPT_END(),
};
struct argparse Argparse;
argparse_init(&Argparse, aOptions, aUsage, 0);
argc = argparse_parse(&Argparse, argc, argv);
if(pConfigFile)
str_copy(Config.m_aConfigFile, pConfigFile, sizeof(Config.m_aConfigFile));
if(pWebDir)
str_copy(Config.m_aWebDir, pWebDir, sizeof(Config.m_aWebDir));
if(pBindAddr)
str_copy(Config.m_aBindAddr, pBindAddr, sizeof(Config.m_aBindAddr));
if(Config.m_aWebDir[strlen(Config.m_aWebDir)-1] != '/')
str_append(Config.m_aWebDir, "/", sizeof(Config.m_aWebDir));
if(!fs_is_dir(Config.m_aWebDir))
{
dbg_msg("main", "ERROR: Can't find web directory: %s", Config.m_aWebDir);
return 1;
}
char aTmp[1024];
str_format(aTmp, sizeof(aTmp), "%s%s", Config.m_aWebDir, Config.m_aJSONFile);
str_copy(Config.m_aJSONFile, aTmp, sizeof(Config.m_aJSONFile));
CMain Main(Config);
RetVal = Main.Run();
return RetVal;
}
-103
View File
@@ -1,103 +0,0 @@
#ifndef MAIN_H
#define MAIN_H
#include <stdint.h>
#include "server.h"
class CConfig
{
public:
bool m_Verbose;
char m_aConfigFile[1024];
char m_aWebDir[1024];
char m_aTemplateFile[1024];
char m_aJSONFile[1024];
char m_aBindAddr[256];
int m_Port;
CConfig();
};
class CMain
{
CConfig m_Config;
CServer m_Server;
struct CClient
{
bool m_Active;
bool m_Disabled;
bool m_Connected;
int m_ClientNetID;
int m_ClientNetType;
char m_aUsername[128];
char m_aName[128];
char m_aType[128];
char m_aHost[128];
char m_aLocation[128];
char m_aPassword[128];
int64 m_TimeConnected;
int64 m_LastUpdate;
struct CStats
{
bool m_Online4;
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
int64_t m_Uptime;
double m_Load_1;
double m_Load_5;
double m_Load_15;
double m_ping_10010;
double m_ping_189;
double m_ping_10086;
int64_t m_time_10010;
int64_t m_time_189;
int64_t m_time_10086;
int64_t m_NetworkRx;
int64_t m_NetworkTx;
int64_t m_NetworkIN;
int64_t m_NetworkOUT;
int64_t m_MemTotal;
int64_t m_MemUsed;
int64_t m_SwapTotal;
int64_t m_SwapUsed;
int64_t m_HDDTotal;
int64_t m_HDDUsed;
int64_t m_tcpCount;
int64_t m_udpCount;
int64_t m_processCount;
int64_t m_threadCount;
double m_CPU;
char m_aCustom[512];
// Options
bool m_Pong;
} m_Stats;
} m_aClients[NET_MAX_CLIENTS];
struct CJSONUpdateThreadData
{
CClient *pClients;
CConfig *pConfig;
volatile short m_ReloadRequired;
} m_JSONUpdateThreadData;
static void JSONUpdateThread(void *pUser);
public:
CMain(CConfig Config);
void OnNewClient(int ClienNettID, int ClientID);
void OnDelClient(int ClientNetID);
int HandleMessage(int ClientNetID, char *pMessage);
int ReadConfig();
int Run();
CClient *Client(int ClientID) { return &m_aClients[ClientID]; }
CClient *ClientNet(int ClientNetID);
const CConfig *Config() const { return &m_Config; }
int ClientNetToClient(int ClientNetID);
};
#endif
-463
View File
@@ -1,463 +0,0 @@
#include <math.h>
#include "netban.h"
bool CNetBan::StrAllnum(const char *pStr)
{
while(*pStr)
{
if(!(*pStr >= '0' && *pStr <= '9'))
return false;
pStr++;
}
return true;
}
CNetBan::CNetHash::CNetHash(const NETADDR *pAddr)
{
if(pAddr->type==NETTYPE_IPV4)
m_Hash = (pAddr->ip[0]+pAddr->ip[1]+pAddr->ip[2]+pAddr->ip[3])&0xFF;
else
m_Hash = (pAddr->ip[0]+pAddr->ip[1]+pAddr->ip[2]+pAddr->ip[3]+pAddr->ip[4]+pAddr->ip[5]+pAddr->ip[6]+pAddr->ip[7]+
pAddr->ip[8]+pAddr->ip[9]+pAddr->ip[10]+pAddr->ip[11]+pAddr->ip[12]+pAddr->ip[13]+pAddr->ip[14]+pAddr->ip[15])&0xFF;
m_HashIndex = 0;
}
CNetBan::CNetHash::CNetHash(const CNetRange *pRange)
{
m_Hash = 0;
m_HashIndex = 0;
for(int i = 0; pRange->m_LB.ip[i] == pRange->m_UB.ip[i]; ++i)
{
m_Hash += pRange->m_LB.ip[i];
++m_HashIndex;
}
m_Hash &= 0xFF;
}
int CNetBan::CNetHash::MakeHashArray(const NETADDR *pAddr, CNetHash aHash[17])
{
int Length = pAddr->type==NETTYPE_IPV4 ? 4 : 16;
aHash[0].m_Hash = 0;
aHash[0].m_HashIndex = 0;
for(int i = 1, Sum = 0; i <= Length; ++i)
{
Sum += pAddr->ip[i-1];
aHash[i].m_Hash = Sum&0xFF;
aHash[i].m_HashIndex = i%Length;
}
return Length;
}
template<class T, int HashCount>
typename CNetBan::CBan<T> *CNetBan::CBanPool<T, HashCount>::Add(const T *pData, const CBanInfo *pInfo, const CNetHash *pNetHash)
{
if(!m_pFirstFree)
return 0;
// create new ban
CBan<T> *pBan = m_pFirstFree;
pBan->m_Data = *pData;
pBan->m_Info = *pInfo;
pBan->m_NetHash = *pNetHash;
if(pBan->m_pNext)
pBan->m_pNext->m_pPrev = pBan->m_pPrev;
if(pBan->m_pPrev)
pBan->m_pPrev->m_pNext = pBan->m_pNext;
else
m_pFirstFree = pBan->m_pNext;
// add it to the hash list
if(m_paaHashList[pNetHash->m_HashIndex][pNetHash->m_Hash])
m_paaHashList[pNetHash->m_HashIndex][pNetHash->m_Hash]->m_pHashPrev = pBan;
pBan->m_pHashPrev = 0;
pBan->m_pHashNext = m_paaHashList[pNetHash->m_HashIndex][pNetHash->m_Hash];
m_paaHashList[pNetHash->m_HashIndex][pNetHash->m_Hash] = pBan;
// insert it into the used list
if(m_pFirstUsed)
{
for(CBan<T> *p = m_pFirstUsed; ; p = p->m_pNext)
{
if(p->m_Info.m_Expires == CBanInfo::EXPIRES_NEVER || (pInfo->m_Expires != CBanInfo::EXPIRES_NEVER && pInfo->m_Expires <= p->m_Info.m_Expires))
{
// insert before
pBan->m_pNext = p;
pBan->m_pPrev = p->m_pPrev;
if(p->m_pPrev)
p->m_pPrev->m_pNext = pBan;
else
m_pFirstUsed = pBan;
p->m_pPrev = pBan;
break;
}
if(!p->m_pNext)
{
// last entry
p->m_pNext = pBan;
pBan->m_pPrev = p;
pBan->m_pNext = 0;
break;
}
}
}
else
{
m_pFirstUsed = pBan;
pBan->m_pNext = pBan->m_pPrev = 0;
}
// update ban count
++m_CountUsed;
return pBan;
}
template<class T, int HashCount>
int CNetBan::CBanPool<T, HashCount>::Remove(CBan<T> *pBan)
{
if(pBan == 0)
return -1;
// remove from hash list
if(pBan->m_pHashNext)
pBan->m_pHashNext->m_pHashPrev = pBan->m_pHashPrev;
if(pBan->m_pHashPrev)
pBan->m_pHashPrev->m_pHashNext = pBan->m_pHashNext;
else
m_paaHashList[pBan->m_NetHash.m_HashIndex][pBan->m_NetHash.m_Hash] = pBan->m_pHashNext;
pBan->m_pHashNext = pBan->m_pHashPrev = 0;
// remove from used list
if(pBan->m_pNext)
pBan->m_pNext->m_pPrev = pBan->m_pPrev;
if(pBan->m_pPrev)
pBan->m_pPrev->m_pNext = pBan->m_pNext;
else
m_pFirstUsed = pBan->m_pNext;
// add to recycle list
if(m_pFirstFree)
m_pFirstFree->m_pPrev = pBan;
pBan->m_pPrev = 0;
pBan->m_pNext = m_pFirstFree;
m_pFirstFree = pBan;
// update ban count
--m_CountUsed;
return 0;
}
template<class T, int HashCount>
void CNetBan::CBanPool<T, HashCount>::Update(CBan<CDataType> *pBan, const CBanInfo *pInfo)
{
pBan->m_Info = *pInfo;
// remove from used list
if(pBan->m_pNext)
pBan->m_pNext->m_pPrev = pBan->m_pPrev;
if(pBan->m_pPrev)
pBan->m_pPrev->m_pNext = pBan->m_pNext;
else
m_pFirstUsed = pBan->m_pNext;
// insert it into the used list
if(m_pFirstUsed)
{
for(CBan<T> *p = m_pFirstUsed; ; p = p->m_pNext)
{
if(p->m_Info.m_Expires == CBanInfo::EXPIRES_NEVER || (pInfo->m_Expires != CBanInfo::EXPIRES_NEVER && pInfo->m_Expires <= p->m_Info.m_Expires))
{
// insert before
pBan->m_pNext = p;
pBan->m_pPrev = p->m_pPrev;
if(p->m_pPrev)
p->m_pPrev->m_pNext = pBan;
else
m_pFirstUsed = pBan;
p->m_pPrev = pBan;
break;
}
if(!p->m_pNext)
{
// last entry
p->m_pNext = pBan;
pBan->m_pPrev = p;
pBan->m_pNext = 0;
break;
}
}
}
else
{
m_pFirstUsed = pBan;
pBan->m_pNext = pBan->m_pPrev = 0;
}
}
template<class T, int HashCount>
void CNetBan::CBanPool<T, HashCount>::Reset()
{
mem_zero(m_paaHashList, sizeof(m_paaHashList));
mem_zero(m_aBans, sizeof(m_aBans));
m_pFirstUsed = 0;
m_CountUsed = 0;
for(int i = 1; i < MAX_BANS-1; ++i)
{
m_aBans[i].m_pNext = &m_aBans[i+1];
m_aBans[i].m_pPrev = &m_aBans[i-1];
}
m_aBans[0].m_pNext = &m_aBans[1];
m_aBans[MAX_BANS-1].m_pPrev = &m_aBans[MAX_BANS-2];
m_pFirstFree = &m_aBans[0];
}
template<class T, int HashCount>
typename CNetBan::CBan<T> *CNetBan::CBanPool<T, HashCount>::Get(int Index) const
{
if(Index < 0 || Index >= Num())
return 0;
for(CNetBan::CBan<T> *pBan = m_pFirstUsed; pBan; pBan = pBan->m_pNext, --Index)
{
if(Index == 0)
return pBan;
}
return 0;
}
template<class T>
void CNetBan::MakeBanInfo(const CBan<T> *pBan, char *pBuf, unsigned BuffSize, int Type) const
{
if(pBan == 0 || pBuf == 0)
{
if(BuffSize > 0)
pBuf[0] = 0;
return;
}
// build type based part
char aBuf[256];
if(Type == MSGTYPE_PLAYER)
str_copy(aBuf, "You have been banned", sizeof(aBuf));
else
{
char aTemp[256];
switch(Type)
{
case MSGTYPE_LIST:
str_format(aBuf, sizeof(aBuf), "%s banned", NetToString(&pBan->m_Data, aTemp, sizeof(aTemp))); break;
case MSGTYPE_BANADD:
str_format(aBuf, sizeof(aBuf), "banned %s", NetToString(&pBan->m_Data, aTemp, sizeof(aTemp))); break;
case MSGTYPE_BANREM:
str_format(aBuf, sizeof(aBuf), "unbanned %s", NetToString(&pBan->m_Data, aTemp, sizeof(aTemp))); break;
default:
aBuf[0] = 0;
}
}
// add info part
if(pBan->m_Info.m_Expires != CBanInfo::EXPIRES_NEVER)
{
int Mins = ((pBan->m_Info.m_Expires-time_timestamp()) + 59) / 60;
if(Mins <= 1)
str_format(pBuf, BuffSize, "%s for 1 minute (%s)", aBuf, pBan->m_Info.m_aReason);
else
str_format(pBuf, BuffSize, "%s for %d minutes (%s)", aBuf, Mins, pBan->m_Info.m_aReason);
}
else
str_format(pBuf, BuffSize, "%s for life (%s)", aBuf, pBan->m_Info.m_aReason);
}
template<class T>
int CNetBan::Ban(T *pBanPool, const typename T::CDataType *pData, int Seconds, const char *pReason)
{
// do not ban localhost
if(NetMatch(pData, &m_LocalhostIPV4) || NetMatch(pData, &m_LocalhostIPV6))
{
dbg_msg("net_ban", "ban failed (localhost)");
return -1;
}
int Stamp = Seconds > 0 ? time_timestamp()+Seconds : CBanInfo::EXPIRES_NEVER;
// set up info
CBanInfo Info = {0};
Info.m_Expires = Stamp;
str_copy(Info.m_aReason, pReason, sizeof(Info.m_aReason));
// check if it already exists
CNetHash NetHash(pData);
CBan<typename T::CDataType> *pBan = pBanPool->Find(pData, &NetHash);
if(pBan)
{
// adjust the ban
pBanPool->Update(pBan, &Info);
char aBuf[128];
MakeBanInfo(pBan, aBuf, sizeof(aBuf), MSGTYPE_LIST);
dbg_msg("net_ban", aBuf);
return 1;
}
// add ban and print result
pBan = pBanPool->Add(pData, &Info, &NetHash);
if(pBan)
{
char aBuf[128];
MakeBanInfo(pBan, aBuf, sizeof(aBuf), MSGTYPE_BANADD);
dbg_msg("net_ban", aBuf);
return 0;
}
else
dbg_msg("net_ban", "ban failed (full banlist)");
return -1;
}
template<class T>
int CNetBan::Unban(T *pBanPool, const typename T::CDataType *pData)
{
CNetHash NetHash(pData);
CBan<typename T::CDataType> *pBan = pBanPool->Find(pData, &NetHash);
if(pBan)
{
char aBuf[256];
MakeBanInfo(pBan, aBuf, sizeof(aBuf), MSGTYPE_BANREM);
pBanPool->Remove(pBan);
dbg_msg("net_ban", aBuf);
return 0;
}
else
dbg_msg("net_ban", "unban failed (invalid entry)");
return -1;
}
void CNetBan::Init()
{
m_BanAddrPool.Reset();
m_BanRangePool.Reset();
net_host_lookup("localhost", &m_LocalhostIPV4, NETTYPE_IPV4);
net_host_lookup("localhost", &m_LocalhostIPV6, NETTYPE_IPV6);
}
void CNetBan::Update()
{
int Now = time_timestamp();
// remove expired bans
char aBuf[256], aNetStr[256];
while(m_BanAddrPool.First() && m_BanAddrPool.First()->m_Info.m_Expires != CBanInfo::EXPIRES_NEVER && m_BanAddrPool.First()->m_Info.m_Expires < Now)
{
str_format(aBuf, sizeof(aBuf), "ban %s expired", NetToString(&m_BanAddrPool.First()->m_Data, aNetStr, sizeof(aNetStr)));
dbg_msg("net_ban", aBuf);
m_BanAddrPool.Remove(m_BanAddrPool.First());
}
while(m_BanRangePool.First() && m_BanRangePool.First()->m_Info.m_Expires != CBanInfo::EXPIRES_NEVER && m_BanRangePool.First()->m_Info.m_Expires < Now)
{
str_format(aBuf, sizeof(aBuf), "ban %s expired", NetToString(&m_BanRangePool.First()->m_Data, aNetStr, sizeof(aNetStr)));
dbg_msg("net_ban", aBuf);
m_BanRangePool.Remove(m_BanRangePool.First());
}
}
int CNetBan::BanAddr(const NETADDR *pAddr, int Seconds, const char *pReason)
{
return Ban(&m_BanAddrPool, pAddr, Seconds, pReason);
}
int CNetBan::BanRange(const CNetRange *pRange, int Seconds, const char *pReason)
{
if(pRange->IsValid())
return Ban(&m_BanRangePool, pRange, Seconds, pReason);
dbg_msg("net_ban", "ban failed (invalid range)");
return -1;
}
int CNetBan::UnbanByAddr(const NETADDR *pAddr)
{
return Unban(&m_BanAddrPool, pAddr);
}
int CNetBan::UnbanByRange(const CNetRange *pRange)
{
if(pRange->IsValid())
return Unban(&m_BanRangePool, pRange);
dbg_msg("net_ban", "ban failed (invalid range)");
return -1;
}
int CNetBan::UnbanByIndex(int Index)
{
int Result;
char aBuf[256];
CBanAddr *pBan = m_BanAddrPool.Get(Index);
if(pBan)
{
NetToString(&pBan->m_Data, aBuf, sizeof(aBuf));
Result = m_BanAddrPool.Remove(pBan);
}
else
{
CBanRange *pBan = m_BanRangePool.Get(Index-m_BanAddrPool.Num());
if(pBan)
{
NetToString(&pBan->m_Data, aBuf, sizeof(aBuf));
Result = m_BanRangePool.Remove(pBan);
}
else
{
dbg_msg("net_ban", "unban failed (invalid index)");
return -1;
}
}
char aMsg[256];
str_format(aMsg, sizeof(aMsg), "unbanned index %i (%s)", Index, aBuf);
dbg_msg("net_ban", aMsg);
return Result;
}
void CNetBan::UnbanAll()
{
m_BanAddrPool.Reset();
m_BanRangePool.Reset();
}
bool CNetBan::IsBanned(const NETADDR *pAddr, char *pBuf, unsigned BufferSize) const
{
CNetHash aHash[17];
int Length = CNetHash::MakeHashArray(pAddr, aHash);
// check ban adresses
CBanAddr *pBan = m_BanAddrPool.Find(pAddr, &aHash[Length]);
if(pBan)
{
MakeBanInfo(pBan, pBuf, BufferSize, MSGTYPE_PLAYER);
return true;
}
// check ban ranges
for(int i = Length-1; i >= 0; --i)
{
for(CBanRange *pBan = m_BanRangePool.First(&aHash[i]); pBan; pBan = pBan->m_pHashNext)
{
if(NetMatch(&pBan->m_Data, pAddr, i, Length))
{
MakeBanInfo(pBan, pBuf, BufferSize, MSGTYPE_PLAYER);
return true;
}
}
}
return false;
}
-179
View File
@@ -1,179 +0,0 @@
#ifndef NETBAN_H
#define NETBAN_H
#include <system.h>
inline int NetComp(const NETADDR *pAddr1, const NETADDR *pAddr2)
{
return mem_comp(pAddr1, pAddr2, pAddr1->type==NETTYPE_IPV4 ? 8 : 20);
}
class CNetRange
{
public:
NETADDR m_LB;
NETADDR m_UB;
bool IsValid() const { return m_LB.type == m_UB.type && NetComp(&m_LB, &m_UB) < 0; }
};
inline int NetComp(const CNetRange *pRange1, const CNetRange *pRange2)
{
return NetComp(&pRange1->m_LB, &pRange2->m_LB) || NetComp(&pRange1->m_UB, &pRange2->m_UB);
}
class CNetBan
{
protected:
bool NetMatch(const NETADDR *pAddr1, const NETADDR *pAddr2) const
{
return NetComp(pAddr1, pAddr2) == 0;
}
bool NetMatch(const CNetRange *pRange, const NETADDR *pAddr, int Start, int Length) const
{
return pRange->m_LB.type == pAddr->type && (Start == 0 || mem_comp(&pRange->m_LB.ip[0], &pAddr->ip[0], Start) == 0) &&
mem_comp(&pRange->m_LB.ip[Start], &pAddr->ip[Start], Length-Start) <= 0 && mem_comp(&pRange->m_UB.ip[Start], &pAddr->ip[Start], Length-Start) >= 0;
}
bool NetMatch(const CNetRange *pRange, const NETADDR *pAddr) const
{
return NetMatch(pRange, pAddr, 0, pRange->m_LB.type==NETTYPE_IPV4 ? 4 : 16);
}
const char *NetToString(const NETADDR *pData, char *pBuffer, unsigned BufferSize) const
{
char aAddrStr[NETADDR_MAXSTRSIZE];
net_addr_str(pData, aAddrStr, sizeof(aAddrStr), false);
str_format(pBuffer, BufferSize, "'%s'", aAddrStr);
return pBuffer;
}
const char *NetToString(const CNetRange *pData, char *pBuffer, unsigned BufferSize) const
{
char aAddrStr1[NETADDR_MAXSTRSIZE], aAddrStr2[NETADDR_MAXSTRSIZE];
net_addr_str(&pData->m_LB, aAddrStr1, sizeof(aAddrStr1), false);
net_addr_str(&pData->m_UB, aAddrStr2, sizeof(aAddrStr2), false);
str_format(pBuffer, BufferSize, "'%s' - '%s'", aAddrStr1, aAddrStr2);
return pBuffer;
}
// todo: move?
static bool StrAllnum(const char *pStr);
class CNetHash
{
public:
int m_Hash;
int m_HashIndex; // matching parts for ranges, 0 for addr
CNetHash() {}
CNetHash(const NETADDR *pAddr);
CNetHash(const CNetRange *pRange);
static int MakeHashArray(const NETADDR *pAddr, CNetHash aHash[17]);
};
struct CBanInfo
{
enum
{
EXPIRES_NEVER=-1,
REASON_LENGTH=64,
};
int m_Expires;
char m_aReason[REASON_LENGTH];
};
template<class T> struct CBan
{
T m_Data;
CBanInfo m_Info;
CNetHash m_NetHash;
// hash list
CBan *m_pHashNext;
CBan *m_pHashPrev;
// used or free list
CBan *m_pNext;
CBan *m_pPrev;
};
template<class T, int HashCount> class CBanPool
{
public:
typedef T CDataType;
CBan<CDataType> *Add(const CDataType *pData, const CBanInfo *pInfo, const CNetHash *pNetHash);
int Remove(CBan<CDataType> *pBan);
void Update(CBan<CDataType> *pBan, const CBanInfo *pInfo);
void Reset();
int Num() const { return m_CountUsed; }
bool IsFull() const { return m_CountUsed == MAX_BANS; }
CBan<CDataType> *First() const { return m_pFirstUsed; }
CBan<CDataType> *First(const CNetHash *pNetHash) const { return m_paaHashList[pNetHash->m_HashIndex][pNetHash->m_Hash]; }
CBan<CDataType> *Find(const CDataType *pData, const CNetHash *pNetHash) const
{
for(CBan<CDataType> *pBan = m_paaHashList[pNetHash->m_HashIndex][pNetHash->m_Hash]; pBan; pBan = pBan->m_pHashNext)
{
if(NetComp(&pBan->m_Data, pData) == 0)
return pBan;
}
return 0;
}
CBan<CDataType> *Get(int Index) const;
private:
enum
{
MAX_BANS=1024,
};
CBan<CDataType> *m_paaHashList[HashCount][256];
CBan<CDataType> m_aBans[MAX_BANS];
CBan<CDataType> *m_pFirstFree;
CBan<CDataType> *m_pFirstUsed;
int m_CountUsed;
};
typedef CBanPool<NETADDR, 1> CBanAddrPool;
typedef CBanPool<CNetRange, 16> CBanRangePool;
typedef CBan<NETADDR> CBanAddr;
typedef CBan<CNetRange> CBanRange;
template<class T> void MakeBanInfo(const CBan<T> *pBan, char *pBuf, unsigned BuffSize, int Type) const;
template<class T> int Ban(T *pBanPool, const typename T::CDataType *pData, int Seconds, const char *pReason);
template<class T> int Unban(T *pBanPool, const typename T::CDataType *pData);
CBanAddrPool m_BanAddrPool;
CBanRangePool m_BanRangePool;
NETADDR m_LocalhostIPV4, m_LocalhostIPV6;
public:
enum
{
MSGTYPE_PLAYER=0,
MSGTYPE_LIST,
MSGTYPE_BANADD,
MSGTYPE_BANREM,
};
virtual ~CNetBan() {}
void Init();
void Update();
virtual int BanAddr(const NETADDR *pAddr, int Seconds, const char *pReason);
virtual int BanRange(const CNetRange *pRange, int Seconds, const char *pReason);
int UnbanByAddr(const NETADDR *pAddr);
int UnbanByRange(const CNetRange *pRange);
int UnbanByIndex(int Index);
void UnbanAll();
bool IsBanned(const NETADDR *pAddr, char *pBuf, unsigned BufferSize) const;
};
#endif
-144
View File
@@ -1,144 +0,0 @@
#include <system.h>
#include "netban.h"
#include "network.h"
bool CNetwork::Open(NETADDR BindAddr, CNetBan *pNetBan)
{
// zero out the whole structure
mem_zero(this, sizeof(*this));
m_Socket.type = NETTYPE_INVALID;
m_Socket.ipv4sock = -1;
m_Socket.ipv6sock = -1;
m_pNetBan = pNetBan;
// open socket
m_Socket = net_tcp_create(BindAddr);
if(!m_Socket.type)
return false;
if(net_tcp_listen(m_Socket, NET_MAX_CLIENTS))
return false;
net_set_non_blocking(m_Socket);
for(int i = 0; i < NET_MAX_CLIENTS; i++)
m_aSlots[i].m_Connection.Reset();
return true;
}
void CNetwork::SetCallbacks(NETFUNC_NEWCLIENT pfnNewClient, NETFUNC_DELCLIENT pfnDelClient, void *pUser)
{
m_pfnNewClient = pfnNewClient;
m_pfnDelClient = pfnDelClient;
m_UserPtr = pUser;
}
int CNetwork::Close()
{
for(int i = 0; i < NET_MAX_CLIENTS; i++)
m_aSlots[i].m_Connection.Disconnect("Closing connection.");
net_tcp_close(m_Socket);
return 0;
}
int CNetwork::Drop(int ClientID, const char *pReason)
{
if(m_pfnDelClient)
m_pfnDelClient(ClientID, pReason, m_UserPtr);
m_aSlots[ClientID].m_Connection.Disconnect(pReason);
return 0;
}
int CNetwork::AcceptClient(NETSOCKET Socket, const NETADDR *pAddr)
{
char aError[256] = { 0 };
int FreeSlot = -1;
// look for free slot or multiple client
for(int i = 0; i < NET_MAX_CLIENTS; i++)
{
if(FreeSlot == -1 && m_aSlots[i].m_Connection.State() == NET_CONNSTATE_OFFLINE)
FreeSlot = i;
if(m_aSlots[i].m_Connection.State() != NET_CONNSTATE_OFFLINE)
{
if(net_addr_comp(pAddr, m_aSlots[i].m_Connection.PeerAddress()) == 0)
{
str_copy(aError, "Only one client per IP allowed.", sizeof(aError));
break;
}
}
}
// accept client
if(!aError[0] && FreeSlot != -1)
{
m_aSlots[FreeSlot].m_Connection.Init(Socket, pAddr);
if(m_pfnNewClient)
m_pfnNewClient(FreeSlot, m_UserPtr);
return 0;
}
// reject client
if(!aError[0])
str_copy(aError, "No free slot available.", sizeof(aError));
net_tcp_send(Socket, aError, str_length(aError));
net_tcp_close(Socket);
return -1;
}
int CNetwork::Update()
{
NETSOCKET Socket;
NETADDR Addr;
if(net_tcp_accept(m_Socket, &Socket, &Addr) > 0)
{
// check if we should just drop the packet
char aBuf[128];
if(NetBan() && NetBan()->IsBanned(&Addr, aBuf, sizeof(aBuf)))
{
// banned, reply with a message and drop
net_tcp_send(Socket, aBuf, str_length(aBuf));
net_tcp_close(Socket);
}
else
AcceptClient(Socket, &Addr);
}
for(int i = 0; i < NET_MAX_CLIENTS; i++)
{
if(m_aSlots[i].m_Connection.State() == NET_CONNSTATE_ONLINE)
m_aSlots[i].m_Connection.Update();
if(m_aSlots[i].m_Connection.State() == NET_CONNSTATE_ERROR)
Drop(i, m_aSlots[i].m_Connection.ErrorString());
}
return 0;
}
int CNetwork::Recv(char *pLine, int MaxLength, int *pClientID)
{
for(int i = 0; i < NET_MAX_CLIENTS; i++)
{
if(m_aSlots[i].m_Connection.State() == NET_CONNSTATE_ONLINE && m_aSlots[i].m_Connection.Recv(pLine, MaxLength))
{
if(pClientID)
*pClientID = i;
return 1;
}
}
return 0;
}
int CNetwork::Send(int ClientID, const char *pLine)
{
if(m_aSlots[ClientID].m_Connection.State() == NET_CONNSTATE_ONLINE)
return m_aSlots[ClientID].m_Connection.Send(pLine);
else
return -1;
}
-87
View File
@@ -1,87 +0,0 @@
#ifndef NETWORK_H
#define NETWORK_H
enum
{
NET_CONNSTATE_OFFLINE=0,
NET_CONNSTATE_CONNECT=1,
NET_CONNSTATE_PENDING=2,
NET_CONNSTATE_ONLINE=3,
NET_CONNSTATE_ERROR=4,
NET_MAX_PACKETSIZE = 1400,
NET_MAX_CLIENTS = 128
};
typedef int (*NETFUNC_DELCLIENT)(int ClientID, const char* pReason, void *pUser);
typedef int (*NETFUNC_NEWCLIENT)(int ClientID, void *pUser);
class CNetworkClient
{
private:
int m_State;
NETADDR m_PeerAddr;
NETSOCKET m_Socket;
char m_aBuffer[NET_MAX_PACKETSIZE];
int m_BufferOffset;
char m_aErrorString[256];
bool m_LineEndingDetected;
char m_aLineEnding[3];
public:
void Init(NETSOCKET Socket, const NETADDR *pAddr);
void Disconnect(const char *pReason);
int State() const { return m_State; }
const NETADDR *PeerAddress() const { return &m_PeerAddr; }
const char *ErrorString() const { return m_aErrorString; }
void Reset();
int Update();
int Send(const char *pLine);
int Recv(char *pLine, int MaxLength);
};
class CNetwork
{
private:
struct CSlot
{
CNetworkClient m_Connection;
};
NETSOCKET m_Socket;
class CNetBan *m_pNetBan;
CSlot m_aSlots[NET_MAX_CLIENTS];
NETFUNC_NEWCLIENT m_pfnNewClient;
NETFUNC_DELCLIENT m_pfnDelClient;
void *m_UserPtr;
public:
void SetCallbacks(NETFUNC_NEWCLIENT pfnNewClient, NETFUNC_DELCLIENT pfnDelClient, void *pUser);
//
bool Open(NETADDR BindAddr, CNetBan *pNetBan);
int Close();
//
int Recv(char *pLine, int MaxLength, int *pClientID = 0);
int Send(int ClientID, const char *pLine);
int Update();
//
int AcceptClient(NETSOCKET Socket, const NETADDR *pAddr);
int Drop(int ClientID, const char *pReason);
// status requests
const NETADDR *ClientAddr(int ClientID) const { return m_aSlots[ClientID].m_Connection.PeerAddress(); }
const NETSOCKET *Socket() const { return &m_Socket; }
class CNetBan *NetBan() const { return m_pNetBan; }
};
#endif
-184
View File
@@ -1,184 +0,0 @@
#include <system.h>
#include "network.h"
void CNetworkClient::Reset()
{
m_State = NET_CONNSTATE_OFFLINE;
mem_zero(&m_PeerAddr, sizeof(m_PeerAddr));
m_aErrorString[0] = 0;
m_Socket.type = NETTYPE_INVALID;
m_Socket.ipv4sock = -1;
m_Socket.ipv6sock = -1;
m_aBuffer[0] = 0;
m_BufferOffset = 0;
m_LineEndingDetected = false;
#if defined(CONF_FAMILY_WINDOWS)
m_aLineEnding[0] = '\r';
m_aLineEnding[1] = '\n';
m_aLineEnding[2] = 0;
#else
m_aLineEnding[0] = '\n';
m_aLineEnding[1] = 0;
m_aLineEnding[2] = 0;
#endif
}
void CNetworkClient::Init(NETSOCKET Socket, const NETADDR *pAddr)
{
Reset();
m_Socket = Socket;
net_set_non_blocking(m_Socket);
m_PeerAddr = *pAddr;
m_State = NET_CONNSTATE_ONLINE;
}
void CNetworkClient::Disconnect(const char *pReason)
{
if(State() == NET_CONNSTATE_OFFLINE)
return;
if(pReason && pReason[0])
Send(pReason);
net_tcp_close(m_Socket);
Reset();
}
int CNetworkClient::Update()
{
if(State() == NET_CONNSTATE_ONLINE)
{
if((int)(sizeof(m_aBuffer)) <= m_BufferOffset)
{
m_State = NET_CONNSTATE_ERROR;
str_copy(m_aErrorString, "too weak connection (out of buffer)", sizeof(m_aErrorString));
return -1;
}
int Bytes = net_tcp_recv(m_Socket, m_aBuffer+m_BufferOffset, (int)(sizeof(m_aBuffer))-m_BufferOffset);
if(Bytes > 0)
{
m_BufferOffset += Bytes;
}
else if(Bytes < 0)
{
if(net_would_block()) // no data received
return 0;
m_State = NET_CONNSTATE_ERROR; // error
str_copy(m_aErrorString, "connection failure", sizeof(m_aErrorString));
return -1;
}
else
{
m_State = NET_CONNSTATE_ERROR;
str_copy(m_aErrorString, "remote end closed the connection", sizeof(m_aErrorString));
return -1;
}
}
return 0;
}
int CNetworkClient::Recv(char *pLine, int MaxLength)
{
if(State() == NET_CONNSTATE_ONLINE)
{
if(m_BufferOffset)
{
// find message start
int StartOffset = 0;
while(m_aBuffer[StartOffset] == '\r' || m_aBuffer[StartOffset] == '\n')
{
// detect clients line ending format
if(!m_LineEndingDetected)
{
m_aLineEnding[0] = m_aBuffer[StartOffset];
if(StartOffset+1 < m_BufferOffset && (m_aBuffer[StartOffset+1] == '\r' || m_aBuffer[StartOffset+1] == '\n') &&
m_aBuffer[StartOffset] != m_aBuffer[StartOffset+1])
m_aLineEnding[1] = m_aBuffer[StartOffset+1];
m_LineEndingDetected = true;
}
if(++StartOffset >= m_BufferOffset)
{
m_BufferOffset = 0;
return 0;
}
}
// find message end
int EndOffset = StartOffset;
while(m_aBuffer[EndOffset] != '\r' && m_aBuffer[EndOffset] != '\n')
{
if(++EndOffset >= m_BufferOffset)
{
if(StartOffset > 0)
{
mem_move(m_aBuffer, m_aBuffer+StartOffset, m_BufferOffset-StartOffset);
m_BufferOffset -= StartOffset;
}
return 0;
}
}
// extract message and update buffer
if(MaxLength-1 < EndOffset-StartOffset)
{
if(StartOffset > 0)
{
mem_move(m_aBuffer, m_aBuffer+StartOffset, m_BufferOffset-StartOffset);
m_BufferOffset -= StartOffset;
}
return 0;
}
mem_copy(pLine, m_aBuffer+StartOffset, EndOffset-StartOffset);
pLine[EndOffset-StartOffset] = 0;
str_sanitize_cc(pLine);
mem_move(m_aBuffer, m_aBuffer+EndOffset, m_BufferOffset-EndOffset);
m_BufferOffset -= EndOffset;
return 1;
}
}
return 0;
}
int CNetworkClient::Send(const char *pLine)
{
if(State() != NET_CONNSTATE_ONLINE)
return -1;
char aBuf[1024];
str_copy(aBuf, pLine, (int)(sizeof(aBuf))-2);
int Length = str_length(aBuf);
aBuf[Length] = m_aLineEnding[0];
aBuf[Length+1] = m_aLineEnding[1];
aBuf[Length+2] = m_aLineEnding[2];
Length += 3;
const char *pData = aBuf;
while(1)
{
int Send = net_tcp_send(m_Socket, pData, Length);
if(Send < 0)
{
m_State = NET_CONNSTATE_ERROR;
str_copy(m_aErrorString, "failed to send packet", sizeof(m_aErrorString));
return -1;
}
if(Send >= Length)
break;
pData += Send;
Length -= Send;
}
return 0;
}
-204
View File
@@ -1,204 +0,0 @@
#include <system.h>
#include "netban.h"
#include "network.h"
#include "main.h"
#include "server.h"
int CServer::NewClientCallback(int ClientID, void *pUser)
{
CServer *pThis = (CServer *)pUser;
char aAddrStr[NETADDR_MAXSTRSIZE];
net_addr_str(pThis->m_Network.ClientAddr(ClientID), aAddrStr, sizeof(aAddrStr), true);
if(pThis->Main()->Config()->m_Verbose)
dbg_msg("server", "Connection accepted. ncid=%d addr=%s'", ClientID, aAddrStr);
pThis->m_aClients[ClientID].m_State = CClient::STATE_CONNECTED;
pThis->m_aClients[ClientID].m_TimeConnected = time_get();
pThis->m_Network.Send(ClientID, "Authentication required:");
return 0;
}
int CServer::DelClientCallback(int ClientID, const char *pReason, void *pUser)
{
CServer *pThis = (CServer *)pUser;
char aAddrStr[NETADDR_MAXSTRSIZE];
net_addr_str(pThis->m_Network.ClientAddr(ClientID), aAddrStr, sizeof(aAddrStr), true);
if(pThis->Main()->Config()->m_Verbose)
dbg_msg("server", "Client dropped. ncid=%d addr=%s reason='%s'", ClientID, aAddrStr, pReason);
if(pThis->m_aClients[ClientID].m_State == CClient::STATE_AUTHED)
pThis->Main()->OnDelClient(ClientID);
pThis->m_aClients[ClientID].m_State = CClient::STATE_EMPTY;
return 0;
}
int CServer::Init(CMain *pMain, const char *Bind, int Port)
{
m_pMain = pMain;
m_NetBan.Init();
for(int i = 0; i < NET_MAX_CLIENTS; i++)
m_aClients[i].m_State = CClient::STATE_EMPTY;
m_Ready = false;
if(Port == 0)
{
dbg_msg("server", "Will not bind to port 0.");
return 1;
}
NETADDR BindAddr;
if(Bind[0] && net_host_lookup(Bind, &BindAddr, NETTYPE_ALL) == 0)
{
// got bindaddr
BindAddr.type = NETTYPE_ALL;
BindAddr.port = Port;
}
else
{
mem_zero(&BindAddr, sizeof(BindAddr));
BindAddr.type = NETTYPE_ALL;
BindAddr.port = Port;
}
if(m_Network.Open(BindAddr, &m_NetBan))
{
m_Network.SetCallbacks(NewClientCallback, DelClientCallback, this);
m_Ready = true;
dbg_msg("server", "Bound to %s:%d", Bind, Port);
return 0;
}
else
dbg_msg("server", "Couldn't open socket. Port (%d) might already be in use.", Port);
return 1;
}
void CServer::Update()
{
if(!m_Ready)
return;
m_NetBan.Update();
m_Network.Update();
char aBuf[NET_MAX_PACKETSIZE];
int ClientID;
while(m_Network.Recv(aBuf, (int)(sizeof(aBuf))-1, &ClientID))
{
dbg_assert(m_aClients[ClientID].m_State != CClient::STATE_EMPTY, "Got message from empty slot.");
if(m_aClients[ClientID].m_State == CClient::STATE_CONNECTED)
{
int ID = -1;
char aUsername[128] = {0};
char aPassword[128] = {0};
const char *pTmp;
if(!(pTmp = str_find(aBuf, ":"))
|| (unsigned)(pTmp - aBuf) > sizeof(aUsername) || (unsigned)(str_length(pTmp) - 1) > sizeof(aPassword))
{
m_Network.NetBan()->BanAddr(m_Network.ClientAddr(ClientID), 60, "You're an idiot, go away.");
m_Network.Drop(ClientID, "Fuck off.");
return;
}
str_copy(aUsername, aBuf, pTmp - aBuf + 1);
str_copy(aPassword, pTmp + 1, sizeof(aPassword));
if(!*aUsername || !*aPassword)
{
m_Network.NetBan()->BanAddr(m_Network.ClientAddr(ClientID), 60, "You're an idiot, go away.");
m_Network.Drop(ClientID, "Username and password must not be blank.");
return;
}
for(int i = 0; i < NET_MAX_CLIENTS; i++)
{
if(!Main()->Client(i)->m_Active)
continue;
if(str_comp(Main()->Client(i)->m_aUsername, aUsername) == 0 && str_comp(Main()->Client(i)->m_aPassword, aPassword) == 0)
ID = i;
}
if(ID == -1)
{
m_Network.NetBan()->BanAddr(m_Network.ClientAddr(ClientID), 60, "Wrong username and/or password.");
m_Network.Drop(ClientID, "Wrong username and/or password.");
}
else if(Main()->Client(ID)->m_ClientNetID != -1)
{
m_Network.Drop(ClientID, "Only one connection per user allowed.");
}
else
{
m_aClients[ClientID].m_State = CClient::STATE_AUTHED;
m_aClients[ClientID].m_LastReceived = time_get();
m_Network.Send(ClientID, "Authentication successful. Access granted.");
if(m_Network.ClientAddr(ClientID)->type == NETTYPE_IPV4)
m_Network.Send(ClientID, "You are connecting via: IPv4");
else if(m_Network.ClientAddr(ClientID)->type == NETTYPE_IPV6)
m_Network.Send(ClientID, "You are connecting via: IPv6");
if(Main()->Config()->m_Verbose)
dbg_msg("server", "ncid=%d authed", ClientID);
Main()->OnNewClient(ClientID, ID);
}
}
else if(m_aClients[ClientID].m_State == CClient::STATE_AUTHED)
{
m_aClients[ClientID].m_LastReceived = time_get();
if(Main()->Config()->m_Verbose)
dbg_msg("server", "ncid=%d cmd='%s'", ClientID, aBuf);
if(str_comp(aBuf, "logout") == 0)
m_Network.Drop(ClientID, "Logout. Bye Bye ~");
else
Main()->HandleMessage(ClientID, aBuf);
}
}
for(int i = 0; i < NET_MAX_CLIENTS; ++i)
{
if(m_aClients[i].m_State == CClient::STATE_CONNECTED &&
time_get() > m_aClients[i].m_TimeConnected + 5 * time_freq())
{
m_Network.NetBan()->BanAddr(m_Network.ClientAddr(i), 30, "Authentication timeout.");
m_Network.Drop(i, "Authentication timeout.");
}
else if(m_aClients[i].m_State == CClient::STATE_AUTHED &&
time_get() > m_aClients[i].m_LastReceived + 15 * time_freq())
m_Network.Drop(i, "Timeout.");
}
}
void CServer::Send(int ClientID, const char *pLine)
{
if(!m_Ready)
return;
if(ClientID == -1)
{
for(int i = 0; i < NET_MAX_CLIENTS; i++)
{
if(m_aClients[i].m_State == CClient::STATE_AUTHED)
m_Network.Send(i, pLine);
}
}
else if(ClientID >= 0 && ClientID < NET_MAX_CLIENTS && m_aClients[ClientID].m_State == CClient::STATE_AUTHED)
m_Network.Send(ClientID, pLine);
}
void CServer::Shutdown()
{
if(!m_Ready)
return;
m_Network.Close();
}
-46
View File
@@ -1,46 +0,0 @@
#ifndef SERVER_H
#define SERVER_H
#include "netban.h"
#include "network.h"
class CServer
{
class CClient
{
public:
enum
{
STATE_EMPTY=0,
STATE_CONNECTED,
STATE_AUTHED,
};
int m_State;
int64 m_TimeConnected;
int64 m_LastReceived;
};
CClient m_aClients[NET_MAX_CLIENTS];
CNetwork m_Network;
CNetBan m_NetBan;
class CMain *m_pMain;
bool m_Ready;
static int NewClientCallback(int ClientID, void *pUser);
static int DelClientCallback(int ClientID, const char *pReason, void *pUser);
public:
int Init(CMain *pMain, const char *Bind, int Port);
void Update();
void Send(int ClientID, const char *pLine);
void Shutdown();
CNetwork *Network() { return &m_Network; }
CNetBan *NetBan() { return &m_NetBan; }
CMain *Main() { return m_pMain; }
};
#endif
-2001
View File
File diff suppressed because it is too large Load Diff
+204
View File
@@ -0,0 +1,204 @@
package main
import (
"crypto/tls"
"fmt"
"net"
"net/url"
"strconv"
"strings"
"time"
)
type CertState struct {
Config SSLCertConfig
ExpireTS int64
Mismatch bool
LastError string
LastCheck time.Time
Checking bool
LastAlarm7 time.Time
LastAlarm3 time.Time
LastAlarm1 time.Time
LastAlarmMismatch time.Time
}
func certKey(config SSLCertConfig) string {
return fmt.Sprintf("%s\x00%s\x00%d", config.Name, config.Domain, config.Port)
}
func (a *App) reconcileCerts(configs []SSLCertConfig) {
a.certMu.Lock()
defer a.certMu.Unlock()
next := make(map[string]*CertState, len(configs))
for _, config := range configs {
key := certKey(config)
if existing := a.certs[key]; existing != nil {
existing.Config = config
next[key] = existing
} else {
next[key] = &CertState{Config: config}
}
}
a.certs = next
}
func (a *App) sslLoop() {
a.runDueSSLChecks()
ticker := time.NewTicker(time.Second)
defer ticker.Stop()
for {
select {
case <-a.ctx.Done():
return
case <-ticker.C:
a.runDueSSLChecks()
}
}
}
func (a *App) runDueSSLChecks() {
now := time.Now()
type dueCheck struct {
key string
config SSLCertConfig
}
due := make([]dueCheck, 0)
a.certMu.Lock()
for key, state := range a.certs {
interval := secondsDuration(state.Config.Interval)
if !state.Checking && (state.LastCheck.IsZero() || now.Sub(state.LastCheck) >= interval) {
state.Checking = true
due = append(due, dueCheck{key: key, config: state.Config})
}
}
a.certMu.Unlock()
for _, check := range due {
check := check
go a.executeSSLCheck(check.key, check.config)
}
}
func (a *App) executeSSLCheck(key string, config SSLCertConfig) {
expireTS, mismatch, err := checkCertificate(config)
now := time.Now()
type notification struct {
message string
}
alerts := make([]notification, 0, 2)
a.certMu.Lock()
state := a.certs[key]
if state == nil {
a.certMu.Unlock()
return
}
state.Checking = false
state.LastCheck = now
if err != nil {
state.LastError = err.Error()
a.certMu.Unlock()
a.wakeStatsWriter()
return
}
state.ExpireTS = expireTS
state.Mismatch = mismatch
state.LastError = ""
if config.Callback != "" && mismatch && (state.LastAlarmMismatch.IsZero() || now.Sub(state.LastAlarmMismatch) >= 24*time.Hour) {
state.LastAlarmMismatch = now
alerts = append(alerts, notification{message: fmt.Sprintf("【SSL证书域名不匹配】%s(%s) 证书域名与配置不一致", config.Name, config.Domain)})
}
days := int((expireTS - now.Unix()) / 86400)
if config.Callback != "" {
var lastAlarm *time.Time
switch {
case days <= 7 && days > 3:
lastAlarm = &state.LastAlarm7
case days <= 3 && days > 1:
lastAlarm = &state.LastAlarm3
case days <= 1:
lastAlarm = &state.LastAlarm1
}
if lastAlarm != nil && (lastAlarm.IsZero() || now.Sub(*lastAlarm) >= 20*time.Hour) {
*lastAlarm = now
expire := time.Unix(expireTS, 0).UTC().Format("2006-01-02 15:04:05")
alerts = append(alerts, notification{message: fmt.Sprintf("【SSL证书提醒】%s(%s) 将在 %d 天后(%s UTC) 到期", config.Name, config.Domain, days, expire)})
}
}
a.certMu.Unlock()
a.wakeStatsWriter()
for _, alert := range alerts {
if err := a.sendCallback(config.Callback, alert.message, "ServerStatusSSL"); err != nil {
a.logger.Printf("SSL certificate %q callback: %v", config.Name, err)
}
}
}
func checkCertificate(config SSLCertConfig) (int64, bool, error) {
host, err := certificateHost(config.Domain)
if err != nil {
return 0, false, err
}
address := net.JoinHostPort(host, strconv.Itoa(config.Port))
dialer := &net.Dialer{Timeout: 6 * time.Second}
connection, err := tls.DialWithDialer(dialer, "tcp", address, &tls.Config{
ServerName: host,
MinVersion: tls.VersionTLS12,
InsecureSkipVerify: true, // VerifyHostname below preserves the legacy chain-independent check.
})
if err != nil {
return 0, false, err
}
defer connection.Close()
certificates := connection.ConnectionState().PeerCertificates
if len(certificates) == 0 {
return 0, false, fmt.Errorf("server returned no certificate")
}
leaf := certificates[0]
mismatch := leaf.VerifyHostname(host) != nil
return leaf.NotAfter.Unix(), mismatch, nil
}
func certificateHost(raw string) (string, error) {
raw = strings.TrimSpace(raw)
if raw == "" {
return "", fmt.Errorf("domain is empty")
}
if parsed, err := url.Parse(raw); err == nil && parsed.Hostname() != "" {
return parsed.Hostname(), nil
}
withoutPath := strings.SplitN(raw, "/", 2)[0]
if host, _, err := net.SplitHostPort(withoutPath); err == nil {
return strings.Trim(host, "[]"), nil
}
host := strings.Trim(withoutPath, "[]")
if host == "" {
return "", fmt.Errorf("domain is invalid")
}
return host, nil
}
func (a *App) sslSnapshot(configs []SSLCertConfig, now time.Time) []any {
a.certMu.RLock()
defer a.certMu.RUnlock()
result := make([]any, 0, len(configs))
for _, config := range configs {
entry := map[string]any{
"name": config.Name, "domain": config.Domain, "port": config.Port,
"expire_ts": int64(0), "expire_days": 0, "mismatch": false,
}
if state := a.certs[certKey(config)]; state != nil {
entry["expire_ts"] = state.ExpireTS
if state.ExpireTS != 0 {
entry["expire_days"] = int((state.ExpireTS - now.Unix()) / 86400)
}
entry["mismatch"] = state.Mismatch
if state.LastError != "" {
entry["error"] = state.LastError
}
}
result = append(result, entry)
}
return result
}
+74
View File
@@ -0,0 +1,74 @@
package main
import (
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"sync/atomic"
"testing"
"time"
)
func TestCertificateCheckAndSnapshot(t *testing.T) {
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(204) }))
defer server.Close()
parsed, err := url.Parse(server.URL)
if err != nil {
t.Fatal(err)
}
port, _ := strconv.Atoi(parsed.Port())
config := SSLCertConfig{Name: "local", Domain: server.URL, Port: port, Interval: 1}
expireTS, _, err := checkCertificate(config)
if err != nil {
t.Fatal(err)
}
if expireTS <= time.Now().Unix() {
t.Fatalf("unexpected expiration %d", expireTS)
}
doc := minimalTestConfig()
doc["sslcerts"] = []any{map[string]any{"name": config.Name, "domain": config.Domain, "port": config.Port, "interval": 1}}
app := newTestApp(t, doc)
app.runDueSSLChecks()
eventually(t, 2*time.Second, func() bool {
app.certMu.RLock()
defer app.certMu.RUnlock()
state := app.certs[certKey(config)]
return state != nil && !state.LastCheck.IsZero() && state.ExpireTS > 0
})
certs := app.SnapshotStats()["sslcerts"].([]any)
if certs[0].(map[string]any)["expire_ts"].(int64) <= time.Now().Unix() {
t.Fatalf("certificate snapshot: %#v", certs[0])
}
}
func TestCallbackDelivery(t *testing.T) {
var calls atomic.Int32
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
calls.Add(1)
if request.Method != http.MethodPost || request.URL.Query().Get("message") == "" {
t.Errorf("unexpected callback request: %s %s", request.Method, request.URL.String())
}
w.WriteHeader(204)
}))
defer server.Close()
doc := minimalTestConfig()
doc["watchdog"] = []any{map[string]any{
"name": "cpu", "rule": "cpu>90", "interval": 60, "callback": server.URL + "?message=",
}}
app := newTestApp(t, doc)
app.nodeMu.Lock()
node := app.nodes["s01"]
node.Connected = true
node.Stats = AgentStats{CPU: 99}
node.HasUpdate = true
app.nodeMu.Unlock()
app.evaluateWatchdogs("s01", false)
eventually(t, time.Second, func() bool { return calls.Load() == 1 })
app.evaluateWatchdogs("s01", false)
time.Sleep(30 * time.Millisecond)
if calls.Load() != 1 {
t.Fatalf("watchdog cooldown failed: calls=%d", calls.Load())
}
}
+247
View File
@@ -0,0 +1,247 @@
package main
import (
"bufio"
"encoding/json"
"errors"
"fmt"
"io"
"net"
"strings"
"time"
)
type AgentServer struct {
app *App
}
func NewAgentServer(app *App) *AgentServer {
return &AgentServer{app: app}
}
func (s *AgentServer) ListenAndServe() error {
listener, err := net.Listen("tcp", s.app.opts.AgentAddr)
if err != nil {
return err
}
s.app.logger.Printf("agent TCP listening on %s", listener.Addr())
return s.Serve(listener)
}
func (s *AgentServer) Serve(listener net.Listener) error {
defer listener.Close()
s.app.agentRunning.Store(true)
defer s.app.agentRunning.Store(false)
go func() {
<-s.app.ctx.Done()
_ = listener.Close()
}()
for {
conn, err := listener.Accept()
if err != nil {
if s.app.ctx.Err() != nil || errors.Is(err, net.ErrClosed) {
return nil
}
return err
}
go s.handleConnection(conn)
}
}
func (s *AgentServer) handleConnection(conn net.Conn) {
defer conn.Close()
if tcpConn, ok := conn.(*net.TCPConn); ok {
_ = tcpConn.SetNoDelay(true)
_ = tcpConn.SetKeepAlive(true)
_ = tcpConn.SetKeepAlivePeriod(10 * time.Second)
}
_ = conn.SetDeadline(time.Now().Add(5 * time.Second))
if _, err := io.WriteString(conn, "Authentication required:\n"); err != nil {
return
}
reader := bufio.NewReaderSize(conn, 64*1024)
credentials, err := reader.ReadString('\n')
if err != nil {
return
}
credentials = strings.TrimSpace(credentials)
parts := strings.SplitN(credentials, ":", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
_, _ = io.WriteString(conn, "Wrong username and/or password.\n")
return
}
family := remoteFamily(conn.RemoteAddr())
connectionID, monitors, apiErr := s.app.connectAgent(parts[0], parts[1], conn, family)
if apiErr != nil {
_, _ = io.WriteString(conn, apiErr.Message+"\n")
return
}
username := parts[0]
defer s.app.disconnectAgent(username, conn, connectionID)
if _, err := io.WriteString(conn, "Authentication successful. Access granted.\n"); err != nil {
return
}
// Existing Python agents use recv() instead of a line reader during the
// handshake. Keep the auth and metadata packets separate for compatibility.
time.Sleep(20 * time.Millisecond)
var metadata strings.Builder
fmt.Fprintf(&metadata, "You are connecting via: IPv%d\n", family)
for index, monitor := range monitors {
payload := map[string]any{"name": monitor.Name, "host": monitor.Host, "interval": monitor.Interval, "type": monitor.Type, "monitor": index}
data, _ := json.Marshal(payload)
metadata.Write(data)
metadata.WriteByte('\n')
}
if _, err := io.WriteString(conn, metadata.String()); err != nil {
return
}
_ = conn.SetDeadline(time.Now().Add(20 * time.Second))
scanner := bufio.NewScanner(reader)
scanner.Buffer(make([]byte, 4096), maxRequestBody)
for scanner.Scan() {
_ = conn.SetDeadline(time.Now().Add(20 * time.Second))
line := strings.TrimSpace(scanner.Text())
switch {
case strings.HasPrefix(line, "update"):
body := strings.TrimSpace(strings.TrimPrefix(line, "update"))
var update AgentStats
if err := json.Unmarshal([]byte(body), &update); err != nil {
if s.app.agentPong(username, connectionID) {
_, _ = io.WriteString(conn, "1\n")
}
continue
}
if !s.app.updateAgent(username, connectionID, update) {
return
}
if s.app.agentPong(username, connectionID) {
_, _ = io.WriteString(conn, "0\n")
}
case strings.HasPrefix(line, "pong"):
value := strings.TrimSpace(strings.TrimPrefix(line, "pong"))
s.app.setAgentPong(username, connectionID, value == "1" || strings.EqualFold(value, "on"))
default:
if s.app.agentPong(username, connectionID) {
_, _ = io.WriteString(conn, "1\n")
}
}
}
}
func remoteFamily(address net.Addr) int {
host, _, err := net.SplitHostPort(address.String())
if err == nil {
if ip := net.ParseIP(host); ip != nil && ip.To4() == nil {
return 6
}
}
return 4
}
func (a *App) connectAgent(username, password string, conn net.Conn, family int) (uint64, []MonitorConfig, *APIError) {
a.configMu.RLock()
defer a.configMu.RUnlock()
var config *ServerConfig
for index := range a.runtime.Servers {
if a.runtime.Servers[index].Username == username {
server := a.runtime.Servers[index]
config = &server
break
}
}
if config == nil || config.Password != password {
return 0, nil, &APIError{Status: 401, Message: "Wrong username and/or password."}
}
if config.Disabled {
return 0, nil, &APIError{Status: 403, Message: "Server is disabled."}
}
a.nodeMu.Lock()
defer a.nodeMu.Unlock()
node := a.nodes[username]
if node == nil {
return 0, nil, &APIError{Status: 404, Message: "Server is not configured."}
}
if node.Connected {
return 0, nil, &APIError{Status: 409, Message: "Only one connection per user allowed."}
}
id := a.connectionID.Add(1)
node.Connected = true
node.Connection = conn
node.ConnectionID = id
node.Family = family
node.HasUpdate = false
node.Pong = false
node.Online4 = family == 4
node.Online6 = family == 6
a.wakeStatsWriter()
return id, append([]MonitorConfig(nil), a.runtime.Monitors...), nil
}
func (a *App) disconnectAgent(username string, conn net.Conn, connectionID uint64) {
a.nodeMu.Lock()
node := a.nodes[username]
if node == nil || node.ConnectionID != connectionID || node.Connection != conn {
a.nodeMu.Unlock()
return
}
node.Connected = false
node.Connection = nil
node.Online4 = false
node.Online6 = false
node.HasUpdate = false
node.Pong = false
a.nodeMu.Unlock()
a.wakeStatsWriter()
time.AfterFunc(25*time.Second, func() {
if a.ctx.Err() != nil {
return
}
a.nodeMu.RLock()
current := a.nodes[username]
stillOffline := current != nil && !current.Connected && current.ConnectionID == connectionID
a.nodeMu.RUnlock()
if stillOffline {
a.evaluateWatchdogs(username, true)
}
})
}
func (a *App) updateAgent(username string, connectionID uint64, update AgentStats) bool {
a.nodeMu.Lock()
node := a.nodes[username]
if node == nil || !node.Connected || node.ConnectionID != connectionID {
a.nodeMu.Unlock()
return false
}
if update.Online4 != nil {
node.Online4 = *update.Online4
}
if update.Online6 != nil {
node.Online6 = *update.Online6
}
node.Stats = update
node.HasUpdate = true
node.LastUpdate = time.Now()
a.nodeMu.Unlock()
a.wakeStatsWriter()
a.evaluateWatchdogs(username, false)
return true
}
func (a *App) setAgentPong(username string, connectionID uint64, enabled bool) {
a.nodeMu.Lock()
defer a.nodeMu.Unlock()
if node := a.nodes[username]; node != nil && node.ConnectionID == connectionID {
node.Pong = enabled
}
}
func (a *App) agentPong(username string, connectionID uint64) bool {
a.nodeMu.RLock()
defer a.nodeMu.RUnlock()
node := a.nodes[username]
return node != nil && node.ConnectionID == connectionID && node.Pong
}
+120
View File
@@ -0,0 +1,120 @@
package main
import (
"bufio"
"encoding/json"
"fmt"
"net"
"strings"
"testing"
"time"
)
func TestAgentProtocolAndTrafficState(t *testing.T) {
app := newTestApp(t, minimalTestConfig())
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
serveDone := make(chan error, 1)
go func() { serveDone <- NewAgentServer(app).Serve(listener) }()
connection, err := net.DialTimeout("tcp", listener.Addr().String(), time.Second)
if err != nil {
t.Fatal(err)
}
defer connection.Close()
reader := bufio.NewReader(connection)
readContains(t, reader, "Authentication required")
if _, err := fmt.Fprintln(connection, "s01:secret"); err != nil {
t.Fatal(err)
}
readContains(t, reader, "Authentication successful")
readContains(t, reader, "You are connecting via: IPv4")
readContains(t, reader, `"monitor":0`)
if _, err := fmt.Fprintln(connection, "pong on"); err != nil {
t.Fatal(err)
}
online6 := true
update := AgentStats{
Uptime: 90061, Load1: 1.25, Load5: 1, Load15: 0.75, CPU: 33.5, CPUCores: 4, CPUModel: "Test CPU",
MemoryTotal: 1024, MemoryUsed: 512, SwapTotal: 128, SwapUsed: 2, HDDTotal: 10000, HDDUsed: 4000,
NetworkRX: 123, NetworkTX: 456, NetworkIn: 1_000_000, NetworkOut: 2_000_000,
Ping10010: 1, Ping189: 2, Ping10086: 3, Time10010: 10, Time189: 20, Time10086: 30,
TCPCount: 10, UDPCount: 2, ProcessCount: 30, ThreadCount: 60, IORead: 1, IOWrite: 2,
OS: "linux", Custom: "example=12", Online6: &online6,
}
payload, _ := json.Marshal(update)
if _, err := fmt.Fprintf(connection, "update %s\n", payload); err != nil {
t.Fatal(err)
}
readContains(t, reader, "0")
eventually(t, time.Second, func() bool {
servers := app.SnapshotStats()["servers"].([]any)
server := servers[0].(map[string]any)
return server["online4"] == true && server["online6"] == true && server["cpu_model"] == "Test CPU" && server["uptime"] == "1 天"
})
result, apiErr := app.ResetTraffic("s01")
if apiErr != nil {
t.Fatal(apiErr)
}
stats := result["stats"].(map[string]any)
if stats["last_network_in"] != int64(1_000_000) {
t.Fatalf("traffic reset result: %#v", stats)
}
duplicate, err := net.DialTimeout("tcp", listener.Addr().String(), time.Second)
if err != nil {
t.Fatal(err)
}
duplicateReader := bufio.NewReader(duplicate)
readContains(t, duplicateReader, "Authentication required")
_, _ = fmt.Fprintln(duplicate, "s01:secret")
readContains(t, duplicateReader, "Only one connection per user")
_ = duplicate.Close()
wrong, err := net.DialTimeout("tcp", listener.Addr().String(), time.Second)
if err != nil {
t.Fatal(err)
}
wrongReader := bufio.NewReader(wrong)
readContains(t, wrongReader, "Authentication required")
_, _ = fmt.Fprintln(wrong, "s01:wrong")
readContains(t, wrongReader, "Wrong username")
_ = wrong.Close()
if apiErr := app.ReloadConfig(); apiErr != nil {
t.Fatal(apiErr)
}
readContains(t, reader, "Server reloading")
eventually(t, time.Second, func() bool {
server := app.SnapshotStats()["servers"].([]any)[0].(map[string]any)
return server["online4"] == false && server["online6"] == false
})
app.cancel()
_ = listener.Close()
select {
case err := <-serveDone:
if err != nil {
t.Fatal(err)
}
case <-time.After(time.Second):
t.Fatal("agent server did not stop")
}
}
func readContains(t *testing.T, reader *bufio.Reader, expected string) string {
t.Helper()
line, err := reader.ReadString('\n')
if err != nil {
t.Fatalf("read %q: %v", expected, err)
}
if !strings.Contains(line, expected) {
t.Fatalf("expected %q in %q", expected, line)
}
return line
}
+87
View File
@@ -0,0 +1,87 @@
package main
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func minimalTestConfig() ConfigDocument {
return ConfigDocument{
"servers": []any{
map[string]any{
"username": "s01", "name": "node1", "type": "kvm", "host": "host1",
"location": "CN", "password": "secret", "monthstart": 1,
},
},
"monitors": []any{
map[string]any{"name": "example", "host": "https://example.com", "interval": 60, "type": "https"},
},
"sslcerts": []any{},
"watchdog": []any{},
}
}
func newTestApp(t *testing.T, doc ConfigDocument) *App {
t.Helper()
directory := t.TempDir()
configPath := filepath.Join(directory, "config.json")
statsPath := filepath.Join(directory, "data", "stats.json")
webDir := filepath.Join(directory, "web")
if err := os.MkdirAll(webDir, 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(webDir, "index.html"), []byte("<!doctype html><title>test-ui</title>"), 0o644); err != nil {
t.Fatal(err)
}
data, err := json.MarshalIndent(doc, "", " ")
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(configPath, data, 0o644); err != nil {
t.Fatal(err)
}
app, err := NewApp(Options{
ConfigPath: configPath,
StatsPath: statsPath,
WebDir: webDir,
HTTPAddr: "127.0.0.1:0",
AgentAddr: "127.0.0.1:0",
AdminToken: "test-token",
})
if err != nil {
t.Fatal(err)
}
t.Cleanup(app.Close)
return app
}
func performRequest(handler http.Handler, method, path, body, token string) *httptest.ResponseRecorder {
request := httptest.NewRequest(method, path, strings.NewReader(body))
if body != "" {
request.Header.Set("Content-Type", "application/json")
}
if token != "" {
request.Header.Set("Authorization", "Bearer "+token)
}
response := httptest.NewRecorder()
handler.ServeHTTP(response, request)
return response
}
func eventually(t *testing.T, timeout time.Duration, condition func() bool) {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if condition() {
return
}
time.Sleep(10 * time.Millisecond)
}
t.Fatal("condition was not met before timeout")
}
+217
View File
@@ -0,0 +1,217 @@
package main
import (
"crypto/tls"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/expr-lang/expr"
)
func compileWatchdog(rule WatchdogConfig, index int) (CompiledWatchdog, error) {
normalized := normalizeLegacyExpression(rule.Rule)
program, err := expr.Compile(normalized, expr.Env(WatchdogEnvironment{}))
if err != nil {
return CompiledWatchdog{}, err
}
return CompiledWatchdog{
WatchdogConfig: rule,
Key: fmt.Sprintf("%d:%s", index, rule.Name),
Normalized: normalized,
Program: program,
}, nil
}
func normalizeLegacyExpression(input string) string {
var output strings.Builder
var quote rune
runes := []rune(input)
for index := 0; index < len(runes); index++ {
current := runes[index]
if quote != 0 {
output.WriteRune(current)
if current == quote && (index == 0 || runes[index-1] != '\\') {
quote = 0
}
continue
}
if current == '\'' || current == '"' {
quote = current
output.WriteRune(current)
continue
}
switch current {
case '&':
output.WriteString("&&")
if index+1 < len(runes) && runes[index+1] == '&' {
index++
}
case '|':
output.WriteString("||")
if index+1 < len(runes) && runes[index+1] == '|' {
index++
}
case '=':
previousOperator := index > 0 && strings.ContainsRune("!<>=", runes[index-1])
nextEqual := index+1 < len(runes) && runes[index+1] == '='
if previousOperator || nextEqual {
output.WriteRune(current)
} else {
output.WriteString("==")
}
default:
output.WriteRune(current)
}
}
return output.String()
}
func (a *App) evaluateWatchdogs(username string, offline bool) {
runtime := a.RuntimeSnapshot()
now := time.Now()
type pendingAlert struct {
rule WatchdogConfig
node ServerConfig
}
pending := make([]pendingAlert, 0)
a.nodeMu.Lock()
node := a.nodes[username]
if node == nil || (!offline && !node.Connected) || (offline && node.Connected) {
a.nodeMu.Unlock()
return
}
stats := node.Stats
if offline {
stats = AgentStats{}
}
environment := watchdogEnvironment(node.Config, stats, node.Online4, node.Online6, node.LastNetworkIn, node.LastNetworkOut)
for _, rule := range runtime.Watchdogs {
result, err := expr.Run(rule.Program, environment)
if err != nil || !expressionTruthy(result) {
continue
}
if last := node.AlarmLast[rule.Key]; !last.IsZero() && now.Sub(last) < secondsDuration(rule.Interval) {
continue
}
node.AlarmLast[rule.Key] = now
if rule.Callback != "" {
pending = append(pending, pendingAlert{rule: rule.WatchdogConfig, node: node.Config})
}
}
a.nodeMu.Unlock()
for _, alert := range pending {
alert := alert
go func() {
message := fmt.Sprintf("【告警名称】 %s \n\n【告警时间】 %s \n\n【用户名】 %s \n\n【节点名】 %s \n\n【虚拟化】 %s \n\n【主机名】 %s \n\n【位 置】 %s",
alert.rule.Name, now.Format("2006-01-02 15:04:05"), alert.node.Username, alert.node.Name, alert.node.Type, alert.node.Host, alert.node.Location)
if err := a.sendCallback(alert.rule.Callback, message, "ServerStatus"); err != nil {
a.logger.Printf("watchdog %q callback: %v", alert.rule.Name, err)
}
}()
}
}
type WatchdogEnvironment struct {
Username string `expr:"username"`
Name string `expr:"name"`
NodeType string `expr:"type"`
Host string `expr:"host"`
Location string `expr:"location"`
Load1 float64 `expr:"load_1"`
Load5 float64 `expr:"load_5"`
Load15 float64 `expr:"load_15"`
Ping10010 float64 `expr:"ping_10010"`
Ping189 float64 `expr:"ping_189"`
Ping10086 float64 `expr:"ping_10086"`
Time10010 float64 `expr:"time_10010"`
Time189 float64 `expr:"time_189"`
Time10086 float64 `expr:"time_10086"`
TCPCount float64 `expr:"tcp_count"`
UDPCount float64 `expr:"udp_count"`
ProcessCount float64 `expr:"process_count"`
ThreadCount float64 `expr:"thread_count"`
NetworkRX float64 `expr:"network_rx"`
NetworkTX float64 `expr:"network_tx"`
NetworkIn float64 `expr:"network_in"`
NetworkOut float64 `expr:"network_out"`
LastNetworkIn float64 `expr:"last_network_in"`
LastNetworkOut float64 `expr:"last_network_out"`
MemoryTotal float64 `expr:"memory_total"`
MemoryUsed float64 `expr:"memory_used"`
SwapTotal float64 `expr:"swap_total"`
SwapUsed float64 `expr:"swap_used"`
HDDTotal float64 `expr:"hdd_total"`
HDDUsed float64 `expr:"hdd_used"`
IORead float64 `expr:"io_read"`
IOWrite float64 `expr:"io_write"`
CPU float64 `expr:"cpu"`
Online4 float64 `expr:"online4"`
Online6 float64 `expr:"online6"`
}
func watchdogEnvironment(config ServerConfig, stats AgentStats, online4, online6 bool, lastNetworkIn, lastNetworkOut int64) WatchdogEnvironment {
boolNumber := func(value bool) float64 {
if value {
return 1
}
return 0
}
return WatchdogEnvironment{
Username: config.Username, Name: config.Name, NodeType: config.Type, Host: config.Host, Location: config.Location,
Load1: stats.Load1, Load5: stats.Load5, Load15: stats.Load15,
Ping10010: stats.Ping10010, Ping189: stats.Ping189, Ping10086: stats.Ping10086,
Time10010: float64(stats.Time10010), Time189: float64(stats.Time189), Time10086: float64(stats.Time10086),
TCPCount: float64(stats.TCPCount), UDPCount: float64(stats.UDPCount),
ProcessCount: float64(stats.ProcessCount), ThreadCount: float64(stats.ThreadCount),
NetworkRX: float64(stats.NetworkRX), NetworkTX: float64(stats.NetworkTX),
NetworkIn: float64(stats.NetworkIn), NetworkOut: float64(stats.NetworkOut),
LastNetworkIn: float64(lastNetworkIn), LastNetworkOut: float64(lastNetworkOut),
MemoryTotal: float64(stats.MemoryTotal), MemoryUsed: float64(stats.MemoryUsed),
SwapTotal: float64(stats.SwapTotal), SwapUsed: float64(stats.SwapUsed),
HDDTotal: float64(stats.HDDTotal), HDDUsed: float64(stats.HDDUsed),
IORead: float64(stats.IORead), IOWrite: float64(stats.IOWrite), CPU: stats.CPU,
Online4: boolNumber(online4), Online6: boolNumber(online6),
}
}
func expressionTruthy(result any) bool {
switch value := result.(type) {
case bool:
return value
case int:
return value != 0
case int64:
return value != 0
case float64:
return value != 0
case string:
parsed, _ := strconv.ParseBool(value)
return parsed
default:
return false
}
}
func (a *App) sendCallback(baseURL, message, signature string) error {
requestURL := baseURL + url.QueryEscape(message)
transport := http.DefaultTransport.(*http.Transport).Clone()
if a.opts.InsecureCallbackTLS {
transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true} //nolint:gosec -- explicit compatibility option
}
client := &http.Client{Timeout: 6 * time.Second, Transport: transport}
response, err := client.Post(requestURL, "application/x-www-form-urlencoded", strings.NewReader("signature="+url.QueryEscape(signature)))
if err != nil {
return err
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return fmt.Errorf("HTTP %s", response.Status)
}
return nil
}
+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
+20
View File
@@ -0,0 +1,20 @@
[Unit]
Description=ServerStatus Go Server
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
WorkingDirectory=/usr/local/ServerStatus/server
EnvironmentFile=-/usr/local/ServerStatus/server/config.conf
ExecStart=/usr/local/ServerStatus/server/serverstatus --config=/usr/local/ServerStatus/server/config.json --web-dir=/usr/local/ServerStatus/web
ExecReload=/bin/kill -HUP $MAINPID
Restart=on-failure
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
ProtectSystem=full
ReadWritePaths=/usr/local/ServerStatus/server /usr/local/ServerStatus/web/json
[Install]
WantedBy=multi-user.target
+1016
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

+191 -97
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" />
ლ(•̀ _ •́ ლ)ლ(•̀ _ •́ ლ)ლ(•̀ _ •́ ლ)
ლ(•̀ _ •́ ლ)ლ(•̀ _ •́ ლ)
ლ(•̀ _ •́ ლ)
by:https://www.cpp.la
-->
<html>
<head>
<title>云监控</title> <title>云监控</title>
<meta charset="utf-8"> <link rel="icon" type="image/svg+xml" href="favicon.svg" />
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <link rel="stylesheet" href="css/app.css?v=20260710-1" />
<meta name="viewport" content="width=device-width, initial-scale=1.0"> </head>
<meta name="description" content="云监控"> <body>
<meta name="author" content="BotoX"> <header class="topbar">
<link rel="stylesheet" href="css/bootstrap.min.css"> <div class="brand" title="云监控">
<link rel="stylesheet" href="css/bootstrap-theme.min.css"> <span class="logo-mark" aria-hidden="true">
<link rel="stylesheet" href="css/dark.css" title="dark"> <svg viewBox="0 0 128 86" width="24" height="18" fill="none" stroke="currentColor" stroke-width="8" stroke-linecap="round" stroke-linejoin="round">
<link rel="stylesheet" href="css/light.css" title="light"> <path d="M4 46H28L38 34L55 82L65 4L74 47H88M100 47H110M122 47H126" />
<style> </svg>
body { </span>
padding-top: 70px; <span class="logo-text"><span class="logo-accent"></span>监控</span>
padding-bottom: 30px;
}
</style>
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!--[if lt IE 9]>
<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>
<nav class="nav" id="navTabs">
<button data-tab="servers" class="active">主机</button>
<button data-tab="monitors">服务</button>
<button data-tab="ssl">证书</button>
<button data-tab="config">配置</button>
</nav>
<div class="actions">
<button id="themeToggle" title="切换主题 (当前: 自动或手动)" aria-label="切换主题">🌓</button>
<span id="lastUpdate" class="muted">更新中...</span>
</div> </div>
</header>
<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;"> <section class="ops-overview" id="overviewCards" aria-label="运行概览"></section>
<strong>Enable JavaScript</strong> , please do it.
<section class="ops-toolbar" id="serversToolbar" aria-label="主机筛选" style="display:none;">
<label class="search-field">
<span>搜索</span>
<input id="serverSearch" type="search" autocomplete="off" placeholder="节点 / 地区 / 系统 / 主机名" />
</label>
<div class="segmented" id="statusFilter" role="group" aria-label="状态筛选">
<button data-filter="all" class="active">全部</button>
<button data-filter="online">在线</button>
<button data-filter="offline">离线</button>
<button data-filter="alert" title="只显示在线但资源、丢包或流量异常的节点">异常</button>
</div> </div>
</noscript> <label class="select-field">
<div style="text-align: center;"> <span>系统</span>
警告:如果出现此消息,请确保您已启用Javascript! <br />否则云监控主服务没启动或已关闭. <select id="osFilter">
</div> <option value="all">全部系统</option>
<p></p> </select>
</div> </label>
<table class="table table-striped table-condensed table-hover"> <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> <thead>
<tr> <tr>
<th id="status4" style="text-align: center;">协议</th> <th data-sort="status">协议</th>
<th id="ipstatus" style="text-align: center;">Flight</th> <th data-sort="traffic">月流量 ↓|↑</th>
<th id="name">节点</th> <th data-sort="name">节点</th>
<th id="type">虚拟化</th> <th data-sort="type">虚拟化</th>
<th id="location">位置</th> <th data-sort="location">位置</th>
<th id="uptime">在线时间</th> <th>在线</th>
<th id="load">负载</th> <th data-sort="load">负载</th>
<th id="network">网络 ↓|↑</th> <th>当前网络 ↓|↑</th>
<th id="traffic">流量 ↓|↑</th> <th>流量 ↓|↑</th>
<th id="cpu">处理器</th> <th data-sort="cpu">CPU</th>
<th id="ram">内存</th> <th data-sort="memory">内存</th>
<th id="hdd">硬盘</th> <th data-sort="hdd">硬盘</th>
<th id="ping">丢包率(CU|CT|CM)</th> <th data-sort="loss" style="text-align:center;">联通|电信|移动</th>
</tr> </tr>
</thead> </thead>
<tbody id="servers"> <tbody id="serversBody"></tbody>
<!-- Servers here \o/ -->
</tbody>
</table> </table>
<br /> </div>
<div id="updated">Updating...</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,并由 Go 服务热重载。</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> </div>
<div class="container"> <footer class="footer">
<p style="text-align: center; font-size: 10px;"> <a href="https://github.com/cppla/ServerStatus" target="_blank" rel="noopener">ServerStatus中文版</a>
<a href="https://github.com/cppla/ServerStatus">ServerStatus中文版</a> </footer>
</p>
</div> <script src="js/app.js?v=20260710-1" defer></script>
<script src="js/jquery.min.js"></script> </body>
<script src="js/bootstrap.min.js"></script>
<script src="js/serverstatus.js"></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)