Skip to content

Commit 5e7a58c

Browse files
committed
chore: complete 2.0.2 maintenance baseline
1 parent 1147f9c commit 5e7a58c

16 files changed

Lines changed: 749 additions & 81 deletions

‎.github/workflows/ci.yml‎

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ jobs:
1515
go-version-file: server/go.mod
1616
cache-dependency-path: server/go.sum
1717

18+
- uses: pnpm/action-setup@v4
19+
with:
20+
version: 11.19.0
21+
22+
- uses: actions/setup-node@v4
23+
with:
24+
node-version: 22
25+
cache: pnpm
26+
1827
- name: Test Go server
1928
working-directory: server
2029
run: |
@@ -26,10 +35,19 @@ jobs:
2635
- name: Check clients, WebUI and shell scripts
2736
run: |
2837
python3 -m py_compile clients/client-linux.py clients/client-psutil.py
29-
python3 -m unittest clients/test_client_args.py
38+
python3 -m unittest discover -s clients -p 'test_*.py'
3039
sh -n clients/entrypoint.sh
40+
sh -n tests/run-webui-server.sh tests/docker-smoke.sh
3141
bash -n status.sh
3242
node --check web/js/app.js
43+
node --check tests/webui/webui.spec.js
44+
node --check playwright.config.js
45+
46+
- name: Test WebUI in Chromium
47+
run: |
48+
pnpm install --frozen-lockfile
49+
pnpm exec playwright install --with-deps chromium
50+
pnpm test:webui
3351
3452
- name: Validate compose files
3553
run: |
@@ -40,3 +58,18 @@ jobs:
4058
run: |
4159
docker build -f Dockerfile.server -t serverstatus-server:test .
4260
docker build -f Dockerfile.client -t serverstatus-client:test .
61+
62+
- name: Test Docker server-client connection
63+
run: tests/docker-smoke.sh
64+
65+
- uses: docker/setup-qemu-action@v3
66+
67+
- uses: docker/setup-buildx-action@v3
68+
69+
- name: Build client image for AMD64 and ARM64
70+
run: |
71+
docker buildx build \
72+
--platform linux/amd64,linux/arm64 \
73+
--file Dockerfile.client \
74+
--output type=cacheonly \
75+
.

‎.gitignore‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
default.sublime-workspace
22

3+
# Browser test dependencies and artifacts
4+
node_modules/
5+
output/
6+
37
# pycharm
48
.idea

‎Dockerfile.client‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM alpine:3.13
1+
FROM alpine:3.22
22

33
WORKDIR /app
44

‎README.md‎

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -304,24 +304,41 @@ Systemd 示例位于 `service/status-server.service`。一键脚本 `status.sh`
304304

305305
## 构建和测试
306306

307+
Go 测试需要 Go `1.25+`;WebUI 行为测试需要 Node.js `20+` 和 pnpm。
308+
307309
```bash
308310
# Go 单元、协议、API、TLS 和回调测试
309311
cd server
310312
go test ./...
311313
go test -race ./...
312314
go vet ./...
313315

314-
# Docker 镜像
316+
# Python 客户端指标、参数与平台识别测试
315317
cd ..
318+
python3 -m unittest discover -s clients -p 'test_*.py'
319+
320+
# WebUI Chromium 行为测试
321+
pnpm install --frozen-lockfile
322+
pnpm exec playwright install chromium
323+
pnpm test:webui
324+
325+
# Docker 镜像
316326
docker build -f Dockerfile.server -t cppla/serverstatus:server .
317327
docker build -f Dockerfile.client -t cppla/serverstatus:client .
318328

329+
# 服务端与客户端真实连接、认证和指标上报
330+
SERVER_IMAGE=cppla/serverstatus:server \
331+
CLIENT_IMAGE=cppla/serverstatus:client \
332+
tests/docker-smoke.sh
333+
319334
# Compose 配置
320335
docker compose -f docker-compose-server.yml config
321336
docker compose -f docker-compose-client.yml config
322337
```
323338

324-
CI 还会检查 Go 格式、Python 客户端、Shell 脚本、WebUI JavaScript、服务端/客户端 Compose 文件和两个 Docker 镜像。
339+
Docker 联通测试在 Linux 上覆盖客户端的 `host` 网络和 `pid` 模式;Docker Desktop 未启用 Host Networking 时会明确提示并使用隔离 bridge 完成本地协议测试。
340+
341+
CI 还会运行 Chromium 行为测试、Docker 服务端/客户端联通测试,并验证客户端镜像可同时构建为 AMD64 和 ARM64。
325342

326343
## 从旧服务端迁移
327344

‎clients/client-linux.py‎

