Repository navigation
Expand file tree
/
Copy pathcheck.sh
More file actions
2360 lines (2209 loc) · 110 KB
/
Copy pathcheck.sh
File metadata and controls
2360 lines (2209 loc) · 110 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env bash
#
# sh.cd — 服务器全面体检: 硬件与性能 · IP 质量 · 网络质量 (CleanIP 出品)
#
# bash <(curl -Ls https://sh.cd)
#
# 在终端里运行会进入菜单, 一键全检按 硬件 → IP → 网络 的顺序一口气测完, 每一项测完立即显示结果, 最后一屏总览。
# IP 情报与评分、BGP 信息来自 CleanIP; 硬件跑分、解锁、邮件端口、三网延迟与回程、测速都在本机实测,
# 测完把结果提交给 sh.cd 排版成报告。
#
# 默认不安装任何软件、不修改系统, 只依赖 bash 与 curl。CPU / 内存跑分与硬盘读写需要 sysbench、fio,
# 系统里没有时先询问, 同意才安装 (-y 直接安装); 不安装则用系统自带工具近似测量。
# 兼容 bash 3.2 (macOS 自带版本): 不用关联数组 / mapfile / ${var,,}; 变量后面紧跟中文或符号时要写 ${var}
# (UTF-8 区域设置下 bash 3.2 会把后面字符的首字节当成变量名, 输出乱码)。
#
# 源码: https://github.com/CleanIP/sh.cd 许可: MIT
VERSION="1.3.4"
API="${SHCD_API:-https://sh.cd}"
UA_BROWSER='Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36'
UA_SELF="sh.cd/$VERSION (+https://sh.cd)"
# ── 参数 ────────────────────────────────────────────────────────────────
LANG_OPT=zh
ONLY_FAMILY=""
PROXY=""
IFACE=""
JSON=0
PRIVATE=0
OPT_NOCOLOR=0
DEEP=0
AUTO_YES=0
FULL=0
GEEKBENCH=0
CN_SPEED=0
ROUTE_FULL=0
CITY_LAT=0
ROUTE_REUSED=0
VIRT=""
STAGES=""
SKIP=","
t() { if [ "$LANG_OPT" = en ]; then printf '%s' "$2"; else printf '%s' "$1"; fi; }
usage() {
if [ "$LANG_OPT" = en ]; then
cat <<EOF
sh.cd v$VERSION — server check-up by CleanIP: hardware · IP quality · network
Usage: bash <(curl -Ls https://sh.cd) [options]
Run without options in a terminal to open the menu.
-H Hardware & performance
-I IP quality
-N Network quality
-A Full check-up: hardware → IP → network (menu option 1)
-A -d All checks: full check-up + deep mode + Geekbench + provincial speed + hop routes (menu 2)
-d Deep mode: ATTO disk table, latency per route hop
-g Geekbench 6 (downloads about 220 MB; results are uploaded publicly to Geekbench Browser)
-p Speed tests to Chinese provincial servers (most block traffic from abroad)
-R Return routes for all 31 provinces x 3 carriers, plus large packets (menu 7)
-c Latency to 223 city-level nodes (on top of the 31 provinces)
-y Install missing tools (sysbench, fio, …) without asking
-4 / -6 IPv4 or IPv6 only (IP quality)
-x PROXY Check a proxy exit, e.g. socks5h://user:pass@host:1080
-i IFACE Use a network interface, e.g. eth0
-S LIST Skip: bench,media,mail,dns,latency,route,speed
-j JSON output (no results page)
-P Don't create a results page (sh.cd/results/…)
-n No colors
-l zh|en Language (-E = English)
-h / -v Help / version
Source: https://github.com/CleanIP/sh.cd
EOF
else
cat <<EOF
sh.cd v$VERSION — CleanIP 服务器全面体检: 硬件与性能 · IP 质量 · 网络质量
用法: bash <(curl -Ls https://sh.cd) [参数]
在终端里不带参数运行会进入菜单。
-H 硬件与性能
-I IP 质量
-N 网络质量
-A 一键全检: 硬件 → IP → 网络 (同菜单第 1 项)
-A -d 全部检测: 一键全检 + 深度模式 + Geekbench + 分省测速 + 回程详情 (同菜单第 2 项)
-d 深度模式: 硬盘 ATTO 块大小表、回程每一跳的延迟
-g Geekbench 6 跑分 (下载约 220 MB, 结果会公开上传到 Geekbench 官网)
-p 国内分省测速 (多数节点拦截境外来源, 国内服务器上测得全)
-R 全省回程: 31 省 × 三网 + 大包 + 教育网回程 (同菜单第 7 项)
-c 市级延迟: 在 31 省之外再测 223 个市级节点
-y 缺少检测工具 (sysbench / fio 等) 时直接安装, 不询问
-4 / -6 只检测 IPv4 或 IPv6 (IP 质量)
-x PROXY 通过代理检测代理的出口, 例: socks5h://user:pass@host:1080
-i IFACE 指定网卡, 例: eth0
-S LIST 跳过部分检测: bench,media,mail,dns,latency,route,speed
-j 输出 JSON (不生成结果页)
-P 不生成结果页 (sh.cd/results/…)
-n 不显示颜色
-l zh|en 语言 (-E 等同 -l en)
-h / -v 帮助 / 版本
源码: https://github.com/CleanIP/sh.cd
EOF
fi
}
# 先扫一遍语言参数, 让 -h 的帮助和参数报错也用对语言
for a in "$@"; do case "$a" in -E | -len* | -lEN*) LANG_OPT=en ;; esac; done
add_stage() { case " $STAGES " in *" $1 "*) ;; *) STAGES="$STAGES $1" ;; esac; }
# -a / -s 是 v0.2 的参数 (全国延迟 / 测速), 现在网络质量默认就包含, 保留兼容
while getopts ":HINAdgpRcy46x:i:asS:jPnl:Ehv" opt; do
case "$opt" in
H) add_stage hw ;;
I) add_stage ip ;;
N) add_stage net ;;
A) add_stage hw; add_stage ip; add_stage net; FULL=1 ;;
d) DEEP=1 ;;
g) GEEKBENCH=1; add_stage hw ;;
p) CN_SPEED=1; add_stage net ;;
R) ROUTE_FULL=1; add_stage route ;;
c) CITY_LAT=1; add_stage net ;;
y) AUTO_YES=1 ;;
4) ONLY_FAMILY=4 ;;
6) ONLY_FAMILY=6 ;;
x) PROXY="$OPTARG" ;;
i) IFACE="$OPTARG" ;;
a | s) ;;
S) SKIP="$SKIP$OPTARG," ;;
j) JSON=1 ;;
P) PRIVATE=1 ;;
n) OPT_NOCOLOR=1 ;;
l) case "$OPTARG" in en* | EN*) LANG_OPT=en ;; *) LANG_OPT=zh ;; esac ;;
E) LANG_OPT=en ;;
h) usage; exit 0 ;;
v) echo "sh.cd $VERSION"; exit 0 ;;
:) printf '%s\n' "$(t "参数 -$OPTARG 缺少值" "Option -$OPTARG requires a value")" >&2; exit 2 ;;
*) usage >&2; exit 2 ;;
esac
done
skipped() { case "$SKIP" in *",$1,"*) return 0 ;; esac; return 1; }
# -A -d = 全部检测: 一键全检 + 深度模式 + Geekbench + 分省测速 + 回程路由详情 (同菜单第 2 项)
if [ "$FULL" = 1 ] && [ "$DEEP" = 1 ]; then
add_stage route
GEEKBENCH=1
CN_SPEED=1
ROUTE_FULL=1
CITY_LAT=1
fi
if ! command -v curl >/dev/null 2>&1; then
printf '%s\n' "$(t "需要 curl, 请先安装: apt install -y curl 或 yum install -y curl" "curl is required: apt install -y curl or yum install -y curl")" >&2
exit 1
fi
# 颜色由服务端按这个开关输出; 重定向到文件或管道时自动关闭。遵循 NO_COLOR 约定。
COLOR=1
if [ "$OPT_NOCOLOR" = 1 ] || [ -n "${NO_COLOR:-}" ] || [ ! -t 1 ]; then COLOR=0; fi
# 终端里交互 (菜单 / 阶段之间询问 / 询问安装); 输出 JSON 或被重定向时不交互
INTERACTIVE=0
if [ "$JSON" = 0 ] && [ -t 1 ] && { [ -t 0 ] || [ -r /dev/tty ]; }; then INTERACTIVE=1; fi
if [ "$COLOR" = 1 ]; then
C_G=$'\033[38;2;53;169;82m' C_Y=$'\033[33m' C_R=$'\033[31m' C_K=$'\033[90m' C_B=$'\033[1m' C_0=$'\033[0m'
else
C_G="" C_Y="" C_R="" C_K="" C_B="" C_0=""
fi
IS_LINUX=0
[ "$(uname -s)" = Linux ] && IS_LINUX=1
TMP=$(mktemp -d 2>/dev/null || mktemp -d -t shcd)
BENCH_FILE=""
cleanup() {
# shellcheck disable=SC2086
pids=$(jobs -p 2>/dev/null)
[ -n "$pids" ] && kill -9 $pids 2>/dev/null
[ -n "$BENCH_FILE" ] && rm -f "$BENCH_FILE" 2>/dev/null
[ -n "$GB_DIR" ] && rm -rf "$GB_DIR" 2>/dev/null
rm -rf "$TMP"
}
trap cleanup EXIT
trap 'exit 130' INT TERM
# ── 进度提示 (只写 stderr, 且只在终端里显示) ────────────────────────────────
progress() {
[ -t 2 ] || return 0
printf '\r\033[K \033[38;2;53;169;82m██\033[0m %s' "$1" >&2
}
progress_done() {
[ -t 2 ] && printf '\r\033[K' >&2
return 0
}
# ── 网络参数 ────────────────────────────────────────────────────────────
# NET 按当前检测的出口重建; 所有出站请求 (含最后提交报告) 都走同一组参数,
# 服务端看到的来源 IP 才是正在检测的那个出口。
NET=()
NET_V6=no
set_net() {
NET=()
[ -n "$1" ] && NET+=("-$1")
[ -n "$IFACE" ] && NET+=(--interface "$IFACE")
[ -n "$PROXY" ] && NET+=(-x "$PROXY")
}
ccurl() { curl "${NET[@]}" "$@"; }
web() { ccurl -sL -m 10 -A "$UA_BROWSER" -H 'Accept-Language: en-US,en;q=0.9' "$@"; }
# 结果写成 "键=值" 一行一个, 提交报告时逐行作为表单字段
put() { printf '%s=%s\n' "$2" "$3" >>"$1/fields"; }
# 字段值去掉竖线与控制字符, 压缩空白
clean() { printf '%s' "$1" | tr -d '\000-\037|' | sed 's/ */ /g; s/^ //; s/ $//'; }
# 在 Linux 上给命令加超时; 没有 timeout 命令时原样执行
with_timeout() {
local s="$1"
shift
if command -v timeout >/dev/null 2>&1; then timeout "$s" "$@"; else "$@"; fi
}
# 按 TCP 建连耗时量延迟 (秒)
# TCP 建连耗时 (秒), 扣掉 DNS 解析; 连不上输出 0。
# time_connect 从请求开始算, 含域名解析: 不缓存 DNS 的机器每次都要重新解析, 2026-09-18 香港机并发解析一次 0.4–2.4 秒,
# 不扣掉会把延迟算高, 解析超过 1 秒还会被当成 SYN 重传。超时放宽到 3 秒也是为了给慢解析留时间。
connect_time() {
ccurl -s -o /dev/null -m 4 --connect-timeout 3 -w '%{time_namelookup} %{time_connect}' "http://$1:$2/" 2>/dev/null |
awk '{ v = $2 - $1; printf "%.6f", ($2 > 0 && v > 0) ? v : 0 }'
}
positive() { awk -v v="$1" 'BEGIN { exit !(v + 0 > 0) }'; }
# ════════════════════════════════════════════════════════════════════════
# 一、硬件与性能
# ════════════════════════════════════════════════════════════════════════
# 字段 (报告按语言排版):
# hw_os=系统|内核|架构 hw_virt=虚拟化代码 hw_uptime=秒|负载 hw_procs=进程|登录用户|运行服务|全部服务
# hw_tz=时区|UTC偏移|区域设置 hw_board=厂商|型号|BIOS厂商|BIOS版本 hw_chipset / hw_nic / hw_gpu=名称;名称
# hw_cpu=型号|核心|线程|插槽|MHz|占用% hw_cache=L1d|L1i|L2|L3 hw_flags=aes,avx2,...
# hw_csteal=宿主偷用%|IO等待% (Linux /proc 两次采样, 非 Linux 不报) hw_ntp=时钟偏移毫秒|chrony (无 chrony 时不报)
# hw_mem=总|已用|可用|Swap总|Swap已用 (字节) hw_overcommit=balloon,ksm|KSM 是否可用
# hw_disk=块设备数|总容量|测试分区容量|已用|可用|设备名|ssd/hdd
# bn_cpu=工具|单线程|多线程|线程数 bn_mem=工具|读|写 bn_r4q1 / bn_r4q32 / bn_s1q1 / bn_s1q8=读KiB/s|读IOPS|读p99微秒|写KiB/s|写IOPS|写p99微秒 (ATTO 同格式)
# bn_atto_<块大小>=同上 (-d) bn_dd=顺序写字节/秒|顺序读字节/秒|4K同步写IOPS (没有 fio 时)
# hw_temp=cpu:52.0,nvme:41.0,… sm_* / mm_* 物理机硬盘 SMART 与内存条 (见 hw_collect_physical) bn_gb Geekbench (-g)
# 虚拟化 / 容器类型, 输出 systemd-detect-virt 风格的代码, 报告里再翻译
detect_virt() {
local v
if [ -f /.dockerenv ]; then echo docker; return; fi
v=$(tr '\0' '\n' 2>/dev/null </proc/1/environ | sed -n 's/^container=//p' | head -n1)
if [ -n "$v" ]; then echo "$v"; return; fi
if [ -d /proc/vz ] && [ ! -d /proc/bc ]; then echo openvz; return; fi
if command -v systemd-detect-virt >/dev/null 2>&1; then
v=$(systemd-detect-virt 2>/dev/null)
if [ -n "$v" ] && [ "$v" != none ]; then echo "$v"; return; fi
fi
v="$(cat /sys/class/dmi/id/sys_vendor /sys/class/dmi/id/product_name 2>/dev/null | tr '\n' ' ')"
case "$v" in
*KVM* | *QEMU* | *OpenStack* | *Bochs*) echo kvm ;;
*VMware*) echo vmware ;;
*VirtualBox*) echo oracle ;;
*Xen* | *"HVM domU"*) echo xen ;;
*Hyper-V* | *"Virtual Machine"*) echo microsoft ;;
*Amazon*) echo amazon ;;
*Google*) echo google ;;
*)
if grep -q '^flags.* hypervisor' /proc/cpuinfo 2>/dev/null; then echo vm
elif [ "$IS_LINUX" = 1 ]; then echo none
elif [ "$(sysctl -n kern.hv_vmm_present 2>/dev/null)" = 1 ]; then echo vm
else echo none; fi
;;
esac
}
# lspci 的行只留设备名, 去掉地址与版本号, 同名去重
pci_names() {
sed -E 's/^[0-9a-fA-F:.]+ [^:]+: //; s/ \(rev [0-9a-f]+\)$//' | tr -d '|;' | awk '!s[$0]++' | head -n 4 | paste -sd';' -
}
cpu_usage() {
[ -r /proc/stat ] || return 0
local a b
a=$(head -n1 /proc/stat)
sleep 0.5
b=$(head -n1 /proc/stat)
printf '%s\n%s\n' "$a" "$b" | awk '{ idle = $5 + $6; tot = 0; for (i = 2; i <= NF; i++) tot += $i
if (NR == 1) { i1 = idle; t1 = tot } else if (tot > t1) printf "%.0f", (1 - (idle - i1) / (tot - t1)) * 100 }'
}
# 宿主偷用 (steal) 与 IO 等待占比: /proc/stat 第一行第 9 列是 steal、第 6 列是 iowait, 都是累计值, 同样采两次算差值。
# 虚拟机看超售主要看这一项, 输出 steal%|iowait% (取整)
cpu_steal() {
[ -r /proc/stat ] || return 0
local a b
a=$(head -n1 /proc/stat)
sleep 0.5
b=$(head -n1 /proc/stat)
printf '%s\n%s\n' "$a" "$b" | awk '{ st = $9; io = $6; tot = 0; for (i = 2; i <= NF; i++) tot += $i
if (NR == 1) { s1 = st; w1 = io; t1 = tot } else if (tot > t1) printf "%d|%d", (st - s1) / (tot - t1) * 100, (io - w1) / (tot - t1) * 100 }'
}
# 跑分目录: 当前目录、家目录等里第一个可写且不是内存盘的
bench_dir() {
local dir fs
for dir in "$PWD" "$HOME" /var/tmp /root /opt /; do
[ -d "$dir" ] && [ -w "$dir" ] || continue
fs=$(stat -f -c %T "$dir" 2>/dev/null)
case "$fs" in tmpfs | ramfs | proc | sysfs) continue ;; esac
echo "$dir"
return
done
}
hw_collect() {
local d="$1" os kernel arch up load procs users sa="" st="" tz dmi=/sys/class/dmi/id pci ntp_off
mkdir -p "$d"
arch=$(uname -m)
kernel=$(uname -r)
if [ -r /etc/os-release ]; then
os=$(. /etc/os-release && printf '%s' "${PRETTY_NAME:-$NAME $VERSION_ID}")
elif [ "$(uname -s)" = Darwin ]; then
os="$(sw_vers -productName 2>/dev/null) $(sw_vers -productVersion 2>/dev/null)"
else
os=$(uname -s)
fi
put "$d" hw_os "$(clean "$os")|$(clean "$kernel")|$(clean "$arch")"
[ -n "$VIRT" ] || VIRT=$(detect_virt)
put "$d" hw_virt "$(clean "$VIRT")"
if [ -r /proc/uptime ]; then
up=$(cut -d. -f1 /proc/uptime)
load=$(cut -d' ' -f1-3 /proc/loadavg)
else
# macOS: "{ sec = 1789500000, usec = 123456 } ..."; 贪婪匹配会取到 usec, 算出两万天
up=$(($(date +%s) - $(sysctl -n kern.boottime 2>/dev/null | sed -E 's/^\{ sec = ([0-9]+),.*/\1/')))
load=$(sysctl -n vm.loadavg 2>/dev/null | tr -d '{}' | awk '{ print $1, $2, $3 }')
fi
put "$d" hw_uptime "$up|$load"
procs=$(ps -e 2>/dev/null | tail -n +2 | wc -l | tr -d ' ')
users=$(who 2>/dev/null | wc -l | tr -d ' ')
if command -v systemctl >/dev/null 2>&1 && [ -d /run/systemd/system ]; then
sa=$(systemctl list-units --type=service --state=running --no-legend 2>/dev/null | wc -l | tr -d ' ')
st=$(systemctl list-units --type=service --all --no-legend 2>/dev/null | wc -l | tr -d ' ')
fi
put "$d" hw_procs "$procs|$users|$sa|$st"
tz=$(timedatectl show -p Timezone --value 2>/dev/null)
[ -z "$tz" ] && [ -L /etc/localtime ] && tz=$(readlink /etc/localtime | sed 's#.*zoneinfo/##')
[ -z "$tz" ] && [ -r /etc/timezone ] && tz=$(cat /etc/timezone)
put "$d" hw_tz "$(clean "${tz:-$(date +%Z)}")|$(date +%z)|$(clean "${LC_ALL:-${LANG:-C}}")"
# 时钟偏移 (毫秒, local 落后为负): 只读 chronyd, 不发包, 没有 chrony 时不报
ntp_off=$(chronyc tracking 2>/dev/null | awk '/slow of NTP time/ { printf "-%.0f", $4 * 1000 } /fast of NTP time/ { printf "%.0f", $4 * 1000 }')
[ -n "$ntp_off" ] && put "$d" hw_ntp "$ntp_off|chrony"
put "$d" hw_board "$(clean "$(cat $dmi/sys_vendor 2>/dev/null)")|$(clean "$(cat $dmi/product_name 2>/dev/null)")|$(clean "$(cat $dmi/bios_vendor 2>/dev/null)")|$(clean "$(cat $dmi/bios_version 2>/dev/null)")"
if command -v lspci >/dev/null 2>&1; then
pci=$(lspci 2>/dev/null)
put "$d" hw_chipset "$(printf '%s\n' "$pci" | grep -E 'Host bridge|ISA bridge' | pci_names)"
put "$d" hw_nic "$(printf '%s\n' "$pci" | grep -E 'Ethernet controller|Network controller' | pci_names)"
put "$d" hw_gpu "$(printf '%s\n' "$pci" | grep -E 'VGA compatible|3D controller|Display controller' | pci_names)"
fi
hw_collect_cpu "$d"
hw_collect_mem "$d"
hw_collect_disk "$d"
hw_collect_temp "$d"
hw_collect_physical "$d"
}
hw_collect_cpu() {
local d="$1" info model threads sockets per cores mhz flags="" fl x s
info=$(LC_ALL=C lscpu 2>/dev/null)
lv() { printf '%s\n' "$info" | sed -n "s/^$1:[[:space:]]*//p" | head -n1; }
model=$(lv 'Model name')
[ -z "$model" ] && model=$(grep -m1 -E '^(model name|Hardware|cpu model)' /proc/cpuinfo 2>/dev/null | cut -d: -f2-)
[ -z "$model" ] && model=$(sysctl -n machdep.cpu.brand_string 2>/dev/null)
threads=$(lv 'CPU(s)')
[ -z "$threads" ] && threads=$(getconf _NPROCESSORS_ONLN 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null)
sockets=$(lv 'Socket(s)')
per=$(lv 'Core(s) per socket')
if [ -n "$sockets" ] && [ -n "$per" ]; then cores=$((sockets * per)); else cores=$(sysctl -n hw.physicalcpu 2>/dev/null || echo "$threads"); fi
mhz=$(lv 'CPU MHz')
[ -z "$mhz" ] && mhz=$(grep -m1 'cpu MHz' /proc/cpuinfo 2>/dev/null | cut -d: -f2 | tr -d ' ')
[ -z "$mhz" ] && mhz=$(lv 'CPU max MHz')
put "$d" hw_cpu "$(clean "$model")|$cores|$threads|${sockets:-1}|${mhz%%.*}|$(cpu_usage)"
s=$(cpu_steal)
[ -n "$s" ] && put "$d" hw_csteal "$s"
# 新版 lscpu 给的是所有实例的合计, 例 "512 KiB (16 instances)"; 原样交上去, 报告里换算成每个实例
put "$d" hw_cache "$(clean "$(lv 'L1d cache')")|$(clean "$(lv 'L1i cache')")|$(clean "$(lv 'L2 cache')")|$(clean "$(lv 'L3 cache')")"
fl=" $(grep -m1 -E '^(flags|Features)' /proc/cpuinfo 2>/dev/null | cut -d: -f2) $(sysctl -n machdep.cpu.features machdep.cpu.leaf7_features 2>/dev/null | tr 'A-Z.' 'a-z_' | tr '\n' ' ') "
# Apple 芯片: sysctl hw.optional.arm.FEAT_* 为 1 表示支持
[ "$(sysctl -n hw.optional.arm.FEAT_AES 2>/dev/null)" = 1 ] && fl="$fl aes "
[ "$(sysctl -n hw.optional.arm.FEAT_SHA256 2>/dev/null)" = 1 ] && fl="$fl sha2 "
for x in vmx svm aes avx avx2 avx512f bmi1 bmi2 sha_ni ept npt sha1 sha2 hypervisor; do
case "$fl" in *" $x "*) flags="$flags,$x" ;; esac
done
put "$d" hw_flags "${flags#,}"
}
hw_collect_mem() {
local d="$1" oc="" ksm=0
if [ -r /proc/meminfo ]; then
put "$d" hw_mem "$(awk '/^MemTotal:/ { t = $2 } /^MemAvailable:/ { a = $2 } /^SwapTotal:/ { st = $2 } /^SwapFree:/ { sf = $2 }
END { printf "%.0f|%.0f|%.0f|%.0f|%.0f", t * 1024, (t - a) * 1024, a * 1024, st * 1024, (st - sf) * 1024 }' /proc/meminfo)"
elif [ "$(uname -s)" = Darwin ]; then
put "$d" hw_mem "$(sysctl -n hw.memsize 2>/dev/null)||||"
fi
# 超开迹象: virtio 气球回收设备、KSM 内存合并
ls /sys/bus/virtio/drivers/virtio_balloon 2>/dev/null | grep -q virtio && oc="balloon"
[ -e /sys/kernel/mm/ksm/run ] && ksm=1
[ "$(cat /sys/kernel/mm/ksm/run 2>/dev/null)" = 1 ] && oc="$oc,ksm"
[ "$IS_LINUX" = 1 ] && put "$d" hw_overcommit "${oc#,}|$ksm"
}
hw_collect_disk() {
local d="$1" n=0 total=0 dev sz dir df src parent rota type=""
for dev in /sys/block/*; do
case "${dev##*/}" in loop* | ram* | zram* | sr* | fd* | dm-* | md* | nbd* | "*") continue ;; esac
sz=$(cat "$dev/size" 2>/dev/null)
[ "${sz:-0}" -gt 0 ] 2>/dev/null || continue
n=$((n + 1))
total=$((total + sz * 512))
done
dir=$(bench_dir)
BENCH_DIR="$dir"
[ -n "$dir" ] || return 0
df=$(df -Pk "$dir" 2>/dev/null | tail -n1)
src=$(printf '%s' "$df" | awk '{ print $1 }')
parent=$(lsblk -no PKNAME "$src" 2>/dev/null | head -n1)
[ -z "$parent" ] && parent=$(basename "$src" | sed -E 's/p?[0-9]+$//')
rota=$(cat "/sys/block/$parent/queue/rotational" 2>/dev/null)
case "$rota" in 0) type=ssd ;; 1) type=hdd ;; esac
put "$d" hw_disk "$n|$total|$(printf '%s' "$df" | awk '{ printf "%.0f|%.0f|%.0f", $2 * 1024, $3 * 1024, $4 * 1024 }')|$(clean "$(basename "$src")")|$type"
}
# ── 温度: 读内核 hwmon / thermal, 不用装 lm-sensors ──────────────────────────
# 每类取最高值: cpu (coretemp 的 Package / k10temp 的 Tctl·Tdie / ARM 的 cpu_thermal) · nvme (Composite) ·
# disk (drivetemp) · gpu · board (主板 / ACPI)。虚拟机通常读不到, 读不到就不报。
hw_collect_temp() {
local d="$1" out
[ "$IS_LINUX" = 1 ] || return 0
out=$(temp_lines | awk '$2 > 0 && $2 < 150000 { if (!($1 in m) || $2 > m[$1]) m[$1] = $2 } END { for (k in m) printf "%s:%.1f\n", k, m[k] / 1000 }' | sort | paste -sd, -)
[ -n "$out" ] && put "$d" hw_temp "$out"
return 0
}
# 每行 "类别 毫摄氏度"。单独成函数: bash 3.2 解析 $( ) 里嵌套的 case 会报语法错误
temp_lines() {
local h name f v label z type
for h in /sys/class/hwmon/hwmon*; do
[ -d "$h" ] || continue
name=$(cat "$h/name" 2>/dev/null)
for f in "$h"/temp*_input; do
[ -r "$f" ] || continue
v=$(cat "$f" 2>/dev/null)
label=$(cat "${f%_input}_label" 2>/dev/null)
case "$name" in
coretemp) case "$label" in Package*) echo "cpu $v" ;; esac ;;
k10temp | zenpower) case "$label" in Tctl | Tdie | "") echo "cpu $v" ;; esac ;;
cpu_thermal | cpu-thermal | soc_thermal) echo "cpu $v" ;;
nvme) case "$label" in Composite | "") echo "nvme $v" ;; esac ;;
drivetemp) echo "disk $v" ;;
amdgpu | nouveau | radeon) echo "gpu $v" ;;
acpitz | pch_*) echo "board $v" ;;
esac
done
done
for z in /sys/class/thermal/thermal_zone*; do
[ -r "$z/temp" ] || continue
type=$(cat "$z/type" 2>/dev/null)
case "$type" in x86_pkg_temp | *cpu* | soc*) echo "cpu $(cat "$z/temp" 2>/dev/null)" ;; esac
done
}
# ── 物理机: 硬盘 SMART 与内存条 (虚拟机上这些都是虚拟化平台模拟的, 不采) ──────────────
# 需要 root (或免密 sudo) 与 smartmontools / dmidecode, 缺工具时随跑分工具一起询问安装。
# sm_<n>=接口|型号|容量字节|nvme/ssd/hdd|pass/fail|通电小时|温度|已用寿命%|累计写入字节|重映射扇区|待映射扇区|无法修复扇区|介质错误
# mm_array=最大容量字节|插槽数|纠错类型 mm_<n>=条数|单条字节|类型|额定速率|实际速率|厂商|型号|Rank|外形
hw_collect_physical() {
local d="$1" sudo="" n=0 dev typ line
[ "$IS_LINUX" = 1 ] && [ "$VIRT" = none ] || return 0
if [ "$(id -u)" != 0 ]; then
if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then sudo="sudo -n"; else put "$d" hw_noroot 1; return 0; fi
fi
if command -v smartctl >/dev/null 2>&1; then
$sudo smartctl --scan 2>/dev/null | head -n 8 | while read -r dev _ typ _; do
line=$($sudo smartctl -i -H -A -d "$typ" "$dev" 2>/dev/null | smart_parse)
[ -n "$line" ] || continue
n=$((n + 1))
put "$d" "sm_$n" "$line"
done
fi
if command -v dmidecode >/dev/null 2>&1; then
$sudo dmidecode -t 16,17 2>/dev/null | dimm_parse "$d"
fi
}
# smartctl -i -H -A 的文本输出 → 一行 sm 字段; 虚拟盘 / 读不到 SMART 的输出空
smart_parse() {
awk '
function num(s) { gsub(/[^0-9]/, "", s); return s }
function after(s) { sub(/^[^:]*:[ \t]*/, "", s); return s }
/^(Device Model|Model Number|Product):/ { model = after($0) }
/^Vendor:/ { vendor = after($0) }
/^User Capacity:/ { cap = num(substr($0, 1, index($0, "bytes"))) }
/^(Total NVM Capacity|Namespace 1 Size\/Capacity):/ && !cap { s = after($0); sub(/\[.*/, "", s); cap = num(s) }
/^Rotation Rate:/ { rot = after($0) }
/^NVMe Version:|NVMe Log/ { proto = "nvme" }
/^ATA Version is:|^SATA Version is:/ { proto = "ata" }
/^Transport protocol:|^Logical Unit id:/ { if (!proto) proto = "scsi" }
/SMART overall-health self-assessment test result:/ { health = ($NF == "PASSED") ? "pass" : "fail" }
/^SMART Health Status:/ { health = ($NF == "OK") ? "pass" : "fail" }
/^Temperature:/ { temp = num($2) }
/^Current Drive Temperature:/ { temp = num($4) }
/^Percentage Used:/ { used = num($3) }
/^Data Units Written:/ { s = $4; w = num(s) * 512000 }
/^Power On Hours:/ { hours = num($4) }
/^Accumulated power on time, hours:minutes/ { s = $NF; sub(/:.*/, "", s); hours = num(s) }
/^Media and Data Integrity Errors:/ { media = num($NF) }
/^Elements in grown defect list:/ { realloc = num($NF) }
# ATA 属性表: ID 名称 标志 当前值 最差 阈值 类型 更新 失败时间 原始值
$1 ~ /^[0-9]+$/ && NF >= 10 {
id = $1; val = $4 + 0; raw = $10
if (id == 9) hours = num(raw)
else if (id == 194 || (id == 190 && !temp)) temp = num(raw)
else if (id == 5) realloc = num(raw)
else if (id == 197) pending = num(raw)
else if (id == 198) uncorrect = num(raw)
else if (id == 241) w = num(raw) * 512
else if (id == 177 || id == 231 || id == 233 || id == 202) { if (used == "") used = 100 - val }
}
END {
if (!proto) proto = (rot == "" ? "" : "ata")
if (model == "" || model ~ /QEMU|VBOX|VMware|Virtual/) exit
if (vendor != "" && index(model, vendor) != 1) model = vendor " " model
gsub(/\|/, " ", model)
kind = proto == "nvme" ? "nvme" : (rot ~ /Solid State/ ? "ssd" : (rot ~ /rpm/ ? "hdd" : ""))
# 大数字用 %.0f 输出, 否则 awk 会写成 6.3e+12
printf "%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s|%s\n", proto, model, cap, kind, health, hours, temp, used, (w == "" ? "" : sprintf("%.0f", w)), realloc, pending, uncorrect, media
}'
}
# dmidecode -t 16,17 → mm_array 与按规格合并的 mm_<n>
dimm_parse() {
local d="$1"
awk '
function bytes(s, n) { n = s + 0; if (s ~ /TB/) return n * 1099511627776; if (s ~ /GB/) return n * 1073741824; if (s ~ /MB/) return n * 1048576; if (s ~ /kB|KB/) return n * 1024; return 0 }
function flush() {
if (dev && size > 0) {
key = sprintf("%.0f|%s|%s|%s|%s|%s|%s|%s", size, type, speed, conf, manu, part, rank, form)
if (!(key in cnt)) order[++n] = key
cnt[key]++
}
dev = 0; size = 0; type = speed = conf = manu = part = rank = form = ""
}
/^Handle / { flush() }
/^Physical Memory Array/ { arr = 1; dev = 0 }
/^Memory Device/ { dev = 1; arr = 0 }
{ line = $0; sub(/^[ \t]+/, "", line); k = line; sub(/:.*/, "", k); v = line; sub(/^[^:]*:[ \t]*/, "", v); gsub(/\|/, " ", v) }
arr && k == "Maximum Capacity" { maxcap = bytes(v) }
arr && k == "Number Of Devices" { slots += v + 0 }
arr && k == "Error Correction Type" { ecc = v }
dev && k == "Size" { size = (v ~ /No Module|Not Installed|Unknown/) ? 0 : bytes(v) }
dev && k == "Type" { type = v }
dev && k == "Speed" { speed = (v ~ /MT\/s|MHz/) ? v + 0 : "" }
dev && k == "Configured Memory Speed" { conf = (v ~ /MT\/s|MHz/) ? v + 0 : "" }
dev && k == "Configured Clock Speed" && conf == "" { conf = (v ~ /MT\/s|MHz/) ? v + 0 : "" }
dev && k == "Manufacturer" { manu = (v ~ /Not Specified|Unknown|^0000|NO DIMM/) ? "" : v }
dev && k == "Part Number" { part = (v ~ /Not Specified|Unknown|NO DIMM/) ? "" : v; gsub(/ +$/, "", part) }
dev && k == "Rank" { rank = (v ~ /Unknown/) ? "" : v + 0 }
dev && k == "Form Factor" { form = v }
END {
flush()
if (maxcap || slots) printf "mm_array=%.0f|%s|%s\n", maxcap, slots, ecc
for (i = 1; i <= n && i <= 6; i++) printf "mm_%d=%d|%s\n", i, cnt[order[i]], order[i]
}' >>"$d/fields"
}
# ── 跑分工具: 缺 sysbench / fio 时询问安装 ────────────────────────────────
ensure_bench_tools() {
local missing="" pkgs="" sudo="" ans pm rc
[ "$IS_LINUX" = 1 ] || return 0
if ! skipped bench; then
command -v sysbench >/dev/null 2>&1 || { missing="$missing sysbench"; pkgs="$pkgs sysbench"; }
command -v fio >/dev/null 2>&1 || { missing="$missing fio"; pkgs="$pkgs fio"; }
fi
# 物理机才读硬盘 SMART 与内存条, 虚拟机上装了也读不到
if [ "$VIRT" = none ]; then
command -v smartctl >/dev/null 2>&1 || { missing="$missing smartctl"; pkgs="$pkgs smartmontools"; }
command -v dmidecode >/dev/null 2>&1 || { missing="$missing dmidecode"; pkgs="$pkgs dmidecode"; }
fi
[ -n "$missing" ] || return 0
if [ "$(id -u)" != 0 ]; then
if command -v sudo >/dev/null 2>&1 && sudo -n true 2>/dev/null; then sudo="sudo -n"; else return 0; fi
fi
for pm in apt-get dnf yum apk pacman zypper; do command -v "$pm" >/dev/null 2>&1 && break; pm=""; done
[ -n "$pm" ] || return 0
if [ "$AUTO_YES" != 1 ]; then
[ "$INTERACTIVE" = 1 ] || return 0
progress_done
printf '\n %s%s%s %s\n' "$C_Y" "!" "$C_0" "$(t "缺少测试工具:$missing" "Missing benchmark tools:$missing")"
printf ' %s\n' "$(t "用于 CPU 跑分、硬盘读写与物理机的硬盘健康、内存条信息; 不安装则跳过或用系统自带工具近似测量" "Used for CPU scores, disk I/O, and disk health / memory modules on bare metal; without them those parts are skipped or approximated")"
printf ' %s [Y/n] ' "$(t "现在用 $pm 安装? 15 秒不回答默认安装" "Install with $pm now? Defaults to yes in 15 s")"
# 超时按默认 (安装) 继续, 一键全检无人值守时不卡在这里; 读不到终端才当作不安装
read -r -t 15 ans 2>/dev/null </dev/tty
rc=$?
if [ "$rc" -gt 128 ]; then ans=y; printf '\n'; elif [ "$rc" != 0 ]; then ans=n; fi
case "$ans" in n | N | no | No) return 0 ;; esac
fi
progress "$(t "安装$pkgs …" "Installing$pkgs …")"
case "$pm" in
apt-get)
$sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq $pkgs >/dev/null 2>&1 ||
{ $sudo apt-get update -qq >/dev/null 2>&1 && $sudo env DEBIAN_FRONTEND=noninteractive apt-get install -y -qq $pkgs >/dev/null 2>&1; }
;;
dnf | yum)
case "$pkgs" in *sysbench*) $sudo "$pm" install -y -q epel-release >/dev/null 2>&1 ;; esac
$sudo "$pm" install -y -q $pkgs >/dev/null 2>&1
;;
apk) $sudo apk add -q $pkgs >/dev/null 2>&1 ;;
pacman) $sudo pacman -Sy --noconfirm $pkgs >/dev/null 2>&1 ;;
zypper) $sudo zypper -n -q install $pkgs >/dev/null 2>&1 ;;
esac
progress_done
}
# ── 跑分 ────────────────────────────────────────────────────────────────
FIO_ENGINE=psync
# 输出 读KiB/s|读IOPS|读p99微秒 或 写KiB/s|写IOPS|写p99微秒。
# 带宽与 IOPS 在 terse v3 固定列 (读 7/8, 写 48/49); p99 按名取 ("99.00%=123" 这类字段, 取微秒值),
# 不依赖列号: 不同 fio 版本的百分位集合可能不一样, 按列数死取会错位。取不到就留空, 报告里跳过。
fio_run() {
local rw="$1" bs="$2" depth="$3" secs="$4" out
out=$(cd "$BENCH_DIR" && with_timeout $((secs + 60)) fio --name=shcd --filename=.shcd-fio --size=512M --direct=1 \
--rw="$rw" --bs="$bs" --iodepth="$depth" --ioengine="$FIO_ENGINE" --runtime="$secs" --time_based \
--group_reporting --output-format=terse --terse-version=3 2>/dev/null | tail -n1)
case "$rw" in
*read) printf '%s' "$out" | awk -F';' '{ p = ""; for (i = 18; i <= 37 && p == ""; i++) if ($i ~ /^99(\.0+)?%=/) { p = $i; sub(/^[^=]*=/, "", p) } printf "%s|%s|%s", $7, $8, p }' ;;
*) printf '%s' "$out" | awk -F';' '{ p = ""; for (i = 59; i <= 78 && p == ""; i++) if ($i ~ /^99(\.0+)?%=/) { p = $i; sub(/^[^=]*=/, "", p) } printf "%s|%s|%s", $48, $49, p }' ;;
esac
}
# dd 统计行换算成 字节/秒。GNU: "8589934592 bytes (8.6 GB, 8.0 GiB) copied, 1.2 s, 7.0 GB/s";
# busybox: "... copied, 1.2 seconds, ..."。容量括号里也有逗号, 不能按逗号拆 (2026-09-16 香港机实测全算成 1 GB/s)
dd_secs() { awk '/copied/ { t = $0; sub(/.*copied, /, "", t); sub(/ s.*/, "", t); print t }'; }
dd_rate() {
awk '/copied/ { b = $1; t = $0; sub(/.*copied, /, "", t); sub(/ s.*/, "", t); if (t + 0 > 0) printf "%.0f", b / t }'
}
hw_bench() {
local d="$1" threads s1 sn r w k
threads=$(getconf _NPROCESSORS_ONLN 2>/dev/null || nproc 2>/dev/null || echo 1)
progress "$(t "[硬件] CPU 跑分…" "[Hardware] CPU benchmark…")"
if command -v sysbench >/dev/null 2>&1; then
s1=$(sysbench cpu --threads=1 --time=5 run 2>/dev/null | awk '/events per second/ { print $4 }')
sn=$(sysbench cpu --threads="$threads" --time=5 run 2>/dev/null | awk '/events per second/ { print $4 }')
put "$d" bn_cpu "sysbench|${s1:-fail}|${sn:-fail}|$threads"
elif command -v openssl >/dev/null 2>&1; then
# 16KB 块的 SHA-256 吞吐 (MB/s), 与 sysbench 分数不可比, 报告里注明
s1=$(openssl speed -seconds 3 -evp sha256 2>/dev/null | awk '/^sha256/ { v = $NF; sub(/k$/, "", v); printf "%.1f", v / 1024 }')
sn=$(openssl speed -multi "$threads" -seconds 3 -evp sha256 2>/dev/null | awk '/^sha256/ { v = $NF; sub(/k$/, "", v); printf "%.1f", v / 1024 }')
put "$d" bn_cpu "openssl|${s1:-fail}|${sn:-fail}|$threads"
fi
progress "$(t "[硬件] 内存读写…" "[Hardware] Memory throughput…")"
if command -v sysbench >/dev/null 2>&1; then
r=$(sysbench memory --memory-block-size=1M --memory-total-size=1000G --memory-oper=read --time=3 run 2>/dev/null | grep -oE '[0-9.]+ MiB/sec' | cut -d' ' -f1)
w=$(sysbench memory --memory-block-size=1M --memory-total-size=1000G --memory-oper=write --time=3 run 2>/dev/null | grep -oE '[0-9.]+ MiB/sec' | cut -d' ' -f1)
put "$d" bn_mem "sysbench|${r:-fail}|${w:-fail}"
fi
# 没有 sysbench 时不测内存: dd 读 /dev/zero 写 /dev/null 测的是系统调用开销, 不是内存带宽 (香港单核机测出 30 GB/s)
[ -n "$BENCH_DIR" ] || return 0
BENCH_FILE="$BENCH_DIR/.shcd-fio"
if command -v fio >/dev/null 2>&1; then
for k in libaio io_uring posixaio; do
if fio --enghelp 2>/dev/null | grep -qw "$k"; then FIO_ENGINE=$k; break; fi
done
progress "$(t "[硬件] 硬盘 4K 随机读写…" "[Hardware] Disk 4K random I/O…")"
put "$d" bn_r4q1 "$(fio_run randread 4k 1 4)|$(fio_run randwrite 4k 1 4)"
put "$d" bn_r4q32 "$(fio_run randread 4k 32 4)|$(fio_run randwrite 4k 32 4)"
progress "$(t "[硬件] 硬盘顺序读写…" "[Hardware] Disk sequential I/O…")"
put "$d" bn_s1q1 "$(fio_run read 1m 1 4)|$(fio_run write 1m 1 4)"
put "$d" bn_s1q8 "$(fio_run read 1m 8 4)|$(fio_run write 1m 8 4)"
if [ "$DEEP" = 1 ]; then
for k in 512 1k 2k 4k 8k 16k 32k 64k 128k 256k 512k 1m 2m 4m 8m 16m 32m 64m; do
progress "$(t "[硬件] ATTO 块大小 ${k}…" "[Hardware] ATTO block size ${k}…")"
put "$d" "bn_atto_$k" "$(fio_run read "$k" 4 2)|$(fio_run write "$k" 4 2)"
done
fi
rm -f "$BENCH_FILE"
elif [ "$IS_LINUX" = 1 ]; then
progress "$(t "[硬件] 硬盘读写 (dd)…" "[Hardware] Disk I/O (dd)…")"
BENCH_FILE="$BENCH_DIR/.shcd-dd"
w=$(dd if=/dev/zero of="$BENCH_FILE" bs=1M count=1024 oflag=direct 2>&1 | dd_rate)
r=$(dd if="$BENCH_FILE" of=/dev/null bs=1M iflag=direct 2>&1 | dd_rate)
k=$(dd if=/dev/zero of="$BENCH_FILE" bs=4k count=2000 oflag=dsync 2>&1 | dd_secs | awk '$1 + 0 > 0 { printf "%.0f", 2000 / $1 }')
put "$d" bn_dd "${w:-fail}|${r:-fail}|${k:-fail}"
rm -f "$BENCH_FILE"
fi
progress_done
}
# ── Geekbench 6 (-g / 全部检测) ────────────────────────────────────────────────
# 从 Geekbench 官方 CDN 下载命令行版 (约 220 MB, 只有 IPv4), 跑完上传到 Geekbench Browser (免费版必须上传, 结果公开), 跑完即删。
# 命令行免费版不在本地输出分数; 结果页有人机验证 (2026-09-17 洛杉矶与本地 curl 都是 403), 不去绕过, 只给出结果页链接。
# 带 key 的「认领结果」链接不提交。
# 官方 CDN 到部分机房很慢 (2026-09-17 洛杉矶机 50 KB/s, 香港与本地 10 MB/s 以上), 连续 20 秒低于 100 KB/s 就放弃。
# bn_gb=版本|ok|||结果页 (单核 / 多核留空) 或 版本|<unsupported|lowmem|nospace|download|fail>
GB_VERSION=6.7.1
GB_DIR=""
hw_geekbench() {
local d="$1" url mem_kb free_kb out result
[ "$IS_LINUX" = 1 ] && [ -n "$BENCH_DIR" ] || return 0
case "$(uname -m)" in
x86_64 | amd64) url="https://cdn.geekbench.com/Geekbench-$GB_VERSION-Linux.tar.gz" ;;
aarch64 | arm64) url="https://cdn.geekbench.com/Geekbench-$GB_VERSION-LinuxARMPreview.tar.gz" ;;
*) put "$d" bn_gb "$GB_VERSION|unsupported"; return 0 ;;
esac
# 内存 1 GB 以下 Geekbench 6 常跑不完 (内存 + Swap 合计不足 1.5 GB 时跳过)
mem_kb=$(awk '/^(MemTotal|SwapTotal):/ { s += $2 } END { print s + 0 }' /proc/meminfo)
[ "$mem_kb" -ge 1500000 ] || { put "$d" bn_gb "$GB_VERSION|lowmem"; return 0; }
free_kb=$(df -Pk "$BENCH_DIR" 2>/dev/null | awk 'NR == 2 { print $4 }')
[ "${free_kb:-0}" -ge 1048576 ] || { put "$d" bn_gb "$GB_VERSION|nospace"; return 0; }
GB_DIR="$BENCH_DIR/.shcd-geekbench"
rm -rf "$GB_DIR"
mkdir -p "$GB_DIR"
progress "$(t "[硬件] 下载 Geekbench $GB_VERSION (约 220 MB)…" "[Hardware] Downloading Geekbench $GB_VERSION (about 220 MB)…")"
if ! curl -4 -sL --speed-limit 102400 --speed-time 20 -m 900 "$url" | tar xz --strip-components=1 -C "$GB_DIR" 2>/dev/null || [ ! -x "$GB_DIR/geekbench6" ]; then
rm -rf "$GB_DIR"
put "$d" bn_gb "$GB_VERSION|download"
return 0
fi
progress "$(t "[硬件] Geekbench $GB_VERSION 跑分, 约 3–10 分钟…" "[Hardware] Geekbench $GB_VERSION, 3–10 minutes…")"
out=$(cd "$GB_DIR" && with_timeout 1800 ./geekbench6 --upload 2>/dev/null)
rm -rf "$GB_DIR"
result=$(printf '%s\n' "$out" | grep -oE 'https://browser\.geekbench\.com/v6/cpu/[0-9]+' | head -n1)
[ -n "$result" ] || { put "$d" bn_gb "$GB_VERSION|fail"; return 0; }
# 免费版命令行不输出分数, 结果页有人机验证, 只给出结果页链接 (单核 / 多核两栏留空)
put "$d" bn_gb "$GB_VERSION|ok|||$result"
}
stage_hw() {
local d="$TMP/hw"
mkdir -p "$d"
: >"$d/fields"
# 先判断虚拟化: 物理机才需要装 SMART / 内存条工具, 工具要在采集前装好
VIRT=$(detect_virt)
ensure_bench_tools
progress "$(t "[硬件] 读取系统与硬件信息…" "[Hardware] Reading system information…")"
hw_collect "$d"
if ! skipped bench; then
hw_bench "$d"
[ "$GEEKBENCH" = 1 ] && hw_geekbench "$d"
fi
progress_done
}
# ════════════════════════════════════════════════════════════════════════
# 二、IP 质量
# ════════════════════════════════════════════════════════════════════════
# ── 流媒体 / AI 解锁 ─────────────────────────────────────────────────────
# 值: yes | no | originals (Netflix 仅自制剧) | web (ChatGPT 仅网页版) | nov6 (检测 IPv6 出口, 但该网站不支持 IPv6) | fail (请求失败或无法判断)
# 地区附在竖线后, 例: yes|US
# 判定依据 2026-09-15 在洛杉矶 (全部可用)、英国与香港 (ChatGPT / Claude / Gemini / TikTok 不可用) 实测过。
probe_netflix() {
local d="$1" c1 c2 region ok=0 reach=0
# 两部非自制剧: 能打开 = 完整解锁; 页面在但片子看不了 = 仅自制剧
c1=$(web -o "$d/nf1" -w '%{http_code}' https://www.netflix.com/title/81280792) || c1=000
c2=$(web -o "$d/nf2" -w '%{http_code}' https://www.netflix.com/title/70143836) || c2=000
region=$(cat "$d/nf1" "$d/nf2" 2>/dev/null | grep -oE '"id":"[A-Z]{2}","countryName"' | head -n1 | cut -d'"' -f4)
if [ "$c1" = 200 ] && ! grep -q 'Oh no' "$d/nf1"; then ok=1; fi
if [ "$c2" = 200 ] && ! grep -q 'Oh no' "$d/nf2"; then ok=1; fi
case "$c1$c2" in *200* | *404*) reach=1 ;; esac
if [ $ok = 1 ]; then echo "yes|$region"
elif [ $reach = 1 ]; then echo "originals|$region"
elif [ "$c1" = 403 ] || [ "$c2" = 403 ]; then echo "no|$region"
else echo "fail"; fi
}
probe_youtube() {
local d="$1" region
web -o "$d/yt" -b 'CONSENT=YES+cb' https://www.youtube.com/premium >/dev/null 2>&1
region=$(grep -oE '"contentRegion":"[A-Z]{2}"' "$d/yt" 2>/dev/null | head -n1 | cut -d'"' -f4)
if grep -q 'www.google.cn' "$d/yt" 2>/dev/null; then echo "no|CN"
# 不支持的国家页面里的 contentRegion 会回落成 US, 地区不能用
elif grep -q 'Premium is not available in your country' "$d/yt" 2>/dev/null; then echo "no|"
elif grep -q 'ad-free' "$d/yt" 2>/dev/null; then echo "yes|$region"
else echo "fail"; fi
}
probe_tiktok() {
local d="$1" out region
out=$(web -o "$d/tt" -w '%{http_code} %{url_effective}' https://www.tiktok.com/explore) || out=000
region=$(grep -oE '"region":"[A-Z]+"' "$d/tt" 2>/dev/null | head -n1 | cut -d'"' -f4)
case "$out" in
000*)
# 连不上 TikTok 但能连上别的网站: 当地网络封锁, 算不可用 (2026-09-17 乌兹别克斯坦家宽实测 TCP 超时);
# 对照站点也连不上才是本机网络问题, 记检测失败
if ccurl -s -m 6 -o /dev/null https://www.google.com/generate_204 2>/dev/null; then echo "no|"; else echo "fail"; fi
;;
# 不可用地区会被跳到 /xx/about 介绍页, region 也不再是两位国家码 (香港实测为 ALISG)
*/about*) echo "no|" ;;
*) if [ ${#region} = 2 ]; then echo "yes|$region"; else echo "fail"; fi ;;
esac
}
probe_prime() {
local d="$1" region
web -o "$d/pv" https://www.primevideo.com >/dev/null 2>&1
region=$(grep -oE '"currentTerritory":"[A-Z]{2}"' "$d/pv" 2>/dev/null | head -n1 | cut -d'"' -f4)
if grep -q '"isServiceRestricted":true' "$d/pv" 2>/dev/null; then echo "no|$region"
elif [ -n "$region" ]; then echo "yes|$region"
else echo "fail"; fi
}
cf_loc() { ccurl -s -m 8 "https://$1/cdn-cgi/trace" 2>/dev/null | sed -n 's/^loc=\([A-Z][A-Z]\)$/\1/p'; }
probe_chatgpt() {
local d="$1" code region
code=$(ccurl -s -m 10 -A "$UA_BROWSER" -o "$d/oa1" -w '%{http_code}' \
-H 'authorization: Bearer null' -H 'origin: https://platform.openai.com' \
https://api.openai.com/compliance/cookie_requirements) || code=000
ccurl -s -m 10 -A "$UA_BROWSER" -o "$d/oa2" https://ios.chat.openai.com/ >/dev/null 2>&1
region=$(cf_loc chatgpt.com)
if grep -q 'unsupported_country' "$d/oa1" "$d/oa2" 2>/dev/null; then echo "no|$region"
elif grep -q 'VPN' "$d/oa2" 2>/dev/null; then echo "web|$region"
elif [ "$code" = 200 ]; then echo "yes|$region"
else echo "fail"; fi
}
probe_claude() {
local out="" region i
region=$(cf_loc claude.ai)
# 不支持的地区 302 到 claude.com/app-unavailable-in-region。
# claude.ai 会随机给非浏览器请求弹 Cloudflare 验证页 (403 "Just a moment", 洛杉矶实测 3 次 2 次), 多试几次
for i in 1 2 3; do
out=$(ccurl -s -m 10 -A "$UA_BROWSER" -o /dev/null -w '%{http_code} %{redirect_url}' https://claude.ai/) || out=000
case "$out" in 403*) sleep 1 ;; *) break ;; esac
done
case "$out" in
*unavailable-in-region*) echo "no|$region" ;;
2* | 3*) echo "yes|$region" ;;
403*)
# 一直被验证页挡着看不到跳转, 退回按出口地区判断 (只列确定不支持的地区)
case "$region" in
CN | HK | MO | RU | BY | IR | KP | SY | CU) echo "no|$region" ;;
"") echo "fail" ;;
*) echo "yes|$region" ;;
esac
;;
*) echo "fail" ;;
esac
}
probe_gemini() {
local d="$1" code region
code=$(web -o "$d/gm" -w '%{http_code}' https://gemini.google.com/) || code=000
# 页面里带出口的三位国家码 (USA / GBR / HKG)。按国家判断, 只列确定不支持的地区。
# 不用页面里的功能开关判断: 常被引用的 45631641 在英国 (可用) 也是 false, 实为美国功能灰度,
# 2026-09-15 用美 / 英 / 港三地页面对比确认; 其余随地区变化的开关也都是灰度, 随时会变。
region=$(grep -oE ',2,1,200,"[A-Z]{3}"' "$d/gm" 2>/dev/null | head -n1 | cut -d'"' -f2)
if [ "$code" != 200 ] || [ -z "$region" ]; then echo "fail"; return; fi
case "$region" in
CHN | HKG | RUS | BLR | IRN | PRK | SYR | CUB) echo "no|$region" ;;
*) echo "yes|$region" ;;
esac
}
# Disney+: 网页客户端的公开设备注册流程, 最后一步返回出口国家与是否在服务地区 (2026-09-16 洛杉矶 / 香港实测)
DISNEY_CLIENT='ZGlzbmV5JmJyb3dzZXImMS4wLjA.Cu56AgSfBTDag5NiRA81oLHkDZfu5L3CKadnefEAY84'
probe_disney() {
local r a tk rt g region sup
r=$(ccurl -s -m 10 -A "$UA_BROWSER" -X POST https://disney.api.edge.bamgrid.com/devices \
-H "authorization: Bearer $DISNEY_CLIENT" -H 'content-type: application/json; charset=UTF-8' \
-d '{"deviceFamily":"browser","applicationRuntime":"chrome","deviceProfile":"windows","attributes":{}}' 2>/dev/null)
a=$(printf '%s' "$r" | grep -oE '"assertion":"[^"]+"' | cut -d'"' -f4)
[ -n "$a" ] || { echo fail; return; }
tk=$(ccurl -s -m 10 -A "$UA_BROWSER" -X POST https://disney.api.edge.bamgrid.com/token \
-H "authorization: Bearer $DISNEY_CLIENT" \
--data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \
--data-urlencode 'latitude=0' --data-urlencode 'longitude=0' --data-urlencode 'platform=browser' \
--data-urlencode "subject_token=$a" \
--data-urlencode 'subject_token_type=urn:bamtech:params:oauth:token-type:device' 2>/dev/null)
case "$tk" in *forbidden-location*) echo "no|"; return ;; esac
rt=$(printf '%s' "$tk" | grep -oE '"refresh_token":"[^"]+"' | cut -d'"' -f4)
[ -n "$rt" ] || { echo fail; return; }
g=$(ccurl -s -m 10 -A "$UA_BROWSER" -X POST https://disney.api.edge.bamgrid.com/graph/v1/device/graphql \
-H "authorization: $DISNEY_CLIENT" -H 'content-type: application/json' \
-d "{\"query\":\"mutation refreshToken(\$input: RefreshTokenInput!) { refreshToken(refreshToken: \$input) { activeSession { sessionId } } }\",\"variables\":{\"input\":{\"refreshToken\":\"$rt\"}}}" 2>/dev/null)
region=$(printf '%s' "$g" | grep -oE '"countryCode":"[A-Z]{2}"' | head -n1 | cut -d'"' -f4)
sup=$(printf '%s' "$g" | grep -oE '"inSupportedLocation":(true|false)' | head -n1 | cut -d: -f2)
if [ -z "$region" ]; then echo fail
elif [ "$sup" = false ]; then echo "no|$region"
else echo "yes|$region"; fi
}
# Reddit: 被封的出口直接 403, 正常返回页面并带国家码
probe_reddit() {
local d="$1" code region
code=$(web -o "$d/rd" -w '%{http_code}' https://www.reddit.com/svc/shreddit/reddit-chat) || code=000
region=$(grep -oE 'country="[A-Z]{2}"' "$d/rd" 2>/dev/null | head -n1 | cut -d'"' -f2)
case "$code" in
200) echo "yes|$region" ;;
403) echo "no|" ;;
*) echo fail ;;
esac
}
# 各服务检测时访问的主域名, 检测 IPv6 出口时先看它有没有 IPv6 地址
MEDIA_HOSTS="netflix:www.netflix.com disney:disney.api.edge.bamgrid.com youtube:www.youtube.com tiktok:www.tiktok.com prime:www.primevideo.com reddit:www.reddit.com chatgpt:api.openai.com claude:claude.ai gemini:gemini.google.com"
run_media() {
local d="$1" fam="$2" item name host
for item in $MEDIA_HOSTS; do
name=${item%%:*}
host=${item#*:}
mkdir -p "$d/m_$name"
# 网站本身没有 IPv6 (TikTok / Prime Video / Reddit, 2026-09-17 核实) 时 IPv6 出口必然连不上, 记 nov6 而不是检测失败
if [ "$fam" = 6 ] && command -v getent >/dev/null 2>&1 && [ -z "$(getent ahostsv6 "$host" 2>/dev/null | grep -v '::ffff:')" ]; then
put "$d/m_$name" "media_$name" nov6
continue
fi
(put "$d/m_$name" "media_$name" "$("probe_$name" "$d/m_$name")") &
done
wait
}
# ── 邮件: 各大邮箱 MX 的 25 端口握手 ─────────────────────────────────────
# 用 bash 的 /dev/tcp 读 SMTP 欢迎语 (220 开头); 自己计时杀进程, 因为 macOS 没有 timeout 命令。
# /dev/tcp 走系统默认出口, 无法绑定网卡也不能走代理, 所以 -x / -i 模式下跳过。
# 25 不通时再试 587 提交端口, 通则记 mail_<名>_587=ok (只记通的): 区分服务商封 25 还是真的不可达。
# 输出 ok (220 欢迎语) | reject (连上了但回 4xx/5xx, 多为对方按 IP 信誉拒收) | fail (连不上或 10 秒内没有欢迎语)
smtp_banner() {
local host="$1" out="$2" port="${3:-25}" i=0 pid
rm -f "$out"
(exec 3<>"/dev/tcp/$host/$port" && IFS= read -r line <&3 && printf '%s' "$line" >"$out") 2>/dev/null &
pid=$!
# 欢迎语慢的邮局要好几秒 (新浪实测 2.9 秒), 12 家并发时更慢, 等 10 秒
while kill -0 "$pid" 2>/dev/null && [ $i -lt 100 ]; do
sleep 0.1
i=$((i + 1))
done
# 必须 -9: 子 shell 继承了顶层的 TERM trap, 而 bash 要等阻塞中的 connect 返回才执行 trap ——
# 25 端口被静默丢包时 connect 会挂两分钟以上 (2026-09-15 香港机实测整个脚本卡死)
kill -9 "$pid" 2>/dev/null
wait "$pid" 2>/dev/null
case "$(cat "$out" 2>/dev/null)" in
220*) echo ok ;;
[45][0-9][0-9]*) echo reject ;;
*) echo fail ;;
esac
}
MAIL_HOSTS="gmail:gmail-smtp-in.l.google.com outlook:outlook-com.olc.protection.outlook.com yahoo:mta5.am0.yahoodns.net icloud:mx01.mail.icloud.com qq:mx1.qq.com 163:163mx01.mxmail.netease.com mailru:mxs.mail.ru aol:mx-aol.mail.gm0.yahoodns.net gmx:mx00.gmx.net mailcom:mx00.mail.com sohu:sohumx.h.a.sohu.com sina:freemx1.sinamail.sina.com.cn"
run_mail() {
local d="$1" item r a
mkdir -p "$d/mail"
for item in $MAIL_HOSTS; do
(
# 2>/dev/null: macOS 的 bash 3.2 在强杀超时连接后会往 stderr 打 "Killed: 9" 作业通知
r=$(smtp_banner "${item#*:}" "$d/mail/banner_${item%%:*}" 2>/dev/null)
# 连不上的再试一次: 同一出口并发连十几家时, 个别邮局会慢一拍 (2026-09-17 洛杉矶 Yahoo 并发时超时, 单独连 0.2 秒)
if [ "$r" = fail ]; then
sleep 1
r=$(smtp_banner "${item#*:}" "$d/mail/banner_${item%%:*}" 2>/dev/null)
fi
put "$d/mail" "mail_${item%%:*}" "$r"
# 25 不通时试一次 587: 通说明只是服务商封 25, 发信走提交端口仍可用
if [ "$r" = fail ]; then
a=$(smtp_banner "${item#*:}" "$d/mail/banner_${item%%:*}_587" 587 2>/dev/null)
[ "$a" = ok ] && put "$d/mail" "mail_${item%%:*}_587" ok
fi
) &
done
wait