Lines changed: 46 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,34 @@ def get_cpu_model():
208208
return vendor
209209
return normalize_cpu_model(lscpu.get('architecture') or platform.machine() or platform.processor())
210210

211+
def get_os_name():
212+
try:
213+
sysname = platform.system().lower()
214+
if sysname.startswith('linux'):
215+
os_name = 'linux'
216+
try:
217+
with open('/etc/os-release') as f:
218+
for line in f:
219+
if line.startswith('ID='):
220+
value = line.strip().split('=', 1)[1].strip().strip('"')
221+
if value:
222+
os_name = value
223+
break
224+
except Exception:
225+
pass
226+
return os_name
227+
if sysname.startswith('darwin'):
228+
return 'darwin'
229+
if sysname.startswith('freebsd'):
230+
return 'freebsd'
231+
if sysname.startswith('openbsd'):
232+
return 'openbsd'
233+
if sysname.startswith('netbsd'):
234+
return 'netbsd'
235+
return sysname or 'unknown'
236+
except Exception:
237+
return 'unknown'
238+
211239
def liuliang():
212240
NET_IN = 0
213241
NET_OUT = 0
@@ -276,6 +304,22 @@ def get_network(ip_version):
276304
}
277305
monitorServer = {}
278306

307+
def update_net_speed(avgrx, avgtx, now_clock=None):
308+
if now_clock is None:
309+
now_clock = time.monotonic()
310+
previous_clock = netSpeed.get("clock", 0.0)
311+
previous_rx = netSpeed.get("avgrx", 0)
312+
previous_tx = netSpeed.get("avgtx", 0)
313+
diff = now_clock - previous_clock
314+
initialized = previous_clock > 0 and diff > 0
315+
netSpeed["diff"] = diff if initialized else 0.0
316+
netSpeed["clock"] = now_clock
317+
netSpeed["netrx"] = int((avgrx - previous_rx) / diff) if initialized and avgrx >= previous_rx else 0
318+
netSpeed["nettx"] = int((avgtx - previous_tx) / diff) if initialized and avgtx >= previous_tx else 0
319+
netSpeed["avgrx"] = avgrx
320+
netSpeed["avgtx"] = avgtx
321+
return netSpeed["netrx"], netSpeed["nettx"]
322+
279323
def _ping_thread(host, mark, port):
280324
lostPacket = 0
281325
packet_queue = Queue(maxsize=PING_PACKET_HISTORY_LEN)
@@ -330,13 +374,7 @@ def _net_speed():
330374
dev = dev[1].split()
331375
avgrx += int(dev[0])
332376
avgtx += int(dev[8])
333-
now_clock = time.time()
334-
netSpeed["diff"] = now_clock - netSpeed["clock"]
335-
netSpeed["clock"] = now_clock
336-
netSpeed["netrx"] = int((avgrx - netSpeed["avgrx"]) / netSpeed["diff"])
337-
netSpeed["nettx"] = int((avgtx - netSpeed["avgtx"]) / netSpeed["diff"])
338-
netSpeed["avgrx"] = avgrx
339-
netSpeed["avgtx"] = avgtx
377+
update_net_speed(avgrx, avgtx)
340378
time.sleep(INTERVAL)
341379

342380
def _disk_io():
@@ -617,34 +655,7 @@ def byte_str(object):
617655
array['tcp'], array['udp'], array['process'], array['thread'] = tupd()
618656
array['io_read'] = diskIO.get("read")
619657
array['io_write'] = diskIO.get("write")
620-
# report OS (normalized)
621-
try:
622-
sysname = platform.system().lower()
623-
if sysname.startswith('linux'):
624-
os_name = 'linux'
625-
# try distro from os-release
626-
try:
627-
with open('/etc/os-release') as f:
628-
for line in f:
629-
if line.startswith('ID='):
630-
val = line.strip().split('=',1)[1].strip().strip('"')
631-
if val: os_name = val
632-
break
633-
except Exception:
634-
pass
635-
elif sysname.startswith('darwin'):
636-
os_name = 'darwin'
637-
elif sysname.startswith('freebsd'):
638-
os_name = 'freebsd'
639-
elif sysname.startswith('openbsd'):
640-
os_name = 'openbsd'
641-
elif sysname.startswith('netbsd'):
642-
os_name = 'netbsd'
643-
else:
644-
os_name = sysname or 'unknown'
645-
except Exception:
646-
os_name = 'unknown'
647-
array['os'] = os_name
658+
array['os'] = get_os_name()
648659
items = []
649660
for _n, st in monitorServer.items():
650661
key = str(_n)

‎clients/client-psutil.py‎

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,32 @@ def get_cpu_model():
148148
return vendor
149149
return get_platform_cpu_arch()
150150

151+
def get_os_name():
152+
try:
153+
sysname = platform.system().lower()
154+
if sysname.startswith('windows'):
155+
return 'windows'
156+
if sysname.startswith('darwin') or 'mac' in sysname:
157+
return 'darwin'
158+
if 'bsd' in sysname:
159+
return 'bsd'
160+
if sysname.startswith('linux'):
161+
os_name = 'linux'
162+
try:
163+
with open('/etc/os-release') as f:
164+
for line in f:
165+
if line.startswith('ID='):
166+
value = line.strip().split('=', 1)[1].strip().strip('"')
167+
if value:
168+
os_name = value
169+
break
170+
except Exception:
171+
pass
172+
return os_name
173+
return sysname or 'unknown'
174+
except Exception:
175+
return 'unknown'
176+
151177
def _get_net_io_counters():
152178
with _net_io_counters_lock:
153179
return psutil.net_io_counters(pernic=True)
@@ -236,6 +262,22 @@ def get_network(ip_version):
236262
}
237263
monitorServer = {}
238264

265+
def update_net_speed(avgrx, avgtx, now_clock=None):
266+
if now_clock is None:
267+
now_clock = time.monotonic()
268+
previous_clock = netSpeed.get("clock", 0.0)
269+
previous_rx = netSpeed.get("avgrx", 0)
270+
previous_tx = netSpeed.get("avgtx", 0)
271+
diff = now_clock - previous_clock
272+
initialized = previous_clock > 0 and diff > 0
273+
netSpeed["diff"] = diff if initialized else 0.0
274+
netSpeed["clock"] = now_clock
275+
netSpeed["netrx"] = int((avgrx - previous_rx) / diff) if initialized and avgrx >= previous_rx else 0
276+
netSpeed["nettx"] = int((avgtx - previous_tx) / diff) if initialized and avgtx >= previous_tx else 0
277+
netSpeed["avgrx"] = avgrx
278+
netSpeed["avgtx"] = avgtx
279+
return netSpeed["netrx"], netSpeed["nettx"]
280+
239281
def _ping_thread(host, mark, port):
240282
lostPacket = 0
241283
packet_queue = Queue(maxsize=PING_PACKET_HISTORY_LEN)
@@ -286,13 +328,7 @@ def _net_speed():
286328
continue
287329
avgrx += stats.bytes_recv
288330
avgtx += stats.bytes_sent
289-
now_clock = time.time()
290-
netSpeed["diff"] = now_clock - netSpeed["clock"]
291-
netSpeed["clock"] = now_clock
292-
netSpeed["netrx"] = int((avgrx - netSpeed["avgrx"]) / netSpeed["diff"])
293-
netSpeed["nettx"] = int((avgtx - netSpeed["avgtx"]) / netSpeed["diff"])
294-
netSpeed["avgrx"] = avgrx
295-
netSpeed["avgtx"] = avgtx
331+
update_net_speed(avgrx, avgtx)
296332
time.sleep(INTERVAL)
297333

298334
def _disk_io():
@@ -570,31 +606,7 @@ def byte_str(object):
570606
array['tcp'], array['udp'], array['process'], array['thread'] = tupd()
571607
array['io_read'] = diskIO.get("read")
572608
array['io_write'] = diskIO.get("write")
573-
# report OS (normalized)
574-
try:
575-
sysname = platform.system().lower()
576-
if sysname.startswith('windows'):
577-
os_name = 'windows'
578-
elif sysname.startswith('darwin') or 'mac' in sysname:
579-
os_name = 'darwin'
580-
elif 'bsd' in sysname:
581-
os_name = 'bsd'
582-
elif sysname.startswith('linux'):
583-
# try distro from os-release
584-
try:
585-
with open('/etc/os-release') as f:
586-
for line in f:
587-
if line.startswith('ID='):
588-
val = line.strip().split('=',1)[1].strip().strip('"')
589-
if val: os_name = val
590-
break
591-
except Exception:
592-
os_name = 'linux'
593-
else:
594-
os_name = sysname or 'unknown'
595-
except Exception:
596-
os_name = 'unknown'
597-
array['os'] = os_name
609+
array['os'] = get_os_name()
598610
items = []
599611
for _n, st in monitorServer.items():
600612
key = str(_n)

‎clients/test_client_args.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
class ClientArgumentTests(unittest.TestCase):
1313
def test_password_with_user_text_does_not_replace_username(self):
14-
if importlib.util.find_spec("psutil") is None:
14+
if "psutil" not in sys.modules and importlib.util.find_spec("psutil") is None:
1515
sys.modules["psutil"] = types.ModuleType("psutil")
1616

1717
arguments = [

0 commit comments

Comments
 (0)