mirror of
http://bgp.hk.skcks.cn:10088/github.com/oneclickvirt/ecs
synced 2026-04-21 05:10:32 +08:00
Compare commits
368 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7b74cbe1e7 | ||
|
|
8137a7ac6a | ||
|
|
04725fef54 | ||
|
|
4e868a384a | ||
|
|
7a4885346b | ||
|
|
21b85b0176 | ||
|
|
8756224376 | ||
|
|
3a4b76ddcd | ||
|
|
a741e293b2 | ||
|
|
e3ca989aac | ||
|
|
0f06aa587d | ||
|
|
f7519a0307 | ||
|
|
6953182cb4 | ||
|
|
22c9eb7611 | ||
|
|
f4d6fe1a8a | ||
|
|
90f7b9a94b | ||
|
|
e5f8f87f7e | ||
|
|
0b87d169a3 | ||
|
|
b8fe9d3d98 | ||
|
|
8f46bd3f4f | ||
|
|
e8c4b2b4a7 | ||
|
|
8016a8fe93 | ||
|
|
bf0030dc49 | ||
|
|
6d02103aba | ||
|
|
5ffb48dcdc | ||
|
|
7c19502950 | ||
|
|
3c434781f5 | ||
|
|
f62636ca3e | ||
|
|
c4b11ae37d | ||
|
|
8f2fe236d5 | ||
|
|
347a0faa7a | ||
|
|
74640e3066 | ||
|
|
3b646eeeda | ||
|
|
eaad433395 | ||
|
|
03af7c423b | ||
|
|
0e96a6499b | ||
|
|
5b44f5f651 | ||
|
|
e4a759fceb | ||
|
|
e5c4b0ce8e | ||
|
|
188a1153e6 | ||
|
|
4c4887f487 | ||
|
|
feac73a427 | ||
|
|
f3048d074c | ||
|
|
58702b54e7 | ||
|
|
1e4d63ef57 | ||
|
|
9f3acacae0 | ||
|
|
e9755f0c20 | ||
|
|
d8b397b31b | ||
|
|
9e22d1bc23 | ||
|
|
284f6b8ba7 | ||
|
|
27203cf850 | ||
|
|
9b6922f55a | ||
|
|
447b25989e | ||
|
|
a50b68d184 | ||
|
|
6b68e643c7 | ||
|
|
14c79b9a89 | ||
|
|
60ae7ec3c3 | ||
|
|
3d78128f6a | ||
|
|
9c67a8d446 | ||
|
|
014dba0ce6 | ||
|
|
b5fdab4b27 | ||
|
|
16c4c2ff92 | ||
|
|
e1991c850f | ||
|
|
0b7c7865ff | ||
|
|
238ea3eb6f | ||
|
|
b859c52ba3 | ||
|
|
f1642843dd | ||
|
|
90ba076afb | ||
|
|
d31d20e16f | ||
|
|
a4084835f9 | ||
|
|
39c2607b42 | ||
|
|
519c0f3e86 | ||
|
|
3b5b8a348e | ||
|
|
858585b4ff | ||
|
|
b891416147 | ||
|
|
8e79568895 | ||
|
|
01aa051c96 | ||
|
|
628a380122 | ||
|
|
2b94d289f3 | ||
|
|
e3676760da | ||
|
|
55fbe111ac | ||
|
|
a37e6b1021 | ||
|
|
06b68ce205 | ||
|
|
9a839de71c | ||
|
|
09afd48ad5 | ||
|
|
cbc5bed3d4 | ||
|
|
834ff78bd9 | ||
|
|
351a4b552f | ||
|
|
d619991f9c | ||
|
|
b065af63d6 | ||
|
|
4d49222094 | ||
|
|
78b5634306 | ||
|
|
a0bc2de61a | ||
|
|
3c45667c55 | ||
|
|
6111554225 | ||
|
|
1bba9cde26 | ||
|
|
280d292c4c | ||
|
|
cf85b67ae4 | ||
|
|
3ebbf186fc | ||
|
|
3ba3301cc7 | ||
|
|
8674a82eb0 | ||
|
|
5116493338 | ||
|
|
c6f3e7a285 | ||
|
|
40f63e6cb1 | ||
|
|
6b42f626b0 | ||
|
|
4503fb55b0 | ||
|
|
cb1634cb68 | ||
|
|
95ece8c28d | ||
|
|
fd44c079b3 | ||
|
|
5c5bd37074 | ||
|
|
1676045e3d | ||
|
|
58e6941bfa | ||
|
|
3989708c4e | ||
|
|
93fd68bf82 | ||
|
|
793c44163a | ||
|
|
b403d71115 | ||
|
|
e4e11dd132 | ||
|
|
1c876f5199 | ||
|
|
fb9ae4d0e0 | ||
|
|
57e87348d4 | ||
|
|
d40bf5a195 | ||
|
|
753738931d | ||
|
|
a702c5416a | ||
|
|
a587d71348 | ||
|
|
110e6d34f9 | ||
|
|
4d18497dd3 | ||
|
|
c676cd83cb | ||
|
|
58e1de4487 | ||
|
|
89eecd2acc | ||
|
|
194dee49fd | ||
|
|
1e88513b8e | ||
|
|
f84023d18b | ||
|
|
2cfd5af3c0 | ||
|
|
ed66e2804a | ||
|
|
174bf303af | ||
|
|
b75f42ffe5 | ||
|
|
56b71ac53f | ||
|
|
1045d3fab8 | ||
|
|
7bd2b59d58 | ||
|
|
cc1da7ea7c | ||
|
|
1a002a1681 | ||
|
|
ee2b55e7eb | ||
|
|
a55cebf94b | ||
|
|
0571a8df13 | ||
|
|
f29a2829f3 | ||
|
|
33b8e0396f | ||
|
|
c259073d1b | ||
|
|
07ebc8cab5 | ||
|
|
1824051e53 | ||
|
|
9f93a2e59d | ||
|
|
8cd09182da | ||
|
|
9bb776d411 | ||
|
|
12f2da9da2 | ||
|
|
7aa70ac1fd | ||
|
|
04ce926582 | ||
|
|
73eb38eed1 | ||
|
|
9bc8a934b1 | ||
|
|
7b729e073b | ||
|
|
111126ae90 | ||
|
|
9397f789be | ||
|
|
5a1dda6483 | ||
|
|
e322c717c0 | ||
|
|
778b33142b | ||
|
|
aa9f361380 | ||
|
|
3236c60359 | ||
|
|
73b0f30ddc | ||
|
|
825da78bd5 | ||
|
|
5d2f3c7f96 | ||
|
|
61247a206e | ||
|
|
f0daad1360 | ||
|
|
2d23fb55a0 | ||
|
|
a73dbf2d0b | ||
|
|
b38dd713d9 | ||
|
|
e66ef1f106 | ||
|
|
f6ee1e40ec | ||
|
|
cb2bf0a7e5 | ||
|
|
ad017db5a6 | ||
|
|
a99f58518a | ||
|
|
2e59bac322 | ||
|
|
4132b1daff | ||
|
|
53296b745a | ||
|
|
74630e9615 | ||
|
|
5ec7924214 | ||
|
|
7a7fdc26a0 | ||
|
|
d4c855de92 | ||
|
|
7c22dee443 | ||
|
|
797496b640 | ||
|
|
5b686abdc8 | ||
|
|
f99a37edbe | ||
|
|
4ff49c8b90 | ||
|
|
1d9257beb3 | ||
|
|
fc6ccb9f92 | ||
|
|
88a2a7fdc9 | ||
|
|
5ff18ed7c7 | ||
|
|
df897db244 | ||
|
|
9a8680491c | ||
|
|
33f81fd6aa | ||
|
|
940703c3f9 | ||
|
|
1c2e9cdab9 | ||
|
|
3e6524fa0e | ||
|
|
026f40dc4c | ||
|
|
110c58d401 | ||
|
|
06e76a9c33 | ||
|
|
6b88a81c02 | ||
|
|
5482506bab | ||
|
|
b7130db8ce | ||
|
|
dc5e3b7852 | ||
|
|
6937e69a0a | ||
|
|
a68d33739c | ||
|
|
94e0441801 | ||
|
|
39be183fda | ||
|
|
dbc1506518 | ||
|
|
149f5673d2 | ||
|
|
c1b7302485 | ||
|
|
bf44ea9324 | ||
|
|
191ddfd668 | ||
|
|
89a99a7428 | ||
|
|
c474c71091 | ||
|
|
43b2c8aca3 | ||
|
|
96117a040e | ||
|
|
c5aeda45bd | ||
|
|
0b2ac51f09 | ||
|
|
ffe1b65a2b | ||
|
|
a4bfd4d143 | ||
|
|
edbcf1c245 | ||
|
|
4c65417ea6 | ||
|
|
2cf7484881 | ||
|
|
d5da2a59b6 | ||
|
|
372deb59eb | ||
|
|
8e4c6dfd3e | ||
|
|
8fc828d416 | ||
|
|
8a3fbd79e6 | ||
|
|
5628f1bb9c | ||
|
|
cadbb2a45c | ||
|
|
56d7560471 | ||
|
|
75e7eb1b25 | ||
|
|
24ba56cfa6 | ||
|
|
ebefd64a3d | ||
|
|
4d83ffea02 | ||
|
|
01a4084462 | ||
|
|
6674093425 | ||
|
|
6d2e56b1ec | ||
|
|
2a736d3e70 | ||
|
|
d02383b8cb | ||
|
|
0caba0ea60 | ||
|
|
6f92b8a994 | ||
|
|
cc34baf9e1 | ||
|
|
84d8963684 | ||
|
|
a94e9a6284 | ||
|
|
06007c191f | ||
|
|
d013b8f90c | ||
|
|
42002fdae1 | ||
|
|
d7628a5a57 | ||
|
|
e5f3ca1ec3 | ||
|
|
e70a295a5f | ||
|
|
58821f2603 | ||
|
|
33a656304b | ||
|
|
33d9a3ccb0 | ||
|
|
1d5758999c | ||
|
|
94ce394e04 | ||
|
|
ede04bd2c2 | ||
|
|
50f6ef1f60 | ||
|
|
e5129ab244 | ||
|
|
15b6ba4eb8 | ||
|
|
6c77f74003 | ||
|
|
61a1508b53 | ||
|
|
022284018d | ||
|
|
1d682213fe | ||
|
|
649a09b50a | ||
|
|
0fa2ff9300 | ||
|
|
b174e5cfa8 | ||
|
|
afd667db59 | ||
|
|
d846dcbf4f | ||
|
|
f47b1e3023 | ||
|
|
63658bb2dc | ||
|
|
b1a8368af3 | ||
|
|
df6d1236cc | ||
|
|
7c64102581 | ||
|
|
58cb4f3831 | ||
|
|
20bddae048 | ||
|
|
3c0590ca8d | ||
|
|
a752eeeeb0 | ||
|
|
630a28f2f2 | ||
|
|
cfd70e100b | ||
|
|
9114f5b97a | ||
|
|
8c0fc16384 | ||
|
|
2ded570639 | ||
|
|
42f0cb3399 | ||
|
|
14adbddeb9 | ||
|
|
27fd06b007 | ||
|
|
88db8df827 | ||
|
|
ec728796f4 | ||
|
|
147e8c1113 | ||
|
|
b9b1d5ca76 | ||
|
|
1500c8342e | ||
|
|
e523ca3c84 | ||
|
|
42943370bb | ||
|
|
7339f0336c | ||
|
|
54bbe16563 | ||
|
|
2a66452f40 | ||
|
|
aab6bf197d | ||
|
|
9206088bad | ||
|
|
48e150036d | ||
|
|
486b767a25 | ||
|
|
5a2e68bf92 | ||
|
|
97d05f4b57 | ||
|
|
4f08a33de8 | ||
|
|
ff8712a743 | ||
|
|
abd38554b6 | ||
|
|
918a9b3a46 | ||
|
|
d9dac50487 | ||
|
|
cd65f04433 | ||
|
|
8b5193eca1 | ||
|
|
4797ff0b34 | ||
|
|
9886cad73e | ||
|
|
ca0470f01a | ||
|
|
5a6d6845c1 | ||
|
|
487dd7c1d2 | ||
|
|
2dbf97de8c | ||
|
|
97e7cae2c2 | ||
|
|
d61a1879f5 | ||
|
|
29dd4ac57e | ||
|
|
dc3eff1fe3 | ||
|
|
0a0f2199bc | ||
|
|
91004d87f5 | ||
|
|
8f41c37203 | ||
|
|
12b1ae0702 | ||
|
|
653cd75a97 | ||
|
|
ea36e88c9f | ||
|
|
c81ebb3c7a | ||
|
|
7896b3ead5 | ||
|
|
eb98a7b857 | ||
|
|
d4d86229de | ||
|
|
651a183382 | ||
|
|
afc313a2a8 | ||
|
|
39ac8d198d | ||
|
|
a70dc2bab1 | ||
|
|
5041a16a9a | ||
|
|
21deb3587e | ||
|
|
3bac30edc2 | ||
|
|
9ef2ec4a9e | ||
|
|
ac33e00e0a | ||
|
|
7a439f7095 | ||
|
|
e3bfa65f66 | ||
|
|
74e33a212c | ||
|
|
bba8595033 | ||
|
|
530181be87 | ||
|
|
7deb986209 | ||
|
|
ea8e5efbd3 | ||
|
|
db88ee8479 | ||
|
|
67a3bfdaad | ||
|
|
e57ce05c0c | ||
|
|
62137f0bb1 | ||
|
|
f9d8c9ee4c | ||
|
|
913a1725c1 | ||
|
|
f1acbd361b | ||
|
|
d6f62f8624 | ||
|
|
94da890522 | ||
|
|
42b94260de | ||
|
|
16e2603176 | ||
|
|
0ed9840038 | ||
|
|
31b53e13b0 | ||
|
|
fb697fa25f | ||
|
|
2510b3ead0 | ||
|
|
b64b8a2d96 | ||
|
|
663614117d | ||
|
|
a96c9b6c7d | ||
|
|
e8f73ba4b8 |
310
.back/create_public_branch.py
Executable file
310
.back/create_public_branch.py
Executable file
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Script to create public branch by removing security dependencies and references.
|
||||
This script properly handles Go file modifications to ensure the code can compile.
|
||||
"""
|
||||
|
||||
import re
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
|
||||
|
||||
def read_file(filepath):
|
||||
"""Read file content."""
|
||||
with open(filepath, 'r', encoding='utf-8') as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def write_file(filepath, content):
|
||||
"""Write content to file."""
|
||||
with open(filepath, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
def modify_go_mod(filepath):
|
||||
"""
|
||||
Modify go.mod to remove privatespeedtest (and optional security) dependencies.
|
||||
Automatically matches module names regardless of version or indirect comment.
|
||||
"""
|
||||
content = read_file(filepath)
|
||||
|
||||
# Modules to remove
|
||||
remove_modules = [
|
||||
r'github\.com/oneclickvirt/privatespeedtest',
|
||||
r'github\.com/oneclickvirt/security',
|
||||
]
|
||||
|
||||
for mod in remove_modules:
|
||||
# Remove full require line (with or without // indirect)
|
||||
content = re.sub(
|
||||
rf'^[ \t]*{mod}[ \t]+v[^\s]+(?:[ \t]+// indirect)?[ \t]*\n',
|
||||
'',
|
||||
content,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
|
||||
write_file(filepath, content)
|
||||
print(f"✓ Removed privatespeedtest/security from {filepath}")
|
||||
|
||||
|
||||
def remove_code_block(lines, start_marker, end_condition='empty_line'):
|
||||
"""
|
||||
Remove code block from lines starting with start_marker.
|
||||
|
||||
Args:
|
||||
lines: List of file lines
|
||||
start_marker: String or list of strings to identify block start
|
||||
end_condition: 'empty_line' (default) or 'closing_brace' or custom function
|
||||
|
||||
Returns:
|
||||
Modified lines with the block removed
|
||||
"""
|
||||
if isinstance(start_marker, str):
|
||||
start_marker = [start_marker]
|
||||
|
||||
result = []
|
||||
skip_mode = False
|
||||
brace_depth = 0
|
||||
|
||||
i = 0
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
|
||||
# Check if we should start skipping
|
||||
if not skip_mode:
|
||||
for marker in start_marker:
|
||||
if marker in line:
|
||||
skip_mode = True
|
||||
if end_condition == 'closing_brace':
|
||||
# Count opening braces on the function declaration line
|
||||
brace_depth = line.count('{') - line.count('}')
|
||||
break
|
||||
|
||||
if not skip_mode:
|
||||
result.append(line)
|
||||
else:
|
||||
# We're in skip mode
|
||||
if end_condition == 'empty_line':
|
||||
# Skip until we find an empty line
|
||||
if line.strip() == '':
|
||||
skip_mode = False
|
||||
# Don't add the empty line, continue to next
|
||||
elif end_condition == 'closing_brace':
|
||||
# Track brace depth
|
||||
brace_depth += line.count('{') - line.count('}')
|
||||
if brace_depth == 0 and '}' in line:
|
||||
# Function ended, skip until next empty line
|
||||
end_condition = 'empty_line'
|
||||
|
||||
i += 1
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def modify_speed_go(filepath):
|
||||
"""
|
||||
Remove privatespeedtest-related code from speed.go.
|
||||
Uses line-by-line processing for reliability.
|
||||
"""
|
||||
content = read_file(filepath)
|
||||
lines = content.split('\n')
|
||||
|
||||
# Remove specific code blocks by their comment markers
|
||||
blocks_to_remove = [
|
||||
'// formatString 格式化字符串到指定宽度',
|
||||
'// printTableRow 打印表格行',
|
||||
'// privateSpeedTest 使用 privatespeedtest 进行单个运营商测速',
|
||||
'// privateSpeedTestWithFallback 使用私有测速,如果失败则回退到 global 节点',
|
||||
'// 对于三网测速(cmcc、cu、ct)和 other,优先使用 privatespeedtest 进行私有测速',
|
||||
]
|
||||
|
||||
for block_marker in blocks_to_remove:
|
||||
lines = remove_code_block(lines, block_marker)
|
||||
|
||||
# Reconstruct content
|
||||
content = '\n'.join(lines)
|
||||
|
||||
# Remove privatespeedtest import
|
||||
content = re.sub(
|
||||
r'\n\s*"github\.com/oneclickvirt/privatespeedtest/pst"\s*\n',
|
||||
'\n',
|
||||
content,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
|
||||
# Remove time import (only used by privatespeedtest)
|
||||
content = re.sub(
|
||||
r'\n\s*"time"\s*\n',
|
||||
'\n',
|
||||
content,
|
||||
flags=re.MULTILINE
|
||||
)
|
||||
|
||||
# Clean up multiple consecutive empty lines (optional)
|
||||
content = re.sub(r'\n{3,}', '\n\n', content)
|
||||
|
||||
write_file(filepath, content)
|
||||
print(f"✓ Removed privatespeedtest from {filepath}")
|
||||
|
||||
def modify_utils_go(filepath):
|
||||
"""
|
||||
Modify utils/utils.go to:
|
||||
1. Replace security/network import with basics/network
|
||||
2. Replace SecurityUploadToken usage with hardcoded token
|
||||
"""
|
||||
content = read_file(filepath)
|
||||
|
||||
# Replace import
|
||||
content = re.sub(
|
||||
r'"github\.com/oneclickvirt/security/network"',
|
||||
r'"github.com/oneclickvirt/basics/network"',
|
||||
content
|
||||
)
|
||||
|
||||
# Replace token usage - find the exact line and replace it
|
||||
content = re.sub(
|
||||
r'\ttoken := network\.SecurityUploadToken',
|
||||
r'\ttoken := "OvwKx5qgJtf7PZgCKbtyojSU.MTcwMTUxNzY1MTgwMw"',
|
||||
content
|
||||
)
|
||||
|
||||
# Update title for public version
|
||||
content = re.sub(
|
||||
r'VPS融合怪测试',
|
||||
r'VPS融合怪测试(非官方编译)',
|
||||
content
|
||||
)
|
||||
content = re.sub(
|
||||
r'VPS Fusion Monster Test',
|
||||
r'VPS Fusion Monster Test (Unofficial)',
|
||||
content
|
||||
)
|
||||
|
||||
write_file(filepath, content)
|
||||
print(f"✓ Modified {filepath}")
|
||||
|
||||
|
||||
def modify_params_go(filepath):
|
||||
"""
|
||||
Modify internal/params/params.go to change security flag default to false.
|
||||
"""
|
||||
content = read_file(filepath)
|
||||
|
||||
# Change default value in struct initialization
|
||||
content = re.sub(
|
||||
r'(\s+SecurityTestStatus:\s+)true,',
|
||||
r'\1false,',
|
||||
content
|
||||
)
|
||||
|
||||
# Change flag default value
|
||||
content = re.sub(
|
||||
r'(c\.GoecsFlag\.BoolVar\(&c\.SecurityTestStatus, "security", )true(, "Enable/Disable security test"\))',
|
||||
r'\1false\2',
|
||||
content
|
||||
)
|
||||
|
||||
write_file(filepath, content)
|
||||
print(f"✓ Modified {filepath}")
|
||||
|
||||
def modify_readme(filepath, is_english=False):
|
||||
"""
|
||||
Modify README files to update Go version and security status.
|
||||
"""
|
||||
content = read_file(filepath)
|
||||
|
||||
# Extract Go version from go.mod
|
||||
go_mod_content = read_file('go.mod')
|
||||
go_version_match = re.search(r'^go (\d+\.\d+(?:\.\d+)?)', go_mod_content, re.MULTILINE)
|
||||
|
||||
if not go_version_match:
|
||||
print(f"⚠ Warning: Could not extract Go version from go.mod")
|
||||
return
|
||||
|
||||
go_version = go_version_match.group(1)
|
||||
|
||||
if is_english:
|
||||
# Update Go version in English README
|
||||
content = re.sub(
|
||||
r'Select go \d+\.\d+\.\d+ version to install',
|
||||
f'Select go {go_version} version to install',
|
||||
content
|
||||
)
|
||||
|
||||
# Update security status
|
||||
content = re.sub(
|
||||
r', binary files compiled in \[securityCheck\][^\)]*\)',
|
||||
', but open sourced',
|
||||
content
|
||||
)
|
||||
|
||||
# Update help text for security flag
|
||||
content = re.sub(
|
||||
r'security\s+Enable/Disable security test \(default true\)',
|
||||
'security Enable/Disable security test (default false)',
|
||||
content
|
||||
)
|
||||
else:
|
||||
# Update Go version in Chinese README
|
||||
content = re.sub(
|
||||
r'选择 go \d+\.\d+\.\d+ 的版本进行安装',
|
||||
f'选择 go {go_version} 的版本进行安装',
|
||||
content
|
||||
)
|
||||
|
||||
# Update security status
|
||||
content = re.sub(
|
||||
r'二进制文件编译至 \[securityCheck\][^\)]*\)',
|
||||
'但已开源',
|
||||
content
|
||||
)
|
||||
|
||||
# Update help text for security flag
|
||||
content = re.sub(
|
||||
r'security\s+Enable/Disable security test \(default true\)',
|
||||
'security Enable/Disable security test (default false)',
|
||||
content
|
||||
)
|
||||
|
||||
write_file(filepath, content)
|
||||
print(f"✓ Modified {filepath}")
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to process all files."""
|
||||
print("Starting public branch creation process...")
|
||||
print()
|
||||
|
||||
# Check if we're in the right directory
|
||||
if not os.path.exists('go.mod'):
|
||||
print("Error: go.mod not found. Please run this script from the project root.")
|
||||
sys.exit(1)
|
||||
|
||||
# Modify Go source files
|
||||
print("Modifying Go source files...")
|
||||
modify_speed_go('internal/tests/speed.go')
|
||||
modify_utils_go('utils/utils.go')
|
||||
modify_params_go('internal/params/params.go')
|
||||
print()
|
||||
|
||||
# Modify go.mod
|
||||
print("Modifying go.mod...")
|
||||
modify_go_mod('go.mod')
|
||||
print()
|
||||
|
||||
# Modify README files
|
||||
print("Modifying README files...")
|
||||
modify_readme('README_ZH.md', is_english=False)
|
||||
modify_readme('README.md', is_english=True)
|
||||
print()
|
||||
|
||||
print("✓ All modifications completed successfully!")
|
||||
print()
|
||||
print("Next steps:")
|
||||
print("1. Run 'go mod tidy' to clean up dependencies")
|
||||
print("2. Run 'go build -o maintest' to verify compilation")
|
||||
print("3. Test the binary with: ./maintest -menu=false -l en -security=false -upload=false")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
99
.github/workflows/build_binary.yaml
vendored
Normal file
99
.github/workflows/build_binary.yaml
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
# 1.20 是 Windows 7/8 Server 2008/2012 最后一个支持版本
|
||||
image: goreleaser/goreleaser-cross:v1.20
|
||||
steps:
|
||||
- run: |
|
||||
git config --global --add safe.directory /__w/ecs/ecs
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Free Disk Space (Ubuntu)
|
||||
uses: jlumbroso/free-disk-space@main
|
||||
with:
|
||||
# this might remove tools that are actually needed,
|
||||
# if set to "true" but frees about 6 GB
|
||||
tool-cache: false
|
||||
|
||||
# all of these default to true, but feel free to set to
|
||||
# "false" if necessary for your workflow
|
||||
android: true
|
||||
dotnet: true
|
||||
haskell: true
|
||||
large-packages: false
|
||||
docker-images: false
|
||||
swap-storage: false
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.25.4
|
||||
|
||||
# - name: Install UPX
|
||||
# run: |
|
||||
# apk add --no-cache upx
|
||||
|
||||
- name: Configure Git for Private Modules
|
||||
run: |
|
||||
git config --global url."https://${{ secrets.GHT }}@github.com/".insteadOf "https://github.com/"
|
||||
git config --global url."git@github.com:".insteadOf "https://github.com/"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GHT }}
|
||||
|
||||
- name: Clean Go cache before build
|
||||
run: |
|
||||
go clean -cache -modcache -testcache
|
||||
df -h
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v6
|
||||
with:
|
||||
distribution: goreleaser
|
||||
# version: latest
|
||||
version: '~> v2'
|
||||
args: release --clean
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GHT }}
|
||||
GOPRIVATE: github.com/oneclickvirt/security,github.com/oneclickvirt/privatespeedtest
|
||||
|
||||
- name: Update goecs.sh with new version
|
||||
run: |
|
||||
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
|
||||
VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
else
|
||||
VERSION=$(git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.1.37")
|
||||
fi
|
||||
echo "Using version: $VERSION"
|
||||
FILE="goecs.sh"
|
||||
BRANCH="master"
|
||||
git config --global user.name "github-actions[bot]"
|
||||
git config --global user.email "github-actions[bot]@users.noreply.github.com"
|
||||
git config --global --unset url."git@github.com:".insteadOf || true
|
||||
git fetch origin $BRANCH
|
||||
git checkout $BRANCH
|
||||
if [ ! -f "$FILE" ]; then
|
||||
echo "Error: $FILE not found"
|
||||
exit 1
|
||||
fi
|
||||
sed -i "s/\(_yellow \"Unable to get version info, using default version \).*\(\".*\)/\1$VERSION\2/" "$FILE"
|
||||
sed -i "s/\(ECS_VERSION=\"\).*\(\"\)/\1$VERSION\2/" "$FILE"
|
||||
if git diff --quiet "$FILE"; then
|
||||
echo "No changes detected in $FILE"
|
||||
exit 0
|
||||
fi
|
||||
git add "$FILE"
|
||||
git commit -m "chore: update ECS_VERSION to $VERSION in goecs.sh"
|
||||
git push origin $BRANCH
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GHT }}
|
||||
43
.github/workflows/build_docker.yaml
vendored
43
.github/workflows/build_docker.yaml
vendored
@@ -1,44 +1,61 @@
|
||||
name: Build and Push Docker Image
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build and Release"]
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
with:
|
||||
platforms: all
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to CNB Registry
|
||||
# - name: Login to CNB Registry
|
||||
# uses: docker/login-action@v2
|
||||
# with:
|
||||
# registry: ${{ secrets.CNB_DOCKER_REGISTRY }}
|
||||
# username: ${{ secrets.CNB_USERNAME }}
|
||||
# password: ${{ secrets.CNB_TOKEN }}
|
||||
|
||||
- name: Login to Aliyun Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ secrets.CNB_DOCKER_REGISTRY }}
|
||||
username: ${{ secrets.CNB_USERNAME }}
|
||||
password: ${{ secrets.CNB_TOKEN }}
|
||||
|
||||
registry: crpi-8tmognxgyb86bm61.cn-guangzhou.personal.cr.aliyuncs.com
|
||||
username: ${{ secrets.ALIYUN_USERNAME }}
|
||||
password: ${{ secrets.ALIYUN_PASSWORD }}
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push Docker images
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64,linux/386,linux/riscv64
|
||||
# linux/mips,linux/mipsle 暂不支持 alpine, linux/s390x 编译卡死
|
||||
# linux/mips,linux/mipsle 暂不支持 alpine, linux/s390x 编译卡死,cnb组织空间不足无法推送
|
||||
# ${{ secrets.CNB_DOCKER_REGISTRY }}/oneclickvirt/ecs:latest
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_USERNAME }}/goecs:latest
|
||||
${{ secrets.CNB_DOCKER_REGISTRY }}/oneclickvirt/ecs:latest
|
||||
crpi-8tmognxgyb86bm61.cn-guangzhou.personal.cr.aliyuncs.com/oneclickvirt/ecs:latest
|
||||
ghcr.io/${{ github.repository_owner }}/goecs:latest
|
||||
|
||||
84
.github/workflows/build_public.yml
vendored
Normal file
84
.github/workflows/build_public.yml
vendored
Normal file
@@ -0,0 +1,84 @@
|
||||
name: Public Build
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build and Release"]
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.25.4'
|
||||
|
||||
- name: Update master branch README files
|
||||
run: |
|
||||
git config --global user.name 'GitHub Actions'
|
||||
git config --global user.email 'actions@github.com'
|
||||
|
||||
if [ -f "go.mod" ]; then
|
||||
GO_VERSION=$(grep "^go " go.mod | head -n 1 | awk '{print $2}')
|
||||
echo "提取到的 Go 版本: $GO_VERSION"
|
||||
if [ -n "$GO_VERSION" ] && [[ "$GO_VERSION" =~ ^[0-9]+\.[0-9]+(\.[0-9]+)?$ ]]; then
|
||||
echo "版本验证成功,开始替换..."
|
||||
if [ -f "README.md" ]; then
|
||||
sed -i "s/选择 go [0-9]\+\.[0-9]\+\.[0-9]\+ 的版本进行安装/选择 go $GO_VERSION 的版本进行安装/g" README.md
|
||||
sed -i 's|但二进制文件编译至 \[securityCheck\].*)|但已开源|g' README.md
|
||||
sed -i 's|security.*Enable/Disable security test (default true)|security Enable/Disable security test (default false)|g' README.md
|
||||
echo "已更新 README.md"
|
||||
fi
|
||||
if [ -f "README_EN.md" ]; then
|
||||
sed -i "s/Select go [0-9]\+\.[0-9]\+\.[0-9]\+ version to install/Select go $GO_VERSION version to install/g" README_EN.md
|
||||
sed -i 's|but binary files compiled in \[securityCheck\].*)|but open sourced|g' README_EN.md
|
||||
sed -i 's|security.*Enable/Disable security test (default true)|security Enable/Disable security test (default false)|g' README_EN.md
|
||||
echo "已更新 README_EN.md"
|
||||
fi
|
||||
git add README.md
|
||||
[ -f "README_EN.md" ] && git add README_EN.md || true
|
||||
git commit -m "Auto update README files" || echo "No changes to commit"
|
||||
git push origin ${{ github.ref_name }}
|
||||
else
|
||||
echo "错误:未能提取到有效的 Go 版本号或版本号格式不正确"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "错误:未找到 go.mod 文件"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
- name: Create public branch
|
||||
run: |
|
||||
# 删除本地 public 分支(如果存在)
|
||||
git branch -D public 2>/dev/null || true
|
||||
# 基于当前分支创建新的 public 分支(完全覆盖)
|
||||
git checkout -b public
|
||||
|
||||
- name: Remove security package references
|
||||
run: |
|
||||
python3 .back/create_public_branch.py
|
||||
rm -f go.sum
|
||||
go clean -modcache
|
||||
go clean -cache -testcache -fuzzcache
|
||||
go mod tidy
|
||||
- name: Update Go version in README files
|
||||
run: |
|
||||
# This step is now handled by the Python script
|
||||
echo "README files already updated by create_public_branch.py"
|
||||
- name: Build and Test
|
||||
run: |
|
||||
go build -o maintest
|
||||
./maintest -menu=false -l en -security=false -upload=false || exit 1
|
||||
rm -rf maintest
|
||||
|
||||
- name: Commit and push changes
|
||||
run: |
|
||||
git add .
|
||||
git commit -m "Auto update public version (no security package)" || echo "No changes to commit"
|
||||
git push -f origin public
|
||||
42
.github/workflows/main.yaml
vendored
42
.github/workflows/main.yaml
vendored
@@ -1,42 +0,0 @@
|
||||
name: goreleaser
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
# 1.20 是 Windows 7/8 Server 2008/2012 最后一个支持版本
|
||||
image: goreleaser/goreleaser-cross:v1.20
|
||||
steps:
|
||||
- run: |
|
||||
git config --global --add safe.directory /__w/ecs/ecs
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: 1.23.4
|
||||
|
||||
- name: Configure Git for Private Modules
|
||||
run: |
|
||||
git config --global url."https://${{ secrets.GHT }}@github.com/".insteadOf "https://github.com/"
|
||||
git config --global url."git@github.com:".insteadOf "https://github.com/"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GHT }}
|
||||
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GHT }}
|
||||
GOPRIVATE: github.com/oneclickvirt/security
|
||||
79
.github/workflows/public-build.yml
vendored
79
.github/workflows/public-build.yml
vendored
@@ -1,79 +0,0 @@
|
||||
name: Public Build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23.4'
|
||||
|
||||
- name: Create public branch
|
||||
run: |
|
||||
git config --global user.name 'GitHub Actions'
|
||||
git config --global user.email 'actions@github.com'
|
||||
git checkout -b public || git checkout public
|
||||
git merge ${{ github.ref_name }} --no-edit || true
|
||||
|
||||
- name: Remove security package references
|
||||
run: |
|
||||
# 移除 network 包中对 security 的引用
|
||||
find . -type f -name "*.go" -exec sed -i 's|"github.com/oneclickvirt/security/network"|"github.com/oneclickvirt/basics/network"|g' {} +
|
||||
# 修改 back/network/network.go
|
||||
cat > back/network/network.go << 'EOF'
|
||||
package network1
|
||||
|
||||
import "github.com/oneclickvirt/basics/network"
|
||||
|
||||
func NetworkCheck(checkType string, enableSecurityCheck bool, language string) (string, string, error) {
|
||||
ipInfo, _, err := network.NetworkCheck(checkType, false, language)
|
||||
return ipInfo, "", err
|
||||
}
|
||||
EOF
|
||||
|
||||
# 修改 utils/utils.go 中的 BasicsAndSecurityCheck 函数
|
||||
sed -i '/SecurityUploadToken/d' utils/utils.go
|
||||
sed -i 's|"github.com/oneclickvirt/security/network"|"github.com/oneclickvirt/basics/network"|g' utils/utils.go
|
||||
|
||||
# 在 utils/utils.go 中添加 token 常量(在 import 语句之后)
|
||||
sed -i '/^import/,/^)/{/^)/a\'$'\n''const token = "OvwKx5qgJtf7PZgCKbtyojSU.MTcwMTUxNzY1MTgwMw"'$'\n''}' utils/utils.go
|
||||
|
||||
# 修改 go.mod,移除私有仓库依赖
|
||||
sed -i '/github.com\/oneclickvirt\/security/d' go.mod
|
||||
|
||||
# 修改 goecs.go,禁用 security 检测
|
||||
sed -i 's|var securityFlag = flag.Bool("security", true,|var securityFlag = flag.Bool("security", false,|g' goecs.go
|
||||
|
||||
# 更新依赖
|
||||
go mod tidy
|
||||
|
||||
# 修改 README.md 和 README_EN.md 中的敏感信息
|
||||
sed -i 's|但二进制文件编译至 \[securityCheck\].*)|但已开源|g' README.md
|
||||
sed -i 's|but binary files compiled in \[securityCheck\].*)|but open sourced|g' README_EN.md
|
||||
|
||||
# 修改命令行帮助信息
|
||||
sed -i 's|security.*Enable/Disable security test (default true)|security Enable/Disable security test (default false)|g' README.md
|
||||
sed -i 's|security.*Enable/Disable security test (default true)|security Enable/Disable security test (default false)|g' README_EN.md
|
||||
|
||||
- name: Build and Test
|
||||
run: |
|
||||
# 构建二进制文件
|
||||
go build -o main
|
||||
|
||||
# 测试无菜单模式是否正常运行(禁用 security 检测)
|
||||
./main -menu=false -l en -security=false -upload=false || exit 1
|
||||
|
||||
- name: Commit and push changes
|
||||
run: |
|
||||
git add .
|
||||
git commit -m "Auto update public version (no security package)" || echo "No changes to commit"
|
||||
git push -f origin public
|
||||
9
.github/workflows/sync.yaml
vendored
9
.github/workflows/sync.yaml
vendored
@@ -1,8 +1,10 @@
|
||||
name: Sync Latest Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
workflow_run:
|
||||
workflows: ["Build and Release"]
|
||||
types:
|
||||
- completed
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
@@ -30,8 +32,9 @@ jobs:
|
||||
- name: Copy repository files
|
||||
run: |
|
||||
cp goecs.sh temp_repo/
|
||||
cp README_EN.md temp_repo/
|
||||
cp README.md temp_repo/
|
||||
[ -f "README_EN.md" ] && cp README_EN.md temp_repo/ || true
|
||||
[ -f "README_ZH.md" ] && cp README_ZH.md temp_repo/ || true
|
||||
cp LICENSE temp_repo/
|
||||
|
||||
- name: Download release assets
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,2 +1,5 @@
|
||||
vendor/
|
||||
.idea/
|
||||
.idea/
|
||||
ecs
|
||||
goecs.txt
|
||||
*.log
|
||||
@@ -1,6 +1,10 @@
|
||||
before:
|
||||
hooks:
|
||||
- go mod tidy -v
|
||||
- go clean -cache
|
||||
|
||||
project_name: goecs
|
||||
|
||||
builds:
|
||||
- id: universal
|
||||
env:
|
||||
@@ -18,6 +22,10 @@ builds:
|
||||
- amd64
|
||||
- mips
|
||||
- mipsle
|
||||
- mips64
|
||||
- mips64le
|
||||
- ppc64
|
||||
- ppc64le
|
||||
- s390x
|
||||
- riscv64
|
||||
gomips:
|
||||
@@ -27,6 +35,7 @@ builds:
|
||||
goarch: arm
|
||||
main: ./
|
||||
binary: goecs
|
||||
|
||||
- id: darwin-amd64
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
@@ -40,6 +49,7 @@ builds:
|
||||
- amd64
|
||||
main: ./
|
||||
binary: goecs
|
||||
|
||||
- id: darwin-arm64
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
@@ -53,18 +63,23 @@ builds:
|
||||
- arm64
|
||||
main: ./
|
||||
binary: goecs
|
||||
|
||||
universal_binaries:
|
||||
- name_template: "goecs"
|
||||
replace: false
|
||||
|
||||
checksum:
|
||||
name_template: "checksums.txt"
|
||||
|
||||
snapshot:
|
||||
name_template: "goecs"
|
||||
|
||||
archives:
|
||||
- name_template: "goecs_{{ .Os }}_{{ .Arch }}"
|
||||
format: zip
|
||||
files:
|
||||
- none*
|
||||
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
@@ -75,4 +90,18 @@ changelog:
|
||||
- Merge pull request
|
||||
- Merge branch
|
||||
- go mod tidy
|
||||
- New translations
|
||||
- New translations
|
||||
|
||||
upx:
|
||||
- enabled: true
|
||||
brute: true
|
||||
goos:
|
||||
- linux
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
- 386
|
||||
- arm64
|
||||
- ppc64le
|
||||
- s390x
|
||||
- riscv64
|
||||
|
||||
24
Dockerfile
24
Dockerfile
@@ -1,10 +1,24 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM alpine:latest
|
||||
# 安装必要的工具
|
||||
RUN apk add --no-cache wget curl bash
|
||||
RUN apk add --no-cache bind-tools
|
||||
# --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main
|
||||
RUN apk add --no-cache grep openssl ca-certificates uuidgen
|
||||
|
||||
RUN apk update && apk add --no-cache wget curl bash || \
|
||||
(echo "Standard repo failed, trying edge repo..." && \
|
||||
apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main wget curl bash)
|
||||
|
||||
RUN apk add --no-cache bind-tools || \
|
||||
(echo "Standard repo failed for bind-tools, trying edge repo..." && \
|
||||
apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main bind-tools)
|
||||
|
||||
RUN apk add --no-cache grep openssl ca-certificates || \
|
||||
(echo "Standard repo failed, trying edge repo..." && \
|
||||
apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main grep openssl ca-certificates)
|
||||
|
||||
RUN apk add --no-cache uuidgen || \
|
||||
apk add --no-cache util-linux || \
|
||||
(echo "Standard repo failed for uuidgen, trying edge repo..." && \
|
||||
apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main uuidgen) || \
|
||||
apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/main util-linux
|
||||
|
||||
RUN export noninteractive=true
|
||||
# 下载并执行 goecs.sh 脚本
|
||||
RUN curl -L https://raw.githubusercontent.com/oneclickvirt/ecs/master/goecs.sh -o goecs.sh && \
|
||||
|
||||
329
README.md
329
README.md
@@ -1,173 +1,189 @@
|
||||
# ECS
|
||||
|
||||
[](https://github.com/oneclickvirt/ecs/actions/workflows/main.yaml)
|
||||
[](https://github.com/oneclickvirt/ecs/actions/workflows/build_binary.yaml)
|
||||
|
||||
[](https://hits.spiritlhl.net)
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Foneclickvirt%2Fecs?ref=badge_shield)
|
||||
|
||||
融合怪测评项目 - GO版本
|
||||
[](https://hits.spiritlhl.net) [](https://github.com/oneclickvirt/ecs/releases)
|
||||
|
||||
(除非必须的环境安装使用shell外无额外shell文件依赖,环境安装只是为了测的更准,极端情况下无环境依赖也可全测项目)
|
||||
Fusion Monster Evaluation Project - GO Version
|
||||
|
||||
如有问题请 [issues](https://github.com/oneclickvirt/ecs/issues) 反馈。
|
||||
(No additional shell file dependencies unless necessary to install the environment using the shell, the environment is installed just to measure more accurately, in extreme cases no environment dependencies can also be fully measured project)
|
||||
|
||||
Go 版本:[https://github.com/oneclickvirt/ecs](https://github.com/oneclickvirt/ecs)
|
||||
Please report any issues via [issues](https://github.com/oneclickvirt/ecs/issues).
|
||||
|
||||
Shell 版本:[https://github.com/spiritLHLS/ecs](https://github.com/spiritLHLS/ecs)
|
||||
Go version: [https://github.com/oneclickvirt/ecs](https://github.com/oneclickvirt/ecs)
|
||||
|
||||
Shell version: [https://github.com/spiritLHLS/ecs/blob/main/README_EN.md](https://github.com/spiritLHLS/ecs/blob/main/README_EN.md)
|
||||
|
||||
---
|
||||
|
||||
## **语言**
|
||||
## **Language**
|
||||
|
||||
[中文文档](README.md) | [English Docs](README_EN.md)
|
||||
[English Docs](README.md) | [中文文档](README_ZH.md)
|
||||
|
||||
---
|
||||
|
||||
## **适配系统和架构**
|
||||
## **Supported Systems and Architectures**
|
||||
|
||||
### **编译与测试支持情况**
|
||||
| 编译支持的架构 | 测试支持的架构 | 编译支持的系统 | 测试支持的系统 |
|
||||
|---------------------------|-----------|---------|-----------|
|
||||
| amd64 | amd64 | Linux | Linux |
|
||||
| arm | arm | Windows | Windows |
|
||||
| arm64 | arm64 | FreeBSD | FreeBSD |
|
||||
| 386 | 386 | OpenBSD | |
|
||||
| mips | | MacOS | |
|
||||
| mipsle | | | |
|
||||
| s390x | s390x | | |
|
||||
| riscv64 | | | |
|
||||
### **Compilation and Testing Support**
|
||||
| Supported for Compilation | Tested on | Supported OS for Compilation | Tested OS |
|
||||
|---------------------------|-----------|------------------------------|-----------|
|
||||
| amd64 | amd64 | Linux | Linux |
|
||||
| arm64 | arm64 | Windows | Windows |
|
||||
| arm | | MacOS(Darwin) | MacOS |
|
||||
| 386 | | FreeBSD | |
|
||||
| mips,mipsle | | Android | |
|
||||
| mips64,mips64le | | | |
|
||||
| ppc64,ppc64le | | | |
|
||||
| s390x | s390x | | |
|
||||
| riscv64 | | | |
|
||||
|
||||
> For more information about the architecture and system, please test or compile it yourself, and open issues if you have any questions.
|
||||
|
||||
> 更多架构与系统请自行测试,如有问题请开 issues。
|
||||
### **Systems Pending Support**
|
||||
|
||||
### **待支持的系统**
|
||||
| 系统 | 说明 |
|
||||
|-----|--------------------------|
|
||||
| MacOS | 存在硬件测试 BUG 未修复,存在环境依赖未修复 |
|
||||
| OS | Notes |
|
||||
|--------|-------------------------------------------------------------------------------------------------|
|
||||
| OpenBSD/NetBSD | Some of Golang's official libraries do not support this system (especially net-related items) |
|
||||
|
||||
---
|
||||
|
||||
## **功能**
|
||||
## **Features**
|
||||
|
||||
- 系统基础信息查询,IP基础信息并发查询:[basics](https://github.com/oneclickvirt/basics)、[gostun](https://github.com/oneclickvirt/gostun)
|
||||
- CPU 测试:[cputest](https://github.com/oneclickvirt/cputest),支持 sysbench(lua/golang版本)、geekbench、winsat
|
||||
- 内存测试:[memorytest](https://github.com/oneclickvirt/memorytest),支持 sysbench、dd
|
||||
- 硬盘测试:[disktest](https://github.com/oneclickvirt/disktest),支持 dd、fio、winsat
|
||||
- 流媒体解锁信息并发查询:[netflix-verify](https://github.com/sjlleo/netflix-verify) 等逻辑,开发至 [CommonMediaTests](https://github.com/oneclickvirt/CommonMediaTests)
|
||||
- 常见流媒体测试并发查询:[UnlockTests](https://github.com/oneclickvirt/UnlockTests),逻辑借鉴 [RegionRestrictionCheck](https://github.com/lmc999/RegionRestrictionCheck) 等
|
||||
- IP 质量/安全信息并发查询:二进制文件编译至 [securityCheck](https://github.com/oneclickvirt/securityCheck)
|
||||
- 邮件端口测试:[portchecker](https://github.com/oneclickvirt/portchecker)
|
||||
- 三网回程测试:借鉴 [zhanghanyun/backtrace](https://github.com/zhanghanyun/backtrace),二次开发至 [oneclickvirt/backtrace](https://github.com/oneclickvirt/backtrace)
|
||||
- 三网路由测试:基于 [NTrace-core](https://github.com/nxtrace/NTrace-core),二次开发至 [nt3](https://github.com/oneclickvirt/nt3)
|
||||
- 网速测试:基于 [speedtest.net](https://github.com/spiritLHLS/speedtest.net-CN-ID) 和 [speedtest.cn](https://github.com/spiritLHLS/speedtest.cn-CN-ID) 数据,开发至 [oneclickvirt/speedtest](https://github.com/oneclickvirt/speedtest)
|
||||
- 三网 Ping 值测试:借鉴 [ecsspeed](https://github.com/spiritLHLS/ecsspeed),二次开发至 [pingtest](https://github.com/oneclickvirt/pingtest)
|
||||
- System basic information query and concurrent IP basic information query: Self-developed [basics](https://github.com/oneclickvirt/basics), [gostun](https://github.com/oneclickvirt/gostun)
|
||||
- CPU test: Self-developed [cputest](https://github.com/oneclickvirt/cputest) supporting sysbench(lua/golang version), geekbench, winsat
|
||||
- Memory test: Self-developed [memorytest](https://github.com/oneclickvirt/memorytest) supporting sysbench, dd, winsat, mbw, stream
|
||||
- Disk test: Self-developed [disktest](https://github.com/oneclickvirt/disktest) supporting dd, fio, winsat
|
||||
- Streaming platform unlock tests concurrent query: Self-developed to [UnlockTests](https://github.com/oneclickvirt/UnlockTests), logic modified from [RegionRestrictionCheck](https://github.com/lmc999/RegionRestrictionCheck) and others
|
||||
- IP quality/security information concurrent query: Self-developed, binary files compiled in [securityCheck](https://github.com/oneclickvirt/securityCheck)
|
||||
- Email port test: Self-developed [portchecker](https://github.com/oneclickvirt/portchecker)
|
||||
- Three-network return path test: Modified from [zhanghanyun/backtrace](https://github.com/zhanghanyun/backtrace) to [oneclickvirt/backtrace](https://github.com/oneclickvirt/backtrace)
|
||||
- Three-network route test: Modified from [NTrace-core](https://github.com/nxtrace/NTrace-core) to [nt3](https://github.com/oneclickvirt/nt3)
|
||||
- Speed test: Based on data from [speedtest.net](https://github.com/spiritLHLS/speedtest.net-CN-ID) and [speedtest.cn](https://github.com/spiritLHLS/speedtest.cn-CN-ID), developed to [oneclickvirt/speedtest](https://github.com/oneclickvirt/speedtest)
|
||||
- Three-network Ping test: Modified from [ecsspeed](https://github.com/spiritLHLS/ecsspeed) to [pingtest](https://github.com/oneclickvirt/pingtest)
|
||||
- Support root or admin environment testing, support non-root or non-admin environment testing, support offline environment for testing, **not yet** support no DNS online environment for testing
|
||||
|
||||
**本项目初次使用建议查看说明:[跳转](https://github.com/oneclickvirt/ecs/blob/master/README_NEW_USER.md)**
|
||||
**For first-time users of this project, it is recommended to check the instructions: [Jump to](https://github.com/oneclickvirt/ecs/blob/master/README_NEW_USER.md)**
|
||||
|
||||
---
|
||||
|
||||
## **使用说明**
|
||||
## **Instructions for Use**
|
||||
|
||||
### **Linux/FreeBSD/OpenBSD/MacOS**
|
||||
### **Linux/FreeBSD/MacOS**
|
||||
|
||||
#### **一键命令**
|
||||
#### **One-click command**
|
||||
|
||||
**一键命令**将**默认安装依赖**,**默认更新包管理器**,**默认非互动模式**
|
||||
**One-Click Command** will **Not install Dependencies** by Default, **Not update Package Manager** by Default, **Non-Interactive Mode** by Default.
|
||||
|
||||
- **国际用户无加速:**
|
||||
- **International users without acceleration:**
|
||||
|
||||
```bash
|
||||
export noninteractive=true && curl -L https://raw.githubusercontent.com/oneclickvirt/ecs/master/goecs.sh -o goecs.sh && chmod +x goecs.sh && bash goecs.sh env && bash goecs.sh install && goecs
|
||||
export noninteractive=true && curl -L https://raw.githubusercontent.com/oneclickvirt/ecs/master/goecs.sh -o goecs.sh && chmod +x goecs.sh && ./goecs.sh install && goecs -l=en
|
||||
```
|
||||
|
||||
- **国际/国内使用 CDN 加速:**
|
||||
- **International/domestic users with CDN acceleration:**
|
||||
|
||||
```bash
|
||||
export noninteractive=true && curl -L https://cdn.spiritlhl.net/https://raw.githubusercontent.com/oneclickvirt/ecs/master/goecs.sh -o goecs.sh && chmod +x goecs.sh && bash goecs.sh env && bash goecs.sh install && goecs
|
||||
export noninteractive=true && curl -L https://cdn.spiritlhl.net/https://raw.githubusercontent.com/oneclickvirt/ecs/master/goecs.sh -o goecs.sh && chmod +x goecs.sh && ./goecs.sh install && goecs -l=en
|
||||
```
|
||||
|
||||
- **国内用户使用 CNB 加速:**
|
||||
- **Domestic users with CNB acceleration:**
|
||||
|
||||
```bash
|
||||
export noninteractive=true && curl -L https://cnb.cool/oneclickvirt/ecs/-/git/raw/main/goecs.sh -o goecs.sh && chmod +x goecs.sh && bash goecs.sh env && bash goecs.sh install && goecs
|
||||
export noninteractive=true && export CN=true && curl -L https://cnb.cool/oneclickvirt/ecs/-/git/raw/main/goecs.sh -o goecs.sh && chmod +x goecs.sh && ./goecs.sh install && goecs -l=en
|
||||
```
|
||||
|
||||
#### **详细说明**
|
||||
- **Short Link:**
|
||||
|
||||
**详细说明**中的命令**可控制是否安装依赖**,**是否更新包管理器**,**默认互动模式可进行选择**
|
||||
```bash
|
||||
export noninteractive=true && curl -L https://bash.spiritlhl.net/goecs -o goecs.sh && chmod +x goecs.sh && bash goecs.sh install && goecs -l=en
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
```bash
|
||||
export noninteractive=true && curl -L https://ba.sh/JrVa -o goecs.sh && chmod +x goecs.sh && ./goecs.sh install && goecs -l=en
|
||||
```
|
||||
|
||||
**For more accurate testing, please follow the detailed instructions below to install and add non-essential dependencies**
|
||||
|
||||
#### **Detailed instructions**
|
||||
|
||||
The following commands control whether dependencies are installed, whether the package manager is updated, and whether interactive or non-interactive mode is used.
|
||||
|
||||
<details>
|
||||
<summary>展开查看详细说明</summary>
|
||||
<summary>Expand to view detailed instructions</summary>
|
||||
|
||||
1. **下载脚本**
|
||||
1. **Download the script**
|
||||
|
||||
**国际用户无加速:**
|
||||
**International users without acceleration:**
|
||||
|
||||
```bash
|
||||
curl -L https://raw.githubusercontent.com/oneclickvirt/ecs/master/goecs.sh -o goecs.sh && chmod +x goecs.sh
|
||||
```
|
||||
|
||||
**国际/国内使用 CDN 加速:**
|
||||
**International/domestic users with CDN acceleration:**
|
||||
|
||||
```bash
|
||||
curl -L https://cdn.spiritlhl.net/https://raw.githubusercontent.com/oneclickvirt/ecs/master/goecs.sh -o goecs.sh && chmod +x goecs.sh
|
||||
```
|
||||
|
||||
**国内用户使用 CNB 加速:**
|
||||
**Domestic users with CNB acceleration:**
|
||||
|
||||
```bash
|
||||
curl -L https://cnb.cool/oneclickvirt/ecs/-/git/raw/main/goecs.sh -o goecs.sh && chmod +x goecs.sh
|
||||
export CN=true && curl -L https://cnb.cool/oneclickvirt/ecs/-/git/raw/main/goecs.sh -o goecs.sh && chmod +x goecs.sh
|
||||
```
|
||||
|
||||
2. **更新包管理器(可选择)并安装环境**
|
||||
2. **Update package manager (optional) and install environment**
|
||||
|
||||
```bash
|
||||
./goecs.sh env
|
||||
```
|
||||
|
||||
**非互动模式:**
|
||||
**Non-interactive mode:**
|
||||
|
||||
```bash
|
||||
export noninteractive=true && ./goecs.sh env
|
||||
```
|
||||
|
||||
3. **安装 `goecs`**
|
||||
3. **Install `goecs`**
|
||||
|
||||
```bash
|
||||
./goecs.sh install
|
||||
```
|
||||
|
||||
4. **升级 `goecs`**
|
||||
4. **Upgrade `goecs`**
|
||||
|
||||
```bash
|
||||
./goecs.sh upgrade
|
||||
```
|
||||
|
||||
5. **卸载 `goecs`**
|
||||
5. **Uninstall `goecs`**
|
||||
|
||||
```bash
|
||||
./goecs.sh uninstall
|
||||
```
|
||||
|
||||
6. **帮助命令**
|
||||
6. **help command**
|
||||
|
||||
```bash
|
||||
./goecs.sh -h
|
||||
```
|
||||
|
||||
7. **唤起菜单**
|
||||
7. **Invoke the menu**
|
||||
|
||||
```bash
|
||||
goecs
|
||||
goecs -l=en
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
#### **命令参数化**
|
||||
#### **Command parameterization**
|
||||
|
||||
<details>
|
||||
<summary>展开查看各参数说明</summary>
|
||||
<summary>Expand to view parameter descriptions</summary>
|
||||
|
||||
```bash
|
||||
Usage: goecs [options]
|
||||
@@ -175,18 +191,24 @@ Usage: goecs [options]
|
||||
Enable/Disable backtrace test (in 'en' language or on windows it always false) (default true)
|
||||
-basic
|
||||
Enable/Disable basic test (default true)
|
||||
-comm
|
||||
Enable/Disable common media test (default true)
|
||||
-ut
|
||||
Enable/Disable unlock media test (default true)
|
||||
-cpu
|
||||
Enable/Disable CPU test (default true)
|
||||
-cpum string
|
||||
Set CPU test method (supported: sysbench, geekbench, winsat) (default "sysbench")
|
||||
-cpu-method string
|
||||
Set CPU test method (supported: sysbench, geekbench, winsat) (default "sysbench")
|
||||
-cput string
|
||||
Set CPU test thread mode (supported: single, multi) (default "multi")
|
||||
-cpu-thread string
|
||||
Set CPU test thread mode (supported: single, multi) (default "multi")
|
||||
-disk
|
||||
Enable/Disable disk test (default true)
|
||||
-diskm string
|
||||
Set disk test method (supported: fio, dd, winsat) (default "fio")
|
||||
-disk-method string
|
||||
Set disk test method (supported: fio, dd, winsat) (default "fio")
|
||||
-diskmc
|
||||
Enable/Disable multiple disk checks, e.g., -diskmc=false
|
||||
-diskp string
|
||||
@@ -194,33 +216,49 @@ Usage: goecs [options]
|
||||
-email
|
||||
Enable/Disable email port test (default true)
|
||||
-h Show help information
|
||||
-help
|
||||
Show help information
|
||||
-l string
|
||||
Set language (supported: en, zh) (default "zh")
|
||||
-lang string
|
||||
Set language (supported: en, zh) (default "zh")
|
||||
-log
|
||||
Enable/Disable logging in the current path
|
||||
-memory
|
||||
Enable/Disable memory test (default true)
|
||||
-memorym string
|
||||
Set memory test method (supported: sysbench, dd, winsat) (default "sysbench")
|
||||
Set memory test method (supported: stream, sysbench, dd, winsat, auto) (default "stream")
|
||||
-memory-method string
|
||||
Set memory test method (supported: stream, sysbench, dd, winsat, auto) (default "stream")
|
||||
-menu
|
||||
Enable/Disable menu mode, disable example: -menu=false (default true)
|
||||
-nt3
|
||||
Enable/Disable NT3 test (in 'en' language or on windows it always false) (default true)
|
||||
-nt3loc string
|
||||
Specify NT3 test location (supported: GZ, SH, BJ, CD for Guangzhou, Shanghai, Beijing, Chengdu) (default "GZ")
|
||||
Specify NT3 test location (supported: GZ, SH, BJ, CD, ALL for Guangzhou, Shanghai, Beijing, Chengdu and all) (default "GZ")
|
||||
-nt3-location string
|
||||
Specify NT3 test location (supported: GZ, SH, BJ, CD, ALL for Guangzhou, Shanghai, Beijing, Chengdu and all) (default "GZ")
|
||||
-nt3t string
|
||||
Set NT3 test type (supported: both, ipv4, ipv6) (default "ipv4")
|
||||
-nt3-type string
|
||||
Set NT3 test type (supported: both, ipv4, ipv6) (default "ipv4")
|
||||
-ping
|
||||
Enable/Disable ping test
|
||||
-security
|
||||
Enable/Disable security test (default true)
|
||||
-speed
|
||||
Enable/Disable speed test (default true)
|
||||
-spnum int
|
||||
Set the number of servers per operator for speed test (default 2)
|
||||
-tgdc
|
||||
Enable/Disable Telegram DC test
|
||||
-upload
|
||||
Enable/Disable upload the result (default true)
|
||||
-ut
|
||||
Enable/Disable unlock media test (default true)
|
||||
-v Display version information
|
||||
-version
|
||||
Display version information
|
||||
-web
|
||||
Enable/Disable popular websites test
|
||||
```
|
||||
</details>
|
||||
|
||||
@@ -228,92 +266,95 @@ Usage: goecs [options]
|
||||
|
||||
### **Windows**
|
||||
|
||||
1. 下载带 exe 文件的压缩包:[Releases](https://github.com/oneclickvirt/ecs/releases)
|
||||
2. 解压后,右键以管理员模式运行。
|
||||
1. Download the compressed file with the .exe file: [Releases](https://github.com/oneclickvirt/ecs/releases)
|
||||
2. After unzipping, right-click and run as administrator.
|
||||
|
||||
PS: If it's a VM environment, it's OK not to run it in administrator mode, because VMs have no native testing tools and will automatically enable alternative methods for testing.
|
||||
PPS: Please refrain from downloading executable files labelled with a GUI for the time being, as they have not been fully adapted. The compressed packages for the CI version are unaffected.
|
||||
|
||||
---
|
||||
|
||||
### **Docker**
|
||||
|
||||
<details>
|
||||
<summary>展开查看使用说明</summary>
|
||||
<summary>Expand to view how to use it</summary>
|
||||
|
||||
国际镜像地址:https://hub.docker.com/r/spiritlhl/goecs
|
||||
International image: https://hub.docker.com/r/spiritlhl/goecs
|
||||
|
||||
请确保执行下述命令前本机已安装Docker
|
||||
Please ensure Docker is installed on your machine before executing the following commands
|
||||
|
||||
特权模式+host网络
|
||||
Privileged mode + host network
|
||||
|
||||
```shell
|
||||
docker run --rm --privileged --network host spiritlhl/goecs:latest -menu=false -l zh
|
||||
docker run --rm --privileged --network host spiritlhl/goecs:latest -menu=false -l=en
|
||||
```
|
||||
|
||||
非特权模式+非host网络
|
||||
Unprivileged mode + non-host network
|
||||
|
||||
```shell
|
||||
docker run --rm spiritlhl/goecs:latest -menu=false -l zh
|
||||
docker run --rm spiritlhl/goecs:latest -menu=false -l=en
|
||||
```
|
||||
|
||||
使用Docker执行测试,硬件测试会有一些偏差和虚拟化架构判断失效,还是推荐直接测试而不使用Docker测试。
|
||||
Using Docker to execute tests will result in some hardware testing bias and virtualization architecture detection failure. Direct testing is recommended over Docker testing.
|
||||
|
||||
国内镜像地址:https://cnb.cool/oneclickvirt/ecs/-/packages/docker/ecs
|
||||
Mirror image: https://cnb.cool/oneclickvirt/ecs/-/packages/docker/ecs
|
||||
|
||||
请确保执行下述命令前本机已安装Docker
|
||||
Please ensure Docker is installed on your machine before executing the following commands
|
||||
|
||||
特权模式+host网络
|
||||
Privileged mode + host network
|
||||
|
||||
```shell
|
||||
docker run --rm --privileged --network host docker.cnb.cool/oneclickvirt/ecs:latest -menu=false -l zh
|
||||
docker run --rm --privileged --network host docker.cnb.cool/oneclickvirt/ecs:latest -menu=false -l=en
|
||||
```
|
||||
|
||||
非特权模式+非host网络
|
||||
Unprivileged mode + non-host network
|
||||
|
||||
```shell
|
||||
docker run --rm docker.cnb.cool/oneclickvirt/ecs:latest -menu=false -l zh
|
||||
docker run --rm docker.cnb.cool/oneclickvirt/ecs:latest -menu=false -l=en
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 从源码进行编译
|
||||
### Compiling from source code
|
||||
|
||||
<details>
|
||||
<summary>展开查看编译说明</summary>
|
||||
<summary>Expand to view compilation instructions</summary>
|
||||
|
||||
1. 克隆仓库的 public 分支(不含私有依赖)
|
||||
1. Clone the public branch of the repository (without private dependencies)
|
||||
```bash
|
||||
git clone -b public https://github.com/oneclickvirt/ecs.git
|
||||
cd ecs
|
||||
```
|
||||
|
||||
2. 安装 Go 环境(如已安装可跳过)
|
||||
2. Install Go environment (skip if already installed)
|
||||
|
||||
Select go 1.25.4 version to install
|
||||
|
||||
```bash
|
||||
# 下载并安装 Go
|
||||
wget https://go.dev/dl/go1.23.4.linux-amd64.tar.gz
|
||||
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.23.4.linux-amd64.tar.gz
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
curl -L https://cdn.spiritlhl.net/https://raw.githubusercontent.com/spiritLHLS/one-click-installation-script/main/install_scripts/go.sh -o go.sh && chmod +x go.sh && bash go.sh
|
||||
```
|
||||
|
||||
3. 编译
|
||||
3. Compile
|
||||
```bash
|
||||
go build -o goecs
|
||||
```
|
||||
|
||||
4. 运行测试
|
||||
4. Run test
|
||||
```bash
|
||||
./goecs -menu=false -l zh
|
||||
./goecs -menu=false -l=en
|
||||
```
|
||||
|
||||
支持的编译参数:
|
||||
- GOOS:支持 linux、windows、darwin、freebsd、openbsd
|
||||
- GOARCH:支持 amd64、arm、arm64、386、mips、mipsle、s390x、riscv64
|
||||
Supported compilation parameters:
|
||||
- GOOS: supports linux, windows, darwin, freebsd, openbsd
|
||||
- GOARCH: supports amd64, arm, arm64, 386, mips, mipsle, s390x, riscv64
|
||||
|
||||
跨平台编译示例:
|
||||
Cross-platform compilation examples:
|
||||
```bash
|
||||
# 编译 Windows 版本
|
||||
# Compile Windows version
|
||||
GOOS=windows GOARCH=amd64 go build -o goecs.exe
|
||||
# 编译 MacOS 版本
|
||||
# Compile MacOS version
|
||||
GOOS=darwin GOARCH=amd64 go build -o goecs_darwin
|
||||
```
|
||||
</details>
|
||||
@@ -322,63 +363,75 @@ GOOS=darwin GOARCH=amd64 go build -o goecs_darwin
|
||||
|
||||
## QA
|
||||
|
||||
#### Q: 为什么默认使用sysbench而不是geekbench
|
||||
#### Q: Why is sysbench used by default instead of geekbench?
|
||||
|
||||
#### A: 比较二者特点
|
||||
#### A: Comparing the characteristics of both:
|
||||
|
||||
| 比较项 | sysbench | geekbench |
|
||||
|------------------|----------|-----------|
|
||||
| 适用范围 | 轻量级,几乎可在任何服务器上运行 | 重量级,小型机器无法运行 |
|
||||
| 测试要求 | 无需网络,无特殊硬件需求 | 需联网,IPV4环境,至少1G内存 |
|
||||
| 开源情况 | 基于LUA,开源,可自行编译各架构版本 | 官方二进制闭源代码,不支持自行编译 |
|
||||
| 测试稳定性 | 核心测试组件10年以上未变 | 每个大版本更新测试项,分数不同版本间难以对比(每个版本对标当前最好的CPU) |
|
||||
| 测试内容 | 仅测试计算性能 | 覆盖多种性能测试,分数加权计算,但部分测试实际不常用 |
|
||||
| 适用场景 | 适合快速测试,仅测试计算性能 | 适合综合全面的测试 |
|
||||
| Comparison | sysbench | geekbench |
|
||||
|------------|----------|-----------|
|
||||
| Application scope | Lightweight, runs on almost any server | Heavyweight, won't run on small machines |
|
||||
| Test requirements | No network needed, no special hardware requirements | Requires internet, IPv4 environment, minimum 1GB memory |
|
||||
| Open source status | Based on LUA, open source, can compile for various architectures | Official binaries are closed source, cannot compile your own version |
|
||||
| Test stability | Core test components unchanged for 10+ years | Each major version updates test items, making scores hard to compare between versions (each version benchmarks against current best CPUs) |
|
||||
| Test content | Only tests computing performance | Covers multiple performance aspects with weighted scores, though some tests aren't commonly used |
|
||||
| Suitable scenarios | Good for quick tests, focuses on computing performance | Good for comprehensive testing |
|
||||
| Ranking | [sysbench.spiritlhl.net](https://sysbench.spiritlhl.net/) | [browser.geekbench.com](https://browser.geekbench.com/) |
|
||||
|
||||
且```goecs```测试使用何种CPU测试方式可使用参数指定,默认只是为了更多用户快速测试的需求
|
||||
Note that `goecs` allows you to specify CPU test method via parameters. The default is chosen for faster testing across more systems.
|
||||
|
||||
#### Q: 为什么使用Golang而不是Rust重构
|
||||
#### Q: Why use Golang instead of Rust for refactoring?
|
||||
|
||||
#### A: 因为网络相关的项目目前以Golang语言为趋势,大多组件有开源生态维护,Rust很多得自己手搓,~~我懒得搞~~我没那个技术力
|
||||
#### A: Because network-related projects currently trend toward Golang, with many components maintained by open source communities. Many Rust components would require building from scratch, ~~I'm too lazy~~ I don't have that technical capability.
|
||||
|
||||
#### Q: 为什么不继续开发Shell版本而是选择重构
|
||||
#### Q: Why not continue developing the Shell version instead of refactoring?
|
||||
|
||||
#### A: 因为太多千奇百怪的环境问题了,还是提前编译好测试的二进制文件比较容易解决环境问题(泛化性更好)
|
||||
#### A: Because there were too many varied environment issues. Pre-compiled binary files are easier for solving environment problems (better generalization).
|
||||
|
||||
#### Q: 每个测试项目的说明有吗?
|
||||
#### Q: Are there explanations for each test item?
|
||||
|
||||
#### A: 每个测试项目有对应的维护仓库,自行点击查看仓库说明
|
||||
#### A: Each test project has its own maintenance repository. Click through to view the repository description.
|
||||
|
||||
#### Q: 测试进行到一半如何手动终止?
|
||||
#### Q: How do I manually terminate a test halfway through?
|
||||
|
||||
#### A: 按ctrl键和c键终止程序,终止后依然会在当前目录下生成goecs.txt文件和分享链接,里面是已经测试到的信息。
|
||||
#### A: Press Ctrl+C to terminate the program. After termination, a goecs.txt file and share link will still be generated in the current directory containing information tested so far.
|
||||
|
||||
#### Q: 非Root环境如何进行测试?
|
||||
#### Q: How do I test in a non-Root environment?
|
||||
|
||||
#### A: 手动执行安装命令,实在装不上也没问题,直接在release中下载对应架构的压缩包解压后执行即可,只要你能执行的了文件。或者你能使用docker的话用docker执行。
|
||||
#### A: Execute the installation command manually. If you can't install it, simply download the appropriate architecture package from releases, extract it, and run the file if you have execution permissions. Alternatively, use Docker if you can.
|
||||
|
||||
## 致谢
|
||||
## Thanks
|
||||
|
||||
感谢 [he.net](https://he.net) [bgp.tools](https://bgp.tools) [ipinfo.io](https://ipinfo.io) [ip.sb](https://ip.sb) [cheervision.co](https://cheervision.co) [scamalytics.com](https://scamalytics.com) [abuseipdb.com](https://www.abuseipdb.com/) [virustotal.com](https://www.virustotal.com/) [ip2location.com](ip2location.com/) [ip-api.com](https://ip-api.com) [ipregistry.co](https://ipregistry.co/) [ipdata.co](https://ipdata.co/) [ipgeolocation.io](https://ipgeolocation.io) [ipwhois.io](https://ipwhois.io) [ipapi.com](https://ipapi.com/) [ipapi.is](https://ipapi.is/) [ipqualityscore.com](https://www.ipqualityscore.com/) [bigdatacloud.com](https://www.bigdatacloud.com/) 等网站提供的API进行检测,感谢互联网各网站提供的查询资源
|
||||
Thank [he.net](https://he.net) [bgp.tools](https://bgp.tools) [ipinfo.io](https://ipinfo.io) [maxmind.com](https://www.maxmind.com/en/home) [cloudflare.com](https://www.cloudflare.com/) [ip.sb](https://ip.sb) [scamalytics.com](https://scamalytics.com) [abuseipdb.com](https://www.abuseipdb.com/) [ip2location.com](https://ip2location.com/) [ip-api.com](https://ip-api.com) [ipregistry.co](https://ipregistry.co/) [ipdata.co](https://ipdata.co/) [ipgeolocation.io](https://ipgeolocation.io) [ipwhois.io](https://ipwhois.io) [ipapi.com](https://ipapi.com/) [ipapi.is](https://ipapi.is/) [ipqualityscore.com](https://www.ipqualityscore.com/) [bigdatacloud.com](https://www.bigdatacloud.com/) [dkly.net](https://data.dkly.net) [virustotal.com](https://www.virustotal.com/) [ipfighter.com](https://ipfighter.com/) [getipintel.net](http://check.getipintel.net/) [fraudlogix.com](https://fraudlogix.com) and others for providing APIs for testing, and thanks to various websites on the Internet for providing query resources.
|
||||
|
||||
感谢
|
||||
Thank
|
||||
|
||||
<a href="https://h501.io/?from=69" target="_blank">
|
||||
<img src="https://github.com/spiritLHLS/ecs/assets/103393591/dfd47230-2747-4112-be69-b5636b34f07f" alt="h501">
|
||||
<img src="https://github.com/spiritLHLS/ecs/assets/103393591/dfd47230-2747-4112-be69-b5636b34f07f" alt="h501" style="height: 50px;">
|
||||
</a>
|
||||
|
||||
提供的免费托管支持本开源项目的共享测试结果存储
|
||||
provided free hosting support for this open source project's shared test results storage
|
||||
|
||||
同时感谢以下平台提供编辑和测试支持
|
||||
Thanks also to the following platforms for editorial and testing support
|
||||
|
||||
<a href="https://www.jetbrains.com/go/" target="_blank">
|
||||
<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/GoLand.png" alt="goland">
|
||||
<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/GoLand.png" alt="goland" style="height: 50px;">
|
||||
</a>
|
||||
|
||||
<a href="https://community.ibm.com/zsystems/form/l1cc-oss-vm-request/" target="_blank">
|
||||
<img src="https://linuxone.cloud.marist.edu/oss/resources/images/linuxonelogo03.png" alt="ibm">
|
||||
<img src="https://linuxone.cloud.marist.edu/oss/resources/images/linuxonelogo03.png" alt="ibm" style="height: 50px;">
|
||||
</a>
|
||||
|
||||
<a href="https://console.zmto.com/?affid=1524" target="_blank">
|
||||
<img src="https://console.zmto.com/templates/2019/dist/images/logo_dark.svg" alt="zmto" style="height: 50px;">
|
||||
</a>
|
||||
|
||||
## History Usage
|
||||
|
||||

|
||||
|
||||
## Stargazers over time
|
||||
|
||||
[](https://starchart.cc/oneclickvirt/ecs)
|
||||
[](https://www.spiritlhl.net)
|
||||
|
||||
## License
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Foneclickvirt%2Fecs?ref=badge_large)
|
||||
|
||||
382
README_EN.md
382
README_EN.md
@@ -1,382 +0,0 @@
|
||||
# ecs
|
||||
|
||||
[](https://github.com/oneclickvirt/ecs/actions/workflows/main.yaml)
|
||||
|
||||
[](https://hits.spiritlhl.net)
|
||||
|
||||
Fusion Monster Evaluation Project - GO Version
|
||||
|
||||
(No additional shell file dependencies unless necessary to install the environment using the shell, the environment is installed just to measure more accurately, in extreme cases no environment dependencies can also be fully measured project)
|
||||
|
||||
Please report any issues via [issues](https://github.com/oneclickvirt/ecs/issues).
|
||||
|
||||
Go version: [https://github.com/oneclickvirt/ecs](https://github.com/oneclickvirt/ecs)
|
||||
|
||||
Shell version: [https://github.com/spiritLHLS/ecs/blob/main/README_EN.md](https://github.com/spiritLHLS/ecs/blob/main/README_EN.md)
|
||||
|
||||
---
|
||||
|
||||
## **Language**
|
||||
|
||||
[中文文档](README.md) | [English Docs](README_EN.md)
|
||||
|
||||
---
|
||||
|
||||
## **Supported Systems and Architectures**
|
||||
|
||||
### **Compilation and Testing Support**
|
||||
| Supported for Compilation | Tested on | Supported OS for Compilation | Tested OS |
|
||||
|---------------------------|-----------|------------------------------|-----------|
|
||||
| amd64 | amd64 | Linux | Linux |
|
||||
| arm | arm | Windows | Windows |
|
||||
| arm64 | arm64 | FreeBSD | FreeBSD |
|
||||
| 386 | 386 | OpenBSD | |
|
||||
| mips | | MacOS | |
|
||||
| mipsle | | | |
|
||||
| s390x | s390x | | |
|
||||
| riscv64 | | | |
|
||||
|
||||
> Please test additional architectures and systems yourself. If you encounter any issues, please open an issue.
|
||||
|
||||
### **Systems Pending Support**
|
||||
| OS | Notes |
|
||||
|--------|--------------------------------------------------------|
|
||||
| MacOS | Hardware testing bugs and environment dependencies unresolved |
|
||||
|
||||
---
|
||||
|
||||
## **Features**
|
||||
|
||||
- System basic information query and concurrent IP basic information query: Self-developed [basics](https://github.com/oneclickvirt/basics), [gostun](https://github.com/oneclickvirt/gostun)
|
||||
- CPU test: Self-developed [cputest](https://github.com/oneclickvirt/cputest) supporting sysbench(lua/golang version), geekbench, winsat
|
||||
- Memory test: Self-developed [memorytest](https://github.com/oneclickvirt/memorytest) supporting sysbench, dd
|
||||
- Disk test: Self-developed [disktest](https://github.com/oneclickvirt/disktest) supporting dd, fio, winsat
|
||||
- Streaming media unlock information concurrent query: Modified from [netflix-verify](https://github.com/sjlleo/netflix-verify) and more to [CommonMediaTests](https://github.com/oneclickvirt/CommonMediaTests)
|
||||
- Common streaming media tests concurrent query: Self-developed to [UnlockTests](https://github.com/oneclickvirt/UnlockTests), logic modified from [RegionRestrictionCheck](https://github.com/lmc999/RegionRestrictionCheck) and others
|
||||
- IP quality/security information concurrent query: Self-developed, binary files compiled in [securityCheck](https://github.com/oneclickvirt/securityCheck)
|
||||
- Email port test: Self-developed [portchecker](https://github.com/oneclickvirt/portchecker)
|
||||
- Three-network return path test: Modified from [zhanghanyun/backtrace](https://github.com/zhanghanyun/backtrace) to [oneclickvirt/backtrace](https://github.com/oneclickvirt/backtrace)
|
||||
- Three-network route test: Modified from [NTrace-core](https://github.com/nxtrace/NTrace-core) to [nt3](https://github.com/oneclickvirt/nt3)
|
||||
- Speed test: Based on data from [speedtest.net](https://github.com/spiritLHLS/speedtest.net-CN-ID) and [speedtest.cn](https://github.com/spiritLHLS/speedtest.cn-CN-ID), developed to [oneclickvirt/speedtest](https://github.com/oneclickvirt/speedtest)
|
||||
- Three-network Ping test: Modified from [ecsspeed](https://github.com/spiritLHLS/ecsspeed) to [pingtest](https://github.com/oneclickvirt/pingtest)
|
||||
|
||||
**For first-time users of this project, it is recommended to check the instructions: [Jump to](https://github.com/oneclickvirt/ecs/blob/master/README_NEW_USER.md)**
|
||||
|
||||
---
|
||||
|
||||
## **Instructions for Use**
|
||||
|
||||
### **Linux/FreeBSD/OpenBSD/MacOS**
|
||||
|
||||
#### **One-click command**
|
||||
|
||||
**One-Click Command** will **Install Dependencies by Default**, **Update Package Manager by Default**, **Default Non-Interactive Mode***
|
||||
|
||||
- **International users without acceleration:**
|
||||
|
||||
```bash
|
||||
export noninteractive=true && curl -L https://raw.githubusercontent.com/oneclickvirt/ecs/master/goecs.sh -o goecs.sh && chmod +x goecs.sh && bash goecs.sh env && bash goecs.sh install && goecs -l en
|
||||
```
|
||||
|
||||
- **International/domestic users with CDN acceleration:**
|
||||
|
||||
```bash
|
||||
export noninteractive=true && curl -L https://cdn.spiritlhl.net/https://raw.githubusercontent.com/oneclickvirt/ecs/master/goecs.sh -o goecs.sh && chmod +x goecs.sh && bash goecs.sh env && bash goecs.sh install && goecs -l en
|
||||
```
|
||||
|
||||
- **Domestic users with CNB acceleration:**
|
||||
|
||||
```bash
|
||||
export noninteractive=true && curl -L https://cnb.cool/oneclickvirt/ecs/-/git/raw/main/goecs.sh -o goecs.sh && chmod +x goecs.sh && bash goecs.sh env && bash goecs.sh install && goecs -l en
|
||||
```
|
||||
|
||||
#### **Detailed instructions**
|
||||
|
||||
**Detailed description** of the commands in **Command **Controls whether to install dependencies**, **Whether to update the package manager**, **Default interaction mode can be selected***
|
||||
|
||||
<details>
|
||||
<summary>Expand to view detailed instructions</summary>
|
||||
|
||||
1. **Download the script**
|
||||
|
||||
**International users without acceleration:**
|
||||
|
||||
```bash
|
||||
curl -L https://raw.githubusercontent.com/oneclickvirt/ecs/master/goecs.sh -o goecs.sh && chmod +x goecs.sh
|
||||
```
|
||||
|
||||
**International/domestic users with CDN acceleration:**
|
||||
|
||||
```bash
|
||||
curl -L https://cdn.spiritlhl.net/https://raw.githubusercontent.com/oneclickvirt/ecs/master/goecs.sh -o goecs.sh && chmod +x goecs.sh
|
||||
```
|
||||
|
||||
**Domestic users with CNB acceleration:**
|
||||
|
||||
```bash
|
||||
curl -L https://cnb.cool/oneclickvirt/ecs/-/git/raw/main/goecs.sh -o goecs.sh && chmod +x goecs.sh
|
||||
```
|
||||
|
||||
2. **Update package manager (optional) and install environment**
|
||||
|
||||
```bash
|
||||
./goecs.sh env
|
||||
```
|
||||
|
||||
**Non-interactive mode:**
|
||||
|
||||
```bash
|
||||
export noninteractive=true && ./goecs.sh env
|
||||
```
|
||||
|
||||
3. **Install `goecs`**
|
||||
|
||||
```bash
|
||||
./goecs.sh install
|
||||
```
|
||||
|
||||
4. **Upgrade `goecs`**
|
||||
|
||||
```bash
|
||||
./goecs.sh upgrade
|
||||
```
|
||||
|
||||
5. **Uninstall `goecs`**
|
||||
|
||||
```bash
|
||||
./goecs.sh uninstall
|
||||
|
||||
6. **help command**
|
||||
|
||||
```bash
|
||||
./goecs.sh -h
|
||||
```
|
||||
|
||||
7. **Invoke the menu**
|
||||
|
||||
```bash
|
||||
goecs -l en
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
#### **Command parameterization**
|
||||
|
||||
<details>
|
||||
<summary>Expand to view parameter descriptions</summary>
|
||||
|
||||
```bash
|
||||
Usage: goecs [options]
|
||||
-backtrace
|
||||
Enable/Disable backtrace test (in 'en' language or on windows it always false) (default true)
|
||||
-basic
|
||||
Enable/Disable basic test (default true)
|
||||
-comm
|
||||
Enable/Disable common media test (default true)
|
||||
-cpu
|
||||
Enable/Disable CPU test (default true)
|
||||
-cpum string
|
||||
Set CPU test method (supported: sysbench, geekbench, winsat) (default "sysbench")
|
||||
-cput string
|
||||
Set CPU test thread mode (supported: single, multi) (default "multi")
|
||||
-disk
|
||||
Enable/Disable disk test (default true)
|
||||
-diskm string
|
||||
Set disk test method (supported: fio, dd, winsat) (default "fio")
|
||||
-diskmc
|
||||
Enable/Disable multiple disk checks, e.g., -diskmc=false
|
||||
-diskp string
|
||||
Set disk test path, e.g., -diskp /root
|
||||
-email
|
||||
Enable/Disable email port test (default true)
|
||||
-h Show help information
|
||||
-l string
|
||||
Set language (supported: en, zh) (default "zh")
|
||||
-log
|
||||
Enable/Disable logging in the current path
|
||||
-memory
|
||||
Enable/Disable memory test (default true)
|
||||
-memorym string
|
||||
Set memory test method (supported: sysbench, dd, winsat) (default "sysbench")
|
||||
-menu
|
||||
Enable/Disable menu mode, disable example: -menu=false (default true)
|
||||
-nt3
|
||||
Enable/Disable NT3 test (in 'en' language or on windows it always false) (default true)
|
||||
-nt3loc string
|
||||
Specify NT3 test location (supported: GZ, SH, BJ, CD for Guangzhou, Shanghai, Beijing, Chengdu) (default "GZ")
|
||||
-nt3t string
|
||||
Set NT3 test type (supported: both, ipv4, ipv6) (default "ipv4")
|
||||
-security
|
||||
Enable/Disable security test (default true)
|
||||
-speed
|
||||
Enable/Disable speed test (default true)
|
||||
-spnum int
|
||||
Set the number of servers per operator for speed test (default 2)
|
||||
-upload
|
||||
Enable/Disable upload the result (default true)
|
||||
-ut
|
||||
Enable/Disable unlock media test (default true)
|
||||
-v Display version information
|
||||
```
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### **Windows**
|
||||
|
||||
1. Download the compressed file with the .exe file: [Releases](https://github.com/oneclickvirt/ecs/releases)
|
||||
2. After unzipping, right-click and run as administrator.
|
||||
|
||||
---
|
||||
|
||||
### **Docker**
|
||||
|
||||
<details>
|
||||
<summary>Expand to view how to use it</summary>
|
||||
|
||||
International image: https://hub.docker.com/r/spiritlhl/goecs
|
||||
|
||||
Please ensure Docker is installed on your machine before executing the following commands
|
||||
|
||||
Privileged mode + host network
|
||||
|
||||
```shell
|
||||
docker run --rm --privileged --network host spiritlhl/goecs:latest -menu=false -l en
|
||||
```
|
||||
|
||||
Unprivileged mode + non-host network
|
||||
|
||||
```shell
|
||||
docker run --rm spiritlhl/goecs:latest -menu=false -l en
|
||||
```
|
||||
|
||||
Using Docker to execute tests will result in some hardware testing bias and virtualization architecture detection failure. Direct testing is recommended over Docker testing.
|
||||
|
||||
Mirror image: https://cnb.cool/oneclickvirt/ecs/-/packages/docker/ecs
|
||||
|
||||
Please ensure Docker is installed on your machine before executing the following commands
|
||||
|
||||
Privileged mode + host network
|
||||
|
||||
```shell
|
||||
docker run --rm --privileged --network host docker.cnb.cool/oneclickvirt/ecs:latest -menu=false -l en
|
||||
```
|
||||
|
||||
Unprivileged mode + non-host network
|
||||
|
||||
```shell
|
||||
docker run --rm docker.cnb.cool/oneclickvirt/ecs:latest -menu=false -l en
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### Compiling from source code
|
||||
|
||||
<details>
|
||||
<summary>Expand to view compilation instructions</summary>
|
||||
|
||||
1. Clone the public branch of the repository (without private dependencies)
|
||||
```bash
|
||||
git clone -b public https://github.com/oneclickvirt/ecs.git
|
||||
cd ecs
|
||||
```
|
||||
|
||||
2. Install Go environment (skip if already installed)
|
||||
```bash
|
||||
# Download and install Go
|
||||
wget https://go.dev/dl/go1.23.4.linux-amd64.tar.gz
|
||||
rm -rf /usr/local/go && tar -C /usr/local -xzf go1.23.4.linux-amd64.tar.gz
|
||||
export PATH=$PATH:/usr/local/go/bin
|
||||
```
|
||||
|
||||
3. Compile
|
||||
```bash
|
||||
go build -o goecs
|
||||
```
|
||||
|
||||
4. Run test
|
||||
```bash
|
||||
./goecs -menu=false -l en
|
||||
```
|
||||
|
||||
Supported compilation parameters:
|
||||
- GOOS: supports linux, windows, darwin, freebsd, openbsd
|
||||
- GOARCH: supports amd64, arm, arm64, 386, mips, mipsle, s390x, riscv64
|
||||
|
||||
Cross-platform compilation examples:
|
||||
```bash
|
||||
# Compile Windows version
|
||||
GOOS=windows GOARCH=amd64 go build -o goecs.exe
|
||||
# Compile MacOS version
|
||||
GOOS=darwin GOARCH=amd64 go build -o goecs_darwin
|
||||
```
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## QA
|
||||
|
||||
#### Q: Why is sysbench used by default instead of geekbench?
|
||||
|
||||
#### A: Comparing the characteristics of both:
|
||||
|
||||
| Comparison | sysbench | geekbench |
|
||||
|------------|----------|-----------|
|
||||
| Application scope | Lightweight, runs on almost any server | Heavyweight, won't run on small machines |
|
||||
| Test requirements | No network needed, no special hardware requirements | Requires internet, IPv4 environment, minimum 1GB memory |
|
||||
| Open source status | Based on LUA, open source, can compile for various architectures | Official binaries are closed source, cannot compile your own version |
|
||||
| Test stability | Core test components unchanged for 10+ years | Each major version updates test items, making scores hard to compare between versions (each version benchmarks against current best CPUs) |
|
||||
| Test content | Only tests computing performance | Covers multiple performance aspects with weighted scores, though some tests aren't commonly used |
|
||||
| Suitable scenarios | Good for quick tests, focuses on computing performance | Good for comprehensive testing |
|
||||
|
||||
Note that `goecs` allows you to specify CPU test method via parameters. The default is chosen for faster testing across more systems.
|
||||
|
||||
#### Q: Why use Golang instead of Rust for refactoring?
|
||||
|
||||
#### A: Because network-related projects currently trend toward Golang, with many components maintained by open source communities. Many Rust components would require building from scratch, ~~I'm too lazy~~ I don't have that technical capability.
|
||||
|
||||
#### Q: Why not continue developing the Shell version instead of refactoring?
|
||||
|
||||
#### A: Because there were too many varied environment issues. Pre-compiled binary files are easier for solving environment problems (better generalization).
|
||||
|
||||
#### Q: Are there explanations for each test item?
|
||||
|
||||
#### A: Each test project has its own maintenance repository. Click through to view the repository description.
|
||||
|
||||
#### Q: How do I manually terminate a test halfway through?
|
||||
|
||||
#### A: Press Ctrl+C to terminate the program. After termination, a goecs.txt file and share link will still be generated in the current directory containing information tested so far.
|
||||
|
||||
#### Q: How do I test in a non-Root environment?
|
||||
|
||||
#### A: Execute the installation command manually. If you can't install it, simply download the appropriate architecture package from releases, extract it, and run the file if you have execution permissions. Alternatively, use Docker if you can.
|
||||
|
||||
## Thanks
|
||||
|
||||
Thank [he.net](https://he.net) [bgp.tools](https://bgp.tools) [ipinfo.io](https://ipinfo.io) [ip.sb](https://ip.sb) [cheervision.co](https://cheervision.co) [scamalytics.com](https://scamalytics.com) [abuseipdb.com](https://www.abuseipdb.com/) [virustotal.com](https://www.virustotal.com/) [ip2location.com](ip2location.com/) [ip-api.com](https://ip-api.com) [ipregistry.co](https://ipregistry.co/) [ipdata.co](https://ipdata.co/) [ipgeolocation.io](https://ipgeolocation.io) [ipwhois.io](https://ipwhois.io) [ipapi.com](https://ipapi.com/) [ipapi.is](https://ipapi.is/) [ipqualityscore.com](https://www.ipqualityscore.com/) [bigdatacloud.com](https://www.bigdatacloud.com/) and others for providing APIs for testing, and thanks to various websites on the Internet for providing query resources.
|
||||
|
||||
Thank
|
||||
|
||||
<a href="https://h501.io/?from=69" target="_blank">
|
||||
<img src="https://github.com/spiritLHLS/ecs/assets/103393591/dfd47230-2747-4112-be69-b5636b34f07f" alt="h501">
|
||||
</a>
|
||||
|
||||
provided free hosting support for this open source project's shared test results storage
|
||||
|
||||
Thanks also to the following platforms for editorial and testing support
|
||||
|
||||
<a href="https://www.jetbrains.com/go/" target="_blank">
|
||||
<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/GoLand.png" alt="goland">
|
||||
</a>
|
||||
|
||||
<a href="https://community.ibm.com/zsystems/form/l1cc-oss-vm-request/" target="_blank">
|
||||
<img src="https://linuxone.cloud.marist.edu/oss/resources/images/linuxonelogo03.png" alt="ibm">
|
||||
</a>
|
||||
|
||||
## Stargazers over time
|
||||
|
||||
[](https://starchart.cc/oneclickvirt/ecs)
|
||||
1046
README_NEW_USER.md
1046
README_NEW_USER.md
File diff suppressed because it is too large
Load Diff
442
README_ZH.md
Normal file
442
README_ZH.md
Normal file
@@ -0,0 +1,442 @@
|
||||
# ECS
|
||||
|
||||
[](https://github.com/oneclickvirt/ecs/actions/workflows/build_binary.yaml)
|
||||
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Foneclickvirt%2Fecs?ref=badge_shield)
|
||||
|
||||
[](https://hits.spiritlhl.net) [](https://github.com/oneclickvirt/ecs/releases)
|
||||
|
||||
融合怪测评项目 - GO版本
|
||||
|
||||
(仅环境安装[非必须]使用shell外无额外shell文件依赖,环境安装只是为了测的更准,极端情况下无环境依赖安装也可全测项目)
|
||||
|
||||
如有问题请 [issues](https://github.com/oneclickvirt/ecs/issues) 反馈。
|
||||
|
||||
Go 版本:[https://github.com/oneclickvirt/ecs](https://github.com/oneclickvirt/ecs)
|
||||
|
||||
Shell 版本:[https://github.com/spiritLHLS/ecs](https://github.com/spiritLHLS/ecs)
|
||||
|
||||
---
|
||||
|
||||
## **语言**
|
||||
|
||||
[English Docs](README.md) | [中文文档](README_ZH.md)
|
||||
|
||||
---
|
||||
|
||||
## **适配系统和架构**
|
||||
|
||||
### **编译与测试支持情况**
|
||||
| 编译支持的架构 | 测试支持的架构 | 编译支持的系统 | 测试支持的系统 |
|
||||
|---------------------------|--------------|---------------------------|---------------|
|
||||
| amd64 | amd64 | Linux | Linux |
|
||||
| arm64 | arm64 | Windows | Windows |
|
||||
| arm | | MacOS(Darwin) | MacOS |
|
||||
| 386 | | FreeBSD | |
|
||||
| mips,mipsle | | Android | |
|
||||
| mips64,mips64le | | | |
|
||||
| ppc64,ppc64le | | | |
|
||||
| s390x | s390x | | |
|
||||
| riscv64 | | | |
|
||||
|
||||
> 更多架构与系统请自行测试或编译,如有问题请开 issues。
|
||||
|
||||
### **待支持的系统**
|
||||
|
||||
| 系统 | 说明 |
|
||||
|----------------|---------------------------|
|
||||
| OpenBSD/NetBSD | 部分Golang的官方库未支持本系统(尤其是net相关项目) |
|
||||
|
||||
---
|
||||
|
||||
## **功能**
|
||||
|
||||
- 系统基础信息查询,IP基础信息并发查询:[basics](https://github.com/oneclickvirt/basics)、[gostun](https://github.com/oneclickvirt/gostun)
|
||||
- CPU 测试:[cputest](https://github.com/oneclickvirt/cputest),支持 sysbench(lua/golang版本)、geekbench、winsat
|
||||
- 内存测试:[memorytest](https://github.com/oneclickvirt/memorytest),支持 sysbench、dd、winsat、mbw、stream
|
||||
- 硬盘测试:[disktest](https://github.com/oneclickvirt/disktest),支持 dd、fio、winsat
|
||||
- 流媒体平台解锁测试并发查询:[UnlockTests](https://github.com/oneclickvirt/UnlockTests),逻辑借鉴 [RegionRestrictionCheck](https://github.com/lmc999/RegionRestrictionCheck) 等
|
||||
- IP 质量/安全信息并发查询:二进制文件编译至 [securityCheck](https://github.com/oneclickvirt/securityCheck)
|
||||
- 邮件端口测试:[portchecker](https://github.com/oneclickvirt/portchecker)
|
||||
- 上游及回程路由线路检测:借鉴 [zhanghanyun/backtrace](https://github.com/zhanghanyun/backtrace),二次开发至 [oneclickvirt/backtrace](https://github.com/oneclickvirt/backtrace)
|
||||
- 三网路由测试:基于 [NTrace-core](https://github.com/nxtrace/NTrace-core),二次开发至 [nt3](https://github.com/oneclickvirt/nt3)
|
||||
- 网速测试:基于 [speedtest.net](https://github.com/spiritLHLS/speedtest.net-CN-ID) 和 [speedtest.cn](https://github.com/spiritLHLS/speedtest.cn-CN-ID) 数据,开发 [oneclickvirt/speedtest](https://github.com/oneclickvirt/speedtest),同时融合私有国内测速节点
|
||||
- 三网 Ping 值测试:借鉴 [ecsspeed](https://github.com/spiritLHLS/ecsspeed),二次开发至 [pingtest](https://github.com/oneclickvirt/pingtest)
|
||||
- 支持root或admin环境下测试,支持非root或非admin环境下测试,支持离线环境下进行测试,**暂未**支持无DNS的在线环境下进行测试
|
||||
|
||||
**本项目初次使用建议查看说明:[跳转](https://github.com/oneclickvirt/ecs/blob/master/README_NEW_USER.md)**
|
||||
|
||||
---
|
||||
|
||||
## **使用说明**
|
||||
|
||||
### **Linux/FreeBSD/MacOS**
|
||||
|
||||
#### **一键命令**
|
||||
|
||||
**一键命令**将默认**不安装依赖**,默认**不更新包管理器**,默认**非互动模式**
|
||||
|
||||
- **国际用户无加速:**
|
||||
|
||||
```bash
|
||||
export noninteractive=true && curl -L https://raw.githubusercontent.com/oneclickvirt/ecs/master/goecs.sh -o goecs.sh && chmod +x goecs.sh && ./goecs.sh install && goecs
|
||||
```
|
||||
|
||||
- **国际/国内使用 CDN 加速:**
|
||||
|
||||
```bash
|
||||
export noninteractive=true && curl -L https://cdn.spiritlhl.net/https://raw.githubusercontent.com/oneclickvirt/ecs/master/goecs.sh -o goecs.sh && chmod +x goecs.sh && ./goecs.sh install && goecs
|
||||
```
|
||||
|
||||
- **国内用户使用 CNB 加速:**
|
||||
|
||||
```bash
|
||||
export noninteractive=true && export CN=true && curl -L https://cnb.cool/oneclickvirt/ecs/-/git/raw/main/goecs.sh -o goecs.sh && chmod +x goecs.sh && ./goecs.sh install && goecs
|
||||
```
|
||||
|
||||
- **短链接:**
|
||||
|
||||
```bash
|
||||
export noninteractive=true && curl -L https://bash.spiritlhl.net/goecs -o goecs.sh && chmod +x goecs.sh && ./goecs.sh install && goecs
|
||||
```
|
||||
|
||||
或
|
||||
|
||||
```bash
|
||||
export noninteractive=true && curl -L https://ba.sh/JrVa -o goecs.sh && chmod +x goecs.sh && ./goecs.sh install && goecs
|
||||
```
|
||||
|
||||
**如果需要测试更准确,请按照下面的详细说明进行安装,添加非必需的依赖**
|
||||
|
||||
#### **详细说明**
|
||||
|
||||
以下命令可控制**是否安装依赖**,**是否更新包管理器**,**互动模式和非交互模式**
|
||||
|
||||
<details>
|
||||
<summary>展开查看详细说明</summary>
|
||||
|
||||
1. **下载脚本**
|
||||
|
||||
**国际用户无加速:**
|
||||
|
||||
```bash
|
||||
curl -L https://raw.githubusercontent.com/oneclickvirt/ecs/master/goecs.sh -o goecs.sh && chmod +x goecs.sh
|
||||
```
|
||||
|
||||
**国际/国内使用 CDN 加速:**
|
||||
|
||||
```bash
|
||||
curl -L https://cdn.spiritlhl.net/https://raw.githubusercontent.com/oneclickvirt/ecs/master/goecs.sh -o goecs.sh && chmod +x goecs.sh
|
||||
```
|
||||
|
||||
**国内用户使用 CNB 加速:**
|
||||
|
||||
```bash
|
||||
export CN=true && curl -L https://cnb.cool/oneclickvirt/ecs/-/git/raw/main/goecs.sh -o goecs.sh && chmod +x goecs.sh
|
||||
```
|
||||
|
||||
2. **更新包管理器(可选择)并安装环境**
|
||||
|
||||
```bash
|
||||
./goecs.sh env
|
||||
```
|
||||
|
||||
**非互动模式:**
|
||||
|
||||
```bash
|
||||
export noninteractive=true && ./goecs.sh env
|
||||
```
|
||||
|
||||
3. **安装 `goecs` 本体(仅下载二进制文件无依赖安装)**
|
||||
|
||||
```bash
|
||||
./goecs.sh install
|
||||
```
|
||||
|
||||
4. **升级 `goecs` 本体**
|
||||
|
||||
```bash
|
||||
./goecs.sh upgrade
|
||||
```
|
||||
|
||||
5. **卸载 `goecs` 本体**
|
||||
|
||||
```bash
|
||||
./goecs.sh uninstall
|
||||
```
|
||||
|
||||
6. **帮助命令**
|
||||
|
||||
```bash
|
||||
./goecs.sh -h
|
||||
```
|
||||
|
||||
7. **唤起菜单**
|
||||
|
||||
```bash
|
||||
goecs
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
#### **命令参数化**
|
||||
|
||||
<details>
|
||||
<summary>展开查看各参数说明</summary>
|
||||
|
||||
```bash
|
||||
Usage: goecs [options]
|
||||
-backtrace
|
||||
Enable/Disable backtrace test (in 'en' language or on windows it always false) (default true)
|
||||
-basic
|
||||
Enable/Disable basic test (default true)
|
||||
-ut
|
||||
Enable/Disable unlock media test (default true)
|
||||
-cpu
|
||||
Enable/Disable CPU test (default true)
|
||||
-cpum string
|
||||
Set CPU test method (supported: sysbench, geekbench, winsat) (default "sysbench")
|
||||
-cpu-method string
|
||||
Set CPU test method (supported: sysbench, geekbench, winsat) (default "sysbench")
|
||||
-cput string
|
||||
Set CPU test thread mode (supported: single, multi) (default "multi")
|
||||
-cpu-thread string
|
||||
Set CPU test thread mode (supported: single, multi) (default "multi")
|
||||
-disk
|
||||
Enable/Disable disk test (default true)
|
||||
-diskm string
|
||||
Set disk test method (supported: fio, dd, winsat) (default "fio")
|
||||
-disk-method string
|
||||
Set disk test method (supported: fio, dd, winsat) (default "fio")
|
||||
-diskmc
|
||||
Enable/Disable multiple disk checks, e.g., -diskmc=false
|
||||
-diskp string
|
||||
Set disk test path, e.g., -diskp /root
|
||||
-email
|
||||
Enable/Disable email port test (default true)
|
||||
-h Show help information
|
||||
-help
|
||||
Show help information
|
||||
-l string
|
||||
Set language (supported: en, zh) (default "zh")
|
||||
-lang string
|
||||
Set language (supported: en, zh) (default "zh")
|
||||
-log
|
||||
Enable/Disable logging in the current path
|
||||
-memory
|
||||
Enable/Disable memory test (default true)
|
||||
-memorym string
|
||||
Set memory test method (supported: stream, sysbench, dd, winsat, auto) (default "stream")
|
||||
-memory-method string
|
||||
Set memory test method (supported: stream, sysbench, dd, winsat, auto) (default "stream")
|
||||
-menu
|
||||
Enable/Disable menu mode, disable example: -menu=false (default true)
|
||||
-nt3
|
||||
Enable/Disable NT3 test (in 'en' language or on windows it always false) (default true)
|
||||
-nt3loc string
|
||||
Specify NT3 test location (supported: GZ, SH, BJ, CD, ALL for Guangzhou, Shanghai, Beijing, Chengdu and all) (default "GZ")
|
||||
-nt3-location string
|
||||
Specify NT3 test location (supported: GZ, SH, BJ, CD, ALL for Guangzhou, Shanghai, Beijing, Chengdu and all) (default "GZ")
|
||||
-nt3t string
|
||||
Set NT3 test type (supported: both, ipv4, ipv6) (default "ipv4")
|
||||
-nt3-type string
|
||||
Set NT3 test type (supported: both, ipv4, ipv6) (default "ipv4")
|
||||
-ping
|
||||
Enable/Disable ping test
|
||||
-security
|
||||
Enable/Disable security test (default true)
|
||||
-speed
|
||||
Enable/Disable speed test (default true)
|
||||
-spnum int
|
||||
Set the number of servers per operator for speed test (default 2)
|
||||
-tgdc
|
||||
Enable/Disable Telegram DC test
|
||||
-upload
|
||||
Enable/Disable upload the result (default true)
|
||||
-v Display version information
|
||||
-version
|
||||
Display version information
|
||||
-web
|
||||
Enable/Disable popular websites test
|
||||
```
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### **Windows**
|
||||
|
||||
1. 下载带 exe 文件的压缩包:[Releases](https://github.com/oneclickvirt/ecs/releases)
|
||||
2. 解压后,右键以管理员模式运行。
|
||||
|
||||
PS:如果是虚拟机环境,不以管理员模式运行也行,因为虚拟机无原生的测试工具,将自动启用替代方法测试。
|
||||
PPS: 暂时不要下载带GUI标签的exe文件,未完整适配,CI版本的压缩包是没问题的。
|
||||
|
||||
---
|
||||
|
||||
### **Docker**
|
||||
|
||||
<details>
|
||||
<summary>展开查看使用说明</summary>
|
||||
|
||||
国际镜像地址:https://hub.docker.com/r/spiritlhl/goecs
|
||||
|
||||
请确保执行下述命令前本机已安装Docker
|
||||
|
||||
特权模式+host网络
|
||||
|
||||
```shell
|
||||
docker run --rm --privileged --network host spiritlhl/goecs:latest -menu=false -l zh
|
||||
```
|
||||
|
||||
非特权模式+非host网络
|
||||
|
||||
```shell
|
||||
docker run --rm spiritlhl/goecs:latest -menu=false -l zh
|
||||
```
|
||||
|
||||
使用Docker执行测试,硬件测试会有一些偏差和虚拟化架构判断失效,还是推荐直接测试而不使用Docker测试。
|
||||
|
||||
国内阿里云镜像加速
|
||||
|
||||
请确保执行下述命令前本机已安装Docker
|
||||
|
||||
特权模式+host网络
|
||||
|
||||
```shell
|
||||
docker run --rm --privileged --network host crpi-8tmognxgyb86bm61.cn-guangzhou.personal.cr.aliyuncs.com/oneclickvirt/ecs:latest -menu=false -l zh
|
||||
```
|
||||
|
||||
非特权模式+非host网络
|
||||
|
||||
```shell
|
||||
docker run --rm crpi-8tmognxgyb86bm61.cn-guangzhou.personal.cr.aliyuncs.com/oneclickvirt/ecs:latest -menu=false -l zh
|
||||
```
|
||||
|
||||
实际上还有CNB镜像地址 https://cnb.cool/oneclickvirt/ecs/-/packages/docker/ecs 但很可惜组织空间不足无法推送了,更推荐使用阿里云镜像加速
|
||||
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
### 从源码进行编译
|
||||
|
||||
<details>
|
||||
<summary>展开查看编译说明</summary>
|
||||
|
||||
1. 克隆仓库的 public 分支(不含私有依赖)
|
||||
```bash
|
||||
git clone -b public https://github.com/oneclickvirt/ecs.git
|
||||
cd ecs
|
||||
```
|
||||
|
||||
2. 安装 Go 环境(如已安装可跳过)
|
||||
|
||||
选择 go 1.25.4 的版本进行安装
|
||||
|
||||
```bash
|
||||
curl -L https://cdn.spiritlhl.net/https://raw.githubusercontent.com/spiritLHLS/one-click-installation-script/main/install_scripts/go.sh -o go.sh && chmod +x go.sh && bash go.sh
|
||||
```
|
||||
|
||||
3. 编译
|
||||
```bash
|
||||
go build -o goecs
|
||||
```
|
||||
|
||||
4. 运行测试
|
||||
```bash
|
||||
./goecs -menu=false -l zh
|
||||
```
|
||||
|
||||
支持的编译参数:
|
||||
- GOOS:支持 linux、windows、darwin、freebsd、openbsd
|
||||
- GOARCH:支持 amd64、arm、arm64、386、mips、mipsle、s390x、riscv64
|
||||
|
||||
跨平台编译示例:
|
||||
```bash
|
||||
# 编译 Windows 版本
|
||||
GOOS=windows GOARCH=amd64 go build -o goecs.exe
|
||||
# 编译 MacOS 版本
|
||||
GOOS=darwin GOARCH=amd64 go build -o goecs_darwin
|
||||
```
|
||||
</details>
|
||||
|
||||
---
|
||||
|
||||
## QA
|
||||
|
||||
#### Q: 为什么默认使用sysbench而不是geekbench
|
||||
|
||||
#### A: 比较二者特点
|
||||
|
||||
| 比较项 | sysbench | geekbench |
|
||||
|------------------|----------|-----------|
|
||||
| 适用范围 | 轻量级,几乎可在任何服务器上运行 | 重量级,小型机器无法运行 |
|
||||
| 测试要求 | 无需网络,无特殊硬件需求 | 需联网,IPV4环境,至少1G内存 |
|
||||
| 开源情况 | 基于LUA,开源,可自行编译各架构版本 | 官方二进制闭源代码,不支持自行编译 |
|
||||
| 测试稳定性 | 核心测试组件10年以上未变 | 每个大版本更新测试项,分数不同版本间难以对比(每个版本对标当前最好的CPU) |
|
||||
| 测试内容 | 仅测试计算性能 | 覆盖多种性能测试,分数加权计算,但部分测试实际不常用 |
|
||||
| 适用场景 | 适合快速测试,仅测试计算性能 | 适合综合全面的测试 |
|
||||
| 排行榜 | [sysbench.spiritlhl.net](https://sysbench.spiritlhl.net/) | [browser.geekbench.com](https://browser.geekbench.com/) |
|
||||
|
||||
且```goecs```测试使用何种CPU测试方式可使用参数指定,默认只是为了更多用户快速测试的需求
|
||||
|
||||
#### Q: 为什么使用Golang而不是Rust重构
|
||||
|
||||
#### A: 因为网络相关的项目目前以Golang语言为趋势,大多组件有开源生态维护,Rust很多得自己手搓,~~我懒得搞~~我没那个技术力
|
||||
|
||||
#### Q: 为什么不继续开发Shell版本而是选择重构
|
||||
|
||||
#### A: 因为太多千奇百怪的环境问题了,还是提前编译好测试的二进制文件比较容易解决环境问题(泛化性更好)
|
||||
|
||||
#### Q: 每个测试项目的说明有吗?
|
||||
|
||||
#### A: 每个测试项目有对应的维护仓库,自行点击查看仓库说明
|
||||
|
||||
#### Q: 测试进行到一半如何手动终止?
|
||||
|
||||
#### A: 按ctrl键和c键终止程序,终止后依然会在当前目录下生成goecs.txt文件和分享链接,里面是已经测试到的信息。
|
||||
|
||||
#### Q: 非Root环境如何进行测试?
|
||||
|
||||
#### A: 手动执行安装命令,实在装不上也没问题,直接在release中下载对应架构的压缩包解压后执行即可,只要你能执行的了文件。或者你能使用docker的话用docker执行。
|
||||
|
||||
## 致谢
|
||||
|
||||
感谢
|
||||
[DKLYDataHub - IP Geolocation Data](https://data.dkly.net)
|
||||
[he.net](https://he.net) [bgp.tools](https://bgp.tools) [ipinfo.io](https://ipinfo.io) [maxmind.com](https://www.maxmind.com/en/home) [cloudflare.com](https://www.cloudflare.com/) [ip.sb](https://ip.sb) [scamalytics.com](https://scamalytics.com) [abuseipdb.com](https://www.abuseipdb.com/) [ip2location.com](https://ip2location.com/) [ip-api.com](https://ip-api.com) [ipregistry.co](https://ipregistry.co/) [ipdata.co](https://ipdata.co/) [ipgeolocation.io](https://ipgeolocation.io) [ipwhois.io](https://ipwhois.io) [ipapi.com](https://ipapi.com/) [ipapi.is](https://ipapi.is/) [ipqualityscore.com](https://www.ipqualityscore.com/) [bigdatacloud.com](https://www.bigdatacloud.com/) [virustotal.com](https://www.virustotal.com/) [ipfighter.com](https://ipfighter.com/) [getipintel.net](http://check.getipintel.net/) [fraudlogix.com](https://fraudlogix.com) 等网站提供的API进行检测,感谢互联网各网站提供的查询资源
|
||||
|
||||
感谢
|
||||
|
||||
<a href="https://h501.io/?from=69" target="_blank">
|
||||
<img src="https://github.com/spiritLHLS/ecs/assets/103393591/dfd47230-2747-4112-be69-b5636b34f07f" alt="h501" style="height: 50px;">
|
||||
</a>
|
||||
|
||||
提供的免费托管支持本开源项目的共享测试结果存储
|
||||
|
||||
同时感谢以下平台提供编辑和测试支持
|
||||
|
||||
<a href="https://www.jetbrains.com/go/" target="_blank">
|
||||
<img src="https://resources.jetbrains.com/storage/products/company/brand/logos/GoLand.png" alt="goland" style="height: 50px;">
|
||||
</a>
|
||||
|
||||
<a href="https://community.ibm.com/zsystems/form/l1cc-oss-vm-request/" target="_blank">
|
||||
<img src="https://linuxone.cloud.marist.edu/oss/resources/images/linuxonelogo03.png" alt="ibm" style="height: 50px;">
|
||||
</a>
|
||||
|
||||
<a href="https://console.zmto.com/?affid=1524" target="_blank">
|
||||
<img src="https://console.zmto.com/templates/2019/dist/images/logo_dark.svg" alt="zmto" style="height: 50px;">
|
||||
</a>
|
||||
|
||||
## History Usage
|
||||
|
||||

|
||||
|
||||
## Stargazers over time
|
||||
|
||||
[](https://www.spiritlhl.net)
|
||||
|
||||
## License
|
||||
[](https://app.fossa.com/projects/git%2Bgithub.com%2Foneclickvirt%2Fecs?ref=badge_large)
|
||||
56
api/api.go
Normal file
56
api/api.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package api
|
||||
|
||||
const (
|
||||
// Version API版本号
|
||||
Version = "v1.0.0"
|
||||
|
||||
// DefaultVersion 默认的ECS版本号
|
||||
DefaultVersion = "v0.1.114"
|
||||
)
|
||||
|
||||
// 测试方法常量
|
||||
const (
|
||||
// CPU测试方法
|
||||
CpuMethodSysbench = "sysbench"
|
||||
CpuMethodGeekbench = "geekbench"
|
||||
CpuMethodWinsat = "winsat"
|
||||
|
||||
// 内存测试方法
|
||||
MemoryMethodStream = "stream"
|
||||
MemoryMethodSysbench = "sysbench"
|
||||
MemoryMethodDD = "dd"
|
||||
MemoryMethodWinsat = "winsat"
|
||||
|
||||
// 硬盘测试方法
|
||||
DiskMethodFio = "fio"
|
||||
DiskMethodDD = "dd"
|
||||
DiskMethodWinsat = "winsat"
|
||||
|
||||
// 线程模式
|
||||
ThreadModeSingle = "single"
|
||||
ThreadModeMulti = "multi"
|
||||
|
||||
// 语言选项
|
||||
LanguageZH = "zh"
|
||||
LanguageEN = "en"
|
||||
|
||||
// IP检测类型
|
||||
CheckTypeIPv4 = "ipv4"
|
||||
CheckTypeIPv6 = "ipv6"
|
||||
CheckTypeAuto = "auto"
|
||||
|
||||
// 测速平台
|
||||
PlatformCN = "cn"
|
||||
PlatformNet = "net"
|
||||
|
||||
// 运营商类型
|
||||
OperatorCMCC = "cmcc" // 中国移动
|
||||
OperatorCU = "cu" // 中国联通
|
||||
OperatorCT = "ct" // 中国电信
|
||||
OperatorGlobal = "global" // 全球节点
|
||||
OperatorOther = "other" // 其他
|
||||
OperatorHK = "hk" // 香港
|
||||
OperatorTW = "tw" // 台湾
|
||||
OperatorJP = "jp" // 日本
|
||||
OperatorSG = "sg" // 新加坡
|
||||
)
|
||||
266
api/config.go
Normal file
266
api/config.go
Normal file
@@ -0,0 +1,266 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/oneclickvirt/ecs/internal/params"
|
||||
)
|
||||
|
||||
// Config 配置接口,导出用于外部调用
|
||||
type Config = params.Config
|
||||
|
||||
// NewConfig 创建默认配置
|
||||
// version: 版本号字符串
|
||||
func NewConfig(version string) *Config {
|
||||
return params.NewConfig(version)
|
||||
}
|
||||
|
||||
// NewDefaultConfig 创建默认配置(使用默认版本号)
|
||||
func NewDefaultConfig() *Config {
|
||||
return params.NewConfig("v0.1.114")
|
||||
}
|
||||
|
||||
// ConfigOption 配置选项函数类型
|
||||
type ConfigOption func(*Config)
|
||||
|
||||
// WithLanguage 设置语言
|
||||
func WithLanguage(lang string) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.Language = lang
|
||||
}
|
||||
}
|
||||
|
||||
// WithCpuTestMethod 设置CPU测试方法
|
||||
// method: "sysbench" 或 "geekbench"
|
||||
func WithCpuTestMethod(method string) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.CpuTestMethod = method
|
||||
}
|
||||
}
|
||||
|
||||
// WithCpuTestThreadMode 设置CPU测试线程模式
|
||||
// mode: "single" 或 "multi"
|
||||
func WithCpuTestThreadMode(mode string) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.CpuTestThreadMode = mode
|
||||
}
|
||||
}
|
||||
|
||||
// WithMemoryTestMethod 设置内存测试方法
|
||||
// method: "stream", "sysbench", "dd"
|
||||
func WithMemoryTestMethod(method string) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.MemoryTestMethod = method
|
||||
}
|
||||
}
|
||||
|
||||
// WithDiskTestMethod 设置硬盘测试方法
|
||||
// method: "fio" 或 "dd"
|
||||
func WithDiskTestMethod(method string) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.DiskTestMethod = method
|
||||
}
|
||||
}
|
||||
|
||||
// WithDiskTestPath 设置硬盘测试路径
|
||||
func WithDiskTestPath(path string) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.DiskTestPath = path
|
||||
}
|
||||
}
|
||||
|
||||
// WithDiskMultiCheck 设置是否进行硬盘多路径检测
|
||||
func WithDiskMultiCheck(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.DiskMultiCheck = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithSpeedTestNum 设置测速节点数量
|
||||
func WithSpeedTestNum(num int) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.SpNum = num
|
||||
}
|
||||
}
|
||||
|
||||
// WithWidth 设置输出宽度
|
||||
func WithWidth(width int) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.Width = width
|
||||
}
|
||||
}
|
||||
|
||||
// WithFilePath 设置输出文件路径
|
||||
func WithFilePath(path string) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.FilePath = path
|
||||
}
|
||||
}
|
||||
|
||||
// WithEnableUpload 设置是否启用上传
|
||||
func WithEnableUpload(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.EnableUpload = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithAnalyzeResult 设置是否启用测试后结果总结分析
|
||||
func WithAnalyzeResult(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.AnalyzeResult = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithEnableLogger 设置是否启用日志
|
||||
func WithEnableLogger(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.EnableLogger = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithBasicTest 设置是否执行基础信息测试
|
||||
func WithBasicTest(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.BasicStatus = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithCpuTest 设置是否执行CPU测试
|
||||
func WithCpuTest(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.CpuTestStatus = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithMemoryTest 设置是否执行内存测试
|
||||
func WithMemoryTest(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.MemoryTestStatus = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithDiskTest 设置是否执行硬盘测试
|
||||
func WithDiskTest(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.DiskTestStatus = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithUnlockTest 设置是否执行流媒体解锁测试
|
||||
func WithUnlockTest(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.UtTestStatus = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithSecurityTest 设置是否执行IP质量测试
|
||||
func WithSecurityTest(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.SecurityTestStatus = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithEmailTest 设置是否执行邮件端口测试
|
||||
func WithEmailTest(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.EmailTestStatus = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithBacktraceTest 设置是否执行回程路由测试
|
||||
func WithBacktraceTest(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.BacktraceStatus = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithNt3Test 设置是否执行三网路由测试
|
||||
func WithNt3Test(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.Nt3Status = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithSpeedTest 设置是否执行测速测试
|
||||
func WithSpeedTest(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.SpeedTestStatus = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithPingTest 设置是否执行PING测试
|
||||
func WithPingTest(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.PingTestStatus = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithTgdcTest 设置是否执行Telegram DC测试
|
||||
func WithTgdcTest(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.TgdcTestStatus = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithWebTest 设置是否执行网站测试
|
||||
func WithWebTest(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.WebTestStatus = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithNt3CheckType 设置三网路由检测类型
|
||||
// checkType: "ipv4", "ipv6" 或 "auto"
|
||||
func WithNt3CheckType(checkType string) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.Nt3CheckType = checkType
|
||||
}
|
||||
}
|
||||
|
||||
// WithNt3Location 设置三网路由检测位置
|
||||
func WithNt3Location(location string) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.Nt3Location = location
|
||||
}
|
||||
}
|
||||
|
||||
// WithAutoChangeDiskMethod 设置是否自动切换硬盘测试方法
|
||||
func WithAutoChangeDiskMethod(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.AutoChangeDiskMethod = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithOnlyChinaTest 设置是否只进行国内测试
|
||||
func WithOnlyChinaTest(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.OnlyChinaTest = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithMenuMode 设置是否启用菜单模式
|
||||
func WithMenuMode(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.MenuMode = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithOnlyIpInfoCheck 设置是否只进行IP信息检测
|
||||
func WithOnlyIpInfoCheck(enable bool) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.OnlyIpInfoCheck = enable
|
||||
}
|
||||
}
|
||||
|
||||
// WithChoice 设置菜单选择
|
||||
func WithChoice(choice string) ConfigOption {
|
||||
return func(c *Config) {
|
||||
c.Choice = choice
|
||||
}
|
||||
}
|
||||
|
||||
// ApplyOptions 应用配置选项
|
||||
func ApplyOptions(config *Config, options ...ConfigOption) *Config {
|
||||
for _, opt := range options {
|
||||
opt(config)
|
||||
}
|
||||
return config
|
||||
}
|
||||
27
api/menu.go
Normal file
27
api/menu.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/oneclickvirt/ecs/internal/menu"
|
||||
"github.com/oneclickvirt/ecs/utils"
|
||||
)
|
||||
|
||||
// GetMenuChoice 获取用户菜单选择
|
||||
// language: 语言 ("zh" 或 "en")
|
||||
// 返回: 用户选择的选项
|
||||
func GetMenuChoice(language string) string {
|
||||
return menu.GetMenuChoice(language)
|
||||
}
|
||||
|
||||
// PrintMenuOptions 打印菜单选项
|
||||
// preCheck: 网络检查结果
|
||||
// config: 配置对象
|
||||
func PrintMenuOptions(preCheck utils.NetCheckResult, config *Config) {
|
||||
menu.PrintMenuOptions(preCheck, config)
|
||||
}
|
||||
|
||||
// HandleMenuMode 处理菜单模式
|
||||
// preCheck: 网络检查结果
|
||||
// config: 配置对象
|
||||
func HandleMenuMode(preCheck utils.NetCheckResult, config *Config) {
|
||||
menu.HandleMenuMode(preCheck, config)
|
||||
}
|
||||
190
api/runner.go
Normal file
190
api/runner.go
Normal file
@@ -0,0 +1,190 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/oneclickvirt/ecs/internal/runner"
|
||||
"github.com/oneclickvirt/ecs/utils"
|
||||
)
|
||||
|
||||
// RunResult 运行结果
|
||||
type RunResult struct {
|
||||
Output string // 完整输出
|
||||
Duration time.Duration // 运行时长
|
||||
StartTime time.Time // 开始时间
|
||||
EndTime time.Time // 结束时间
|
||||
}
|
||||
|
||||
// RunAllTests 执行所有测试(高级接口)
|
||||
// preCheck: 网络检查结果
|
||||
// config: 配置对象
|
||||
// 返回: 运行结果
|
||||
func RunAllTests(preCheck utils.NetCheckResult, config *Config) *RunResult {
|
||||
var (
|
||||
wg1, wg2, wg3 sync.WaitGroup
|
||||
basicInfo, securityInfo, emailInfo, mediaInfo, ptInfo string
|
||||
output, tempOutput string
|
||||
outputMutex sync.Mutex
|
||||
infoMutex sync.Mutex
|
||||
)
|
||||
|
||||
startTime := time.Now()
|
||||
|
||||
switch config.Language {
|
||||
case "zh":
|
||||
runner.RunChineseTests(preCheck, config, &wg1, &wg2, &wg3,
|
||||
&basicInfo, &securityInfo, &emailInfo, &mediaInfo, &ptInfo,
|
||||
&output, tempOutput, startTime, &outputMutex, &infoMutex)
|
||||
case "en":
|
||||
runner.RunEnglishTests(preCheck, config, &wg1, &wg2, &wg3,
|
||||
&basicInfo, &securityInfo, &emailInfo, &mediaInfo, &ptInfo,
|
||||
&output, tempOutput, startTime, &outputMutex, &infoMutex)
|
||||
default:
|
||||
runner.RunChineseTests(preCheck, config, &wg1, &wg2, &wg3,
|
||||
&basicInfo, &securityInfo, &emailInfo, &mediaInfo, &ptInfo,
|
||||
&output, tempOutput, startTime, &outputMutex, &infoMutex)
|
||||
}
|
||||
if config.AnalyzeResult {
|
||||
output = runner.AppendAnalysisSummary(config, output, tempOutput, &outputMutex)
|
||||
}
|
||||
|
||||
endTime := time.Now()
|
||||
|
||||
return &RunResult{
|
||||
Output: output,
|
||||
Duration: endTime.Sub(startTime),
|
||||
StartTime: startTime,
|
||||
EndTime: endTime,
|
||||
}
|
||||
}
|
||||
|
||||
// RunBasicTests 运行基础信息测试
|
||||
func RunBasicTests(preCheck utils.NetCheckResult, config *Config) string {
|
||||
var (
|
||||
basicInfo, securityInfo string
|
||||
output, tempOutput string
|
||||
outputMutex sync.Mutex
|
||||
)
|
||||
return runner.RunBasicTests(preCheck, config, &basicInfo, &securityInfo, output, tempOutput, &outputMutex)
|
||||
}
|
||||
|
||||
// RunCPUTest 运行CPU测试
|
||||
func RunCPUTest(config *Config) string {
|
||||
var (
|
||||
output, tempOutput string
|
||||
outputMutex sync.Mutex
|
||||
)
|
||||
return runner.RunCPUTest(config, output, tempOutput, &outputMutex)
|
||||
}
|
||||
|
||||
// RunMemoryTest 运行内存测试
|
||||
func RunMemoryTest(config *Config) string {
|
||||
var (
|
||||
output, tempOutput string
|
||||
outputMutex sync.Mutex
|
||||
)
|
||||
return runner.RunMemoryTest(config, output, tempOutput, &outputMutex)
|
||||
}
|
||||
|
||||
// RunDiskTest 运行硬盘测试
|
||||
func RunDiskTest(config *Config) string {
|
||||
var (
|
||||
output, tempOutput string
|
||||
outputMutex sync.Mutex
|
||||
)
|
||||
return runner.RunDiskTest(config, output, tempOutput, &outputMutex)
|
||||
}
|
||||
|
||||
// RunIpInfoCheck 执行IP信息检测
|
||||
func RunIpInfoCheck(config *Config) string {
|
||||
var (
|
||||
output, tempOutput string
|
||||
outputMutex sync.Mutex
|
||||
)
|
||||
return runner.RunIpInfoCheck(config, output, tempOutput, &outputMutex)
|
||||
}
|
||||
|
||||
// RunStreamingTests 运行流媒体测试
|
||||
func RunStreamingTests(config *Config, mediaInfo string) string {
|
||||
var (
|
||||
wg1 sync.WaitGroup
|
||||
output, tempOutput string
|
||||
outputMutex sync.Mutex
|
||||
infoMutex sync.Mutex
|
||||
)
|
||||
return runner.RunStreamingTests(config, &wg1, &mediaInfo, output, tempOutput, &outputMutex, &infoMutex)
|
||||
}
|
||||
|
||||
// RunSecurityTests 运行安全测试
|
||||
func RunSecurityTests(config *Config, securityInfo string) string {
|
||||
var (
|
||||
output, tempOutput string
|
||||
outputMutex sync.Mutex
|
||||
)
|
||||
return runner.RunSecurityTests(config, securityInfo, output, tempOutput, &outputMutex)
|
||||
}
|
||||
|
||||
// RunEmailTests 运行邮件端口测试
|
||||
func RunEmailTests(config *Config, emailInfo string) string {
|
||||
var (
|
||||
wg2 sync.WaitGroup
|
||||
output, tempOutput string
|
||||
outputMutex sync.Mutex
|
||||
infoMutex sync.Mutex
|
||||
)
|
||||
return runner.RunEmailTests(config, &wg2, &emailInfo, output, tempOutput, &outputMutex, &infoMutex)
|
||||
}
|
||||
|
||||
// RunNetworkTests 运行网络测试(中文模式)
|
||||
func RunNetworkTests(config *Config, ptInfo string) string {
|
||||
var (
|
||||
wg3 sync.WaitGroup
|
||||
output, tempOutput string
|
||||
outputMutex sync.Mutex
|
||||
infoMutex sync.Mutex
|
||||
)
|
||||
return runner.RunNetworkTests(config, &wg3, &ptInfo, output, tempOutput, &outputMutex, &infoMutex)
|
||||
}
|
||||
|
||||
// RunSpeedTests 运行测速测试(中文模式)
|
||||
func RunSpeedTests(config *Config) string {
|
||||
var (
|
||||
output, tempOutput string
|
||||
outputMutex sync.Mutex
|
||||
)
|
||||
return runner.RunSpeedTests(config, output, tempOutput, &outputMutex)
|
||||
}
|
||||
|
||||
// RunEnglishNetworkTests 运行网络测试(英文模式)
|
||||
func RunEnglishNetworkTests(config *Config, ptInfo string) string {
|
||||
var (
|
||||
wg3 sync.WaitGroup
|
||||
output, tempOutput string
|
||||
outputMutex sync.Mutex
|
||||
)
|
||||
return runner.RunEnglishNetworkTests(config, &wg3, &ptInfo, output, tempOutput, &outputMutex)
|
||||
}
|
||||
|
||||
// RunEnglishSpeedTests 运行测速测试(英文模式)
|
||||
func RunEnglishSpeedTests(config *Config) string {
|
||||
var (
|
||||
output, tempOutput string
|
||||
outputMutex sync.Mutex
|
||||
)
|
||||
return runner.RunEnglishSpeedTests(config, output, tempOutput, &outputMutex)
|
||||
}
|
||||
|
||||
// AppendTimeInfo 添加时间信息
|
||||
func AppendTimeInfo(config *Config, output string, startTime time.Time) string {
|
||||
var (
|
||||
tempOutput string
|
||||
outputMutex sync.Mutex
|
||||
)
|
||||
return runner.AppendTimeInfo(config, output, tempOutput, startTime, &outputMutex)
|
||||
}
|
||||
|
||||
// HandleUploadResults 处理上传结果
|
||||
func HandleUploadResults(config *Config, output string) {
|
||||
runner.HandleUploadResults(config, output)
|
||||
}
|
||||
101
api/tests.go
Normal file
101
api/tests.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/oneclickvirt/ecs/internal/tests"
|
||||
)
|
||||
|
||||
// TestResult 测试结果结构
|
||||
type TestResult struct {
|
||||
TestMethod string // 实际使用的测试方法
|
||||
Output string // 测试输出结果
|
||||
Success bool // 是否成功
|
||||
Error error // 错误信息
|
||||
}
|
||||
|
||||
// CpuTest CPU测试公共接口
|
||||
// language: 语言 ("zh" 或 "en")
|
||||
// testMethod: 测试方法 ("sysbench" 或 "geekbench")
|
||||
// testThread: 线程模式 ("single" 或 "multi")
|
||||
// 返回: (实际测试方法, 测试结果)
|
||||
func CpuTest(language, testMethod, testThread string) (string, string) {
|
||||
return tests.CpuTest(language, testMethod, testThread)
|
||||
}
|
||||
|
||||
// MemoryTest 内存测试公共接口
|
||||
// language: 语言 ("zh" 或 "en")
|
||||
// testMethod: 测试方法 ("stream", "sysbench", "dd")
|
||||
// 返回: (实际测试方法, 测试结果)
|
||||
func MemoryTest(language, testMethod string) (string, string) {
|
||||
return tests.MemoryTest(language, testMethod)
|
||||
}
|
||||
|
||||
// DiskTest 硬盘测试公共接口
|
||||
// language: 语言 ("zh" 或 "en")
|
||||
// testMethod: 测试方法 ("fio" 或 "dd")
|
||||
// testPath: 测试路径
|
||||
// isMultiCheck: 是否多路径检测
|
||||
// autoChange: 是否自动切换方法
|
||||
// 返回: (实际测试方法, 测试结果)
|
||||
func DiskTest(language, testMethod, testPath string, isMultiCheck, autoChange bool) (string, string) {
|
||||
return tests.DiskTest(language, testMethod, testPath, isMultiCheck, autoChange)
|
||||
}
|
||||
|
||||
// MediaTest 流媒体解锁测试公共接口
|
||||
// language: 语言 ("zh" 或 "en")
|
||||
// 返回: 测试结果
|
||||
func MediaTest(language string) string {
|
||||
return tests.MediaTest(language)
|
||||
}
|
||||
|
||||
// SpeedTestShowHead 显示测速表头
|
||||
// language: 语言 ("zh" 或 "en")
|
||||
func SpeedTestShowHead(language string) {
|
||||
tests.ShowHead(language)
|
||||
}
|
||||
|
||||
// SpeedTestNearby 就近节点测速
|
||||
func SpeedTestNearby() {
|
||||
tests.NearbySP()
|
||||
}
|
||||
|
||||
// SpeedTestCustom 自定义测速
|
||||
// platform: 平台 ("cn" 或 "net")
|
||||
// operator: 运营商 ("cmcc", "cu", "ct", "global", "other" 等)
|
||||
// num: 测试节点数量
|
||||
// language: 语言 ("zh" 或 "en")
|
||||
func SpeedTestCustom(platform, operator string, num int, language string) {
|
||||
tests.CustomSP(platform, operator, num, language)
|
||||
}
|
||||
|
||||
// NextTrace3Check 三网路由追踪测试
|
||||
// language: 语言 ("zh" 或 "en")
|
||||
// location: 位置
|
||||
// checkType: 检测类型 ("ipv4", "ipv6")
|
||||
func NextTrace3Check(language, location, checkType string) {
|
||||
tests.NextTrace3Check(language, location, checkType)
|
||||
}
|
||||
|
||||
// UpstreamsCheck 上游及回程线路检测
|
||||
func UpstreamsCheck(language string) {
|
||||
tests.UpstreamsCheck(language)
|
||||
}
|
||||
|
||||
// GetIPv4Address 获取当前IPv4地址
|
||||
func GetIPv4Address() string {
|
||||
return tests.IPV4
|
||||
}
|
||||
|
||||
// GetIPv6Address 获取当前IPv6地址
|
||||
func GetIPv6Address() string {
|
||||
return tests.IPV6
|
||||
}
|
||||
|
||||
// SetIPv4Address 设置IPv4地址(用于测试)
|
||||
func SetIPv4Address(ipv4 string) {
|
||||
tests.IPV4 = ipv4
|
||||
}
|
||||
|
||||
// SetIPv6Address 设置IPv6地址(用于测试)
|
||||
func SetIPv6Address(ipv6 string) {
|
||||
tests.IPV6 = ipv6
|
||||
}
|
||||
91
api/utils.go
Normal file
91
api/utils.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/oneclickvirt/ecs/utils"
|
||||
)
|
||||
|
||||
// NetCheckResult 网络检查结果
|
||||
type NetCheckResult = utils.NetCheckResult
|
||||
|
||||
// StatsResponse 统计信息响应
|
||||
type StatsResponse = utils.StatsResponse
|
||||
|
||||
// GitHubRelease GitHub发布信息
|
||||
type GitHubRelease = utils.GitHubRelease
|
||||
|
||||
// CheckPublicAccess 检查公网访问能力
|
||||
// timeout: 超时时间
|
||||
// 返回: 网络检查结果
|
||||
func CheckPublicAccess(timeout time.Duration) NetCheckResult {
|
||||
return utils.CheckPublicAccess(timeout)
|
||||
}
|
||||
|
||||
// GetGoescStats 获取goecs统计信息
|
||||
// 返回: (统计响应, 错误)
|
||||
func GetGoescStats() (*StatsResponse, error) {
|
||||
return utils.GetGoescStats()
|
||||
}
|
||||
|
||||
// GetLatestEcsRelease 获取最新的ECS版本信息
|
||||
// 返回: (GitHub发布信息, 错误)
|
||||
func GetLatestEcsRelease() (*GitHubRelease, error) {
|
||||
return utils.GetLatestEcsRelease()
|
||||
}
|
||||
|
||||
// PrintHead 打印程序头部信息
|
||||
// language: 语言 ("zh" 或 "en")
|
||||
// width: 显示宽度
|
||||
// version: 版本号
|
||||
func PrintHead(language string, width int, version string) {
|
||||
utils.PrintHead(language, width, version)
|
||||
}
|
||||
|
||||
// PrintCenteredTitle 打印居中标题
|
||||
// title: 标题文本
|
||||
// width: 显示宽度
|
||||
func PrintCenteredTitle(title string, width int) {
|
||||
utils.PrintCenteredTitle(title, width)
|
||||
}
|
||||
|
||||
// ProcessAndUpload 处理并上传结果
|
||||
// output: 输出内容
|
||||
// filePath: 文件路径
|
||||
// enableUpload: 是否启用上传
|
||||
// 返回: (HTTP URL, HTTPS URL)
|
||||
func ProcessAndUpload(output, filePath string, enableUpload bool, language string) (string, string) {
|
||||
return utils.ProcessAndUpload(output, filePath, enableUpload, language)
|
||||
}
|
||||
|
||||
// BasicsAndSecurityCheck 基础信息和安全检查
|
||||
// language: 语言
|
||||
// checkType: 检查类型
|
||||
// securityTestStatus: 是否执行安全测试
|
||||
// 返回: (IPv4地址, IPv6地址, 基础信息, 安全信息, 检查类型)
|
||||
func BasicsAndSecurityCheck(language, checkType string, securityTestStatus bool) (string, string, string, string, string) {
|
||||
return utils.BasicsAndSecurityCheck(language, checkType, securityTestStatus)
|
||||
}
|
||||
|
||||
// OnlyBasicsIpInfo 仅获取基础IP信息
|
||||
// language: 语言
|
||||
// 返回: (IPv4地址, IPv6地址, IP信息)
|
||||
func OnlyBasicsIpInfo(language string) (string, string, string) {
|
||||
return utils.OnlyBasicsIpInfo(language)
|
||||
}
|
||||
|
||||
// FormatGoecsNumber 格式化数字显示
|
||||
// num: 数字
|
||||
// 返回: 格式化后的字符串
|
||||
func FormatGoecsNumber(num int) string {
|
||||
return utils.FormatGoecsNumber(num)
|
||||
}
|
||||
|
||||
// PrintAndCapture 打印并捕获输出
|
||||
// fn: 执行的函数
|
||||
// tempOutput: 临时输出
|
||||
// existingOutput: 现有输出
|
||||
// 返回: 捕获的输出
|
||||
func PrintAndCapture(fn func(), tempOutput, existingOutput string) string {
|
||||
return utils.PrintAndCapture(fn, tempOutput, existingOutput)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package basic1
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test_basic(t *testing.T) {
|
||||
Basic("zh")
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package basic1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/oneclickvirt/basics/network"
|
||||
"github.com/oneclickvirt/basics/system"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 本包不在main中使用,仅做测试使用,真正调用的在 utils 中的 BasicsAndSecurityCheck
|
||||
func Basic(language string) {
|
||||
ipInfo, _, _ := network.NetworkCheck("both", false, language)
|
||||
systemInfo := system.CheckSystemInfo(language)
|
||||
basicInfo := strings.ReplaceAll(systemInfo+ipInfo, "\n\n", "\n")
|
||||
fmt.Printf(basicInfo)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package network1
|
||||
|
||||
import "github.com/oneclickvirt/security/network"
|
||||
|
||||
// 本包在main中不使用
|
||||
func NetworkCheck(checkType string, enableSecurityCheck bool, language string) (string, string, error) {
|
||||
return network.NetworkCheck(checkType, enableSecurityCheck, language)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package network1
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIpv4SecurityCheck(t *testing.T) {
|
||||
// 单项测试
|
||||
//result1, _ := Ipv4SecurityCheck("8.8.8.8", nil, "zh")
|
||||
//fmt.Println(result1)
|
||||
//result2, _ := Ipv6SecurityCheck("2001:4860:4860::8844", nil, "zh")
|
||||
//fmt.Println(result2)
|
||||
|
||||
// 全项测试
|
||||
ipInfo, securityInfo, _ := NetworkCheck("both", true, "zh")
|
||||
fmt.Println("--------------------------------------------------")
|
||||
fmt.Printf(ipInfo)
|
||||
fmt.Println("--------------------------------------------------")
|
||||
fmt.Printf(securityInfo)
|
||||
fmt.Println("--------------------------------------------------")
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package port
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/oneclickvirt/portchecker/email"
|
||||
)
|
||||
|
||||
// 常用端口阻断检测 TCP/UDP/ICMP 协议
|
||||
// 本包不在main中使用
|
||||
func EmailCheck() {
|
||||
res := email.EmailCheck()
|
||||
fmt.Println(res)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package port
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
EmailCheck()
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package backtrace
|
||||
|
||||
import (
|
||||
"github.com/oneclickvirt/backtrace/bk"
|
||||
)
|
||||
|
||||
func BackTrace() {
|
||||
backtrace.BackTrace()
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package backtrace
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
//func TestGeneratePrefixMap(t *testing.T) {
|
||||
// prefix := "223.119.8.0/21"
|
||||
// prefixList := GeneratePrefixList(prefix)
|
||||
// if prefixList != nil {
|
||||
// // 打印生成的IP地址前缀列表
|
||||
// for _, ip := range prefixList {
|
||||
// fmt.Println(ip)
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
||||
func TestBackTrace(t *testing.T) {
|
||||
BackTrace()
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package commediatest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/oneclickvirt/CommonMediaTests/commediatests"
|
||||
)
|
||||
|
||||
func ComMediaTest(language string) {
|
||||
res := commediatests.MediaTests(language)
|
||||
fmt.Printf(res)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package commediatest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMedia(t *testing.T) {
|
||||
ComMediaTest("zh")
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package cputest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/oneclickvirt/cputest/cpu"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func CpuTest(language, testMethod, testThread string) {
|
||||
var res string
|
||||
if runtime.GOOS == "windows" {
|
||||
if testMethod != "winsat" && testMethod != "" {
|
||||
res = "Detected host is Windows, using Winsat for testing.\n"
|
||||
}
|
||||
res += cpu.WinsatTest(language, testThread)
|
||||
} else {
|
||||
switch testMethod {
|
||||
case "sysbench":
|
||||
res = cpu.SysBenchTest(language, testThread)
|
||||
if res == "" {
|
||||
res = "Sysbench test failed, switching to Geekbench for testing.\n"
|
||||
res += cpu.GeekBenchTest(language, testThread)
|
||||
}
|
||||
case "geekbench":
|
||||
res = cpu.GeekBenchTest(language, testThread)
|
||||
if res == "" {
|
||||
res = "Geekbench test failed, switching to Sysbench for testing.\n"
|
||||
res += cpu.SysBenchTest(language, testThread)
|
||||
}
|
||||
default:
|
||||
res = "Invalid test method specified.\n"
|
||||
}
|
||||
}
|
||||
if !strings.Contains(res, "\n") && res != "" {
|
||||
res += "\n"
|
||||
}
|
||||
fmt.Print(res)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package cputest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
CpuTest("zh", "sysbench", "1")
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package disktest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/oneclickvirt/disktest/disk"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func DiskTest(language, testMethod, testPath string, isMultiCheck bool, autoChange bool) {
|
||||
var res string
|
||||
if runtime.GOOS == "windows" {
|
||||
if testMethod != "winsat" && testMethod != "" {
|
||||
res = "Detected host is Windows, using Winsat for testing.\n"
|
||||
}
|
||||
res = disk.WinsatTest(language, isMultiCheck, testPath)
|
||||
} else {
|
||||
switch testMethod {
|
||||
case "fio":
|
||||
res = disk.FioTest(language, isMultiCheck, testPath)
|
||||
if res == "" && autoChange {
|
||||
res = "Fio test failed, switching to DD for testing.\n"
|
||||
res += disk.DDTest(language, isMultiCheck, testPath)
|
||||
}
|
||||
case "dd":
|
||||
res = disk.DDTest(language, isMultiCheck, testPath)
|
||||
if res == "" && autoChange {
|
||||
res = "DD test failed, switching to Fio for testing.\n"
|
||||
res += disk.FioTest(language, isMultiCheck, testPath)
|
||||
}
|
||||
default:
|
||||
res = "Unsupported test method specified.\n"
|
||||
}
|
||||
}
|
||||
//fmt.Println("--------------------------------------------------")
|
||||
if !strings.Contains(res, "\n") && res != "" {
|
||||
res += "\n"
|
||||
}
|
||||
fmt.Printf(res)
|
||||
//fmt.Println("--------------------------------------------------")
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package disktest
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestDiskIoTest(t *testing.T) {
|
||||
DiskTest("zh", "sysbench", "", false)
|
||||
}
|
||||
131
go.mod
131
go.mod
@@ -1,71 +1,82 @@
|
||||
module github.com/oneclickvirt/ecs
|
||||
|
||||
go 1.23.4
|
||||
go 1.25.4
|
||||
|
||||
require (
|
||||
github.com/imroc/req/v3 v3.49.0
|
||||
github.com/oneclickvirt/CommonMediaTests v0.0.4-20250329123841
|
||||
github.com/oneclickvirt/UnlockTests v0.0.26-20250329125926
|
||||
github.com/oneclickvirt/backtrace v0.0.4-20250329130043
|
||||
github.com/oneclickvirt/basics v0.0.11-20250404123515
|
||||
github.com/oneclickvirt/cputest v0.0.10-20250404151448
|
||||
github.com/charmbracelet/bubbletea v1.3.10
|
||||
github.com/charmbracelet/lipgloss v1.1.0
|
||||
github.com/imroc/req/v3 v3.54.0
|
||||
github.com/oneclickvirt/UnlockTests v0.0.35-20260207053956
|
||||
github.com/oneclickvirt/backtrace v0.0.8-20251109090457
|
||||
github.com/oneclickvirt/basics v0.0.16-20251112033526
|
||||
github.com/oneclickvirt/cputest v0.0.12-20251111095842
|
||||
github.com/oneclickvirt/defaultset v0.0.2-20240624082446
|
||||
github.com/oneclickvirt/disktest v0.0.7-20250406062845
|
||||
github.com/oneclickvirt/gostun v0.0.3-20250329105202
|
||||
github.com/oneclickvirt/memorytest v0.0.5-20250406063420
|
||||
github.com/oneclickvirt/nt3 v0.0.4-20250329125813
|
||||
github.com/oneclickvirt/pingtest v0.0.6-20250329130728
|
||||
github.com/oneclickvirt/portchecker v0.0.3-20250329125750
|
||||
github.com/oneclickvirt/security v0.0.4-20250401123241
|
||||
github.com/oneclickvirt/speedtest v0.0.9-20250329130205
|
||||
github.com/oneclickvirt/disktest v0.0.10-20250924030424
|
||||
github.com/oneclickvirt/gostun v0.0.5-20250727155022
|
||||
github.com/oneclickvirt/memorytest v0.0.10-20251218032900
|
||||
github.com/oneclickvirt/nt3 v0.0.11-20260112140912
|
||||
github.com/oneclickvirt/pingtest v0.0.9-20251104112920
|
||||
github.com/oneclickvirt/portchecker v0.0.3-20250728015900
|
||||
github.com/oneclickvirt/privatespeedtest v0.0.1-20260112130218
|
||||
github.com/oneclickvirt/security v0.0.8-20260202071316
|
||||
github.com/oneclickvirt/speedtest v0.0.11-20251102151740
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/PuerkitoBio/goquery v1.9.2 // indirect
|
||||
github.com/StackExchange/wmi v1.2.1 // indirect
|
||||
github.com/andybalholm/brotli v1.1.1 // indirect
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/andybalholm/cascadia v1.3.2 // indirect
|
||||
github.com/cloudflare/circl v1.5.0 // indirect
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
||||
github.com/charmbracelet/x/ansi v0.10.1 // indirect
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/ghodss/yaml v1.0.0 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/gofrs/uuid/v5 v5.2.0 // indirect
|
||||
github.com/google/gopacket v1.1.19 // indirect
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huin/goupnp v1.2.0 // indirect
|
||||
github.com/icholy/digest v1.1.0 // indirect
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 // indirect
|
||||
github.com/jaypipes/ghw v0.12.0 // indirect
|
||||
github.com/jaypipes/pcidb v1.0.0 // indirect
|
||||
github.com/jaypipes/ghw v0.17.0 // indirect
|
||||
github.com/jaypipes/pcidb v1.0.1 // indirect
|
||||
github.com/jsdelivr/globalping-cli v1.5.1 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.11 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/koron/go-ssdp v0.0.4 // indirect
|
||||
github.com/libp2p/go-nat v0.2.0 // indirect
|
||||
github.com/libp2p/go-netroute v0.2.1 // indirect
|
||||
github.com/lionsoul2014/ip2region v2.11.2+incompatible // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-localereader v0.0.1 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/miekg/dns v1.1.61 // indirect
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/nxtrace/NTrace-core v1.3.7 // indirect
|
||||
github.com/oneclickvirt/dd v0.0.1-20250406062523 // indirect
|
||||
github.com/oneclickvirt/fio v0.0.1-20250406060851 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.22.1 // indirect
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/muesli/termenv v0.16.0 // indirect
|
||||
github.com/nxtrace/NTrace-core v1.5.0 // indirect
|
||||
github.com/oneclickvirt/dd v0.0.2-20250808062818 // indirect
|
||||
github.com/oneclickvirt/fio v0.0.2-20250808045755 // indirect
|
||||
github.com/oneclickvirt/mbw v0.0.1-20250808061222 // indirect
|
||||
github.com/oneclickvirt/stream v0.0.2-20250924154001 // indirect
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.7 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/stun/v2 v2.0.0 // indirect
|
||||
@@ -75,44 +86,40 @@ require (
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/prometheus-community/pro-bing v0.4.1 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.48.2 // indirect
|
||||
github.com/refraction-networking/utls v1.6.7 // indirect
|
||||
github.com/quic-go/quic-go v0.55.0 // indirect
|
||||
github.com/refraction-networking/utls v1.7.3 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rodaine/table v1.3.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/schollz/progressbar/v3 v3.14.4 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/schollz/progressbar/v3 v3.17.1 // indirect
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.24.5 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/showwin/speedtest-go v1.7.7 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.19.0 // indirect
|
||||
github.com/shirou/gopsutil/v4 v4.25.6 // indirect
|
||||
github.com/showwin/speedtest-go v1.7.10 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/spf13/viper v1.21.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||
github.com/tklauser/numcpus v0.8.0 // indirect
|
||||
github.com/tsosunchia/powclient v0.1.5 // indirect
|
||||
github.com/tklauser/numcpus v0.9.0 // indirect
|
||||
github.com/tsosunchia/powclient v0.2.0 // indirect
|
||||
github.com/xjasonlyu/windivert-go v0.0.0-20201010013527-4239d0afa76f // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.27.0 // indirect
|
||||
golang.org/x/crypto v0.32.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.34.0 // indirect
|
||||
golang.org/x/sync v0.10.0 // indirect
|
||||
golang.org/x/sys v0.29.0 // indirect
|
||||
golang.org/x/term v0.28.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
golang.org/x/tools v0.29.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.45.0 // indirect
|
||||
golang.org/x/mod v0.29.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/sys v0.38.0 // indirect
|
||||
golang.org/x/term v0.37.0 // indirect
|
||||
golang.org/x/text v0.31.0 // indirect
|
||||
golang.org/x/tools v0.38.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
howett.net/plist v1.0.0 // indirect
|
||||
)
|
||||
|
||||
291
go.sum
291
go.sum
@@ -2,41 +2,57 @@ github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4
|
||||
github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk=
|
||||
github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA=
|
||||
github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8=
|
||||
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
|
||||
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss=
|
||||
github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU=
|
||||
github.com/cloudflare/circl v1.5.0 h1:hxIWksrX6XN5a1L2TI/h53AGPhNHoUBo+TD1ms9+pys=
|
||||
github.com/cloudflare/circl v1.5.0/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
||||
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
|
||||
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
||||
github.com/charmbracelet/x/ansi v0.10.1 h1:rL3Koar5XvX0pHGfovN03f5cxLbCF2YvLeyz7D2jVDQ=
|
||||
github.com/charmbracelet/x/ansi v0.10.1/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
||||
github.com/chengxilo/virtualterm v1.0.4 h1:Z6IpERbRVlfB8WkOmtbHiDbBANU7cimRIof7mk9/PwM=
|
||||
github.com/chengxilo/virtualterm v1.0.4/go.mod h1:DyxxBZz/x1iqJjFxTFcr6/x+jSpqN0iwWCOK1q10rlY=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
|
||||
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
|
||||
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
|
||||
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/gofrs/uuid/v5 v5.2.0 h1:qw1GMx6/y8vhVsx626ImfKMuS5CvJmhIKKtuyvfajMM=
|
||||
github.com/gofrs/uuid/v5 v5.2.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
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/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
@@ -46,24 +62,25 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
|
||||
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
|
||||
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
|
||||
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/huin/goupnp v1.2.0 h1:uOKW26NG1hsSSbXIZ1IR7XP9Gjd1U8pnLaCMgntmkmY=
|
||||
github.com/huin/goupnp v1.2.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8=
|
||||
github.com/imroc/req/v3 v3.49.0 h1:5Rac2qvz7Dq0E3PeBo/c2szV3hagPQIGLoHtfBmYhu4=
|
||||
github.com/imroc/req/v3 v3.49.0/go.mod h1:XZf4t94DNJzcA0UOBlA68hmSrWsAyvN407ADdH4mzCA=
|
||||
github.com/icholy/digest v1.1.0 h1:HfGg9Irj7i+IX1o1QAmPfIBNu/Q5A5Tu3n/MED9k9H4=
|
||||
github.com/icholy/digest v1.1.0/go.mod h1:QNrsSGQ5v7v9cReDI0+eyjsXGUoRSUZQHeQ5C4XLa0Y=
|
||||
github.com/imroc/req/v3 v3.54.0 h1:kwWJSpT7OvjJ/Q8ykp+69Ye5H486RKDcgEoepw1Ren4=
|
||||
github.com/imroc/req/v3 v3.54.0/go.mod h1:P8gCJjG/XNUFeP6WOi40VAXfYwT+uPM00xvoBWiwzUQ=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
|
||||
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
|
||||
github.com/jaypipes/ghw v0.12.0 h1:xU2/MDJfWmBhJnujHY9qwXQLs3DBsf0/Xa9vECY0Tho=
|
||||
github.com/jaypipes/ghw v0.12.0/go.mod h1:jeJGbkRB2lL3/gxYzNYzEDETV1ZJ56OKr+CSeSEym+g=
|
||||
github.com/jaypipes/pcidb v1.0.0 h1:vtZIfkiCUE42oYbJS0TAq9XSfSmcsgo9IdxSm9qzYU8=
|
||||
github.com/jaypipes/pcidb v1.0.0/go.mod h1:TnYUvqhPBzCKnH34KrIX22kAeEbDCSRJ9cqLRCuNDfk=
|
||||
github.com/jaypipes/ghw v0.17.0 h1:EVLJeNcy5z6GK/Lqby0EhBpynZo+ayl8iJWY0kbEUJA=
|
||||
github.com/jaypipes/ghw v0.17.0/go.mod h1:In8SsaDqlb1oTyrbmTC14uy+fbBMvp+xdqX51MidlD8=
|
||||
github.com/jaypipes/pcidb v1.0.1 h1:WB2zh27T3nwg8AE8ei81sNRb9yWBii3JGNJtT7K9Oic=
|
||||
github.com/jaypipes/pcidb v1.0.1/go.mod h1:6xYUz/yYEyOkIkUt2t2J2folIuZ4Yg6uByCGFXMCeE4=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jsdelivr/globalping-cli v1.5.1 h1:7RZNmIljSBXe0xBeOoGQHXZNwHo6zDuQ0BI9hF12gLY=
|
||||
github.com/jsdelivr/globalping-cli v1.5.1/go.mod h1:Gw70OWvN6hIt0t4hftyUhcHuJQMTn4CvoobJiaTU0qg=
|
||||
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/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw=
|
||||
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
|
||||
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
|
||||
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
@@ -74,16 +91,16 @@ github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk=
|
||||
github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk=
|
||||
github.com/libp2p/go-netroute v0.2.1 h1:V8kVrpD8GK0Riv15/7VN6RbUQ3URNZVosw7H2v9tksU=
|
||||
github.com/libp2p/go-netroute v0.2.1/go.mod h1:hraioZr0fhBjG0ZRXJJ6Zj2IVEVNx6tDTFQfSmcq7mQ=
|
||||
github.com/lionsoul2014/ip2region v2.11.2+incompatible h1:+VRsGcrHz8ewXI/2UzTptJlACsxD/p4xCxuql4u2nKU=
|
||||
github.com/lionsoul2014/ip2region v2.11.2+incompatible/go.mod h1:+ZBN7PBoh5gG6/y0ZQ85vJDBe21WnfbRrQQwTfliJJI=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM=
|
||||
github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
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/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
|
||||
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
|
||||
@@ -92,55 +109,59 @@ github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2Em
|
||||
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
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/nxtrace/NTrace-core v1.3.7 h1:ZnTbPrPqpyeraCvUyNbQTNyl4Gz3NRQDh06WdIIHh90=
|
||||
github.com/nxtrace/NTrace-core v1.3.7/go.mod h1:aW2owz9I+W5i+gJEDmnWli75mB+fuO4UTwdOPMcQHpE=
|
||||
github.com/oneclickvirt/CommonMediaTests v0.0.4-20250329123841 h1:Zef93z9UiZQwRAKnnZYALmpBKvvuVaq34MEsuWwk6nc=
|
||||
github.com/oneclickvirt/CommonMediaTests v0.0.4-20250329123841/go.mod h1:DAmFPRjFV5p9fEzUUSml5jJGn2f1NZJQCzTxITHDjc4=
|
||||
github.com/oneclickvirt/UnlockTests v0.0.26-20250329125926 h1:H5//xwVjDR02bQ1hLa3G7LnwccsudPMjBVt7WCx2y/U=
|
||||
github.com/oneclickvirt/UnlockTests v0.0.26-20250329125926/go.mod h1:yXWIZB6iLS88pEd9m4QJi1GENn+7I91zA72y5ONz2Oc=
|
||||
github.com/oneclickvirt/backtrace v0.0.4-20250329130043 h1:d3ubN7FTY2xDKN8mmLc1RbvccpTkvfMADtMBvFCRdt0=
|
||||
github.com/oneclickvirt/backtrace v0.0.4-20250329130043/go.mod h1:zvsC7xY/WZqs5KL2JB967OVnuqjNbxu9bW6wXRLo5h8=
|
||||
github.com/oneclickvirt/basics v0.0.11-20250404123515 h1:2kisK9tBG/Km/HLQFA82lm/B+AWFJp3drjrtGvz8lhw=
|
||||
github.com/oneclickvirt/basics v0.0.11-20250404123515/go.mod h1:yN1IEOXN6v/GJqJSA70Pooo6nXBI/6rq72vTY72wJMQ=
|
||||
github.com/oneclickvirt/cputest v0.0.10-20250404151448 h1:ovGtCwFXG0qmpyNDRqcNDIiAmhrtemCjIUXTJ1fPH0o=
|
||||
github.com/oneclickvirt/cputest v0.0.10-20250404151448/go.mod h1:MmaHN9+XMntI3rLycwj8Ne31fG18IfNoa8N2utDK1CY=
|
||||
github.com/oneclickvirt/dd v0.0.1-20250406062523 h1:jegTww4fuoFEqwFozvGJEqUNI/5ew3QJ0XcKZZ/zuTs=
|
||||
github.com/oneclickvirt/dd v0.0.1-20250406062523/go.mod h1:tImu9sPTkLWo2tf1dEN1xQzrylWKauj9hbU8PHfyAeU=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
|
||||
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
||||
github.com/nxtrace/NTrace-core v1.5.0 h1:n+a/FObw/+CcqvhuSQiWcm1q+ODtfo7Wt3VmaIx504I=
|
||||
github.com/nxtrace/NTrace-core v1.5.0/go.mod h1:/jME48iJ7QaVTzsrTPQyTJ+yExhjeWjax2L6uBd4ckk=
|
||||
github.com/oneclickvirt/UnlockTests v0.0.35-20260207053956 h1:yccGrw/sYOHZMaFJghPVN3Xn6JyTOXsEQc9v0I92k3M=
|
||||
github.com/oneclickvirt/UnlockTests v0.0.35-20260207053956/go.mod h1:oOa6wj/qECtRMxwBO6D7o0L0F0Q/5sQ747OCnFQqoGE=
|
||||
github.com/oneclickvirt/backtrace v0.0.8-20251109090457 h1:599/R/qMAtfPCPG1bPoi6KbjNJzVkKtxm8dvVIdtn5o=
|
||||
github.com/oneclickvirt/backtrace v0.0.8-20251109090457/go.mod h1:mj9TSow7FNszBb3bQj2Hhm41LwBo7HQP6sgaPtovKdM=
|
||||
github.com/oneclickvirt/basics v0.0.16-20251112033526 h1:bgoLaqStV3a6mbPiM++0mYizd278GVa6J6yeIiusV+A=
|
||||
github.com/oneclickvirt/basics v0.0.16-20251112033526/go.mod h1:2PV+1ge01zb0Sqzj2V2I7P0wAdFSLF1XgAiumchJJbg=
|
||||
github.com/oneclickvirt/cputest v0.0.12-20251111095842 h1:ixZUvIkSlsIZfsg+dNDKq/FTofEtUjfA2LtpTrNr/6s=
|
||||
github.com/oneclickvirt/cputest v0.0.12-20251111095842/go.mod h1:vjlH8tkPFft1tlLOpeNskXVvurxkHaJ3+dgFxQGLXY4=
|
||||
github.com/oneclickvirt/dd v0.0.2-20250808062818 h1:0KHrKkdpL5oBE1OHsrRd2siRw4/2k6f9LBaP7T4JpOc=
|
||||
github.com/oneclickvirt/dd v0.0.2-20250808062818/go.mod h1:tImu9sPTkLWo2tf1dEN1xQzrylWKauj9hbU8PHfyAeU=
|
||||
github.com/oneclickvirt/defaultset v0.0.2-20240624082446 h1:5Pg3mK/u/vQvSz7anu0nxzrNdELi/AcDAU1mMsmPzyc=
|
||||
github.com/oneclickvirt/defaultset v0.0.2-20240624082446/go.mod h1:e9Jt4tf2sbemCtc84/XgKcHy9EZ2jkc5x2sW1NiJS+E=
|
||||
github.com/oneclickvirt/disktest v0.0.7-20250406062845 h1:2UAn1TYdRs6IXAqRhsf3WSd2yuVfV481a48B3d7Yz2o=
|
||||
github.com/oneclickvirt/disktest v0.0.7-20250406062845/go.mod h1:sqVu6HwbnLmbnRj4389Xn08c301IhLnWCcbaEk2WzEc=
|
||||
github.com/oneclickvirt/fio v0.0.1-20250406060851 h1:b7xHKpPmU4q0NmvigRCEr3tQuAV/83ZIAGtHycLegw8=
|
||||
github.com/oneclickvirt/fio v0.0.1-20250406060851/go.mod h1:NIq+XYTey68KNERGIy/oRDlzpwLzBVoHOCiqX8didsE=
|
||||
github.com/oneclickvirt/gostun v0.0.3-20250329105202 h1:aJ6E91Lp94lq8iWRcCaxpXTjqOOaWvufr5oras6cFtM=
|
||||
github.com/oneclickvirt/gostun v0.0.3-20250329105202/go.mod h1:f7DPEXAxbmwXSW33dbxtb0/KzqvOBWhTs2Or5xBerQA=
|
||||
github.com/oneclickvirt/memorytest v0.0.5-20250406063420 h1:eHqpqFIx8Ss062uyNf7Ruv7FC4AdZbElR7u9vX2Oj3g=
|
||||
github.com/oneclickvirt/memorytest v0.0.5-20250406063420/go.mod h1:HTd0sSxRjT4BcV8kcCh4fF2Nia0xgZNaVjhefsnypic=
|
||||
github.com/oneclickvirt/nt3 v0.0.4-20250329125813 h1:ZK5DAz8GUa6oyRE/cWmUKkE02tfgDah4TXvN181em7c=
|
||||
github.com/oneclickvirt/nt3 v0.0.4-20250329125813/go.mod h1:lDpHmjWwLZd6pO8cZCwzrc5rwd8dK+TIYk+DGmRpSGg=
|
||||
github.com/oneclickvirt/pingtest v0.0.6-20250329130728 h1:XYMfpIj32Wuej5G7f7/NB5/i3I9DMX/mF3ZS8N5KKp8=
|
||||
github.com/oneclickvirt/pingtest v0.0.6-20250329130728/go.mod h1:d3Ntx5m9lMll3a/k3+2B+5emj//vgDh4/NHTxs2qQE8=
|
||||
github.com/oneclickvirt/portchecker v0.0.3-20250329125750 h1:TTNL0pnQlRsn046kW59I/9UWRpihttFHWnU7Ixycggk=
|
||||
github.com/oneclickvirt/portchecker v0.0.3-20250329125750/go.mod h1:HQxSTrqM8/QFqHMTBZ7S8H9eEO5FkUXU1eb7ZX5Mk+k=
|
||||
github.com/oneclickvirt/security v0.0.4-20250401123241 h1:myeAQ1wOKIHx5r9qs9dCwx/5FepY+hZu/fDNquMwKaw=
|
||||
github.com/oneclickvirt/security v0.0.4-20250401123241/go.mod h1:Cyo3hwh1irn6yWnTh+YVKtkkVzHlwZgm7t7qR3IPRQA=
|
||||
github.com/oneclickvirt/speedtest v0.0.9-20250329130205 h1:XWM6FhObi+2bEkntPcAAKkiS9w7r6j79DOtmlbq4hhs=
|
||||
github.com/oneclickvirt/speedtest v0.0.9-20250329130205/go.mod h1:zd5ZgIGslmtQLQehEfRjyumlvgDHTpCSMchKfKXoASI=
|
||||
github.com/onsi/ginkgo/v2 v2.22.1 h1:QW7tbJAUDyVDVOM5dFa7qaybo+CRfR7bemlQUN6Z8aM=
|
||||
github.com/onsi/ginkgo/v2 v2.22.1/go.mod h1:S6aTpoRsSq2cZOd+pssHAlKW/Q/jZt6cPrPlnj4a1xM=
|
||||
github.com/onsi/gomega v1.36.1 h1:bJDPBO7ibjxcbHMgSCoo4Yj18UWbKDlLwX1x9sybDcw=
|
||||
github.com/onsi/gomega v1.36.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
|
||||
github.com/oneclickvirt/disktest v0.0.10-20250924030424 h1:56Aq2xygO/vA/co5vJ7/MQTNijIDl8eYbVk8uCWN4mI=
|
||||
github.com/oneclickvirt/disktest v0.0.10-20250924030424/go.mod h1:Vp3iMVBD4ccReDJz5n5SlzUdq0kDuVhpRklQk21KT+8=
|
||||
github.com/oneclickvirt/fio v0.0.2-20250808045755 h1:eWihCRWcalJjPIdrF8dMe68ZiPnMkSfHC8ENvElp/xE=
|
||||
github.com/oneclickvirt/fio v0.0.2-20250808045755/go.mod h1:NIq+XYTey68KNERGIy/oRDlzpwLzBVoHOCiqX8didsE=
|
||||
github.com/oneclickvirt/gostun v0.0.5-20250727155022 h1:/e3gSUrOp1tg/1NTRx+P8B51OGcP26Q6//5EoSIjOvk=
|
||||
github.com/oneclickvirt/gostun v0.0.5-20250727155022/go.mod h1:pfp7MFZJK9n/KTLAVqqFcCAns4xqMykmjI+1UeF/vdE=
|
||||
github.com/oneclickvirt/mbw v0.0.1-20250808061222 h1:WGXOe6QvHiDRhPVMI0VcctjzW08kGvJf50yq5YeZCtw=
|
||||
github.com/oneclickvirt/mbw v0.0.1-20250808061222/go.mod h1:0Vq6NRpyLmGUdfHfL3uDcFsuZhi7KlG+OCs5ky2757Y=
|
||||
github.com/oneclickvirt/memorytest v0.0.10-20251218032900 h1:SmRFfPLyGfTVWIgC50lEGgOpbqahtMHIlyOMSbrhj9Y=
|
||||
github.com/oneclickvirt/memorytest v0.0.10-20251218032900/go.mod h1:4kiHsEWkW9r3/1ZcV5xIweU0smiKP0IRfQj74AUIiVI=
|
||||
github.com/oneclickvirt/nt3 v0.0.11-20260112140912 h1:e3tgkEmydsML6ziOdWwsVGwysTRYS82SuWrP0HnIw9g=
|
||||
github.com/oneclickvirt/nt3 v0.0.11-20260112140912/go.mod h1:u/y3sMhyt4wiQlR7yS68CudwjXCa/4V6ozWI7awsCws=
|
||||
github.com/oneclickvirt/pingtest v0.0.9-20251104112920 h1:j3Fjhy0YHT/VF7iuAVVELaRXkquvRd64tWWfFLJs01o=
|
||||
github.com/oneclickvirt/pingtest v0.0.9-20251104112920/go.mod h1:gxwsxxwitNQiGq2OI0ZogYoOLwc8DtuOdSRe6/EvRqs=
|
||||
github.com/oneclickvirt/portchecker v0.0.3-20250728015900 h1:AomzdppSOFB70AJESQhlp0IPbsHTTJGimAWDk2TzCWM=
|
||||
github.com/oneclickvirt/portchecker v0.0.3-20250728015900/go.mod h1:9sjMDPCd4Z40wkYB0S9gQPGH8YPtnNE1ZJthVIuHUzA=
|
||||
github.com/oneclickvirt/privatespeedtest v0.0.1-20260112130218 h1:h2k2fHtrsIIP/x/apEWkQGlTKuIumz8GrUR/df41YhE=
|
||||
github.com/oneclickvirt/privatespeedtest v0.0.1-20260112130218/go.mod h1:IXOlKKX4DUNqxOaW/K9bcdrBiWxo0jGSLXeBeo7NrTo=
|
||||
github.com/oneclickvirt/security v0.0.8-20260202071316 h1:ULZWXC99IzrdFEG05D2/MQklKAhztQNc6UYCE3fEQeU=
|
||||
github.com/oneclickvirt/security v0.0.8-20260202071316/go.mod h1:aPMIwqsz7wiUH1cqvtRr9+QcQRkKzlUWecDM6SGVddc=
|
||||
github.com/oneclickvirt/speedtest v0.0.11-20251102151740 h1:1NUrNt5ay6/xVNC5x62UrQjPqK8jgbKtyjBml/3boZg=
|
||||
github.com/oneclickvirt/speedtest v0.0.11-20251102151740/go.mod h1:fy0II2Wo7kDWVBKTwcHdodZwyfmJo0g8N9V02EwQDZE=
|
||||
github.com/oneclickvirt/stream v0.0.2-20250924154001 h1:GuJWdiPkoK84+y/+oHKr2Ghl3c/MzS9Z5m1nM+lMmy4=
|
||||
github.com/oneclickvirt/stream v0.0.2-20250924154001/go.mod h1:oWaizaHTC2VQciBC9RfaLbAOf8qeR6n20/gY7QxriDE=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1 h1:G3wwjdN9JmIK2o/ermkHM+98oX5fS+k5MbwsmL4MRQE=
|
||||
github.com/oschwald/maxminddb-golang v1.13.1/go.mod h1:K4pgV9N/GcK694KSTmVSDTODk4IsCNThNdTmnaBZ/F8=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
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/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
|
||||
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
@@ -162,43 +183,35 @@ github.com/prometheus-community/pro-bing v0.4.1 h1:aMaJwyifHZO0y+h8+icUz0xbToHbi
|
||||
github.com/prometheus-community/pro-bing v0.4.1/go.mod h1:aLsw+zqCaDoa2RLVVSX3+UiCkBBXTMtZC3c7EkfWnAE=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
|
||||
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
|
||||
github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
|
||||
github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
|
||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||
github.com/refraction-networking/utls v1.7.3 h1:L0WRhHY7Oq1T0zkdzVZMR6zWZv+sXbHB9zcuvsAEqCo=
|
||||
github.com/refraction-networking/utls v1.7.3/go.mod h1:TUhh27RHMGtQvjQq+RyO11P6ZNQNBb3N0v7wsEjKAIQ=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rodaine/table v1.3.0 h1:4/3S3SVkHnVZX91EHFvAMV7K42AnJ0XuymRR2C5HlGE=
|
||||
github.com/rodaine/table v1.3.0/go.mod h1:47zRsHar4zw0jgxGxL9YtFfs7EGN6B/TaS+/Dmk4WxU=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/schollz/progressbar/v3 v3.14.4 h1:W9ZrDSJk7eqmQhd3uxFNNcTr0QL+xuGNI9dEMrw0r74=
|
||||
github.com/schollz/progressbar/v3 v3.14.4/go.mod h1:aT3UQ7yGm+2ZjeXPqsjTenwL3ddUiuZ0kfQ/2tHlyNI=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sagikazarmark/locafero v0.12.0 h1:/NQhBAkUb4+fH1jivKHWusDYFjMOOKU88eegjfxfHb4=
|
||||
github.com/sagikazarmark/locafero v0.12.0/go.mod h1:sZh36u/YSZ918v0Io+U9ogLYQJ9tLLBmM4eneO6WwsI=
|
||||
github.com/schollz/progressbar/v3 v3.17.1 h1:bI1MTaoQO+v5kzklBjYNRQLoVpe0zbyRZNK6DFkVC5U=
|
||||
github.com/schollz/progressbar/v3 v3.17.1/go.mod h1:RzqpnsPQNjUyIgdglUjRLgD7sVnxN1wpmBMV+UiEbL4=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
|
||||
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
|
||||
github.com/shirou/gopsutil/v4 v4.24.5 h1:gGsArG5K6vmsh5hcFOHaPm87UD003CaDMkAOweSQjhM=
|
||||
github.com/shirou/gopsutil/v4 v4.24.5/go.mod h1:aoebb2vxetJ/yIDZISmduFvVNPHqXQ9SEJwRXxkf0RA=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/showwin/speedtest-go v1.7.7 h1:VmK75SZOTKiuWjIVrs+mo7ZoKEw0utiGCvpnurS0olU=
|
||||
github.com/showwin/speedtest-go v1.7.7/go.mod h1:uLgdWCNarXxlYsL2E5TOZpCIwpgSWnEANZp7gfHXHu0=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
|
||||
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
|
||||
github.com/showwin/speedtest-go v1.7.10 h1:9o5zb7KsuzZKn+IE2//z5btLKJ870JwO6ETayUkqRFw=
|
||||
github.com/showwin/speedtest-go v1.7.10/go.mod h1:Ei7OCTmNPdWofMadzcfgq1rUO7mvJy9Jycj//G7vyfA=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
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=
|
||||
@@ -209,23 +222,28 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
|
||||
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
||||
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||
github.com/tklauser/numcpus v0.8.0 h1:Mx4Wwe/FjZLeQsK/6kt2EOepwwSl7SmJrK5bV/dXYgY=
|
||||
github.com/tklauser/numcpus v0.8.0/go.mod h1:ZJZlAY+dmR4eut8epnzf0u/VwodKmryxR8txiloSqBE=
|
||||
github.com/tsosunchia/powclient v0.1.5 h1:hpixFWoPbWSEC0zc9osSltyjtr1+SnhCueZVLkEpyyU=
|
||||
github.com/tsosunchia/powclient v0.1.5/go.mod h1:yNlzyq+w9llYZV+0q7nrX83ULy4ghq2mCjpTLJFJ2pg=
|
||||
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
|
||||
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
|
||||
github.com/tsosunchia/powclient v0.2.0 h1:BDrI3O69CbzarbD+CnnY10Kuwn8xlmtQR0m5tBp+BG8=
|
||||
github.com/tsosunchia/powclient v0.2.0/go.mod h1:fkb7tTW+HMH3ZWZzQUgwvvFKMj/8Ys+C8Sm/uGQzDA0=
|
||||
github.com/xjasonlyu/windivert-go v0.0.0-20201010013527-4239d0afa76f h1:glX3VZCYwW1/OmFxOjazfCtBLxXB3YNZk9LF2lYx+Lw=
|
||||
github.com/xjasonlyu/windivert-go v0.0.0-20201010013527-4239d0afa76f/go.mod h1:gh//RKyt2Gesx3eOj3ulzrSQ60ySj2UA4qnOdrtarvg=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
@@ -233,27 +251,29 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
|
||||
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
|
||||
golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
|
||||
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8 h1:yqrTHse8TCMW1M1ZCP+VAR/l0kKxwaAIqN/il7x4voA=
|
||||
golang.org/x/exp v0.0.0-20250106191152-7588d65b2ba8/go.mod h1:tujkw807nyEEAamNbDrEGzRav+ilXA7PCRAd6xsmwiU=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8 h1:yixxcjnhBmY0nkL253HFVIm0JsFHwrHdT3Yh6szTnfY=
|
||||
golang.org/x/exp v0.0.0-20240613232115-7f521ea00fb8/go.mod h1:jj3sYF3dwk5D+ghuXyeI3r5MFf+NT2An6/9dOA95KSI=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
|
||||
golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
@@ -262,71 +282,66 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
|
||||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
|
||||
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
|
||||
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201008064518-c1f3e3309c71/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
|
||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
|
||||
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
|
||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
|
||||
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.29.0 h1:Xx0h3TtM9rzQpQuR4dKLrdglAmCEN5Oi+P74JdhdzXE=
|
||||
golang.org/x/tools v0.29.0/go.mod h1:KMQVMRsVxU6nHCFXrBPhDB8XncLNLM0lIy/F14RP588=
|
||||
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
|
||||
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk=
|
||||
google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
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=
|
||||
gotest.tools/v3 v3.5.1 h1:EENdUnS3pdur5nybKYIh2Vfgc8IUNBjxDPSjtiJcOzU=
|
||||
gotest.tools/v3 v3.5.1/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU=
|
||||
howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM=
|
||||
howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
|
||||
642
goecs.go
642
goecs.go
@@ -1,631 +1,111 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/oneclickvirt/CommonMediaTests/commediatests"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
unlocktestmodel "github.com/oneclickvirt/UnlockTests/model"
|
||||
backtraceori "github.com/oneclickvirt/backtrace/bk"
|
||||
backtracemodel "github.com/oneclickvirt/backtrace/model"
|
||||
basicmodel "github.com/oneclickvirt/basics/model"
|
||||
cputestmodel "github.com/oneclickvirt/cputest/model"
|
||||
disktestmodel "github.com/oneclickvirt/disktest/disk"
|
||||
"github.com/oneclickvirt/ecs/backtrace"
|
||||
"github.com/oneclickvirt/ecs/commediatest"
|
||||
"github.com/oneclickvirt/ecs/cputest"
|
||||
"github.com/oneclickvirt/ecs/disktest"
|
||||
"github.com/oneclickvirt/ecs/memorytest"
|
||||
"github.com/oneclickvirt/ecs/ntrace"
|
||||
"github.com/oneclickvirt/ecs/speedtest"
|
||||
"github.com/oneclickvirt/ecs/unlocktest"
|
||||
menu "github.com/oneclickvirt/ecs/internal/menu"
|
||||
params "github.com/oneclickvirt/ecs/internal/params"
|
||||
"github.com/oneclickvirt/ecs/internal/runner"
|
||||
"github.com/oneclickvirt/ecs/utils"
|
||||
gostunmodel "github.com/oneclickvirt/gostun/model"
|
||||
memorytestmodel "github.com/oneclickvirt/memorytest/memory"
|
||||
nt3model "github.com/oneclickvirt/nt3/model"
|
||||
ptmodel "github.com/oneclickvirt/pingtest/model"
|
||||
"github.com/oneclickvirt/pingtest/pt"
|
||||
"github.com/oneclickvirt/portchecker/email"
|
||||
speedtestmodel "github.com/oneclickvirt/speedtest/model"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ecsVersion = "v0.1.28"
|
||||
menuMode bool
|
||||
onlyChinaTest bool
|
||||
input, choice string
|
||||
showVersion bool
|
||||
enableLogger bool
|
||||
language string
|
||||
cpuTestMethod, cpuTestThreadMode string
|
||||
memoryTestMethod string
|
||||
diskTestMethod, diskTestPath string
|
||||
diskMultiCheck bool
|
||||
nt3CheckType, nt3Location string
|
||||
spNum int
|
||||
width = 82
|
||||
basicStatus, cpuTestStatus, memoryTestStatus, diskTestStatus bool
|
||||
commTestStatus, utTestStatus, securityTestStatus, emailTestStatus bool
|
||||
backtraceStatus, nt3Status, speedTestStatus, pingTestStatus bool
|
||||
autoChangeDiskTestMethod = true
|
||||
filePath = "goecs.txt"
|
||||
enabelUpload = true
|
||||
help bool
|
||||
goecsFlag = flag.NewFlagSet("goecs", flag.ContinueOnError)
|
||||
finish bool
|
||||
ecsVersion = "v0.1.119" // 融合怪版本号
|
||||
configs = params.NewConfig(ecsVersion) // 全局配置实例
|
||||
userSetFlags = make(map[string]bool) // 用于跟踪哪些参数是用户显式设置的
|
||||
)
|
||||
|
||||
func getMenuChoice(language string) string {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer signal.Stop(sigChan)
|
||||
inputChan := make(chan string, 1)
|
||||
go func() {
|
||||
select {
|
||||
case <-sigChan:
|
||||
fmt.Println("\n程序在选择过程中被用户中断")
|
||||
os.Exit(0)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
go func() {
|
||||
var input string
|
||||
fmt.Print("请输入选项 / Please enter your choice: ")
|
||||
fmt.Scanln(&input)
|
||||
input = strings.TrimSpace(input)
|
||||
input = strings.TrimRight(input, "\n")
|
||||
select {
|
||||
case inputChan <- input:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}()
|
||||
select {
|
||||
case input := <-inputChan:
|
||||
re := regexp.MustCompile(`^\d+$`) // 正则表达式匹配纯数字
|
||||
if re.MatchString(input) {
|
||||
inChoice := input
|
||||
switch inChoice {
|
||||
case "1", "2", "3", "4", "5", "6", "7", "8", "9", "10":
|
||||
return inChoice
|
||||
default:
|
||||
if language == "zh" {
|
||||
fmt.Println("无效的选项")
|
||||
} else {
|
||||
fmt.Println("Invalid choice")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if language == "zh" {
|
||||
fmt.Println("输入错误,请输入一个纯数字")
|
||||
} else {
|
||||
fmt.Println("Invalid input, please enter a number")
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
return ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
goecsFlag.BoolVar(&help, "h", false, "Show help information")
|
||||
goecsFlag.BoolVar(&showVersion, "v", false, "Display version information")
|
||||
goecsFlag.BoolVar(&menuMode, "menu", true, "Enable/Disable menu mode, disable example: -menu=false") // true 默认启用菜单栏模式
|
||||
goecsFlag.StringVar(&language, "l", "zh", "Set language (supported: en, zh)")
|
||||
goecsFlag.BoolVar(&basicStatus, "basic", true, "Enable/Disable basic test")
|
||||
goecsFlag.BoolVar(&cpuTestStatus, "cpu", true, "Enable/Disable CPU test")
|
||||
goecsFlag.BoolVar(&memoryTestStatus, "memory", true, "Enable/Disable memory test")
|
||||
goecsFlag.BoolVar(&diskTestStatus, "disk", true, "Enable/Disable disk test")
|
||||
goecsFlag.BoolVar(&commTestStatus, "comm", true, "Enable/Disable common media test")
|
||||
goecsFlag.BoolVar(&utTestStatus, "ut", true, "Enable/Disable unlock media test")
|
||||
goecsFlag.BoolVar(&securityTestStatus, "security", true, "Enable/Disable security test")
|
||||
goecsFlag.BoolVar(&emailTestStatus, "email", true, "Enable/Disable email port test")
|
||||
goecsFlag.BoolVar(&backtraceStatus, "backtrace", true, "Enable/Disable backtrace test (in 'en' language or on windows it always false)")
|
||||
goecsFlag.BoolVar(&nt3Status, "nt3", true, "Enable/Disable NT3 test (in 'en' language or on windows it always false)")
|
||||
goecsFlag.BoolVar(&speedTestStatus, "speed", true, "Enable/Disable speed test")
|
||||
goecsFlag.StringVar(&cpuTestMethod, "cpum", "sysbench", "Set CPU test method (supported: sysbench, geekbench, winsat)")
|
||||
goecsFlag.StringVar(&cpuTestThreadMode, "cput", "multi", "Set CPU test thread mode (supported: single, multi)")
|
||||
goecsFlag.StringVar(&memoryTestMethod, "memorym", "sysbench", "Set memory test method (supported: sysbench, dd, winsat)")
|
||||
goecsFlag.StringVar(&diskTestMethod, "diskm", "fio", "Set disk test method (supported: fio, dd, winsat)")
|
||||
goecsFlag.StringVar(&diskTestPath, "diskp", "", "Set disk test path, e.g., -diskp /root")
|
||||
goecsFlag.BoolVar(&diskMultiCheck, "diskmc", false, "Enable/Disable multiple disk checks, e.g., -diskmc=false")
|
||||
goecsFlag.StringVar(&nt3Location, "nt3loc", "GZ", "Specify NT3 test location (supported: GZ, SH, BJ, CD for Guangzhou, Shanghai, Beijing, Chengdu)")
|
||||
goecsFlag.StringVar(&nt3CheckType, "nt3t", "ipv4", "Set NT3 test type (supported: both, ipv4, ipv6)")
|
||||
goecsFlag.IntVar(&spNum, "spnum", 2, "Set the number of servers per operator for speed test")
|
||||
goecsFlag.BoolVar(&enableLogger, "log", false, "Enable/Disable logging in the current path")
|
||||
goecsFlag.BoolVar(&enabelUpload, "upload", true, "Enable/Disable upload the result")
|
||||
goecsFlag.Parse(os.Args[1:])
|
||||
if help {
|
||||
fmt.Printf("Usage: %s [options]\n", os.Args[0])
|
||||
goecsFlag.PrintDefaults()
|
||||
return
|
||||
}
|
||||
if showVersion {
|
||||
fmt.Println(ecsVersion)
|
||||
return
|
||||
}
|
||||
if enableLogger {
|
||||
func initLogger() {
|
||||
if configs.EnableLogger {
|
||||
gostunmodel.EnableLoger = true
|
||||
basicmodel.EnableLoger = true
|
||||
cputestmodel.EnableLoger = true
|
||||
memorytestmodel.EnableLoger = true
|
||||
disktestmodel.EnableLoger = true
|
||||
commediatests.EnableLoger = true
|
||||
unlocktestmodel.EnableLoger = true
|
||||
ptmodel.EnableLoger = true
|
||||
backtraceori.EnableLoger = true
|
||||
backtracemodel.EnableLoger = true
|
||||
nt3model.EnableLoger = true
|
||||
speedtestmodel.EnableLoger = true
|
||||
}
|
||||
}
|
||||
|
||||
func handleLanguageSpecificSettings() {
|
||||
if configs.Language == "en" {
|
||||
configs.BacktraceStatus = false
|
||||
configs.Nt3Status = false
|
||||
}
|
||||
if !configs.EnableUpload {
|
||||
configs.SecurityTestStatus = false
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
configs.ParseFlags(os.Args[1:])
|
||||
if configs.HandleHelpAndVersion("goecs") {
|
||||
return
|
||||
}
|
||||
initLogger()
|
||||
utils.CheckAndFixAndroidDNS(configs.Language)
|
||||
preCheck := utils.CheckPublicAccess(3 * time.Second)
|
||||
go func() {
|
||||
http.Get("https://hits.spiritlhl.net/goecs.svg?action=hit&title=Hits&title_bg=%23555555&count_bg=%230eecf8&edge_flat=false")
|
||||
if preCheck.Connected {
|
||||
http.Get("https://hits.spiritlhl.net/goecs.svg?action=hit&title=Hits&title_bg=%23555555&count_bg=%230eecf8&edge_flat=false")
|
||||
}
|
||||
}()
|
||||
if menuMode {
|
||||
basicStatus, cpuTestStatus, memoryTestStatus, diskTestStatus = false, false, false, false
|
||||
commTestStatus, utTestStatus, securityTestStatus, emailTestStatus = false, false, false, false
|
||||
backtraceStatus, nt3Status, speedTestStatus = false, false, false
|
||||
autoChangeDiskTestMethod = true
|
||||
switch language {
|
||||
case "zh":
|
||||
fmt.Println("VPS融合怪版本: ", ecsVersion)
|
||||
fmt.Println("1. 融合怪完全体")
|
||||
fmt.Println("2. 极简版(系统信息+CPU+内存+磁盘+测速节点5个)")
|
||||
fmt.Println("3. 精简版(系统信息+CPU+内存+磁盘+常用流媒体+路由+测速节点5个)")
|
||||
fmt.Println("4. 精简网络版(系统信息+CPU+内存+磁盘+回程+路由+测速节点5个)")
|
||||
fmt.Println("5. 精简解锁版(系统信息+CPU+内存+磁盘IO+御三家+常用流媒体+测速节点5个)")
|
||||
fmt.Println("6. 网络单项(IP质量检测+三网回程+三网路由与延迟+测速节点11个)")
|
||||
fmt.Println("7. 解锁单项(御三家解锁+常用流媒体解锁)")
|
||||
fmt.Println("8. 硬件单项(系统信息+CPU+内存+dd磁盘测试+fio磁盘测试)")
|
||||
fmt.Println("9. IP质量检测(15个数据库的IP检测+邮件端口检测)")
|
||||
fmt.Println("10. 三网回程线路+广州三网路由+全国三网延迟")
|
||||
case "en":
|
||||
fmt.Println("VPS Fusion Monster Test Version: ", ecsVersion)
|
||||
fmt.Println("1. VPS Fusion Monster Test Comprehensive Test Suite")
|
||||
fmt.Println("2. Minimal Test Suite (System Info + CPU + Memory + Disk + 5 Speed Test Nodes)")
|
||||
fmt.Println("3. Standard Test Suite (System Info + CPU + Memory + Disk + Basic Unlock Tests + 5 Speed Test Nodes)")
|
||||
fmt.Println("4. Network-Focused Test Suite (System Info + CPU + Memory + Disk + 5 Speed Test Nodes)")
|
||||
fmt.Println("5. Unlock-Focused Test Suite (System Info + CPU + Memory + Disk IO + Basic Unlock Tests + Common Streaming Services + 5 Speed Test Nodes)")
|
||||
fmt.Println("6. Network-Only Test (IP Quality Test + 5 Speed Test Nodes)")
|
||||
fmt.Println("7. Unlock-Only Test (Basic Unlock Tests + Common Streaming Services Unlock)")
|
||||
fmt.Println("8. Hardware-Only Test (System Info + CPU + Memory + dd Disk Test + fio Disk Test)")
|
||||
fmt.Println("9. IP Quality Test (IP Test with 15 Databases + Email Port Test)")
|
||||
}
|
||||
Loop:
|
||||
for {
|
||||
choice = getMenuChoice(language)
|
||||
switch choice {
|
||||
case "1":
|
||||
basicStatus = true
|
||||
cpuTestStatus = true
|
||||
memoryTestStatus = true
|
||||
diskTestStatus = true
|
||||
commTestStatus = true
|
||||
utTestStatus = true
|
||||
securityTestStatus = true
|
||||
emailTestStatus = true
|
||||
backtraceStatus = true
|
||||
nt3Status = true
|
||||
speedTestStatus = true
|
||||
onlyChinaTest = utils.CheckChina(enableLogger)
|
||||
break Loop
|
||||
case "2":
|
||||
basicStatus = true
|
||||
cpuTestStatus = true
|
||||
memoryTestStatus = true
|
||||
diskTestStatus = true
|
||||
speedTestStatus = true
|
||||
break Loop
|
||||
case "3":
|
||||
basicStatus = true
|
||||
cpuTestStatus = true
|
||||
memoryTestStatus = true
|
||||
diskTestStatus = true
|
||||
utTestStatus = true
|
||||
nt3Status = true
|
||||
speedTestStatus = true
|
||||
break Loop
|
||||
case "4":
|
||||
basicStatus = true
|
||||
cpuTestStatus = true
|
||||
memoryTestStatus = true
|
||||
diskTestStatus = true
|
||||
backtraceStatus = true
|
||||
nt3Status = true
|
||||
speedTestStatus = true
|
||||
break Loop
|
||||
case "5":
|
||||
basicStatus = true
|
||||
cpuTestStatus = true
|
||||
memoryTestStatus = true
|
||||
diskTestStatus = true
|
||||
commTestStatus = true
|
||||
utTestStatus = true
|
||||
speedTestStatus = true
|
||||
break Loop
|
||||
case "6":
|
||||
securityTestStatus = true
|
||||
speedTestStatus = true
|
||||
backtraceStatus = true
|
||||
nt3Status = true
|
||||
break Loop
|
||||
case "7":
|
||||
commTestStatus = true
|
||||
utTestStatus = true
|
||||
enabelUpload = false
|
||||
break Loop
|
||||
case "8":
|
||||
basicStatus = true
|
||||
cpuTestStatus = true
|
||||
memoryTestStatus = true
|
||||
diskTestStatus = true
|
||||
securityTestStatus = false
|
||||
autoChangeDiskTestMethod = false
|
||||
break Loop
|
||||
case "9":
|
||||
securityTestStatus = true
|
||||
emailTestStatus = true
|
||||
break Loop
|
||||
case "10":
|
||||
backtraceStatus = true
|
||||
nt3Status = true
|
||||
pingTestStatus = true
|
||||
enabelUpload = false
|
||||
break Loop
|
||||
default:
|
||||
if language == "zh" {
|
||||
fmt.Println("无效的选项")
|
||||
} else {
|
||||
fmt.Println("Invalid choice")
|
||||
}
|
||||
}
|
||||
}
|
||||
if configs.MenuMode {
|
||||
menu.HandleMenuMode(preCheck, configs)
|
||||
} else {
|
||||
configs.OnlyIpInfoCheck = true
|
||||
}
|
||||
if language == "en" {
|
||||
backtraceStatus = false
|
||||
nt3Status = false
|
||||
}
|
||||
if !enabelUpload {
|
||||
securityTestStatus = false
|
||||
handleLanguageSpecificSettings()
|
||||
if !preCheck.Connected {
|
||||
configs.EnableUpload = false
|
||||
}
|
||||
var (
|
||||
startTime time.Time
|
||||
wg1, wg2, wg3 sync.WaitGroup
|
||||
basicInfo, securityInfo, emailInfo, mediaInfo, ptInfo string
|
||||
output, tempOutput string
|
||||
outputMutex sync.Mutex
|
||||
infoMutex sync.Mutex // 保护并发字符串写入
|
||||
)
|
||||
// 信号处理部分
|
||||
startTime := time.Now()
|
||||
uploadDone := make(chan bool, 1)
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
// 启动一个goroutine来等待信号
|
||||
go func() {
|
||||
startTime = time.Now()
|
||||
select {
|
||||
case <-sig:
|
||||
if !finish {
|
||||
endTime := time.Now()
|
||||
duration := endTime.Sub(startTime)
|
||||
minutes := int(duration.Minutes())
|
||||
seconds := int(duration.Seconds()) % 60
|
||||
currentTime := time.Now().Format("Mon Jan 2 15:04:05 MST 2006")
|
||||
var mu sync.Mutex
|
||||
mu.Lock()
|
||||
output = utils.PrintAndCapture(func() {
|
||||
utils.PrintCenteredTitle("", width)
|
||||
fmt.Printf("Cost Time : %d min %d sec\n", minutes, seconds)
|
||||
fmt.Printf("Current Time : %s\n", currentTime)
|
||||
utils.PrintCenteredTitle("", width)
|
||||
}, tempOutput, output)
|
||||
mu.Unlock()
|
||||
// 创建一个通道来传递上传结果
|
||||
resultChan := make(chan struct {
|
||||
httpURL string
|
||||
httpsURL string
|
||||
}, 1) // 使用带缓冲的通道,避免可能的阻塞
|
||||
// 启动上传
|
||||
go func() {
|
||||
httpURL, httpsURL := utils.ProcessAndUpload(output, filePath, enabelUpload)
|
||||
resultChan <- struct {
|
||||
httpURL string
|
||||
httpsURL string
|
||||
}{httpURL, httpsURL}
|
||||
uploadDone <- true
|
||||
}()
|
||||
// 等待上传完成或超时
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
if result.httpURL != "" || result.httpsURL != "" {
|
||||
if language == "en" {
|
||||
fmt.Printf("Upload successfully!\nHttp URL: %s\nHttps URL: %s\n", result.httpURL, result.httpsURL)
|
||||
} else {
|
||||
fmt.Printf("上传成功!\nHttp URL: %s\nHttps URL: %s\n", result.httpURL, result.httpsURL)
|
||||
}
|
||||
}
|
||||
// 给打印操作一些时间完成
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
|
||||
fmt.Println("Press Enter to exit...")
|
||||
fmt.Scanln()
|
||||
}
|
||||
os.Exit(0)
|
||||
case <-time.After(30 * time.Second):
|
||||
fmt.Println("上传超时,程序退出")
|
||||
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
|
||||
fmt.Println("Press Enter to exit...")
|
||||
fmt.Scanln()
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
}()
|
||||
switch language {
|
||||
go runner.HandleSignalInterrupt(sig, configs, &startTime, &output, tempOutput, uploadDone, &outputMutex)
|
||||
switch configs.Language {
|
||||
case "zh":
|
||||
output = utils.PrintAndCapture(func() {
|
||||
utils.PrintHead(language, width, ecsVersion)
|
||||
if basicStatus || securityTestStatus {
|
||||
if basicStatus {
|
||||
utils.PrintCenteredTitle("系统基础信息", width)
|
||||
}
|
||||
basicInfo, securityInfo, nt3CheckType = utils.BasicsAndSecurityCheck(language, nt3CheckType, securityTestStatus)
|
||||
if basicStatus {
|
||||
fmt.Printf(basicInfo)
|
||||
} else if (input == "6" || input == "9") && securityTestStatus {
|
||||
scanner := bufio.NewScanner(strings.NewReader(basicInfo))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.Contains(line, "IPV") {
|
||||
fmt.Println(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, tempOutput, output)
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if cpuTestStatus {
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("CPU测试-通过%s测试", cpuTestMethod), width)
|
||||
cputest.CpuTest(language, cpuTestMethod, cpuTestThreadMode)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if memoryTestStatus {
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("内存测试-通过%s测试", memoryTestMethod), width)
|
||||
memorytest.MemoryTest(language, memoryTestMethod)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if diskTestStatus && autoChangeDiskTestMethod {
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("硬盘测试-通过%s测试", diskTestMethod), width)
|
||||
disktest.DiskTest(language, diskTestMethod, diskTestPath, diskMultiCheck, autoChangeDiskTestMethod)
|
||||
} else if diskTestStatus && !autoChangeDiskTestMethod {
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("硬盘测试-通过%s测试", "dd"), width)
|
||||
disktest.DiskTest(language, "dd", diskTestPath, diskMultiCheck, autoChangeDiskTestMethod)
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("硬盘测试-通过%s测试", "fio"), width)
|
||||
disktest.DiskTest(language, "fio", diskTestPath, diskMultiCheck, autoChangeDiskTestMethod)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
if onlyChinaTest || pingTestStatus {
|
||||
wg3.Add(1)
|
||||
go func() {
|
||||
defer wg3.Done()
|
||||
ptInfo = pt.PingTest()
|
||||
}()
|
||||
}
|
||||
if emailTestStatus {
|
||||
wg2.Add(1)
|
||||
go func() {
|
||||
defer wg2.Done()
|
||||
emailInfo = email.EmailCheck()
|
||||
}()
|
||||
}
|
||||
if utTestStatus && !onlyChinaTest {
|
||||
wg1.Add(1)
|
||||
go func() {
|
||||
defer wg1.Done()
|
||||
mediaInfo = unlocktest.MediaTest(language)
|
||||
}()
|
||||
}
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if commTestStatus && !onlyChinaTest {
|
||||
utils.PrintCenteredTitle("御三家流媒体解锁", width)
|
||||
commediatest.ComMediaTest(language)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if utTestStatus && !onlyChinaTest {
|
||||
utils.PrintCenteredTitle("跨国流媒体解锁", width)
|
||||
wg1.Wait()
|
||||
fmt.Printf(mediaInfo)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if securityTestStatus {
|
||||
utils.PrintCenteredTitle("IP质量检测", width)
|
||||
fmt.Printf(securityInfo)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if emailTestStatus {
|
||||
utils.PrintCenteredTitle("邮件端口检测", width)
|
||||
wg2.Wait()
|
||||
fmt.Println(emailInfo)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
if runtime.GOOS != "windows" {
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if backtraceStatus && !onlyChinaTest {
|
||||
utils.PrintCenteredTitle("三网回程线路检测", width)
|
||||
backtrace.BackTrace()
|
||||
}
|
||||
}, tempOutput, output)
|
||||
// nexttrace 在win上不支持检测,报错 bind: An invalid argument was supplied.
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if nt3Status && !onlyChinaTest {
|
||||
utils.PrintCenteredTitle("三网回程路由检测", width)
|
||||
ntrace.TraceRoute3(language, nt3Location, nt3CheckType)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if onlyChinaTest || pingTestStatus {
|
||||
utils.PrintCenteredTitle("三网ICMP的PING值检测", width)
|
||||
wg3.Wait()
|
||||
fmt.Println(ptInfo)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
}
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if speedTestStatus {
|
||||
utils.PrintCenteredTitle("就近节点测速", width)
|
||||
speedtest.ShowHead(language)
|
||||
if choice == "1" || !menuMode {
|
||||
speedtest.NearbySP()
|
||||
speedtest.CustomSP("net", "global", 2, language)
|
||||
speedtest.CustomSP("net", "cu", spNum, language)
|
||||
speedtest.CustomSP("net", "ct", spNum, language)
|
||||
speedtest.CustomSP("net", "cmcc", spNum, language)
|
||||
} else if choice == "2" || choice == "3" || choice == "4" || choice == "5" {
|
||||
speedtest.CustomSP("net", "global", 4, language)
|
||||
} else if choice == "6" {
|
||||
speedtest.CustomSP("net", "global", 11, language)
|
||||
}
|
||||
}
|
||||
}, tempOutput, output)
|
||||
endTime := time.Now()
|
||||
duration := endTime.Sub(startTime)
|
||||
minutes := int(duration.Minutes())
|
||||
seconds := int(duration.Seconds()) % 60
|
||||
currentTime := time.Now().Format("Mon Jan 2 15:04:05 MST 2006")
|
||||
output = utils.PrintAndCapture(func() {
|
||||
utils.PrintCenteredTitle("", width)
|
||||
fmt.Printf("花费 : %d 分 %d 秒\n", minutes, seconds)
|
||||
fmt.Printf("时间 : %s\n", currentTime)
|
||||
utils.PrintCenteredTitle("", width)
|
||||
}, tempOutput, output)
|
||||
runner.RunChineseTests(preCheck, configs, &wg1, &wg2, &wg3, &basicInfo, &securityInfo, &emailInfo, &mediaInfo, &ptInfo, &output, tempOutput, startTime, &outputMutex, &infoMutex)
|
||||
case "en":
|
||||
output = utils.PrintAndCapture(func() {
|
||||
utils.PrintHead(language, width, ecsVersion)
|
||||
if basicStatus || securityTestStatus {
|
||||
if basicStatus {
|
||||
utils.PrintCenteredTitle("System-Basic-Information", width)
|
||||
}
|
||||
basicInfo, securityInfo, nt3CheckType = utils.BasicsAndSecurityCheck(language, nt3CheckType, securityTestStatus)
|
||||
if basicStatus {
|
||||
fmt.Printf(basicInfo)
|
||||
} else if (input == "6" || input == "9") && securityTestStatus {
|
||||
scanner := bufio.NewScanner(strings.NewReader(basicInfo))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.Contains(line, "IPV") {
|
||||
fmt.Println(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, tempOutput, output)
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if cpuTestStatus {
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("CPU-Test--%s-Method", cpuTestMethod), width)
|
||||
cputest.CpuTest(language, cpuTestMethod, cpuTestThreadMode)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if memoryTestStatus {
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("Memory-Test--%s-Method", memoryTestMethod), width)
|
||||
memorytest.MemoryTest(language, memoryTestMethod)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if diskTestStatus && autoChangeDiskTestMethod {
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("Disk-Test--%s-Method", diskTestMethod), width)
|
||||
disktest.DiskTest(language, diskTestMethod, diskTestPath, diskMultiCheck, autoChangeDiskTestMethod)
|
||||
} else if diskTestStatus && !autoChangeDiskTestMethod {
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("Disk-Test--%s-Method", "dd"), width)
|
||||
disktest.DiskTest(language, "dd", diskTestPath, diskMultiCheck, autoChangeDiskTestMethod)
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("Disk-Test--%s-Method", "fio"), width)
|
||||
disktest.DiskTest(language, "fio", diskTestPath, diskMultiCheck, autoChangeDiskTestMethod)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
if utTestStatus {
|
||||
wg1.Add(1)
|
||||
go func() {
|
||||
defer wg1.Done()
|
||||
mediaInfo = unlocktest.MediaTest(language)
|
||||
}()
|
||||
}
|
||||
if emailTestStatus {
|
||||
wg2.Add(1)
|
||||
go func() {
|
||||
defer wg2.Done()
|
||||
emailInfo = email.EmailCheck()
|
||||
}()
|
||||
}
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if utTestStatus {
|
||||
utils.PrintCenteredTitle("Cross-Border-Streaming-Media-Unlock", width)
|
||||
wg1.Wait()
|
||||
fmt.Printf(mediaInfo)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if securityTestStatus {
|
||||
utils.PrintCenteredTitle("IP-Quality-Check", width)
|
||||
fmt.Printf(securityInfo)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if emailTestStatus {
|
||||
utils.PrintCenteredTitle("Email-Port-Check", width)
|
||||
wg2.Wait()
|
||||
fmt.Println(emailInfo)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
output = utils.PrintAndCapture(func() {
|
||||
if speedTestStatus {
|
||||
utils.PrintCenteredTitle("Speed-Test", width)
|
||||
speedtest.ShowHead(language)
|
||||
speedtest.NearbySP()
|
||||
speedtest.CustomSP("net", "global", -1, language)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
endTime := time.Now()
|
||||
duration := endTime.Sub(startTime)
|
||||
minutes := int(duration.Minutes())
|
||||
seconds := int(duration.Seconds()) % 60
|
||||
currentTime := time.Now().Format("Mon Jan 2 15:04:05 MST 2006")
|
||||
output = utils.PrintAndCapture(func() {
|
||||
utils.PrintCenteredTitle("", width)
|
||||
fmt.Printf("Cost Time : %d min %d sec\n", minutes, seconds)
|
||||
fmt.Printf("Current Time : %s\n", currentTime)
|
||||
utils.PrintCenteredTitle("", width)
|
||||
}, tempOutput, output)
|
||||
runner.RunEnglishTests(preCheck, configs, &wg1, &wg2, &wg3, &basicInfo, &securityInfo, &emailInfo, &mediaInfo, &ptInfo, &output, tempOutput, startTime, &outputMutex, &infoMutex)
|
||||
default:
|
||||
fmt.Println("Unsupported language")
|
||||
}
|
||||
httpURL, httpsURL := utils.ProcessAndUpload(output, filePath, enabelUpload)
|
||||
if httpURL != "" || httpsURL != "" {
|
||||
if language == "en" {
|
||||
fmt.Printf("Upload successfully!\nHttp URL: %s\nHttps URL: %s\n", httpURL, httpsURL)
|
||||
} else {
|
||||
fmt.Printf("上传成功!\nHttp URL: %s\nHttps URL: %s\n", httpURL, httpsURL)
|
||||
}
|
||||
if configs.AnalyzeResult {
|
||||
output = runner.AppendAnalysisSummary(configs, output, tempOutput, &outputMutex)
|
||||
}
|
||||
finish = true
|
||||
if preCheck.Connected {
|
||||
runner.HandleUploadResults(configs, output)
|
||||
}
|
||||
configs.Finish = true
|
||||
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
|
||||
fmt.Println("Press Enter to exit...")
|
||||
fmt.Scanln()
|
||||
|
||||
497
goecs.sh
497
goecs.sh
@@ -1,43 +1,46 @@
|
||||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
# From https://github.com/oneclickvirt/ecs
|
||||
# 2025.04.02
|
||||
# 2025.10.08
|
||||
|
||||
# curl -L https://raw.githubusercontent.com/oneclickvirt/ecs/master/goecs.sh -o goecs.sh && chmod +x goecs.sh
|
||||
# 或
|
||||
# curl -L https://cnb.cool/oneclickvirt/ecs/-/git/raw/main/goecs.sh -o goecs.sh && chmod +x goecs.sh
|
||||
|
||||
cat <<"EOF"
|
||||
GGGGGGGG OOOOOOO EEEEEEEE CCCCCCCCC SSSSSSSSSS
|
||||
GG GG OO OO EE CC SS
|
||||
GG OO OO EE CC SS
|
||||
GG OO OO EE CC SS
|
||||
GG OO OO EEEEEEEE CC SSSSSSSSSS
|
||||
GG GGGGGG OO OO EE CC SS
|
||||
GG GG OO OO EE CC SS
|
||||
GG GG OO OO EE CC SS
|
||||
GGGGGGGG OOOOOOO EEEEEEEE CCCCCCCCC SSSSSSSSSS
|
||||
,ad8888ba, ,ad8888ba, 88888888888 ,ad8888ba, ad88888ba
|
||||
d8"' `"8b d8"' `"8b 88 d8"' `"8b d8" "8b
|
||||
d8' d8' `8b 88 d8' Y8a
|
||||
88 88 88 88aaaaa 88 `"Y8aaaaa,
|
||||
88 88888 88 88 88""""" 88 `"""""8b,
|
||||
Y8, 88 Y8, ,8P 88 Y8, `8b
|
||||
Y8a. .a88 Y8a. .a8P 88 Y8a. .a8P Y8a a8P
|
||||
`"Y88888P" `"Y8888Y"' 88888888888 `"Y8888Y"' "Y88888P"
|
||||
EOF
|
||||
|
||||
cd /root >/dev/null 2>&1
|
||||
if [ ! -d "/usr/bin/" ]; then
|
||||
mkdir -p "/usr/bin/"
|
||||
fi
|
||||
_red() { echo -e "\033[31m\033[01m$@\033[0m"; }
|
||||
_green() { echo -e "\033[32m\033[01m$@\033[0m"; }
|
||||
_yellow() { echo -e "\033[33m\033[01m$@\033[0m"; }
|
||||
_blue() { echo -e "\033[36m\033[01m$@\033[0m"; }
|
||||
reading() { read -rp "$(_green "$1")" "$2"; }
|
||||
_red() { printf "\033[31m\033[01m%s\033[0m\n" "$*"; }
|
||||
_green() { printf "\033[32m\033[01m%s\033[0m\n" "$*"; }
|
||||
_yellow() { printf "\033[33m\033[01m%s\033[0m\n" "$*"; }
|
||||
_blue() { printf "\033[36m\033[01m%s\033[0m\n" "$*"; }
|
||||
reading() {
|
||||
printf "\033[32m\033[01m%s\033[0m" "$1"
|
||||
read "$2"
|
||||
}
|
||||
|
||||
check_cdn() {
|
||||
local o_url=$1
|
||||
for cdn_url in "${cdn_urls[@]}"; do
|
||||
if curl -sL -k "$cdn_url$o_url" --max-time 6 | grep -q "success" >/dev/null 2>&1; then
|
||||
export cdn_success_url="$cdn_url"
|
||||
return
|
||||
local o_url="$1"
|
||||
local cdn_url
|
||||
for cdn_url in $cdn_urls; do
|
||||
if curl -4 -sL -k "$cdn_url$o_url" --max-time 6 | grep -q "success" >/dev/null 2>&1; then
|
||||
cdn_success_url="$cdn_url"
|
||||
return 0
|
||||
fi
|
||||
sleep 0.5
|
||||
done
|
||||
export cdn_success_url=""
|
||||
cdn_success_url=""
|
||||
return 1
|
||||
}
|
||||
|
||||
check_cdn_file() {
|
||||
@@ -52,9 +55,9 @@ check_cdn_file() {
|
||||
download_file() {
|
||||
local url="$1"
|
||||
local output="$2"
|
||||
if ! wget -O "$output" "$url"; then
|
||||
if ! wget -O "$output" "$url" 2>/dev/null; then
|
||||
_yellow "wget failed, trying curl..."
|
||||
if ! curl -L -o "$output" "$url"; then
|
||||
if ! curl -L -o "$output" "$url" 2>/dev/null; then
|
||||
_red "Both wget and curl failed. Unable to download the file."
|
||||
return 1
|
||||
fi
|
||||
@@ -63,29 +66,28 @@ download_file() {
|
||||
}
|
||||
|
||||
check_china() {
|
||||
_yellow "正在检测IP所在区域......"
|
||||
if [[ -z "${CN}" ]]; then
|
||||
# 首先尝试通过 ipapi.co 检测
|
||||
_yellow "Detecting IP region......"
|
||||
if [ -z "${CN}" ]; then
|
||||
if curl -m 6 -s https://ipapi.co/json | grep -q 'China'; then
|
||||
_yellow "根据ipapi.co提供的信息,当前IP可能在中国"
|
||||
_yellow "According to ipapi.co, this IP may be located in China"
|
||||
if [ "$noninteractive" != "true" ]; then
|
||||
reading "是否使用中国镜像完成安装? ([y]/n) " input
|
||||
reading "Use China mirror for installation? ([y]/n) " input
|
||||
case $input in
|
||||
[yY][eE][sS] | [yY] | "")
|
||||
_green "已选择使用中国镜像"
|
||||
_green "China mirror selected"
|
||||
CN=true
|
||||
;;
|
||||
[nN][oO] | [nN])
|
||||
_yellow "已选择不使用中国镜像"
|
||||
_yellow "China mirror not selected"
|
||||
CN=false
|
||||
;;
|
||||
*)
|
||||
_green "已选择使用中国镜像"
|
||||
_green "China mirror selected"
|
||||
CN=true
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# 在非交互模式下默认不使用中国镜像
|
||||
# In non-interactive mode, default to not using China mirror
|
||||
CN=false
|
||||
fi
|
||||
else
|
||||
@@ -96,38 +98,61 @@ check_china() {
|
||||
|
||||
get_memory_size() {
|
||||
if [ -f /proc/meminfo ]; then
|
||||
local mem_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}')
|
||||
local mem_kb
|
||||
mem_kb=$(grep MemTotal /proc/meminfo | awk '{print $2}')
|
||||
echo $((mem_kb / 1024)) # Convert to MB
|
||||
return
|
||||
return 0
|
||||
fi
|
||||
if command -v free >/dev/null 2>&1; then
|
||||
local mem_kb=$(free -m | awk '/^Mem:/ {print $2}')
|
||||
local mem_kb
|
||||
mem_kb=$(free -m | awk '/^Mem:/ {print $2}')
|
||||
echo "$mem_kb" # Already in MB
|
||||
return
|
||||
return 0
|
||||
fi
|
||||
if command -v sysctl >/dev/null 2>&1; then
|
||||
local mem_bytes=$(sysctl -n hw.memsize 2>/dev/null || sysctl -n hw.physmem 2>/dev/null)
|
||||
local mem_bytes
|
||||
mem_bytes=$(sysctl -n hw.memsize 2>/dev/null || sysctl -n hw.physmem 2>/dev/null)
|
||||
if [ -n "$mem_bytes" ]; then
|
||||
echo $((mem_bytes / 1024 / 1024)) # Convert to MB
|
||||
return
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
echo 0
|
||||
return 1
|
||||
}
|
||||
|
||||
cleanup_epel() {
|
||||
_yellow "Cleaning up EPEL repositories..."
|
||||
rm -f /etc/yum.repos.d/*epel*
|
||||
yum clean all
|
||||
yum clean all >/dev/null 2>&1
|
||||
}
|
||||
|
||||
goecs_check() {
|
||||
# Get system and architecture info with error handling
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
INSTALL_CMD="apt-get -y install"
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
INSTALL_CMD="yum -y install"
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
INSTALL_CMD="dnf -y install"
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
INSTALL_CMD="pacman -S --noconfirm"
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
INSTALL_CMD="apk add"
|
||||
elif command -v zypper >/dev/null 2>&1; then
|
||||
INSTALL_CMD="zypper install -y"
|
||||
fi
|
||||
if ! command -v unzip >/dev/null 2>&1; then
|
||||
_green "Installing unzip"
|
||||
${INSTALL_CMD} unzip
|
||||
fi
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
_green "Installing curl"
|
||||
${INSTALL_CMD} curl
|
||||
fi
|
||||
os=$(uname -s 2>/dev/null || echo "Unknown")
|
||||
arch=$(uname -m 2>/dev/null || echo "Unknown")
|
||||
# First check for China IP
|
||||
check_china
|
||||
# Get latest version number with multiple backup sources
|
||||
ECS_VERSION=""
|
||||
ECS_VERSION="0.1.118"
|
||||
for api in \
|
||||
"https://api.github.com/repos/oneclickvirt/ecs/releases/latest" \
|
||||
"https://githubapi.spiritlhl.workers.dev/repos/oneclickvirt/ecs/releases/latest" \
|
||||
@@ -139,24 +164,24 @@ goecs_check() {
|
||||
sleep 1
|
||||
done
|
||||
if [ -z "$ECS_VERSION" ]; then
|
||||
_yellow "Unable to get version info, using default version 0.1.28"
|
||||
ECS_VERSION="0.1.28"
|
||||
_yellow "Unable to get version info, using default version 0.1.118"
|
||||
ECS_VERSION="0.1.118"
|
||||
fi
|
||||
# Check if original goecs command exists
|
||||
version_output=""
|
||||
for cmd_path in "goecs" "./goecs" "/usr/bin/goecs" "/usr/local/bin/goecs"; do
|
||||
if [ -x "$(command -v $cmd_path 2>/dev/null)" ]; then
|
||||
if command -v "$cmd_path" >/dev/null 2>&1; then
|
||||
version_output=$($cmd_path -v command 2>/dev/null)
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -n "$version_output" ]; then
|
||||
extracted_version=${version_output//v/}
|
||||
extracted_version=${version_output#*v}
|
||||
extracted_version=${extracted_version#v}
|
||||
if [ -n "$extracted_version" ]; then
|
||||
ecs_version=$ECS_VERSION
|
||||
if [[ "$(echo -e "$extracted_version\n$ecs_version" | sort -V | tail -n 1)" == "$extracted_version" ]]; then
|
||||
if [ "$(printf '%s\n%s\n' "$extracted_version" "$ecs_version" | sort -V | tail -n 1)" = "$extracted_version" ]; then
|
||||
_green "goecs version ($extracted_version) is up to date, no upgrade needed"
|
||||
return
|
||||
return 0
|
||||
else
|
||||
_yellow "goecs version ($extracted_version) < $ecs_version, upgrade needed, starting in 5 seconds"
|
||||
rm -rf /usr/bin/goecs /usr/local/bin/goecs ./goecs
|
||||
@@ -166,12 +191,11 @@ goecs_check() {
|
||||
_green "goecs not found, installation needed, starting in 5 seconds"
|
||||
fi
|
||||
sleep 5
|
||||
# Download corresponding version with error handling
|
||||
if [[ "$CN" == true ]]; then
|
||||
if [ "$CN" = "true" ]; then
|
||||
_yellow "Using China mirror for download..."
|
||||
base_url="https://cnb.cool/oneclickvirt/ecs/-/git/raw/main"
|
||||
else
|
||||
cdn_urls=("https://cdn0.spiritlhl.top/" "http://cdn3.spiritlhl.net/" "http://cdn1.spiritlhl.net/" "http://cdn2.spiritlhl.net/")
|
||||
cdn_urls="https://cdn0.spiritlhl.top/ http://cdn3.spiritlhl.net/ http://cdn1.spiritlhl.net/ http://cdn2.spiritlhl.net/"
|
||||
check_cdn_file
|
||||
if [ -n "$cdn_success_url" ]; then
|
||||
base_url="${cdn_success_url}https://github.com/oneclickvirt/ecs/releases/download/v${ECS_VERSION}"
|
||||
@@ -179,7 +203,6 @@ goecs_check() {
|
||||
base_url="https://github.com/oneclickvirt/ecs/releases/download/v${ECS_VERSION}"
|
||||
fi
|
||||
fi
|
||||
# Build download URL with architecture support
|
||||
local zip_file=""
|
||||
case $os in
|
||||
Linux|linux|LINUX)
|
||||
@@ -217,7 +240,6 @@ goecs_check() {
|
||||
esac
|
||||
download_url="${base_url}/${zip_file}"
|
||||
_green "Downloading $download_url"
|
||||
# Download file with retry mechanism
|
||||
local max_retries=3
|
||||
local retry_count=0
|
||||
while [ $retry_count -lt $max_retries ]; do
|
||||
@@ -232,39 +254,42 @@ goecs_check() {
|
||||
_red "Download failed, please check your network connection or download manually"
|
||||
return 1
|
||||
fi
|
||||
# Extract and install with error handling
|
||||
if ! unzip -o goecs.zip >/dev/null 2>&1; then
|
||||
_red "Extraction failed"
|
||||
return 1
|
||||
fi
|
||||
rm -f goecs.zip README.md LICENSE README_EN.md
|
||||
# Set execution permissions and install
|
||||
chmod 777 goecs
|
||||
installed_to_system=false
|
||||
for install_path in "/usr/bin" "/usr/local/bin"; do
|
||||
if [ -d "$install_path" ]; then
|
||||
cp -f goecs "$install_path/"
|
||||
break
|
||||
if cp -f goecs "$install_path/" 2>/dev/null; then
|
||||
installed_to_system=true
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
# Set system parameters
|
||||
if [ "$installed_to_system" = "false" ]; then
|
||||
_yellow "Insufficient permissions to install to system path, goecs is kept in the current directory"
|
||||
_yellow "Please use the following command to run: ./goecs"
|
||||
fi
|
||||
if [ "$os" != "Darwin" ]; then
|
||||
PARAM="net.ipv4.ping_group_range"
|
||||
NEW_VALUE="0 2147483647"
|
||||
if [ -f /etc/sysctl.conf ]; then
|
||||
if grep -q "^$PARAM" /etc/sysctl.conf; then
|
||||
sed -i "s/^$PARAM.*/$PARAM = $NEW_VALUE/" /etc/sysctl.conf
|
||||
if grep -q "^$PARAM" /etc/sysctl.conf 2>/dev/null; then
|
||||
sed -i "s/^$PARAM.*/$PARAM = $NEW_VALUE/" /etc/sysctl.conf 2>/dev/null || true
|
||||
else
|
||||
echo "$PARAM = $NEW_VALUE" >> /etc/sysctl.conf
|
||||
echo "$PARAM = $NEW_VALUE" >> /etc/sysctl.conf 2>/dev/null || true
|
||||
fi
|
||||
sysctl -p >/dev/null 2>&1
|
||||
sysctl -p >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
# Set special permissions
|
||||
setcap cap_net_raw=+ep goecs 2>/dev/null
|
||||
setcap cap_net_raw=+ep /usr/bin/goecs 2>/dev/null
|
||||
setcap cap_net_raw=+ep /usr/local/bin/goecs 2>/dev/null
|
||||
setcap cap_net_raw=+ep goecs 2>/dev/null || true
|
||||
setcap cap_net_raw=+ep /usr/bin/goecs 2>/dev/null || true
|
||||
setcap cap_net_raw=+ep /usr/local/bin/goecs 2>/dev/null || true
|
||||
_green "goecs installation complete, current version:"
|
||||
goecs -v || ./goecs -v
|
||||
goecs -v 2>/dev/null || ./goecs -v
|
||||
}
|
||||
|
||||
InstallSysbench() {
|
||||
@@ -293,203 +318,214 @@ InstallSysbench() {
|
||||
else
|
||||
Var_OSRelease="unknown" # 未知系统分支
|
||||
fi
|
||||
local mem_size=$(get_memory_size)
|
||||
local mem_size
|
||||
mem_size=$(get_memory_size)
|
||||
if [ -z "$mem_size" ] || [ "$mem_size" -eq 0 ]; then
|
||||
echo "Error: Unable to determine memory size or memory size is zero."
|
||||
elif [ $mem_size -lt 1024 ]; then
|
||||
elif [ "$mem_size" -lt 1024 ]; then
|
||||
_red "Warning: Your system has less than 1GB RAM (${mem_size}MB)"
|
||||
if [ "$noninteractive" != "true" ]; then
|
||||
reading "Do you want to continue with EPEL installation? (y/N): " confirm
|
||||
if [[ ! $confirm =~ ^[Yy]$ ]]; then
|
||||
_yellow "Skipping EPEL installation"
|
||||
return 1
|
||||
fi
|
||||
case "$confirm" in
|
||||
[Yy]*)
|
||||
;;
|
||||
*)
|
||||
_yellow "Skipping EPEL installation"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
case "$Var_OSRelease" in
|
||||
ubuntu | debian | astra)
|
||||
! apt-get install -y sysbench && apt-get --fix-broken install -y && apt-get install --no-install-recommends -y sysbench ;;
|
||||
centos | rhel | almalinux | redhat | opencloudos)
|
||||
(yum -y install epel-release && yum -y install sysbench) || (dnf install epel-release -y && dnf install sysbench -y) ;;
|
||||
fedora)
|
||||
ubuntu | debian | astra)
|
||||
if ! apt-get install -y sysbench; then
|
||||
apt-get --fix-broken install -y
|
||||
apt-get install --no-install-recommends -y sysbench
|
||||
fi
|
||||
;;
|
||||
centos | rhel | almalinux | redhat | opencloudos)
|
||||
if ! yum -y install epel-release || ! yum -y install sysbench; then
|
||||
if command -v dnf >/dev/null 2>&1; then
|
||||
dnf install epel-release -y
|
||||
dnf install sysbench -y
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
fedora)
|
||||
dnf -y install sysbench ;;
|
||||
arch)
|
||||
pacman -S --needed --noconfirm sysbench && pacman -S --needed --noconfirm libaio && ldconfig ;;
|
||||
freebsd)
|
||||
arch)
|
||||
pacman -S --needed --noconfirm sysbench
|
||||
pacman -S --needed --noconfirm libaio
|
||||
ldconfig
|
||||
;;
|
||||
freebsd)
|
||||
pkg install -y sysbench ;;
|
||||
alpinelinux)
|
||||
if [ "$noninteractive" != "true" ]; then
|
||||
reading "Do you want to continue with sysbench installation? (y/N): " confirm
|
||||
if [[ ! $confirm =~ ^[Yy]$ ]]; then
|
||||
_yellow "Skipping sysbench installation"
|
||||
return 1
|
||||
fi
|
||||
case "$confirm" in
|
||||
[Yy]*)
|
||||
;;
|
||||
*)
|
||||
_yellow "Skipping sysbench installation"
|
||||
return 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
ALPINE_VERSION=$(grep -o '^[0-9]\+\.[0-9]\+' /etc/alpine-release)
|
||||
COMMUNITY_REPO="http://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VERSION}/community"
|
||||
if grep -q "^${COMMUNITY_REPO}" /etc/apk/repositories; then
|
||||
echo "Community repository is already enabled."
|
||||
else
|
||||
if ! grep -q "^${COMMUNITY_REPO}" /etc/apk/repositories; then
|
||||
echo "Enabling community repository..."
|
||||
echo "${COMMUNITY_REPO}" >> /etc/apk/repositories
|
||||
echo "Community repository has been added."
|
||||
echo "Updating apk package index..."
|
||||
apk update && echo "Package index updated successfully."
|
||||
else
|
||||
echo "Community repository is already enabled."
|
||||
fi
|
||||
if apk info sysbench >/dev/null 2>&1; then
|
||||
echo -e "${Msg_Info}Sysbench already installed."
|
||||
echo "Sysbench already installed."
|
||||
else
|
||||
apk add --no-cache sysbench
|
||||
if [ $? -ne 0 ]; then
|
||||
echo -e "${Msg_Warning}Sysbench Module not found, installing ..." && echo -e "${Msg_Warning}SysBench Current not support Alpine Linux, Skipping..." && Var_Skip_SysBench="1"
|
||||
if ! apk add --no-cache sysbench; then
|
||||
echo "Sysbench Module not found, installing ..."
|
||||
echo "SysBench Current not support Alpine Linux, Skipping..."
|
||||
Var_Skip_SysBench="1"
|
||||
else
|
||||
echo -e "${Msg_Success}Sysbench installed successfully."
|
||||
echo "Sysbench installed successfully."
|
||||
fi
|
||||
fi ;;
|
||||
*)
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
_red "Sysbench Install Error: Unknown OS release: $Var_OSRelease" ;;
|
||||
esac
|
||||
if [[ $SYSTEM =~ ^(CentOS|RHEL|AlmaLinux)$ ]]; then
|
||||
_yellow "Installing EPEL repository..."
|
||||
if ! yum -y install epel-release; then
|
||||
_red "EPEL installation failed!"
|
||||
cleanup_epel
|
||||
_yellow "Attempting to continue without EPEL..."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
Check_SysBench() {
|
||||
if [ ! -f "/usr/bin/sysbench" ] && [ ! -f "/usr/local/bin/sysbench" ]; then
|
||||
InstallSysbench
|
||||
fi
|
||||
# 尝试编译安装
|
||||
if [ ! -f "/usr/bin/sysbench" ] && [ ! -f "/usr/local/bin/sysbench" ]; then
|
||||
echo -e "${Msg_Warning}Sysbench Module install Failure, trying compile modules ..."
|
||||
Check_Sysbench_InstantBuild
|
||||
fi
|
||||
source ~/.bashrc
|
||||
# 最终检测
|
||||
if [ "$(command -v sysbench)" ] || [ -f "/usr/bin/sysbench" ] || [ -f "/usr/local/bin/sysbench" ]; then
|
||||
_yellow "Install sysbench successfully!"
|
||||
else
|
||||
_red "SysBench Moudle install Failure! Try Restart Bench or Manually install it! (/usr/bin/sysbench)"
|
||||
_blue "Will try to test with geekbench5 instead later."
|
||||
fi
|
||||
sleep 3
|
||||
}
|
||||
|
||||
Check_Sysbench_InstantBuild() {
|
||||
if [ "${Var_OSRelease}" = "centos" ] || [ "${Var_OSRelease}" = "rhel" ] || [ "${Var_OSRelease}" = "almalinux" ] || [ "${Var_OSRelease}" = "ubuntu" ] || [ "${Var_OSRelease}" = "debian" ] || [ "${Var_OSRelease}" = "fedora" ] || [ "${Var_OSRelease}" = "arch" ] || [ "${Var_OSRelease}" = "astra" ]; then
|
||||
local os_sysbench=${Var_OSRelease}
|
||||
if [ "$os_sysbench" = "astra" ]; then
|
||||
os_sysbench="debian"
|
||||
fi
|
||||
if [ "$os_sysbench" = "opencloudos" ]; then
|
||||
os_sysbench="centos"
|
||||
fi
|
||||
echo -e "${Msg_Info}Release Detected: ${os_sysbench}"
|
||||
echo -e "${Msg_Info}Preparing compile enviorment ..."
|
||||
prepare_compile_env "${os_sysbench}"
|
||||
echo -e "${Msg_Info}Downloading Source code (Version 1.0.20)..."
|
||||
mkdir -p /tmp/sysbench_install/src/
|
||||
mv /tmp/sysbench-1.0.20 /tmp/sysbench_install/src/
|
||||
echo -e "${Msg_Info}Compiling Sysbench Module ..."
|
||||
cd /tmp/sysbench_install/src/sysbench-1.0.20
|
||||
./autogen.sh && ./configure --without-mysql && make -j8 && make install
|
||||
echo -e "${Msg_Info}Cleaning up ..."
|
||||
cd /tmp && rm -rf /tmp/sysbench_install/src/sysbench*
|
||||
else
|
||||
echo -e "${Msg_Warning}Unsupported operating system: ${Var_OSRelease}"
|
||||
fi
|
||||
}
|
||||
|
||||
prepare_compile_env() {
|
||||
local system="$1"
|
||||
if [ "${system}" = "centos" ] || [ "${system}" = "rhel" ] || [ "${system}" = "almalinux" ]; then
|
||||
yum install -y epel-release
|
||||
yum install -y wget curl make gcc gcc-c++ make automake libtool pkgconfig libaio-devel
|
||||
elif [ "${system}" = "ubuntu" ] || [ "${system}" = "debian" ]; then
|
||||
! apt-get update && apt-get --fix-broken install -y && apt-get update
|
||||
! apt-get -y install --no-install-recommends curl wget make automake libtool pkg-config libaio-dev unzip && apt-get --fix-broken install -y && apt-get -y install --no-install-recommends curl wget make automake libtool pkg-config libaio-dev unzip
|
||||
elif [ "${system}" = "fedora" ]; then
|
||||
dnf install -y wget curl gcc gcc-c++ make automake libtool pkgconfig libaio-devel
|
||||
elif [ "${system}" = "arch" ]; then
|
||||
pacman -S --needed --noconfirm wget curl gcc gcc make automake libtool pkgconfig libaio lib32-libaio
|
||||
else
|
||||
echo -e "${Msg_Warning}Unsupported operating system: ${system}"
|
||||
case "$SYSTEM" in
|
||||
CentOS|RHEL|AlmaLinux)
|
||||
_yellow "Installing EPEL repository..."
|
||||
if ! yum -y install epel-release; then
|
||||
_red "EPEL installation failed!"
|
||||
cleanup_epel
|
||||
_yellow "Attempting to continue without EPEL..."
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
}
|
||||
|
||||
env_check() {
|
||||
REGEX=("debian|astra" "ubuntu" "centos|red hat|kernel|oracle linux|alma|rocky" "'amazon linux'" "fedora" "arch" "freebsd" "alpine" "openbsd" "opencloudos")
|
||||
RELEASE=("Debian" "Ubuntu" "CentOS" "CentOS" "Fedora" "Arch" "FreeBSD" "Alpine" "OpenBSD" "OpenCloudOS")
|
||||
PACKAGE_UPDATE=("apt-get update" "apt-get update" "yum -y update" "yum -y update" "yum -y update" "pacman -Sy" "pkg update" "apk update" "pkg_add -qu" "yum -y update")
|
||||
PACKAGE_INSTALL=("apt-get -y install" "apt-get -y install" "yum -y install" "yum -y install" "yum -y install" "pacman -Sy --noconfirm --needed" "pkg install -y" "apk add --no-cache" "pkg_add -I" "yum -y install")
|
||||
PACKAGE_REMOVE=("apt-get -y remove" "apt-get -y remove" "yum -y remove" "yum -y remove" "yum -y remove" "pacman -Rsc --noconfirm" "pkg delete" "apk del" "pkg_delete -I" "yum -y remove")
|
||||
PACKAGE_UNINSTALL=("apt-get -y autoremove" "apt-get -y autoremove" "yum -y autoremove" "yum -y autoremove" "yum -y autoremove" "pacman -Rns --noconfirm" "pkg autoremove" "apk autoremove" "pkg_delete -a" "yum -y autoremove")
|
||||
# Check system information
|
||||
# 检测是否为 macOS 系统
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
_green "Detected macOS system"
|
||||
_green "macOS has built-in tools, skipping dependency installation"
|
||||
_green "Environment preparation complete."
|
||||
_green "Next command is: ./goecs.sh install"
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ -f /etc/opencloudos-release ]; then
|
||||
SYS="opencloudos"
|
||||
elif [ -s /etc/os-release ]; then
|
||||
SYS="$(grep -i pretty_name /etc/os-release | cut -d \" -f2)"
|
||||
elif [ -x "$(type -p hostnamectl)" ]; then
|
||||
SYS="$(hostnamectl | grep -i system | cut -d : -f2 | xargs)"
|
||||
elif [ -x "$(type -p lsb_release)" ]; then
|
||||
elif command -v hostnamectl >/dev/null 2>&1; then
|
||||
SYS="$(hostnamectl | grep -i system | cut -d : -f2 | sed 's/^ *//')"
|
||||
elif command -v lsb_release >/dev/null 2>&1; then
|
||||
SYS="$(lsb_release -sd)"
|
||||
elif [ -s /etc/lsb-release ]; then
|
||||
SYS="$(grep -i description /etc/lsb-release | cut -d \" -f2)"
|
||||
elif [ -s /etc/redhat-release ]; then
|
||||
SYS="$(grep . /etc/redhat-release)"
|
||||
SYS="$(cat /etc/redhat-release)"
|
||||
elif [ -s /etc/issue ]; then
|
||||
SYS="$(grep . /etc/issue | cut -d '\' -f1 | sed '/^[ ]*$/d')"
|
||||
SYS="$(head -n1 /etc/issue | cut -d '\' -f1 | sed '/^[ ]*$/d')"
|
||||
else
|
||||
SYS="$(uname -s)"
|
||||
fi
|
||||
# Match operating system
|
||||
SYSTEM=""
|
||||
for ((int = 0; int < ${#REGEX[@]}; int++)); do
|
||||
if [[ $(echo "$SYS" | tr '[:upper:]' '[:lower:]') =~ ${REGEX[int]} ]]; then
|
||||
SYSTEM="${RELEASE[int]}"
|
||||
UPDATE_CMD=${PACKAGE_UPDATE[int]}
|
||||
INSTALL_CMD=${PACKAGE_INSTALL[int]}
|
||||
REMOVE_CMD=${PACKAGE_REMOVE[int]}
|
||||
UNINSTALL_CMD=${PACKAGE_UNINSTALL[int]}
|
||||
break
|
||||
fi
|
||||
done
|
||||
# If system is unrecognized, try common package managers
|
||||
sys_lower=$(echo "$SYS" | tr '[:upper:]' '[:lower:]')
|
||||
if echo "$sys_lower" | grep -E "debian|astra" >/dev/null 2>&1; then
|
||||
SYSTEM="Debian"
|
||||
UPDATE_CMD="apt-get update"
|
||||
INSTALL_CMD="apt-get -y install"
|
||||
REMOVE_CMD="apt-get -y remove"
|
||||
UNINSTALL_CMD="apt-get -y autoremove"
|
||||
elif echo "$sys_lower" | grep -E "ubuntu" >/dev/null 2>&1; then
|
||||
SYSTEM="Ubuntu"
|
||||
UPDATE_CMD="apt-get update"
|
||||
INSTALL_CMD="apt-get -y install"
|
||||
REMOVE_CMD="apt-get -y remove"
|
||||
UNINSTALL_CMD="apt-get -y autoremove"
|
||||
elif echo "$sys_lower" | grep -E "centos|red hat|kernel|oracle linux|alma|rocky" >/dev/null 2>&1; then
|
||||
SYSTEM="CentOS"
|
||||
UPDATE_CMD="yum -y update"
|
||||
INSTALL_CMD="yum -y install"
|
||||
REMOVE_CMD="yum -y remove"
|
||||
UNINSTALL_CMD="yum -y autoremove"
|
||||
elif echo "$sys_lower" | grep -E "amazon linux" >/dev/null 2>&1; then
|
||||
SYSTEM="CentOS"
|
||||
UPDATE_CMD="yum -y update"
|
||||
INSTALL_CMD="yum -y install"
|
||||
REMOVE_CMD="yum -y remove"
|
||||
UNINSTALL_CMD="yum -y autoremove"
|
||||
elif echo "$sys_lower" | grep -E "fedora" >/dev/null 2>&1; then
|
||||
SYSTEM="Fedora"
|
||||
UPDATE_CMD="yum -y update"
|
||||
INSTALL_CMD="yum -y install"
|
||||
REMOVE_CMD="yum -y remove"
|
||||
UNINSTALL_CMD="yum -y autoremove"
|
||||
elif echo "$sys_lower" | grep -E "arch" >/dev/null 2>&1; then
|
||||
SYSTEM="Arch"
|
||||
UPDATE_CMD="pacman -Sy"
|
||||
INSTALL_CMD="pacman -Sy --noconfirm --needed"
|
||||
REMOVE_CMD="pacman -Rsc --noconfirm"
|
||||
UNINSTALL_CMD="pacman -Rns --noconfirm"
|
||||
elif echo "$sys_lower" | grep -E "freebsd" >/dev/null 2>&1; then
|
||||
SYSTEM="FreeBSD"
|
||||
UPDATE_CMD="pkg update"
|
||||
INSTALL_CMD="pkg install -y"
|
||||
REMOVE_CMD="pkg delete"
|
||||
UNINSTALL_CMD="pkg autoremove"
|
||||
elif echo "$sys_lower" | grep -E "alpine" >/dev/null 2>&1; then
|
||||
SYSTEM="Alpine"
|
||||
UPDATE_CMD="apk update"
|
||||
INSTALL_CMD="apk add --no-cache"
|
||||
REMOVE_CMD="apk del"
|
||||
UNINSTALL_CMD="apk autoremove"
|
||||
elif echo "$sys_lower" | grep -E "openbsd" >/dev/null 2>&1; then
|
||||
SYSTEM="OpenBSD"
|
||||
UPDATE_CMD="pkg_add -qu"
|
||||
INSTALL_CMD="pkg_add -I"
|
||||
REMOVE_CMD="pkg_delete -I"
|
||||
UNINSTALL_CMD="pkg_delete -a"
|
||||
elif echo "$sys_lower" | grep -E "opencloudos" >/dev/null 2>&1; then
|
||||
SYSTEM="OpenCloudOS"
|
||||
UPDATE_CMD="yum -y update"
|
||||
INSTALL_CMD="yum -y install"
|
||||
REMOVE_CMD="yum -y remove"
|
||||
UNINSTALL_CMD="yum -y autoremove"
|
||||
fi
|
||||
if [ -z "$SYSTEM" ]; then
|
||||
_yellow "Unable to recognize system, trying common package managers..."
|
||||
# Try apt
|
||||
if command -v apt-get >/dev/null 2>&1; then
|
||||
SYSTEM="Unknown-Debian"
|
||||
UPDATE_CMD="apt-get update"
|
||||
INSTALL_CMD="apt-get -y install"
|
||||
REMOVE_CMD="apt-get -y remove"
|
||||
UNINSTALL_CMD="apt-get -y autoremove"
|
||||
# Try yum
|
||||
elif command -v yum >/dev/null 2>&1; then
|
||||
SYSTEM="Unknown-RHEL"
|
||||
UPDATE_CMD="yum -y update"
|
||||
INSTALL_CMD="yum -y install"
|
||||
REMOVE_CMD="yum -y remove"
|
||||
UNINSTALL_CMD="yum -y autoremove"
|
||||
# Try dnf
|
||||
elif command -v dnf >/dev/null 2>&1; then
|
||||
SYSTEM="Unknown-Fedora"
|
||||
UPDATE_CMD="dnf -y update"
|
||||
INSTALL_CMD="dnf -y install"
|
||||
REMOVE_CMD="dnf -y remove"
|
||||
UNINSTALL_CMD="dnf -y autoremove"
|
||||
# Try pacman
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
SYSTEM="Unknown-Arch"
|
||||
UPDATE_CMD="pacman -Sy"
|
||||
INSTALL_CMD="pacman -S --noconfirm"
|
||||
REMOVE_CMD="pacman -R --noconfirm"
|
||||
UNINSTALL_CMD="pacman -Rns --noconfirm"
|
||||
# Try apk
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
SYSTEM="Unknown-Alpine"
|
||||
UPDATE_CMD="apk update"
|
||||
@@ -510,7 +546,7 @@ env_check() {
|
||||
_green "System information: $SYSTEM"
|
||||
_green "Update command: $UPDATE_CMD"
|
||||
_green "Install command: $INSTALL_CMD"
|
||||
cdn_urls=("https://cdn0.spiritlhl.top/" "http://cdn3.spiritlhl.net/" "http://cdn1.spiritlhl.net/" "http://cdn2.spiritlhl.net/")
|
||||
cdn_urls="https://cdn0.spiritlhl.top/ http://cdn3.spiritlhl.net/ http://cdn1.spiritlhl.net/ http://cdn2.spiritlhl.net/"
|
||||
check_cdn_file
|
||||
_yellow "Warning: System update will be performed"
|
||||
_yellow "This operation may:"
|
||||
@@ -520,66 +556,52 @@ env_check() {
|
||||
_yellow "4. Affect subsequent system startups"
|
||||
if [ "$noninteractive" != "true" ]; then
|
||||
reading "Continue with system update? (y/N): " update_confirm
|
||||
if [[ ! $update_confirm =~ ^[Yy]$ ]]; then
|
||||
_yellow "Skipping system update"
|
||||
_yellow "Note: Some packages may fail to install"
|
||||
else
|
||||
_green "Updating system package manager..."
|
||||
if ! ${UPDATE_CMD} 2>/dev/null; then
|
||||
_red "System update failed!"
|
||||
fi
|
||||
fi
|
||||
case "$update_confirm" in
|
||||
[Yy]*)
|
||||
_green "Updating system package manager..."
|
||||
if ! ${UPDATE_CMD} 2>/dev/null; then
|
||||
_red "System update failed!"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
_yellow "Skipping system update"
|
||||
_yellow "Note: Some packages may fail to install"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
# Install necessary commands
|
||||
for cmd in sudo wget tar unzip iproute2 systemd-detect-virt dd fio; do
|
||||
if ! command -v "$cmd" >/dev/null 2>&1; then
|
||||
_green "Installing $cmd"
|
||||
${INSTALL_CMD} "$cmd"
|
||||
fi
|
||||
done
|
||||
# sysbench installation
|
||||
if ! command -v sysbench >/dev/null 2>&1; then
|
||||
_green "Installing sysbench"
|
||||
${INSTALL_CMD} sysbench
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Unable to download sysbench through package manager, attempting compilation..."
|
||||
wget -O /tmp/sysbench.zip "${cdn_success_url}https://github.com/akopytov/sysbench/archive/1.0.20.zip" || curl -Lk -o /tmp/sysbench.zip "${cdn_success_url}https://github.com/akopytov/sysbench/archive/1.0.20.zip"
|
||||
if [ ! -f /tmp/sysbench.zip ]; then
|
||||
wget -q -O /tmp/sysbench.zip "https://hub.fgit.cf/akopytov/sysbench/archive/1.0.20.zip"
|
||||
fi
|
||||
chmod +x /tmp/sysbench.zip
|
||||
unzip /tmp/sysbench.zip -d /tmp
|
||||
Check_SysBench
|
||||
if ! ${INSTALL_CMD} sysbench; then
|
||||
_red "Unable to install sysbench through package manager"
|
||||
_yellow "Sysbench installation skipped"
|
||||
fi
|
||||
fi
|
||||
# geekbench and speedtest installation
|
||||
if ! command -v geekbench >/dev/null 2>&1; then
|
||||
_green "Installing geekbench"
|
||||
curl -L "${cdn_success_url}https://raw.githubusercontent.com/oneclickvirt/cputest/main/dgb.sh" -o dgb.sh && chmod +x dgb.sh
|
||||
bash dgb.sh -v gb5
|
||||
sh dgb.sh -v gb5
|
||||
rm -rf dgb.sh
|
||||
fi
|
||||
if ! command -v speedtest >/dev/null 2>&1; then
|
||||
_green "Installing speedtest"
|
||||
curl -L "${cdn_success_url}https://raw.githubusercontent.com/oneclickvirt/speedtest/main/dspt.sh" -o dspt.sh && chmod +x dspt.sh
|
||||
bash dspt.sh
|
||||
sh dspt.sh
|
||||
rm -rf dspt.sh
|
||||
rm -rf speedtest.tar.gz
|
||||
fi
|
||||
if ! command -v ping >/dev/null 2>&1; then
|
||||
_green "Installing ping"
|
||||
${INSTALL_CMD} iputils-ping >/dev/null 2>&1
|
||||
${INSTALL_CMD} ping >/dev/null 2>&1
|
||||
${INSTALL_CMD} iputils-ping >/dev/null 2>&1 || ${INSTALL_CMD} ping >/dev/null 2>&1
|
||||
fi
|
||||
# MacOS support
|
||||
if [ "$(uname -s)" = "Darwin" ]; then
|
||||
echo "Detected MacOS, installing sysbench iproute2mac..."
|
||||
brew install --force sysbench iproute2mac
|
||||
else
|
||||
if ! grep -q "^net.ipv4.ping_group_range = 0 2147483647$" /etc/sysctl.conf; then
|
||||
echo "net.ipv4.ping_group_range = 0 2147483647" >> /etc/sysctl.conf
|
||||
sysctl -p
|
||||
fi
|
||||
if ! grep -q "^net.ipv4.ping_group_range = 0 2147483647$" /etc/sysctl.conf 2>/dev/null; then
|
||||
echo "net.ipv4.ping_group_range = 0 2147483647" >> /etc/sysctl.conf 2>/dev/null
|
||||
sysctl -p >/dev/null 2>&1
|
||||
fi
|
||||
_green "Environment preparation complete."
|
||||
_green "Next command is: ./goecs.sh install"
|
||||
@@ -596,6 +618,7 @@ show_help() {
|
||||
可用命令:
|
||||
|
||||
./goecs.sh env 检查并安装依赖包
|
||||
注意: macOS系统会自动跳过依赖安装
|
||||
警告: 此命令会执行系统更新(可选择),可能:
|
||||
1. 耗时较长
|
||||
2. 导致网络短暂中断
|
||||
@@ -607,7 +630,7 @@ show_help() {
|
||||
3. 关键服务失败
|
||||
推荐:
|
||||
环境依赖安装过程中挂起执行
|
||||
|
||||
|
||||
可选组件:
|
||||
sysbench/geekbench (CPU性能测试)
|
||||
sudo, tar, unzip, dd, fio
|
||||
@@ -623,6 +646,7 @@ show_help() {
|
||||
Available commands:
|
||||
|
||||
./goecs.sh env Check and Install dependencies
|
||||
Note: macOS systems will skip dependency installation
|
||||
Warning: This command performs system update(optional), which may:
|
||||
1. Take considerable time
|
||||
2. Cause temporary network interruptions
|
||||
@@ -634,7 +658,7 @@ Available commands:
|
||||
3. Critical service failures
|
||||
Recommended:
|
||||
Hanging execution during environment dependency installation
|
||||
|
||||
|
||||
Optional components:
|
||||
sysbench/geekbench (CPU testing)
|
||||
sudo, tar, unzip, dd, fio
|
||||
@@ -668,4 +692,3 @@ case "$1" in
|
||||
show_help
|
||||
;;
|
||||
esac
|
||||
|
||||
|
||||
652
internal/analysis/summary.go
Normal file
652
internal/analysis/summary.go
Normal file
@@ -0,0 +1,652 @@
|
||||
package analysis
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/oneclickvirt/ecs/internal/params"
|
||||
)
|
||||
|
||||
var (
|
||||
mbpsRe = regexp.MustCompile(`(?i)(\d+(?:\.\d+)?)\s*mbps`)
|
||||
msRe = regexp.MustCompile(`(?i)(\d+(?:\.\d+)?)\s*ms`)
|
||||
|
||||
cpuModelZhRe = regexp.MustCompile(`(?im)^\s*CPU\s*型号\s*[::]\s*(.+?)\s*$`)
|
||||
cpuModelEnRe = regexp.MustCompile(`(?im)^\s*CPU\s*Model\s*[::]\s*(.+?)\s*$`)
|
||||
|
||||
threadScoreEnRe = regexp.MustCompile(`(?im)^\s*(\d+)\s*Thread\(s\)\s*Test\s*:\s*([0-9][0-9,]*(?:\.[0-9]+)?)\s*$`)
|
||||
threadScoreZhRe = regexp.MustCompile(`(?im)^\s*(\d+)\s*线程测试\((?:单核|多核)\)得分\s*[::]\s*([0-9][0-9,]*(?:\.[0-9]+)?)\s*$`)
|
||||
gbSingleRe = regexp.MustCompile(`(?im)^\s*Single-Core\s*Score\s*[::]\s*([0-9][0-9,]*(?:\.[0-9]+)?)\s*$`)
|
||||
gbMultiRe = regexp.MustCompile(`(?im)^\s*Multi-Core\s*Score\s*[::]\s*([0-9][0-9,]*(?:\.[0-9]+)?)\s*$`)
|
||||
|
||||
alphaNumRe = regexp.MustCompile(`[a-z0-9]+`)
|
||||
)
|
||||
|
||||
const (
|
||||
cpuStatsPrimaryURL = "https://raw.githubusercontent.com/oneclickvirt/ecs/ranks/cpu_statistics.json"
|
||||
cpuStatsFallbackURL = "https://github.com/oneclickvirt/ecs/raw/refs/heads/ranks/cpu_statistics.json"
|
||||
cpuCDNProbeTestURL = "https://raw.githubusercontent.com/spiritLHLS/ecs/main/back/test"
|
||||
cpuStatsCacheTTL = 30 * time.Minute
|
||||
cpuStatsFailCacheTTL = 5 * time.Minute
|
||||
cpuStatsRequestTimout = 6 * time.Second
|
||||
)
|
||||
|
||||
var cpuStatsCDNList = []string{
|
||||
"https://cdn.spiritlhl.net/",
|
||||
"http://cdn3.spiritlhl.net/",
|
||||
"http://cdn1.spiritlhl.net/",
|
||||
"http://cdn2.spiritlhl.net/",
|
||||
}
|
||||
|
||||
type cpuStatsEntry struct {
|
||||
CPUPrefix string `json:"cpu_prefix"`
|
||||
CPUModel string `json:"cpu_model"`
|
||||
SampleCount int `json:"sample_count"`
|
||||
MaxSingle float64 `json:"max_single_score"`
|
||||
MaxMulti float64 `json:"max_multi_score"`
|
||||
AvgSingle float64 `json:"avg_single_score"`
|
||||
AvgMulti float64 `json:"avg_multi_score"`
|
||||
Rank int `json:"rank"`
|
||||
TypicalCores int `json:"typical_cores"`
|
||||
TypicalThread int `json:"typical_threads"`
|
||||
}
|
||||
|
||||
type cpuStatsPayload struct {
|
||||
CPUStatistics []cpuStatsEntry `json:"cpu_statistics"`
|
||||
}
|
||||
|
||||
var (
|
||||
cpuStatsMu sync.Mutex
|
||||
cachedCPUStats *cpuStatsPayload
|
||||
cpuStatsExpireAt time.Time
|
||||
)
|
||||
|
||||
func parseFloatsByRegex(content string, re *regexp.Regexp) []float64 {
|
||||
matches := re.FindAllStringSubmatch(content, -1)
|
||||
vals := make([]float64, 0, len(matches))
|
||||
for _, m := range matches {
|
||||
if len(m) < 2 {
|
||||
continue
|
||||
}
|
||||
v, err := strconv.ParseFloat(m[1], 64)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
vals = append(vals, v)
|
||||
}
|
||||
return vals
|
||||
}
|
||||
|
||||
func parseFloatString(s string) (float64, bool) {
|
||||
clean := strings.ReplaceAll(strings.TrimSpace(s), ",", "")
|
||||
v, err := strconv.ParseFloat(clean, 64)
|
||||
if err != nil {
|
||||
return 0, false
|
||||
}
|
||||
return v, true
|
||||
}
|
||||
|
||||
func extractCPUModel(output string) string {
|
||||
for _, re := range []*regexp.Regexp{cpuModelZhRe, cpuModelEnRe} {
|
||||
m := re.FindStringSubmatch(output)
|
||||
if len(m) >= 2 {
|
||||
model := strings.TrimSpace(m[1])
|
||||
if model != "" {
|
||||
return model
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func extractCPUScores(output string) (single float64, singleOK bool, multi float64, multiOK bool) {
|
||||
for _, re := range []*regexp.Regexp{threadScoreEnRe, threadScoreZhRe} {
|
||||
matches := re.FindAllStringSubmatch(output, -1)
|
||||
for _, m := range matches {
|
||||
if len(m) < 3 {
|
||||
continue
|
||||
}
|
||||
threads, err := strconv.Atoi(strings.TrimSpace(m[1]))
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
score, ok := parseFloatString(m[2])
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if threads == 1 {
|
||||
single, singleOK = score, true
|
||||
continue
|
||||
}
|
||||
if threads > 1 && (!multiOK || score > multi) {
|
||||
multi, multiOK = score, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !singleOK {
|
||||
if m := gbSingleRe.FindStringSubmatch(output); len(m) >= 2 {
|
||||
if v, ok := parseFloatString(m[1]); ok {
|
||||
single, singleOK = v, true
|
||||
}
|
||||
}
|
||||
}
|
||||
if !multiOK {
|
||||
if m := gbMultiRe.FindStringSubmatch(output); len(m) >= 2 {
|
||||
if v, ok := parseFloatString(m[1]); ok {
|
||||
multi, multiOK = v, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func normalizeCPUString(s string) string {
|
||||
s = strings.ToLower(s)
|
||||
b := strings.Builder{}
|
||||
b.Grow(len(s))
|
||||
for _, r := range s {
|
||||
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
|
||||
b.WriteRune(r)
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func cpuTokens(s string) []string {
|
||||
lower := strings.ToLower(s)
|
||||
raw := alphaNumRe.FindAllString(lower, -1)
|
||||
if len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[string]struct{}, len(raw))
|
||||
out := make([]string, 0, len(raw))
|
||||
for _, t := range raw {
|
||||
if len(t) < 2 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[t]; ok {
|
||||
continue
|
||||
}
|
||||
seen[t] = struct{}{}
|
||||
out = append(out, t)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func fuzzyScoreCPUModel(model string, entry cpuStatsEntry) float64 {
|
||||
nm := normalizeCPUString(model)
|
||||
ne := normalizeCPUString(entry.CPUModel)
|
||||
np := normalizeCPUString(entry.CPUPrefix)
|
||||
if nm == "" || (ne == "" && np == "") {
|
||||
return 0
|
||||
}
|
||||
|
||||
if nm == ne || nm == np {
|
||||
return 1
|
||||
}
|
||||
|
||||
containsScore := 0.0
|
||||
for _, candidate := range []string{ne, np} {
|
||||
if candidate == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(candidate, nm) || strings.Contains(nm, candidate) {
|
||||
shortLen := len(nm)
|
||||
if len(candidate) < shortLen {
|
||||
shortLen = len(candidate)
|
||||
}
|
||||
longLen := len(nm)
|
||||
if len(candidate) > longLen {
|
||||
longLen = len(candidate)
|
||||
}
|
||||
if longLen > 0 {
|
||||
ratio := float64(shortLen) / float64(longLen)
|
||||
if ratio > containsScore {
|
||||
containsScore = ratio
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
modelTokens := cpuTokens(model)
|
||||
if len(modelTokens) == 0 {
|
||||
return containsScore
|
||||
}
|
||||
|
||||
entryTokenSet := make(map[string]struct{})
|
||||
for _, t := range cpuTokens(entry.CPUModel + " " + entry.CPUPrefix) {
|
||||
entryTokenSet[t] = struct{}{}
|
||||
}
|
||||
overlap := 0
|
||||
for _, t := range modelTokens {
|
||||
if _, ok := entryTokenSet[t]; ok {
|
||||
overlap++
|
||||
}
|
||||
}
|
||||
overlapScore := float64(overlap) / float64(len(modelTokens))
|
||||
|
||||
if containsScore > overlapScore {
|
||||
return containsScore
|
||||
}
|
||||
return overlapScore
|
||||
}
|
||||
|
||||
func loadCPUStats() *cpuStatsPayload {
|
||||
cpuStatsMu.Lock()
|
||||
defer cpuStatsMu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
if now.Before(cpuStatsExpireAt) {
|
||||
return cachedCPUStats
|
||||
}
|
||||
|
||||
client := req.C()
|
||||
client.SetTimeout(cpuStatsRequestTimout)
|
||||
endpoints := []string{cpuStatsPrimaryURL, cpuStatsFallbackURL}
|
||||
|
||||
availableCDN := detectAvailableCPUCDN(client)
|
||||
for _, endpoint := range endpoints {
|
||||
urls := []string{}
|
||||
if availableCDN != "" {
|
||||
urls = append(urls, availableCDN+endpoint)
|
||||
}
|
||||
urls = append(urls, endpoint)
|
||||
|
||||
for _, u := range urls {
|
||||
payload := tryDecodeCPUStatsFromURL(client, u)
|
||||
if payload == nil {
|
||||
continue
|
||||
}
|
||||
cachedCPUStats = payload
|
||||
cpuStatsExpireAt = now.Add(cpuStatsCacheTTL)
|
||||
return cachedCPUStats
|
||||
}
|
||||
}
|
||||
|
||||
cachedCPUStats = nil
|
||||
cpuStatsExpireAt = now.Add(cpuStatsFailCacheTTL)
|
||||
return nil
|
||||
}
|
||||
|
||||
func detectAvailableCPUCDN(client *req.Client) string {
|
||||
for _, baseURL := range cpuStatsCDNList {
|
||||
if checkCPUCDN(client, baseURL) {
|
||||
return baseURL
|
||||
}
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func checkCPUCDN(client *req.Client, baseURL string) bool {
|
||||
resp, err := client.R().SetHeader("User-Agent", "goecs-summary/1.0").Get(baseURL + cpuCDNProbeTestURL)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return false
|
||||
}
|
||||
|
||||
b, err := io.ReadAll(io.LimitReader(resp.Body, 4<<10))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(string(b), "success")
|
||||
}
|
||||
|
||||
func tryDecodeCPUStatsFromURL(client *req.Client, u string) *cpuStatsPayload {
|
||||
resp, err := client.R().SetHeader("User-Agent", "goecs-summary/1.0").Get(u)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var payload cpuStatsPayload
|
||||
dec := json.NewDecoder(io.LimitReader(resp.Body, 8<<20))
|
||||
if err := dec.Decode(&payload); err != nil {
|
||||
return nil
|
||||
}
|
||||
if len(payload.CPUStatistics) == 0 {
|
||||
return nil
|
||||
}
|
||||
return &payload
|
||||
}
|
||||
|
||||
func matchCPUStatsEntry(model string, payload *cpuStatsPayload) *cpuStatsEntry {
|
||||
if payload == nil || model == "" || len(payload.CPUStatistics) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
trimModel := strings.TrimSpace(model)
|
||||
normModel := normalizeCPUString(trimModel)
|
||||
|
||||
for i := range payload.CPUStatistics {
|
||||
entry := &payload.CPUStatistics[i]
|
||||
if strings.EqualFold(strings.TrimSpace(entry.CPUModel), trimModel) {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
|
||||
for i := range payload.CPUStatistics {
|
||||
entry := &payload.CPUStatistics[i]
|
||||
if normModel == normalizeCPUString(entry.CPUModel) || normModel == normalizeCPUString(entry.CPUPrefix) {
|
||||
return entry
|
||||
}
|
||||
}
|
||||
|
||||
bestIdx := -1
|
||||
bestScore := 0.0
|
||||
for i := range payload.CPUStatistics {
|
||||
score := fuzzyScoreCPUModel(trimModel, payload.CPUStatistics[i])
|
||||
if score > bestScore {
|
||||
bestScore = score
|
||||
bestIdx = i
|
||||
continue
|
||||
}
|
||||
if score == bestScore && bestIdx >= 0 && payload.CPUStatistics[i].SampleCount > payload.CPUStatistics[bestIdx].SampleCount {
|
||||
bestIdx = i
|
||||
}
|
||||
}
|
||||
|
||||
if bestIdx >= 0 && bestScore >= 0.45 {
|
||||
return &payload.CPUStatistics[bestIdx]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func cpuTierText(score float64, lang string) string {
|
||||
if lang == "zh" {
|
||||
switch {
|
||||
case score >= 5000:
|
||||
return "按 README_NEW_USER 的 Sysbench 口径,单核 >5000 可视为高性能第一梯队。"
|
||||
case score < 500:
|
||||
return "按 README_NEW_USER 的 Sysbench 口径,单核 <500 属于偏弱性能。"
|
||||
default:
|
||||
return "按 README_NEW_USER 的 Sysbench 口径,可按每约 1000 分视作一个性能档位。"
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
case score >= 5000:
|
||||
return "Per README_NEW_USER Sysbench guidance, single-core > 5000 is considered first-tier high performance."
|
||||
case score < 500:
|
||||
return "Per README_NEW_USER Sysbench guidance, single-core < 500 is considered weak performance."
|
||||
default:
|
||||
return "Per README_NEW_USER Sysbench guidance, roughly every 1000 points is about one performance tier."
|
||||
}
|
||||
}
|
||||
|
||||
func summarizeCPUWithRanking(finalOutput, lang string) []string {
|
||||
model := extractCPUModel(finalOutput)
|
||||
single, singleOK, multi, multiOK := extractCPUScores(finalOutput)
|
||||
if !singleOK && !multiOK {
|
||||
return nil
|
||||
}
|
||||
|
||||
stats := loadCPUStats()
|
||||
entry := matchCPUStatsEntry(model, stats)
|
||||
|
||||
var score float64
|
||||
var avg float64
|
||||
var max float64
|
||||
kind := "single"
|
||||
|
||||
if singleOK && entry != nil && entry.AvgSingle > 0 && entry.MaxSingle > 0 {
|
||||
score, avg, max = single, entry.AvgSingle, entry.MaxSingle
|
||||
} else if multiOK && entry != nil && entry.AvgMulti > 0 && entry.MaxMulti > 0 {
|
||||
score, avg, max = multi, entry.AvgMulti, entry.MaxMulti
|
||||
kind = "multi"
|
||||
} else if singleOK {
|
||||
score = single
|
||||
} else {
|
||||
score = multi
|
||||
kind = "multi"
|
||||
}
|
||||
|
||||
lines := make([]string, 0, 4)
|
||||
if lang == "zh" {
|
||||
if kind == "single" {
|
||||
lines = append(lines, fmt.Sprintf("CPU: 检测到单核得分 %.2f。", score))
|
||||
} else {
|
||||
lines = append(lines, fmt.Sprintf("CPU: 检测到多核得分 %.2f。", score))
|
||||
}
|
||||
} else {
|
||||
if kind == "single" {
|
||||
lines = append(lines, fmt.Sprintf("CPU: detected single-core score %.2f.", score))
|
||||
} else {
|
||||
lines = append(lines, fmt.Sprintf("CPU: detected multi-core score %.2f.", score))
|
||||
}
|
||||
}
|
||||
|
||||
if kind == "single" {
|
||||
lines = append(lines, cpuTierText(score, lang))
|
||||
}
|
||||
|
||||
if entry == nil || avg <= 0 || max <= 0 {
|
||||
if lang == "zh" {
|
||||
if model != "" {
|
||||
lines = append(lines, fmt.Sprintf("CPU 对标: 未在在线榜单中稳定匹配到型号 \"%s\",已仅给出本机分数解读。", model))
|
||||
} else {
|
||||
lines = append(lines, "CPU 对标: 未提取到 CPU 型号,已仅给出本机分数解读。")
|
||||
}
|
||||
} else {
|
||||
if model != "" {
|
||||
lines = append(lines, fmt.Sprintf("CPU ranking: no reliable online match found for model \"%s\"; local score interpretation only.", model))
|
||||
} else {
|
||||
lines = append(lines, "CPU ranking: CPU model not found in output; local score interpretation only.")
|
||||
}
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
reachAvg := score >= avg
|
||||
gapToMax := max - score
|
||||
fullBlood := false
|
||||
if max > 0 {
|
||||
ratioDiff := (score - max) / max
|
||||
if ratioDiff < 0 {
|
||||
ratioDiff = -ratioDiff
|
||||
}
|
||||
fullBlood = ratioDiff <= 0.05
|
||||
}
|
||||
pctOfAvg := score / avg * 100
|
||||
pctOfMax := score / max * 100
|
||||
|
||||
if lang == "zh" {
|
||||
lines = append(lines,
|
||||
fmt.Sprintf("CPU 对标: 匹配 \"%s\"(样本 %d,排名 #%d)。", entry.CPUModel, entry.SampleCount, entry.Rank),
|
||||
fmt.Sprintf("平均分达标: %s(本机 %.2f,均值 %.2f,达成率 %.2f%%)。", map[bool]string{true: "是", false: "否"}[reachAvg], score, avg, pctOfAvg),
|
||||
fmt.Sprintf("满血对比: 满血分 %.2f,本机为 %.2f%%,差值 %.2f。", max, pctOfMax, gapToMax),
|
||||
fmt.Sprintf("满血判定(±5%%波动): %s。", map[bool]string{true: "是", false: "否"}[fullBlood]),
|
||||
)
|
||||
} else {
|
||||
lines = append(lines,
|
||||
fmt.Sprintf("CPU ranking: matched \"%s\" (samples %d, rank #%d).", entry.CPUModel, entry.SampleCount, entry.Rank),
|
||||
fmt.Sprintf("Average-level check: %s (local %.2f vs avg %.2f, %.2f%% of avg).", map[bool]string{true: "pass", false: "below avg"}[reachAvg], score, avg, pctOfAvg),
|
||||
fmt.Sprintf("Full-blood comparison: max %.2f, local is %.2f%% of max, gap %.2f.", max, pctOfMax, gapToMax),
|
||||
fmt.Sprintf("Full-blood status (within ±5%%): %s.", map[bool]string{true: "yes", false: "no"}[fullBlood]),
|
||||
)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
func summarizeBandwidth(vals []float64, lang string) string {
|
||||
if len(vals) == 0 {
|
||||
if lang == "zh" {
|
||||
return "测速: 未检测到有效 Mbps 数据。"
|
||||
}
|
||||
return "Speed: no valid Mbps values found."
|
||||
}
|
||||
sort.Float64s(vals)
|
||||
maxV := vals[len(vals)-1]
|
||||
if lang == "zh" {
|
||||
switch {
|
||||
case maxV >= 2000:
|
||||
return fmt.Sprintf("测速: 峰值约 %.2f Mbps,属于高带宽网络。", maxV)
|
||||
case maxV >= 800:
|
||||
return fmt.Sprintf("测速: 峰值约 %.2f Mbps,带宽表现较好。", maxV)
|
||||
case maxV >= 200:
|
||||
return fmt.Sprintf("测速: 峰值约 %.2f Mbps,带宽中等可用。", maxV)
|
||||
default:
|
||||
return fmt.Sprintf("测速: 峰值约 %.2f Mbps,带宽偏低,建议关注线路与机型。", maxV)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case maxV >= 2000:
|
||||
return fmt.Sprintf("Speed: peak around %.2f Mbps, high-bandwidth profile.", maxV)
|
||||
case maxV >= 800:
|
||||
return fmt.Sprintf("Speed: peak around %.2f Mbps, strong bandwidth performance.", maxV)
|
||||
case maxV >= 200:
|
||||
return fmt.Sprintf("Speed: peak around %.2f Mbps, moderate and usable bandwidth.", maxV)
|
||||
default:
|
||||
return fmt.Sprintf("Speed: peak around %.2f Mbps, relatively limited bandwidth.", maxV)
|
||||
}
|
||||
}
|
||||
|
||||
func summarizeLatency(vals []float64, lang string) string {
|
||||
if len(vals) == 0 {
|
||||
if lang == "zh" {
|
||||
return "延迟: 未检测到有效 ms 数据。"
|
||||
}
|
||||
return "Latency: no valid ms values found."
|
||||
}
|
||||
sort.Float64s(vals)
|
||||
minV := vals[0]
|
||||
if lang == "zh" {
|
||||
switch {
|
||||
case minV <= 15:
|
||||
return fmt.Sprintf("延迟: 最优约 %.2f ms,实时交互体验优秀。", minV)
|
||||
case minV <= 45:
|
||||
return fmt.Sprintf("延迟: 最优约 %.2f ms,整体交互体验良好。", minV)
|
||||
case minV <= 90:
|
||||
return fmt.Sprintf("延迟: 最优约 %.2f ms,可用但有一定时延。", minV)
|
||||
default:
|
||||
return fmt.Sprintf("延迟: 最优约 %.2f ms,时延偏高,建议优化线路。", minV)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case minV <= 15:
|
||||
return fmt.Sprintf("Latency: best around %.2f ms, excellent for interactive workloads.", minV)
|
||||
case minV <= 45:
|
||||
return fmt.Sprintf("Latency: best around %.2f ms, generally responsive.", minV)
|
||||
case minV <= 90:
|
||||
return fmt.Sprintf("Latency: best around %.2f ms, usable with moderate delay.", minV)
|
||||
default:
|
||||
return fmt.Sprintf("Latency: best around %.2f ms, relatively high and may impact responsiveness.", minV)
|
||||
}
|
||||
}
|
||||
|
||||
func testedScopes(config *params.Config) []string {
|
||||
scopes := make([]string, 0, 8)
|
||||
if config.BasicStatus {
|
||||
scopes = append(scopes, "basic")
|
||||
}
|
||||
if config.CpuTestStatus {
|
||||
scopes = append(scopes, "cpu")
|
||||
}
|
||||
if config.MemoryTestStatus {
|
||||
scopes = append(scopes, "memory")
|
||||
}
|
||||
if config.DiskTestStatus {
|
||||
scopes = append(scopes, "disk")
|
||||
}
|
||||
if config.UtTestStatus {
|
||||
scopes = append(scopes, "unlock")
|
||||
}
|
||||
if config.SecurityTestStatus {
|
||||
scopes = append(scopes, "security")
|
||||
}
|
||||
if config.Nt3Status || config.BacktraceStatus || config.PingTestStatus || config.TgdcTestStatus || config.WebTestStatus {
|
||||
scopes = append(scopes, "network")
|
||||
}
|
||||
if config.SpeedTestStatus {
|
||||
scopes = append(scopes, "speed")
|
||||
}
|
||||
return scopes
|
||||
}
|
||||
|
||||
func scopesText(scopes []string, lang string) string {
|
||||
if len(scopes) == 0 {
|
||||
if lang == "zh" {
|
||||
return "无"
|
||||
}
|
||||
return "none"
|
||||
}
|
||||
labelsZh := map[string]string{
|
||||
"basic": "系统基础", "cpu": "CPU", "memory": "内存", "disk": "磁盘", "unlock": "解锁", "security": "IP质量", "network": "网络路由", "speed": "带宽测速",
|
||||
}
|
||||
labelsEn := map[string]string{
|
||||
"basic": "system basics", "cpu": "CPU", "memory": "memory", "disk": "disk", "unlock": "unlock", "security": "IP quality", "network": "network route", "speed": "bandwidth",
|
||||
}
|
||||
out := make([]string, 0, len(scopes))
|
||||
for _, s := range scopes {
|
||||
if lang == "zh" {
|
||||
out = append(out, labelsZh[s])
|
||||
} else {
|
||||
out = append(out, labelsEn[s])
|
||||
}
|
||||
}
|
||||
return strings.Join(out, ", ")
|
||||
}
|
||||
|
||||
// GenerateSummary creates a concise post-test summary from final output.
|
||||
func GenerateSummary(config *params.Config, finalOutput string) string {
|
||||
lang := config.Language
|
||||
scopes := testedScopes(config)
|
||||
bandwidthVals := parseFloatsByRegex(finalOutput, mbpsRe)
|
||||
latencyVals := parseFloatsByRegex(finalOutput, msRe)
|
||||
cpuLines := summarizeCPUWithRanking(finalOutput, lang)
|
||||
|
||||
if lang == "zh" {
|
||||
lines := []string{
|
||||
"测试结果总结:",
|
||||
fmt.Sprintf("- 本次覆盖: %s", scopesText(scopes, lang)),
|
||||
}
|
||||
for _, line := range cpuLines {
|
||||
lines = append(lines, "- "+line)
|
||||
}
|
||||
if config.SpeedTestStatus {
|
||||
lines = append(lines, "- "+summarizeBandwidth(bandwidthVals, lang))
|
||||
lines = append(lines, "- 参考 README_NEW_USER: 一般境外机器带宽 100Mbps 起步,是否够用应以业务下载/传输需求为准。")
|
||||
}
|
||||
if config.PingTestStatus || config.TgdcTestStatus || config.WebTestStatus || config.BacktraceStatus || config.Nt3Status {
|
||||
lines = append(lines, "- "+summarizeLatency(latencyVals, lang))
|
||||
lines = append(lines, "- 参考 README_NEW_USER: 延迟 >= 9999ms 可视为目标不可用。")
|
||||
}
|
||||
lines = append(lines, "- 建议: 结合业务场景(高并发计算/存储/跨境网络)重点参考对应分项。")
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
lines := []string{
|
||||
"Test Summary:",
|
||||
fmt.Sprintf("- Scope covered: %s", scopesText(scopes, lang)),
|
||||
}
|
||||
for _, line := range cpuLines {
|
||||
lines = append(lines, "- "+line)
|
||||
}
|
||||
if config.SpeedTestStatus {
|
||||
lines = append(lines, "- "+summarizeBandwidth(bandwidthVals, lang))
|
||||
lines = append(lines, "- README_NEW_USER note: offshore servers commonly start around 100Mbps; evaluate against your actual workload needs.")
|
||||
}
|
||||
if config.PingTestStatus || config.TgdcTestStatus || config.WebTestStatus || config.BacktraceStatus || config.Nt3Status {
|
||||
lines = append(lines, "- "+summarizeLatency(latencyVals, lang))
|
||||
lines = append(lines, "- README_NEW_USER note: latency >= 9999ms should be treated as unavailable target.")
|
||||
}
|
||||
lines = append(lines, "- Suggestion: prioritize the metrics that match your workload (compute, storage, or cross-region networking).")
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
344
internal/menu/menu.go
Normal file
344
internal/menu/menu.go
Normal file
@@ -0,0 +1,344 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/oneclickvirt/ecs/internal/params"
|
||||
"github.com/oneclickvirt/ecs/utils"
|
||||
)
|
||||
|
||||
// GetMenuChoice prompts user for menu choice
|
||||
func GetMenuChoice(language string) string {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
|
||||
defer signal.Stop(sigChan)
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-sigChan:
|
||||
if language == "zh" {
|
||||
fmt.Println("\n程序在选择过程中被用户中断")
|
||||
} else {
|
||||
fmt.Println("\nProgram interrupted by user during selection")
|
||||
}
|
||||
os.Exit(0)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
var input string
|
||||
if language == "zh" {
|
||||
fmt.Print("请输入选项: ")
|
||||
} else {
|
||||
fmt.Print("Please enter your choice: ")
|
||||
}
|
||||
fmt.Scanln(&input)
|
||||
input = strings.TrimSpace(input)
|
||||
input = strings.TrimRight(input, "\n")
|
||||
|
||||
re := regexp.MustCompile(`^\d+$`)
|
||||
if re.MatchString(input) {
|
||||
inChoice := input
|
||||
switch inChoice {
|
||||
case "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10":
|
||||
return inChoice
|
||||
default:
|
||||
if language == "zh" {
|
||||
fmt.Println("无效的选项")
|
||||
} else {
|
||||
fmt.Println("Invalid choice")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if language == "zh" {
|
||||
fmt.Println("输入错误,请输入一个纯数字")
|
||||
} else {
|
||||
fmt.Println("Invalid input, please enter a number")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// PrintMenuOptions displays menu options
|
||||
func PrintMenuOptions(preCheck utils.NetCheckResult, config *params.Config) {
|
||||
var stats *utils.StatsResponse
|
||||
var statsErr error
|
||||
var githubInfo *utils.GitHubRelease
|
||||
var githubErr error
|
||||
if preCheck.Connected {
|
||||
var pwg sync.WaitGroup
|
||||
pwg.Add(2)
|
||||
go func() {
|
||||
defer pwg.Done()
|
||||
stats, statsErr = utils.GetGoescStats()
|
||||
}()
|
||||
go func() {
|
||||
defer pwg.Done()
|
||||
githubInfo, githubErr = utils.GetLatestEcsRelease()
|
||||
}()
|
||||
pwg.Wait()
|
||||
} else {
|
||||
statsErr = fmt.Errorf("network not connected")
|
||||
githubErr = fmt.Errorf("network not connected")
|
||||
}
|
||||
var statsInfo string
|
||||
var cmp int
|
||||
if preCheck.Connected {
|
||||
if statsErr != nil {
|
||||
statsInfo = "NULL"
|
||||
} else {
|
||||
switch config.Language {
|
||||
case "zh":
|
||||
statsInfo = fmt.Sprintf("总使用量: %s | 今日使用: %s",
|
||||
utils.FormatGoecsNumber(stats.Total),
|
||||
utils.FormatGoecsNumber(stats.Daily))
|
||||
case "en":
|
||||
statsInfo = fmt.Sprintf("Total Usage: %s | Daily Usage: %s",
|
||||
utils.FormatGoecsNumber(stats.Total),
|
||||
utils.FormatGoecsNumber(stats.Daily))
|
||||
}
|
||||
}
|
||||
if githubErr == nil {
|
||||
cmp = utils.CompareVersions(config.EcsVersion, githubInfo.TagName)
|
||||
} else {
|
||||
cmp = 0
|
||||
}
|
||||
}
|
||||
switch config.Language {
|
||||
case "zh":
|
||||
fmt.Printf("VPS融合怪版本: %s\n", config.EcsVersion)
|
||||
if preCheck.Connected {
|
||||
switch cmp {
|
||||
case -1:
|
||||
fmt.Printf("检测到新版本 %s 如有必要请更新!\n", githubInfo.TagName)
|
||||
}
|
||||
fmt.Printf("使用统计: %s\n", statsInfo)
|
||||
}
|
||||
fmt.Println("1. 融合怪完全体(能测全测)")
|
||||
fmt.Println("2. 极简版(系统信息+CPU+内存+磁盘+测速节点5个)")
|
||||
fmt.Println("3. 精简版(系统信息+CPU+内存+磁盘+跨国平台解锁+路由+测速节点5个)")
|
||||
fmt.Println("4. 精简网络版(系统信息+CPU+内存+磁盘+回程+路由+测速节点5个)")
|
||||
fmt.Println("5. 精简解锁版(系统信息+CPU+内存+磁盘IO+跨国平台解锁+测速节点5个)")
|
||||
fmt.Println("6. 网络单项(IP质量检测+上游及三网回程+广州三网回程详细路由+全国延迟+TGDC+网站延迟+测速节点11个)")
|
||||
fmt.Println("7. 解锁单项(跨国平台解锁)")
|
||||
fmt.Println("8. 硬件单项(系统信息+CPU+dd磁盘测试+fio磁盘测试)")
|
||||
fmt.Println("9. IP质量检测(15个数据库的IP质量检测+邮件端口检测)")
|
||||
fmt.Println("10. 三网回程线路检测+三网回程详细路由(北京上海广州成都)+全国延迟+TGDC+网站延迟")
|
||||
fmt.Println("0. 退出程序")
|
||||
case "en":
|
||||
fmt.Printf("VPS Fusion Monster Test Version: %s\n", config.EcsVersion)
|
||||
if preCheck.Connected {
|
||||
switch cmp {
|
||||
case -1:
|
||||
fmt.Printf("New version detected %s update if necessary!\n", githubInfo.TagName)
|
||||
}
|
||||
fmt.Printf("%s\n", statsInfo)
|
||||
}
|
||||
fmt.Println("1. VPS Fusion Monster Test (Full Test)")
|
||||
fmt.Println("2. Minimal Test Suite (System Info + CPU + Memory + Disk + 5 Speed Test Nodes)")
|
||||
fmt.Println("3. Standard Test Suite (System Info + CPU + Memory + Disk + International Platform Unlock + Routing + 5 Speed Test Nodes)")
|
||||
fmt.Println("4. Network-Focused Test Suite (System Info + CPU + Memory + Disk + Backtrace + Routing + 5 Speed Test Nodes)")
|
||||
fmt.Println("5. Unlock-Focused Test Suite (System Info + CPU + Memory + Disk IO + International Platform Unlock + 5 Speed Test Nodes)")
|
||||
fmt.Println("6. Network-Only Test (IP Quality Test + Upstream & 3-Network Backtrace + Guangzhou 3-Network Detailed Routing + National Latency + TGDC + Websites + 11 Speed Test Nodes)")
|
||||
fmt.Println("7. Unlock-Only Test (International Platform Unlock)")
|
||||
fmt.Println("8. Hardware-Only Test (System Info + CPU + Memory + dd Disk Test + fio Disk Test)")
|
||||
fmt.Println("9. IP Quality Test (IP Test with 15 Databases + Email Port Test)")
|
||||
fmt.Println("0. Exit Program")
|
||||
}
|
||||
}
|
||||
|
||||
// HandleMenuMode handles menu selection using the interactive TUI
|
||||
func HandleMenuMode(preCheck utils.NetCheckResult, config *params.Config) {
|
||||
savedParams := config.SaveUserSetParams()
|
||||
config.BasicStatus = false
|
||||
config.CpuTestStatus = false
|
||||
config.MemoryTestStatus = false
|
||||
config.DiskTestStatus = false
|
||||
config.UtTestStatus = false
|
||||
config.SecurityTestStatus = false
|
||||
config.EmailTestStatus = false
|
||||
config.BacktraceStatus = false
|
||||
config.Nt3Status = false
|
||||
config.SpeedTestStatus = false
|
||||
config.TgdcTestStatus = false
|
||||
config.WebTestStatus = false
|
||||
config.AutoChangeDiskMethod = true
|
||||
|
||||
result := RunTuiMenu(preCheck, config)
|
||||
if result.quit {
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// Update language if changed by TUI selection
|
||||
config.Language = result.language
|
||||
|
||||
if result.custom {
|
||||
config.Choice = "custom"
|
||||
applyCustomResult(result, preCheck, config)
|
||||
if config.SpeedTestStatus {
|
||||
config.OnlyChinaTest = utils.CheckChina(config.EnableLogger, config.Language)
|
||||
}
|
||||
} else {
|
||||
config.Choice = result.choice
|
||||
switch result.choice {
|
||||
case "0":
|
||||
os.Exit(0)
|
||||
case "1":
|
||||
SetFullTestStatus(preCheck, config)
|
||||
config.OnlyChinaTest = utils.CheckChina(config.EnableLogger, config.Language)
|
||||
case "2":
|
||||
SetMinimalTestStatus(preCheck, config)
|
||||
case "3":
|
||||
SetStandardTestStatus(preCheck, config)
|
||||
case "4":
|
||||
SetNetworkFocusedTestStatus(preCheck, config)
|
||||
case "5":
|
||||
SetUnlockFocusedTestStatus(preCheck, config)
|
||||
case "6":
|
||||
SetNetworkOnlyTestStatus(config)
|
||||
case "7":
|
||||
SetUnlockOnlyTestStatus(config)
|
||||
case "8":
|
||||
SetHardwareOnlyTestStatus(preCheck, config)
|
||||
case "9":
|
||||
SetIPQualityTestStatus(config)
|
||||
case "10":
|
||||
config.Nt3Location = "ALL"
|
||||
SetRouteTestStatus(config)
|
||||
}
|
||||
}
|
||||
config.RestoreUserSetParams(savedParams)
|
||||
}
|
||||
|
||||
// SetFullTestStatus enables all tests
|
||||
func SetFullTestStatus(preCheck utils.NetCheckResult, config *params.Config) {
|
||||
config.BasicStatus = true
|
||||
config.CpuTestStatus = true
|
||||
config.MemoryTestStatus = true
|
||||
config.DiskTestStatus = true
|
||||
if preCheck.Connected {
|
||||
config.UtTestStatus = true
|
||||
config.SecurityTestStatus = true
|
||||
config.EmailTestStatus = true
|
||||
config.BacktraceStatus = true
|
||||
config.Nt3Status = true
|
||||
config.SpeedTestStatus = true
|
||||
config.TgdcTestStatus = true
|
||||
config.WebTestStatus = true
|
||||
}
|
||||
}
|
||||
|
||||
// SetMinimalTestStatus sets minimal test configuration
|
||||
func SetMinimalTestStatus(preCheck utils.NetCheckResult, config *params.Config) {
|
||||
config.BasicStatus = true
|
||||
config.CpuTestStatus = true
|
||||
config.MemoryTestStatus = true
|
||||
config.DiskTestStatus = true
|
||||
if preCheck.Connected {
|
||||
config.SpeedTestStatus = true
|
||||
}
|
||||
}
|
||||
|
||||
// SetStandardTestStatus sets standard test configuration
|
||||
func SetStandardTestStatus(preCheck utils.NetCheckResult, config *params.Config) {
|
||||
config.BasicStatus = true
|
||||
config.CpuTestStatus = true
|
||||
config.MemoryTestStatus = true
|
||||
config.DiskTestStatus = true
|
||||
if preCheck.Connected {
|
||||
config.UtTestStatus = true
|
||||
config.Nt3Status = true
|
||||
config.SpeedTestStatus = true
|
||||
}
|
||||
}
|
||||
|
||||
// SetNetworkFocusedTestStatus sets network-focused test configuration
|
||||
func SetNetworkFocusedTestStatus(preCheck utils.NetCheckResult, config *params.Config) {
|
||||
config.BasicStatus = true
|
||||
config.CpuTestStatus = true
|
||||
config.MemoryTestStatus = true
|
||||
config.DiskTestStatus = true
|
||||
if preCheck.Connected {
|
||||
config.BacktraceStatus = true
|
||||
config.Nt3Status = true
|
||||
config.SpeedTestStatus = true
|
||||
}
|
||||
}
|
||||
|
||||
// SetUnlockFocusedTestStatus sets unlock-focused test configuration
|
||||
func SetUnlockFocusedTestStatus(preCheck utils.NetCheckResult, config *params.Config) {
|
||||
config.BasicStatus = true
|
||||
config.CpuTestStatus = true
|
||||
config.MemoryTestStatus = true
|
||||
config.DiskTestStatus = true
|
||||
if preCheck.Connected {
|
||||
config.UtTestStatus = true
|
||||
config.SpeedTestStatus = true
|
||||
}
|
||||
}
|
||||
|
||||
// SetNetworkOnlyTestStatus sets network-only test configuration
|
||||
func SetNetworkOnlyTestStatus(config *params.Config) {
|
||||
config.OnlyIpInfoCheck = true
|
||||
config.SecurityTestStatus = true
|
||||
config.SpeedTestStatus = true
|
||||
config.BacktraceStatus = true
|
||||
config.Nt3Status = true
|
||||
config.PingTestStatus = true
|
||||
config.TgdcTestStatus = true
|
||||
config.WebTestStatus = true
|
||||
}
|
||||
|
||||
// SetUnlockOnlyTestStatus sets unlock-only test configuration
|
||||
func SetUnlockOnlyTestStatus(config *params.Config) {
|
||||
config.OnlyIpInfoCheck = true
|
||||
config.UtTestStatus = true
|
||||
}
|
||||
|
||||
// SetHardwareOnlyTestStatus sets hardware-only test configuration
|
||||
func SetHardwareOnlyTestStatus(preCheck utils.NetCheckResult, config *params.Config) {
|
||||
_ = preCheck
|
||||
config.BasicStatus = true
|
||||
config.CpuTestStatus = true
|
||||
config.MemoryTestStatus = true
|
||||
config.DiskTestStatus = true
|
||||
config.SecurityTestStatus = false
|
||||
config.AutoChangeDiskMethod = false
|
||||
}
|
||||
|
||||
// SetIPQualityTestStatus sets IP quality test configuration
|
||||
func SetIPQualityTestStatus(config *params.Config) {
|
||||
config.OnlyIpInfoCheck = true
|
||||
config.SecurityTestStatus = true
|
||||
config.EmailTestStatus = true
|
||||
}
|
||||
|
||||
// SetRouteTestStatus sets route test configuration
|
||||
func SetRouteTestStatus(config *params.Config) {
|
||||
config.OnlyIpInfoCheck = true
|
||||
config.BacktraceStatus = true
|
||||
config.Nt3Status = true
|
||||
config.PingTestStatus = true
|
||||
config.TgdcTestStatus = true
|
||||
config.WebTestStatus = true
|
||||
}
|
||||
|
||||
// PrintInvalidChoice prints invalid choice message
|
||||
func PrintInvalidChoice(language string) {
|
||||
if language == "zh" {
|
||||
fmt.Println("无效的选项")
|
||||
} else {
|
||||
fmt.Println("Invalid choice")
|
||||
}
|
||||
}
|
||||
999
internal/menu/tui.go
Normal file
999
internal/menu/tui.go
Normal file
@@ -0,0 +1,999 @@
|
||||
package menu
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
textinput "github.com/charmbracelet/bubbles/textinput"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
"github.com/oneclickvirt/ecs/internal/params"
|
||||
"github.com/oneclickvirt/ecs/utils"
|
||||
)
|
||||
|
||||
var (
|
||||
tTitleStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("39"))
|
||||
tInfoStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("244"))
|
||||
tWarnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("214"))
|
||||
tSelStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("120")).Bold(true)
|
||||
tNormStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("252"))
|
||||
tDimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
|
||||
tHelpStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
|
||||
tSectStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("214")).Bold(true)
|
||||
tChkOnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("120"))
|
||||
tChkOffStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("240"))
|
||||
tBtnStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("0")).Background(lipgloss.Color("120")).Bold(true).Padding(0, 2)
|
||||
tBtnDimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("252")).Background(lipgloss.Color("238")).Padding(0, 2)
|
||||
tPanelStyle = lipgloss.NewStyle().BorderStyle(lipgloss.NormalBorder()).BorderForeground(lipgloss.Color("238")).Padding(0, 1)
|
||||
)
|
||||
|
||||
type menuPhase int
|
||||
|
||||
const (
|
||||
phaseLang menuPhase = iota
|
||||
phaseMain
|
||||
phaseCustom
|
||||
)
|
||||
|
||||
type mainMenuItem struct {
|
||||
id string
|
||||
zh string
|
||||
en string
|
||||
descZh string
|
||||
descEn string
|
||||
needNet bool
|
||||
}
|
||||
|
||||
type testToggle struct {
|
||||
key string
|
||||
nameZh string
|
||||
nameEn string
|
||||
descZh string
|
||||
descEn string
|
||||
enabled bool
|
||||
needNet bool
|
||||
}
|
||||
|
||||
type advOption struct {
|
||||
value string
|
||||
labelZh string
|
||||
labelEn string
|
||||
descZh string
|
||||
descEn string
|
||||
}
|
||||
|
||||
type advSetting struct {
|
||||
key string
|
||||
nameZh string
|
||||
nameEn string
|
||||
descZh string
|
||||
descEn string
|
||||
kind string // option | bool | text
|
||||
options []advOption
|
||||
current int
|
||||
boolVal bool
|
||||
textVal string
|
||||
}
|
||||
|
||||
type tuiResult struct {
|
||||
choice string
|
||||
language string
|
||||
quit bool
|
||||
custom bool
|
||||
toggles []testToggle
|
||||
advanced []advSetting
|
||||
}
|
||||
|
||||
type tuiModel struct {
|
||||
phase menuPhase
|
||||
config *params.Config
|
||||
preCheck utils.NetCheckResult
|
||||
langPreset bool
|
||||
|
||||
langCursor int
|
||||
mainCursor int
|
||||
mainItems []mainMenuItem
|
||||
|
||||
customCursor int
|
||||
toggles []testToggle
|
||||
advanced []advSetting
|
||||
customTotal int
|
||||
|
||||
editingText bool
|
||||
editingIdx int
|
||||
textInput textinput.Model
|
||||
|
||||
statsTotal int
|
||||
statsDaily int
|
||||
hasStats bool
|
||||
cmpVersion int
|
||||
newVersion string
|
||||
|
||||
result tuiResult
|
||||
width int
|
||||
height int
|
||||
}
|
||||
|
||||
func defaultMainItems() []mainMenuItem {
|
||||
return []mainMenuItem{
|
||||
{id: "1", zh: "融合怪完全体(能测全测)", en: "Full Test (All Available Tests)", descZh: "系统信息、CPU、内存、磁盘、解锁、IP质量、邮件端口、回程、NT3、测速、TGDC、网站延迟。", descEn: "Runs all available modules: system, compute, memory, disk, unlock, security, routing and speed.", needNet: false},
|
||||
{id: "2", zh: "极简版", en: "Minimal Suite", descZh: "基础系统+CPU+内存+磁盘+5个测速节点,适合快速健康检查。", descEn: "Basic system + CPU + memory + disk + 5 speed nodes for quick health checks.", needNet: false},
|
||||
{id: "3", zh: "精简版", en: "Standard Suite", descZh: "在极简版基础上增加平台解锁与路由能力评估。", descEn: "Minimal suite plus unlock and routing capability checks.", needNet: false},
|
||||
{id: "4", zh: "精简网络版", en: "Network Suite", descZh: "强调网络回程与路由质量,辅以基础硬件测试。", descEn: "Network-focused profile with backtrace/routing plus basic hardware checks.", needNet: false},
|
||||
{id: "5", zh: "精简解锁版", en: "Unlock Suite", descZh: "以流媒体和平台解锁能力为主,附基础性能测试。", descEn: "Unlock-focused profile with essential compute/storage checks.", needNet: false},
|
||||
{id: "6", zh: "网络单项", en: "Network Only", descZh: "仅网络维度:IP质量、回程、NT3、延迟、TGDC、网站和测速。", descEn: "Network-only profile: IP quality, route, latency, TGDC, websites, speed.", needNet: true},
|
||||
{id: "7", zh: "解锁单项", en: "Unlock Only", descZh: "仅进行跨国平台解锁与流媒体可用性检测。", descEn: "Unlock-only profile for cross-border media/service availability.", needNet: true},
|
||||
{id: "8", zh: "硬件单项", en: "Hardware Only", descZh: "系统信息、CPU、内存、dd/fio 磁盘测试。", descEn: "Hardware-only profile with system, CPU, memory and disk tests.", needNet: false},
|
||||
{id: "9", zh: "IP质量检测", en: "IP Quality", descZh: "15库 IP质量 + 邮件端口,适合网络身份风险评估。", descEn: "IP quality across multiple datasets plus email port checks.", needNet: true},
|
||||
{id: "10", zh: "三网回程线路", en: "3-Network Route", descZh: "三网回程、NT3路由、延迟、TGDC、网站延迟专项。", descEn: "3-network backtrace + NT3 route + latency/TGDC/website checks.", needNet: true},
|
||||
{id: "custom", zh: ">>> 高级自定义(全参数模式)", en: ">>> Advanced Custom (Full Parameters)", descZh: "按参数逐项配置,支持测试项、方法、路径、上传和结果分析。", descEn: "Configure per-parameter with test toggles, methods, paths, upload and analysis.", needNet: false},
|
||||
{id: "0", zh: "退出程序", en: "Exit Program", descZh: "退出当前程序。", descEn: "Exit program.", needNet: false},
|
||||
}
|
||||
}
|
||||
|
||||
func defaultTestToggles() []testToggle {
|
||||
return []testToggle{
|
||||
{key: "basic", nameZh: "基础系统信息", nameEn: "Basic System Info", descZh: "操作系统、CPU型号、内核、虚拟化等基础信息。", descEn: "OS, CPU model, kernel, virtualization and base environment info.", enabled: true, needNet: false},
|
||||
{key: "cpu", nameZh: "CPU测试", nameEn: "CPU Test", descZh: "按所选方法执行 CPU 计算性能测试。", descEn: "Run CPU compute benchmarks using selected method.", enabled: true, needNet: false},
|
||||
{key: "memory", nameZh: "内存测试", nameEn: "Memory Test", descZh: "按所选方法测试内存吞吐和访问性能。", descEn: "Run memory throughput and access benchmarks by selected method.", enabled: true, needNet: false},
|
||||
{key: "disk", nameZh: "磁盘测试", nameEn: "Disk Test", descZh: "按所选方法执行磁盘读写性能测试。", descEn: "Run disk read/write benchmark using selected method/path.", enabled: true, needNet: false},
|
||||
{key: "ut", nameZh: "跨国平台解锁", nameEn: "Streaming Unlock", descZh: "检测多类海外流媒体与服务可用性。", descEn: "Check availability of cross-border streaming/services.", enabled: false, needNet: true},
|
||||
{key: "security", nameZh: "IP质量检测", nameEn: "IP Quality Check", descZh: "多库 IP 信誉、风险和质量信息检测。", descEn: "IP reputation/risk/quality checks across multiple datasets.", enabled: false, needNet: true},
|
||||
{key: "email", nameZh: "邮件端口检测", nameEn: "Email Port Check", descZh: "检查常见邮件相关端口连通能力。", descEn: "Check common mail-related port connectivity.", enabled: false, needNet: true},
|
||||
{key: "backtrace", nameZh: "回程路由", nameEn: "Backtrace Route", descZh: "检测上游及三网回程路径。", descEn: "Inspect upstream and 3-network return routes.", enabled: false, needNet: true},
|
||||
{key: "nt3", nameZh: "NT3路由", nameEn: "NT3 Route", descZh: "按指定地区与协议执行详细路由追踪。", descEn: "Run detailed route trace by selected location/protocol.", enabled: false, needNet: true},
|
||||
{key: "speed", nameZh: "测速", nameEn: "Speed Test", descZh: "测试下载/上传带宽与延迟。", descEn: "Measure download/upload bandwidth and latency.", enabled: false, needNet: true},
|
||||
{key: "ping", nameZh: "Ping测试", nameEn: "Ping Test", descZh: "全国/多地区延迟质量测试。", descEn: "Latency quality checks across multiple regions.", enabled: false, needNet: true},
|
||||
{key: "tgdc", nameZh: "Telegram DC测试", nameEn: "Telegram DC Test", descZh: "检测 Telegram 数据中心延迟表现。", descEn: "Measure latency to Telegram data centers.", enabled: false, needNet: true},
|
||||
{key: "web", nameZh: "网站延迟", nameEn: "Website Latency", descZh: "检测常见网站访问延迟。", descEn: "Check latency to commonly used websites.", enabled: false, needNet: true},
|
||||
}
|
||||
}
|
||||
|
||||
func option(value, zh, en, descZh, descEn string) advOption {
|
||||
return advOption{value: value, labelZh: zh, labelEn: en, descZh: descZh, descEn: descEn}
|
||||
}
|
||||
|
||||
func defaultAdvSettings(config *params.Config) []advSetting {
|
||||
adv := []advSetting{
|
||||
{
|
||||
key: "cpum", nameZh: "CPU测试方法", nameEn: "CPU Method", kind: "option",
|
||||
descZh: "选择 CPU 压测方法,不同方法偏向不同负载模型。",
|
||||
descEn: "Choose CPU benchmark method. Different methods model different workloads.",
|
||||
options: []advOption{
|
||||
option("sysbench", "Sysbench", "Sysbench", "通用基准,稳定易比较。", "General-purpose benchmark with stable comparability."),
|
||||
option("geekbench", "Geekbench", "Geekbench", "偏综合应用模型,便于横向对比。", "Application-like synthetic benchmark for broad comparison."),
|
||||
option("winsat", "WinSAT", "WinSAT", "Windows 场景下常用基准。", "Common benchmark in Windows environments."),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "cput", nameZh: "CPU线程模式", nameEn: "CPU Thread Mode", kind: "option",
|
||||
descZh: "单线程看核心峰值,多线程看整机并发能力。",
|
||||
descEn: "Single-core shows peak core power; multi-core shows parallel throughput.",
|
||||
options: []advOption{
|
||||
option("multi", "多线程", "Multi-thread", "评估整机并发算力。", "Evaluate full-machine parallel compute capability."),
|
||||
option("single", "单线程", "Single-thread", "评估单核心峰值性能。", "Evaluate peak single-core performance."),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "memorym", nameZh: "内存测试方法", nameEn: "Memory Method", kind: "option",
|
||||
descZh: "选择内存测试方法,结果关注带宽与访问效率。",
|
||||
descEn: "Choose memory benchmark method to evaluate bandwidth/access efficiency.",
|
||||
options: []advOption{
|
||||
option("stream", "STREAM", "STREAM", "侧重带宽测试。", "Focused on memory bandwidth."),
|
||||
option("sysbench", "Sysbench", "Sysbench", "通用内存压测。", "General-purpose memory stress benchmark."),
|
||||
option("dd", "dd", "dd", "基于系统工具的简化测试。", "Simple system-tool-based measurement."),
|
||||
option("winsat", "WinSAT", "WinSAT", "Windows 环境内存基准。", "Windows-oriented memory benchmark."),
|
||||
option("auto", "自动", "Auto", "自动选择可用且优先方法。", "Automatically select the preferred available method."),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "diskm", nameZh: "磁盘测试方法", nameEn: "Disk Method", kind: "option",
|
||||
descZh: "选择磁盘测试方法,评估顺序/随机读写能力。",
|
||||
descEn: "Choose disk method to evaluate sequential/random I/O performance.",
|
||||
options: []advOption{
|
||||
option("fio", "FIO", "FIO", "更全面的磁盘 I/O 基准。", "Comprehensive disk I/O benchmark."),
|
||||
option("dd", "dd", "dd", "快速顺序写读基准。", "Quick sequential write/read benchmark."),
|
||||
option("winsat", "WinSAT", "WinSAT", "Windows 磁盘基准。", "Disk benchmark for Windows environments."),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "diskp", nameZh: "磁盘测试路径", nameEn: "Disk Test Path", kind: "text",
|
||||
descZh: "自定义磁盘测试目录。留空表示默认路径。",
|
||||
descEn: "Custom disk test directory. Empty means default path.",
|
||||
textVal: config.DiskTestPath,
|
||||
},
|
||||
{
|
||||
key: "diskmc", nameZh: "多磁盘检测", nameEn: "Multi-Disk Check", kind: "bool",
|
||||
descZh: "启用后尝试检测并测试多磁盘路径。",
|
||||
descEn: "When enabled, detect and benchmark multiple disk paths.",
|
||||
boolVal: config.DiskMultiCheck,
|
||||
},
|
||||
{
|
||||
key: "nt3loc", nameZh: "NT3测试地区", nameEn: "NT3 Location", kind: "option",
|
||||
descZh: "选择路由追踪地区。显示中文全称,内部仍使用标准参数值。",
|
||||
descEn: "Choose route trace region. Full names are shown while preserving standard values.",
|
||||
options: []advOption{
|
||||
option("GZ", "广州", "Guangzhou", "从广州节点进行追踪。", "Trace from Guangzhou node."),
|
||||
option("SH", "上海", "Shanghai", "从上海节点进行追踪。", "Trace from Shanghai node."),
|
||||
option("BJ", "北京", "Beijing", "从北京节点进行追踪。", "Trace from Beijing node."),
|
||||
option("CD", "成都", "Chengdu", "从成都节点进行追踪。", "Trace from Chengdu node."),
|
||||
option("ALL", "全部地区", "All Regions", "依次测试全部地区节点。", "Run route traces from all supported regions."),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "nt3t", nameZh: "NT3协议类型", nameEn: "NT3 Protocol", kind: "option",
|
||||
descZh: "指定 NT3 路由检测协议栈。",
|
||||
descEn: "Select protocol stack used by NT3 route checks.",
|
||||
options: []advOption{
|
||||
option("ipv4", "仅 IPv4", "IPv4 Only", "仅测试 IPv4 路由路径。", "Test IPv4 routing only."),
|
||||
option("ipv6", "仅 IPv6", "IPv6 Only", "仅测试 IPv6 路由路径。", "Test IPv6 routing only."),
|
||||
option("both", "IPv4 + IPv6", "IPv4 + IPv6", "同时测试 IPv4 与 IPv6。", "Test both IPv4 and IPv6."),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "spnum", nameZh: "测速节点数/运营商", nameEn: "Speed Nodes per ISP", kind: "option",
|
||||
descZh: "每个运营商选择的测速节点数量。",
|
||||
descEn: "Number of speed test nodes selected per ISP.",
|
||||
options: []advOption{
|
||||
option("1", "1 个", "1 node", "最快速,覆盖最少。", "Fastest run with least coverage."),
|
||||
option("2", "2 个", "2 nodes", "默认平衡。", "Default balanced option."),
|
||||
option("3", "3 个", "3 nodes", "覆盖更广,耗时增加。", "Broader coverage with more runtime."),
|
||||
option("4", "4 个", "4 nodes", "更完整网络采样。", "More complete network sampling."),
|
||||
option("5", "5 个", "5 nodes", "高覆盖,耗时较高。", "High coverage with longer runtime."),
|
||||
option("6", "6 个", "6 nodes", "深度采样。", "Deep sampling."),
|
||||
option("7", "7 个", "7 nodes", "深度采样。", "Deep sampling."),
|
||||
option("8", "8 个", "8 nodes", "深度采样。", "Deep sampling."),
|
||||
option("9", "9 个", "9 nodes", "深度采样。", "Deep sampling."),
|
||||
option("10", "10 个", "10 nodes", "最全面,耗时最高。", "Most comprehensive, longest runtime."),
|
||||
},
|
||||
},
|
||||
{
|
||||
key: "log", nameZh: "调试日志", nameEn: "Debug Logger", kind: "bool",
|
||||
descZh: "启用后输出更多调试日志,便于排障。",
|
||||
descEn: "Enable verbose logs for troubleshooting.",
|
||||
boolVal: config.EnableLogger,
|
||||
},
|
||||
{
|
||||
key: "upload", nameZh: "上传并生成分享链接", nameEn: "Upload & Share Link", kind: "bool",
|
||||
descZh: "启用后上传测试结果并生成可分享链接。",
|
||||
descEn: "Upload final result and generate a shareable link.",
|
||||
boolVal: config.EnableUpload,
|
||||
},
|
||||
{
|
||||
key: "analysis", nameZh: "测试后结果总结分析", nameEn: "Post-Test Summary Analysis", kind: "bool",
|
||||
descZh: "测试结束后生成简明总结,提炼优势、短板和用途建议。",
|
||||
descEn: "Generate concise final summary with strengths, limits and usage hints.",
|
||||
boolVal: config.AnalyzeResult,
|
||||
},
|
||||
{
|
||||
key: "filepath", nameZh: "结果文件名", nameEn: "Result File Name", kind: "text",
|
||||
descZh: "上传前本地结果文件名。",
|
||||
descEn: "Local result filename used before upload.",
|
||||
textVal: config.FilePath,
|
||||
},
|
||||
{
|
||||
key: "width", nameZh: "输出宽度", nameEn: "Output Width", kind: "option",
|
||||
descZh: "控制终端输出排版宽度。",
|
||||
descEn: "Controls console output formatting width.",
|
||||
options: []advOption{
|
||||
option("72", "72 列", "72 cols", "紧凑显示。", "Compact layout."),
|
||||
option("82", "82 列", "82 cols", "默认宽度。", "Default width."),
|
||||
option("100", "100 列", "100 cols", "更宽显示。", "Wider layout."),
|
||||
option("120", "120 列", "120 cols", "宽屏显示。", "Wide-screen layout."),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i := range adv {
|
||||
switch adv[i].key {
|
||||
case "cpum":
|
||||
adv[i].current = optionIndexByValue(adv[i].options, config.CpuTestMethod)
|
||||
case "cput":
|
||||
adv[i].current = optionIndexByValue(adv[i].options, config.CpuTestThreadMode)
|
||||
case "memorym":
|
||||
adv[i].current = optionIndexByValue(adv[i].options, config.MemoryTestMethod)
|
||||
case "diskm":
|
||||
adv[i].current = optionIndexByValue(adv[i].options, config.DiskTestMethod)
|
||||
case "nt3loc":
|
||||
adv[i].current = optionIndexByValue(adv[i].options, config.Nt3Location)
|
||||
case "nt3t":
|
||||
adv[i].current = optionIndexByValue(adv[i].options, config.Nt3CheckType)
|
||||
case "spnum":
|
||||
adv[i].current = optionIndexByValue(adv[i].options, strconv.Itoa(config.SpNum))
|
||||
case "width":
|
||||
adv[i].current = optionIndexByValue(adv[i].options, strconv.Itoa(config.Width))
|
||||
}
|
||||
}
|
||||
|
||||
return adv
|
||||
}
|
||||
|
||||
func optionIndexByValue(options []advOption, value string) int {
|
||||
for i, opt := range options {
|
||||
if opt.value == value {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func newTuiModel(preCheck utils.NetCheckResult, config *params.Config, langPreset bool, statsTotal, statsDaily int, hasStats bool, cmpVersion int, newVersion string) tuiModel {
|
||||
toggles := defaultTestToggles()
|
||||
advanced := defaultAdvSettings(config)
|
||||
ti := textinput.New()
|
||||
ti.Prompt = "> "
|
||||
ti.Placeholder = ""
|
||||
ti.CharLimit = 255
|
||||
ti.Width = 45
|
||||
m := tuiModel{
|
||||
config: config,
|
||||
preCheck: preCheck,
|
||||
langPreset: langPreset,
|
||||
mainItems: defaultMainItems(),
|
||||
toggles: toggles,
|
||||
advanced: advanced,
|
||||
customTotal: len(toggles) + len(advanced) + 1,
|
||||
statsTotal: statsTotal,
|
||||
statsDaily: statsDaily,
|
||||
hasStats: hasStats,
|
||||
cmpVersion: cmpVersion,
|
||||
newVersion: newVersion,
|
||||
width: config.Width,
|
||||
height: 24,
|
||||
textInput: ti,
|
||||
}
|
||||
if langPreset {
|
||||
m.phase = phaseMain
|
||||
m.result.language = config.Language
|
||||
} else {
|
||||
m.phase = phaseLang
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (m tuiModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m tuiModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.WindowSizeMsg:
|
||||
m.width = msg.Width
|
||||
m.height = msg.Height
|
||||
return m, nil
|
||||
case tea.KeyMsg:
|
||||
switch m.phase {
|
||||
case phaseLang:
|
||||
return m.updateLang(msg)
|
||||
case phaseMain:
|
||||
return m.updateMain(msg)
|
||||
case phaseCustom:
|
||||
return m.updateCustom(msg)
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m tuiModel) View() string {
|
||||
switch m.phase {
|
||||
case phaseLang:
|
||||
return m.viewLang()
|
||||
case phaseMain:
|
||||
return m.viewMain()
|
||||
case phaseCustom:
|
||||
return m.viewCustom()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m tuiModel) updateLang(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
switch msg.String() {
|
||||
case "up", "k":
|
||||
if m.langCursor > 0 {
|
||||
m.langCursor--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.langCursor < 1 {
|
||||
m.langCursor++
|
||||
}
|
||||
case "1":
|
||||
m.result.language = "zh"
|
||||
m.phase = phaseMain
|
||||
case "2":
|
||||
m.result.language = "en"
|
||||
m.phase = phaseMain
|
||||
case "enter":
|
||||
if m.langCursor == 0 {
|
||||
m.result.language = "zh"
|
||||
} else {
|
||||
m.result.language = "en"
|
||||
}
|
||||
m.phase = phaseMain
|
||||
case "q", "ctrl+c":
|
||||
m.result.quit = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m tuiModel) viewLang() string {
|
||||
var s strings.Builder
|
||||
s.WriteString("\n")
|
||||
s.WriteString(tTitleStyle.Render(" VPS融合怪测试 / VPS Fusion Monster Test"))
|
||||
s.WriteString("\n\n")
|
||||
s.WriteString(tInfoStyle.Render(" 请选择语言 / Please select language:"))
|
||||
s.WriteString("\n\n")
|
||||
langs := []string{"1. 中文", "2. English"}
|
||||
for i, l := range langs {
|
||||
cursor := " "
|
||||
style := tNormStyle
|
||||
if m.langCursor == i {
|
||||
cursor = " > "
|
||||
style = tSelStyle
|
||||
}
|
||||
s.WriteString(fmt.Sprintf("%s%s\n", cursor, style.Render(l)))
|
||||
}
|
||||
s.WriteString("\n")
|
||||
s.WriteString(tHelpStyle.Render(" ↑/↓ Navigate Enter Confirm 1/2 Quick-Select q Quit"))
|
||||
s.WriteString("\n")
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (m tuiModel) updateMain(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
key := msg.String()
|
||||
switch key {
|
||||
case "up", "k":
|
||||
if m.mainCursor > 0 {
|
||||
m.mainCursor--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.mainCursor < len(m.mainItems)-1 {
|
||||
m.mainCursor++
|
||||
}
|
||||
case "home":
|
||||
m.mainCursor = 0
|
||||
case "end":
|
||||
m.mainCursor = len(m.mainItems) - 1
|
||||
case "enter":
|
||||
item := m.mainItems[m.mainCursor]
|
||||
if item.needNet && !m.preCheck.Connected {
|
||||
return m, nil
|
||||
}
|
||||
if item.id == "custom" {
|
||||
m.phase = phaseCustom
|
||||
m.customCursor = 0
|
||||
return m, nil
|
||||
}
|
||||
m.result.choice = item.id
|
||||
return m, tea.Quit
|
||||
case "q", "ctrl+c":
|
||||
m.result.quit = true
|
||||
return m, tea.Quit
|
||||
default:
|
||||
for i, item := range m.mainItems {
|
||||
if key == item.id {
|
||||
if item.needNet && !m.preCheck.Connected {
|
||||
return m, nil
|
||||
}
|
||||
if item.id == "custom" {
|
||||
m.phase = phaseCustom
|
||||
m.customCursor = 0
|
||||
return m, nil
|
||||
}
|
||||
m.mainCursor = i
|
||||
m.result.choice = item.id
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m tuiModel) selectedMainDesc(lang string) string {
|
||||
item := m.mainItems[m.mainCursor]
|
||||
if lang == "zh" {
|
||||
return item.descZh
|
||||
}
|
||||
return item.descEn
|
||||
}
|
||||
|
||||
func (m tuiModel) viewMain() string {
|
||||
lang := m.result.language
|
||||
var s strings.Builder
|
||||
s.WriteString("\n")
|
||||
if lang == "zh" {
|
||||
s.WriteString(tTitleStyle.Render(fmt.Sprintf(" VPS融合怪 %s", m.config.EcsVersion)))
|
||||
} else {
|
||||
s.WriteString(tTitleStyle.Render(fmt.Sprintf(" VPS Fusion Monster %s", m.config.EcsVersion)))
|
||||
}
|
||||
s.WriteString("\n")
|
||||
if m.preCheck.Connected && m.cmpVersion == -1 {
|
||||
if lang == "zh" {
|
||||
s.WriteString(tWarnStyle.Render(fmt.Sprintf(" ! 检测到新版本 %s 如有必要请更新", m.newVersion)))
|
||||
} else {
|
||||
s.WriteString(tWarnStyle.Render(fmt.Sprintf(" ! New version %s detected", m.newVersion)))
|
||||
}
|
||||
s.WriteString("\n")
|
||||
}
|
||||
if m.preCheck.Connected && m.hasStats {
|
||||
if lang == "zh" {
|
||||
s.WriteString(tInfoStyle.Render(fmt.Sprintf(" 总使用量: %s | 今日使用: %s", utils.FormatGoecsNumber(m.statsTotal), utils.FormatGoecsNumber(m.statsDaily))))
|
||||
} else {
|
||||
s.WriteString(tInfoStyle.Render(fmt.Sprintf(" Total Usage: %s | Daily Usage: %s", utils.FormatGoecsNumber(m.statsTotal), utils.FormatGoecsNumber(m.statsDaily))))
|
||||
}
|
||||
s.WriteString("\n")
|
||||
}
|
||||
s.WriteString("\n")
|
||||
if lang == "zh" {
|
||||
s.WriteString(tSectStyle.Render(" 请选择测试方案:"))
|
||||
} else {
|
||||
s.WriteString(tSectStyle.Render(" Select Test Suite:"))
|
||||
}
|
||||
s.WriteString("\n\n")
|
||||
for i, item := range m.mainItems {
|
||||
cursor := " "
|
||||
style := tNormStyle
|
||||
if m.mainCursor == i {
|
||||
cursor = " > "
|
||||
style = tSelStyle
|
||||
}
|
||||
label := item.en
|
||||
if lang == "zh" {
|
||||
label = item.zh
|
||||
}
|
||||
prefix := ""
|
||||
switch {
|
||||
case item.id == "custom":
|
||||
prefix = ""
|
||||
case item.id == "0":
|
||||
prefix = " 0. "
|
||||
default:
|
||||
prefix = fmt.Sprintf("%2s. ", item.id)
|
||||
}
|
||||
suffix := ""
|
||||
if item.needNet && !m.preCheck.Connected {
|
||||
style = tDimStyle
|
||||
if lang == "zh" {
|
||||
suffix = " [需要网络]"
|
||||
} else {
|
||||
suffix = " [No Network]"
|
||||
}
|
||||
}
|
||||
s.WriteString(fmt.Sprintf("%s%s%s\n", cursor, style.Render(prefix+label), tDimStyle.Render(suffix)))
|
||||
}
|
||||
s.WriteString("\n")
|
||||
panelTitle := " 当前选项说明"
|
||||
panelBody := m.selectedMainDesc(lang)
|
||||
if lang == "en" {
|
||||
panelTitle = " Selected Option Description"
|
||||
}
|
||||
s.WriteString(tSectStyle.Render(panelTitle) + "\n")
|
||||
s.WriteString(tPanelStyle.Width(maxInt(60, m.width-6)).Render(panelBody) + "\n")
|
||||
s.WriteString("\n")
|
||||
if lang == "zh" {
|
||||
s.WriteString(tHelpStyle.Render(" ↑/↓/j/k 移动 Enter 确认 数字 快速选择 q 退出"))
|
||||
} else {
|
||||
s.WriteString(tHelpStyle.Render(" Up/Down/j/k Navigate Enter Confirm Number Quick-Select q Quit"))
|
||||
}
|
||||
s.WriteString("\n")
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func (m *tuiModel) startEditText(settingIdx int) {
|
||||
m.editingText = true
|
||||
m.editingIdx = settingIdx
|
||||
m.textInput.SetValue(m.advanced[settingIdx].textVal)
|
||||
m.textInput.Focus()
|
||||
}
|
||||
|
||||
func (m *tuiModel) stopEditText(save bool) {
|
||||
if save {
|
||||
m.advanced[m.editingIdx].textVal = strings.TrimSpace(m.textInput.Value())
|
||||
}
|
||||
m.textInput.Blur()
|
||||
m.editingText = false
|
||||
}
|
||||
|
||||
func (m tuiModel) updateCustom(msg tea.KeyMsg) (tea.Model, tea.Cmd) {
|
||||
if m.editingText {
|
||||
switch msg.String() {
|
||||
case "enter":
|
||||
m.stopEditText(true)
|
||||
return m, nil
|
||||
case "esc":
|
||||
m.stopEditText(false)
|
||||
return m, nil
|
||||
case "ctrl+c":
|
||||
m.result.quit = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
var cmd tea.Cmd
|
||||
m.textInput, cmd = m.textInput.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
key := msg.String()
|
||||
switch key {
|
||||
case "up", "k":
|
||||
if m.customCursor > 0 {
|
||||
m.customCursor--
|
||||
}
|
||||
case "down", "j":
|
||||
if m.customCursor < m.customTotal-1 {
|
||||
m.customCursor++
|
||||
}
|
||||
case "home":
|
||||
m.customCursor = 0
|
||||
case "end":
|
||||
m.customCursor = m.customTotal - 1
|
||||
case " ", "enter", "right", "l", "left", "h":
|
||||
if m.customCursor < len(m.toggles) {
|
||||
t := &m.toggles[m.customCursor]
|
||||
if t.needNet && !m.preCheck.Connected {
|
||||
break
|
||||
}
|
||||
t.enabled = !t.enabled
|
||||
break
|
||||
}
|
||||
if m.customCursor == m.customTotal-1 {
|
||||
m.result.custom = true
|
||||
m.result.choice = "custom"
|
||||
m.result.toggles = m.toggles
|
||||
m.result.advanced = m.advanced
|
||||
return m, tea.Quit
|
||||
}
|
||||
advIdx := m.customCursor - len(m.toggles)
|
||||
if advIdx >= 0 && advIdx < len(m.advanced) {
|
||||
a := &m.advanced[advIdx]
|
||||
switch a.kind {
|
||||
case "bool":
|
||||
a.boolVal = !a.boolVal
|
||||
case "option":
|
||||
if key == "left" || key == "h" {
|
||||
a.current = (a.current - 1 + len(a.options)) % len(a.options)
|
||||
} else {
|
||||
a.current = (a.current + 1) % len(a.options)
|
||||
}
|
||||
case "text":
|
||||
if key == "enter" || key == " " {
|
||||
m.startEditText(advIdx)
|
||||
}
|
||||
}
|
||||
}
|
||||
case "a":
|
||||
allEnabled := true
|
||||
for _, t := range m.toggles {
|
||||
if !t.enabled && (!t.needNet || m.preCheck.Connected) {
|
||||
allEnabled = false
|
||||
break
|
||||
}
|
||||
}
|
||||
for i := range m.toggles {
|
||||
if m.toggles[i].needNet && !m.preCheck.Connected {
|
||||
continue
|
||||
}
|
||||
m.toggles[i].enabled = !allEnabled
|
||||
}
|
||||
case "esc":
|
||||
m.phase = phaseMain
|
||||
return m, nil
|
||||
case "q", "ctrl+c":
|
||||
m.result.quit = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m tuiModel) currentCustomDescription(lang string) string {
|
||||
if m.customCursor < len(m.toggles) {
|
||||
t := m.toggles[m.customCursor]
|
||||
if lang == "zh" {
|
||||
return t.descZh
|
||||
}
|
||||
return t.descEn
|
||||
}
|
||||
if m.customCursor == m.customTotal-1 {
|
||||
if lang == "zh" {
|
||||
return "确认当前高级自定义配置并开始测试。"
|
||||
}
|
||||
return "Confirm current advanced custom configuration and start tests."
|
||||
}
|
||||
idx := m.customCursor - len(m.toggles)
|
||||
a := m.advanced[idx]
|
||||
if a.kind == "option" {
|
||||
op := a.options[a.current]
|
||||
if lang == "zh" {
|
||||
return a.descZh + " 当前选项: " + op.labelZh + "。" + op.descZh
|
||||
}
|
||||
return a.descEn + " Current option: " + op.labelEn + ". " + op.descEn
|
||||
}
|
||||
if a.kind == "bool" {
|
||||
if lang == "zh" {
|
||||
state := "关闭"
|
||||
if a.boolVal {
|
||||
state = "开启"
|
||||
}
|
||||
return a.descZh + " 当前状态: " + state + "。"
|
||||
}
|
||||
state := "OFF"
|
||||
if a.boolVal {
|
||||
state = "ON"
|
||||
}
|
||||
return a.descEn + " Current state: " + state + "."
|
||||
}
|
||||
if lang == "zh" {
|
||||
return a.descZh + " 当前值: " + a.textVal
|
||||
}
|
||||
return a.descEn + " Current value: " + a.textVal
|
||||
}
|
||||
|
||||
func (m tuiModel) advDisplayValue(a advSetting, lang string) string {
|
||||
switch a.kind {
|
||||
case "option":
|
||||
op := a.options[a.current]
|
||||
if lang == "zh" {
|
||||
return op.labelZh
|
||||
}
|
||||
return op.labelEn
|
||||
case "bool":
|
||||
if a.boolVal {
|
||||
if lang == "zh" {
|
||||
return "开启"
|
||||
}
|
||||
return "ON"
|
||||
}
|
||||
if lang == "zh" {
|
||||
return "关闭"
|
||||
}
|
||||
return "OFF"
|
||||
case "text":
|
||||
if strings.TrimSpace(a.textVal) == "" {
|
||||
if lang == "zh" {
|
||||
return "(默认)"
|
||||
}
|
||||
return "(default)"
|
||||
}
|
||||
return a.textVal
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (m tuiModel) viewCustom() string {
|
||||
lang := m.result.language
|
||||
var s strings.Builder
|
||||
s.WriteString("\n")
|
||||
if lang == "zh" {
|
||||
s.WriteString(tTitleStyle.Render(" 高级自定义参数模式"))
|
||||
} else {
|
||||
s.WriteString(tTitleStyle.Render(" Advanced Custom Parameter Mode"))
|
||||
}
|
||||
s.WriteString("\n\n")
|
||||
if lang == "zh" {
|
||||
s.WriteString(tSectStyle.Render(" 测试开关 (空格切换, a 全选/全不选):"))
|
||||
} else {
|
||||
s.WriteString(tSectStyle.Render(" Test Toggles (Space to toggle, a all/none):"))
|
||||
}
|
||||
s.WriteString("\n\n")
|
||||
for i, t := range m.toggles {
|
||||
cursor := " "
|
||||
style := tNormStyle
|
||||
if m.customCursor == i {
|
||||
cursor = " > "
|
||||
style = tSelStyle
|
||||
}
|
||||
if t.needNet && !m.preCheck.Connected {
|
||||
style = tDimStyle
|
||||
}
|
||||
check := tChkOffStyle.Render("[ ]")
|
||||
if t.enabled {
|
||||
check = tChkOnStyle.Render("[x]")
|
||||
}
|
||||
name := t.nameEn
|
||||
if lang == "zh" {
|
||||
name = t.nameZh
|
||||
}
|
||||
s.WriteString(fmt.Sprintf("%s%s %s\n", cursor, check, style.Render(name)))
|
||||
}
|
||||
s.WriteString("\n")
|
||||
if lang == "zh" {
|
||||
s.WriteString(tSectStyle.Render(" 参数设置 (Enter/空格切换, ←/→改选项):"))
|
||||
} else {
|
||||
s.WriteString(tSectStyle.Render(" Parameter Settings (Enter/Space switch, Left/Right cycle):"))
|
||||
}
|
||||
s.WriteString("\n\n")
|
||||
for i, a := range m.advanced {
|
||||
idx := len(m.toggles) + i
|
||||
cursor := " "
|
||||
if m.customCursor == idx {
|
||||
cursor = " > "
|
||||
}
|
||||
style := tNormStyle
|
||||
if m.customCursor == idx {
|
||||
style = tSelStyle
|
||||
}
|
||||
name := a.nameEn
|
||||
if lang == "zh" {
|
||||
name = a.nameZh
|
||||
}
|
||||
value := m.advDisplayValue(a, lang)
|
||||
if a.kind == "option" {
|
||||
value = "< " + value + " >"
|
||||
}
|
||||
s.WriteString(fmt.Sprintf("%s%-26s %s\n", cursor, style.Render(name+":"), tDimStyle.Render(value)))
|
||||
}
|
||||
|
||||
s.WriteString("\n")
|
||||
confirmIdx := m.customTotal - 1
|
||||
if m.customCursor == confirmIdx {
|
||||
if lang == "zh" {
|
||||
s.WriteString(fmt.Sprintf(" %s\n", tBtnStyle.Render(">> 开始测试 <<")))
|
||||
} else {
|
||||
s.WriteString(fmt.Sprintf(" %s\n", tBtnStyle.Render(">> Start Test <<")))
|
||||
}
|
||||
} else {
|
||||
if lang == "zh" {
|
||||
s.WriteString(fmt.Sprintf(" %s\n", tBtnDimStyle.Render(">> 开始测试 <<")))
|
||||
} else {
|
||||
s.WriteString(fmt.Sprintf(" %s\n", tBtnDimStyle.Render(">> Start Test <<")))
|
||||
}
|
||||
}
|
||||
|
||||
s.WriteString("\n")
|
||||
panelTitle := " 当前项说明"
|
||||
if lang == "en" {
|
||||
panelTitle = " Current Item Description"
|
||||
}
|
||||
s.WriteString(tSectStyle.Render(panelTitle) + "\n")
|
||||
s.WriteString(tPanelStyle.Width(maxInt(60, m.width-6)).Render(m.currentCustomDescription(lang)) + "\n")
|
||||
|
||||
if m.editingText {
|
||||
if lang == "zh" {
|
||||
s.WriteString("\n" + tWarnStyle.Render(" 文本编辑模式: Enter 保存, Esc 取消") + "\n")
|
||||
} else {
|
||||
s.WriteString("\n" + tWarnStyle.Render(" Text edit mode: Enter save, Esc cancel") + "\n")
|
||||
}
|
||||
s.WriteString(" " + m.textInput.View() + "\n")
|
||||
}
|
||||
|
||||
s.WriteString("\n")
|
||||
if lang == "zh" {
|
||||
s.WriteString(tHelpStyle.Render(" ↑/↓ 移动 Enter/空格 切换 ←/→ 改选项 a 全选 Esc 返回 q 退出"))
|
||||
} else {
|
||||
s.WriteString(tHelpStyle.Render(" Up/Down Move Enter/Space Toggle Left/Right Cycle a All Esc Back q Quit"))
|
||||
}
|
||||
s.WriteString("\n")
|
||||
return s.String()
|
||||
}
|
||||
|
||||
func maxInt(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func RunTuiMenu(preCheck utils.NetCheckResult, config *params.Config) tuiResult {
|
||||
var statsTotal, statsDaily int
|
||||
var hasStats bool
|
||||
var cmpVersion int
|
||||
var newVersion string
|
||||
if preCheck.Connected {
|
||||
var wg sync.WaitGroup
|
||||
var stats *utils.StatsResponse
|
||||
var statsErr error
|
||||
var githubInfo *utils.GitHubRelease
|
||||
var githubErr error
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
stats, statsErr = utils.GetGoescStats()
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
githubInfo, githubErr = utils.GetLatestEcsRelease()
|
||||
}()
|
||||
wg.Wait()
|
||||
if statsErr == nil {
|
||||
statsTotal = stats.Total
|
||||
statsDaily = stats.Daily
|
||||
hasStats = true
|
||||
}
|
||||
if githubErr == nil {
|
||||
cmpVersion = utils.CompareVersions(config.EcsVersion, githubInfo.TagName)
|
||||
newVersion = githubInfo.TagName
|
||||
}
|
||||
}
|
||||
langPreset := config.UserSetFlags["lang"] || config.UserSetFlags["l"]
|
||||
m := newTuiModel(preCheck, config, langPreset, statsTotal, statsDaily, hasStats, cmpVersion, newVersion)
|
||||
p := tea.NewProgram(m, tea.WithAltScreen())
|
||||
finalModel, err := p.Run()
|
||||
if err != nil {
|
||||
fmt.Printf("Error running menu: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return finalModel.(tuiModel).result
|
||||
}
|
||||
|
||||
func applyCustomResult(result tuiResult, preCheck utils.NetCheckResult, config *params.Config) {
|
||||
for _, t := range result.toggles {
|
||||
enabled := t.enabled
|
||||
if t.needNet && !preCheck.Connected {
|
||||
enabled = false
|
||||
}
|
||||
switch t.key {
|
||||
case "basic":
|
||||
config.BasicStatus = enabled
|
||||
case "cpu":
|
||||
config.CpuTestStatus = enabled
|
||||
case "memory":
|
||||
config.MemoryTestStatus = enabled
|
||||
case "disk":
|
||||
config.DiskTestStatus = enabled
|
||||
case "ut":
|
||||
config.UtTestStatus = enabled
|
||||
case "security":
|
||||
config.SecurityTestStatus = enabled
|
||||
case "email":
|
||||
config.EmailTestStatus = enabled
|
||||
case "backtrace":
|
||||
config.BacktraceStatus = enabled
|
||||
case "nt3":
|
||||
config.Nt3Status = enabled
|
||||
case "speed":
|
||||
config.SpeedTestStatus = enabled
|
||||
case "ping":
|
||||
config.PingTestStatus = enabled
|
||||
case "tgdc":
|
||||
config.TgdcTestStatus = enabled
|
||||
case "web":
|
||||
config.WebTestStatus = enabled
|
||||
}
|
||||
}
|
||||
|
||||
for _, a := range result.advanced {
|
||||
switch a.key {
|
||||
case "cpum":
|
||||
config.CpuTestMethod = a.options[a.current].value
|
||||
case "cput":
|
||||
config.CpuTestThreadMode = a.options[a.current].value
|
||||
case "memorym":
|
||||
config.MemoryTestMethod = a.options[a.current].value
|
||||
case "diskm":
|
||||
config.DiskTestMethod = a.options[a.current].value
|
||||
case "diskp":
|
||||
config.DiskTestPath = strings.TrimSpace(a.textVal)
|
||||
case "diskmc":
|
||||
config.DiskMultiCheck = a.boolVal
|
||||
case "nt3loc":
|
||||
config.Nt3Location = a.options[a.current].value
|
||||
case "nt3t":
|
||||
config.Nt3CheckType = a.options[a.current].value
|
||||
case "spnum":
|
||||
if v, err := strconv.Atoi(a.options[a.current].value); err == nil {
|
||||
config.SpNum = v
|
||||
}
|
||||
case "log":
|
||||
config.EnableLogger = a.boolVal
|
||||
case "upload":
|
||||
config.EnableUpload = a.boolVal
|
||||
case "analysis":
|
||||
config.AnalyzeResult = a.boolVal
|
||||
case "filepath":
|
||||
if strings.TrimSpace(a.textVal) != "" {
|
||||
config.FilePath = strings.TrimSpace(a.textVal)
|
||||
}
|
||||
case "width":
|
||||
if v, err := strconv.Atoi(a.options[a.current].value); err == nil {
|
||||
config.Width = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !config.BasicStatus && !config.CpuTestStatus && !config.MemoryTestStatus && !config.DiskTestStatus {
|
||||
config.OnlyIpInfoCheck = true
|
||||
}
|
||||
config.AutoChangeDiskMethod = true
|
||||
}
|
||||
485
internal/params/params.go
Normal file
485
internal/params/params.go
Normal file
@@ -0,0 +1,485 @@
|
||||
package params
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Config holds all configuration parameters
|
||||
type Config struct {
|
||||
EcsVersion string
|
||||
MenuMode bool
|
||||
OnlyChinaTest bool
|
||||
Input string
|
||||
Choice string
|
||||
ShowVersion bool
|
||||
EnableLogger bool
|
||||
Language string
|
||||
CpuTestMethod string
|
||||
CpuTestThreadMode string
|
||||
MemoryTestMethod string
|
||||
DiskTestMethod string
|
||||
DiskTestPath string
|
||||
DiskMultiCheck bool
|
||||
Nt3CheckType string
|
||||
Nt3Location string
|
||||
SpNum int
|
||||
Width int
|
||||
BasicStatus bool
|
||||
CpuTestStatus bool
|
||||
MemoryTestStatus bool
|
||||
DiskTestStatus bool
|
||||
UtTestStatus bool
|
||||
SecurityTestStatus bool
|
||||
EmailTestStatus bool
|
||||
BacktraceStatus bool
|
||||
Nt3Status bool
|
||||
SpeedTestStatus bool
|
||||
PingTestStatus bool
|
||||
TgdcTestStatus bool
|
||||
WebTestStatus bool
|
||||
AutoChangeDiskMethod bool
|
||||
FilePath string
|
||||
EnableUpload bool
|
||||
AnalyzeResult bool
|
||||
OnlyIpInfoCheck bool
|
||||
Help bool
|
||||
Finish bool
|
||||
UserSetFlags map[string]bool
|
||||
GoecsFlag *flag.FlagSet
|
||||
}
|
||||
|
||||
// NewConfig creates a new Config with default values
|
||||
func NewConfig(version string) *Config {
|
||||
return &Config{
|
||||
EcsVersion: version,
|
||||
MenuMode: true,
|
||||
OnlyChinaTest: false,
|
||||
Input: "",
|
||||
Choice: "",
|
||||
ShowVersion: false,
|
||||
EnableLogger: false,
|
||||
Language: "zh",
|
||||
CpuTestMethod: "sysbench",
|
||||
CpuTestThreadMode: "multi",
|
||||
MemoryTestMethod: "stream",
|
||||
DiskTestMethod: "fio",
|
||||
DiskTestPath: "",
|
||||
DiskMultiCheck: false,
|
||||
Nt3CheckType: "ipv4",
|
||||
SpNum: 2,
|
||||
Width: 82,
|
||||
BasicStatus: true,
|
||||
CpuTestStatus: true,
|
||||
MemoryTestStatus: true,
|
||||
DiskTestStatus: true,
|
||||
UtTestStatus: true,
|
||||
SecurityTestStatus: true,
|
||||
EmailTestStatus: true,
|
||||
BacktraceStatus: true,
|
||||
Nt3Status: true,
|
||||
SpeedTestStatus: true,
|
||||
PingTestStatus: false,
|
||||
TgdcTestStatus: false,
|
||||
WebTestStatus: false,
|
||||
AutoChangeDiskMethod: true,
|
||||
FilePath: "goecs.txt",
|
||||
EnableUpload: true,
|
||||
AnalyzeResult: false,
|
||||
OnlyIpInfoCheck: false,
|
||||
Help: false,
|
||||
Finish: false,
|
||||
UserSetFlags: make(map[string]bool),
|
||||
GoecsFlag: flag.NewFlagSet("goecs", flag.ContinueOnError),
|
||||
}
|
||||
}
|
||||
|
||||
// normalizeBoolArgs preprocesses args so that bool flags written as
|
||||
// "-flag true" or "-flag false" (space-separated) are converted to
|
||||
// "-flag=true" / "-flag=false" that the standard flag package understands.
|
||||
// This also strips any duplicate spaces that may appear between tokens when
|
||||
// args have been assembled by shell scripts or other callers.
|
||||
func normalizeBoolArgs(args []string) []string {
|
||||
// All known boolean flag names (without leading dash).
|
||||
boolFlags := map[string]bool{
|
||||
"h": true, "help": true, "v": true, "version": true,
|
||||
"menu": true, "basic": true, "cpu": true, "memory": true,
|
||||
"disk": true, "ut": true, "security": true, "email": true,
|
||||
"backtrace": true, "nt3": true, "speed": true, "ping": true,
|
||||
"tgdc": true, "web": true, "log": true, "upload": true,
|
||||
"analysis": true, "analyze": true,
|
||||
"diskmc": true,
|
||||
}
|
||||
|
||||
out := make([]string, 0, len(args))
|
||||
i := 0
|
||||
for i < len(args) {
|
||||
arg := args[i]
|
||||
// Skip empty tokens that can appear from split on multiple spaces.
|
||||
if arg == "" {
|
||||
i++
|
||||
continue
|
||||
}
|
||||
|
||||
// Detect flag tokens: -flag or --flag (without embedded =).
|
||||
if strings.HasPrefix(arg, "-") && !strings.Contains(arg, "=") {
|
||||
name := strings.TrimLeft(arg, "-")
|
||||
if boolFlags[name] {
|
||||
// Peek at next token: if it is "true" or "false", merge.
|
||||
if i+1 < len(args) {
|
||||
next := strings.ToLower(strings.TrimSpace(args[i+1]))
|
||||
if next == "true" || next == "false" {
|
||||
out = append(out, arg+"="+next)
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
out = append(out, arg)
|
||||
i++
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ParseFlags parses command line flags
|
||||
func (c *Config) ParseFlags(args []string) {
|
||||
args = normalizeBoolArgs(args)
|
||||
c.GoecsFlag.BoolVar(&c.Help, "h", false, "Show help information")
|
||||
c.GoecsFlag.BoolVar(&c.Help, "help", false, "Show help information")
|
||||
c.GoecsFlag.BoolVar(&c.ShowVersion, "v", false, "Display version information")
|
||||
c.GoecsFlag.BoolVar(&c.ShowVersion, "version", false, "Display version information")
|
||||
c.GoecsFlag.BoolVar(&c.MenuMode, "menu", true, "Enable/Disable menu mode, disable example: -menu=false")
|
||||
c.GoecsFlag.StringVar(&c.Language, "lang", "zh", "Set language (supported: en, zh)")
|
||||
c.GoecsFlag.StringVar(&c.Language, "l", "zh", "Set language (supported: en, zh)")
|
||||
c.GoecsFlag.BoolVar(&c.BasicStatus, "basic", true, "Enable/Disable basic test")
|
||||
c.GoecsFlag.BoolVar(&c.CpuTestStatus, "cpu", true, "Enable/Disable CPU test")
|
||||
c.GoecsFlag.BoolVar(&c.MemoryTestStatus, "memory", true, "Enable/Disable memory test")
|
||||
c.GoecsFlag.BoolVar(&c.DiskTestStatus, "disk", true, "Enable/Disable disk test")
|
||||
c.GoecsFlag.BoolVar(&c.UtTestStatus, "ut", true, "Enable/Disable unlock media test")
|
||||
c.GoecsFlag.BoolVar(&c.SecurityTestStatus, "security", true, "Enable/Disable security test")
|
||||
c.GoecsFlag.BoolVar(&c.EmailTestStatus, "email", true, "Enable/Disable email port test")
|
||||
c.GoecsFlag.BoolVar(&c.BacktraceStatus, "backtrace", true, "Enable/Disable backtrace test (in 'en' language or on windows it always false)")
|
||||
c.GoecsFlag.BoolVar(&c.Nt3Status, "nt3", true, "Enable/Disable NT3 test (in 'en' language or on windows it always false)")
|
||||
c.GoecsFlag.BoolVar(&c.SpeedTestStatus, "speed", true, "Enable/Disable speed test")
|
||||
c.GoecsFlag.BoolVar(&c.PingTestStatus, "ping", false, "Enable/Disable ping test")
|
||||
c.GoecsFlag.BoolVar(&c.TgdcTestStatus, "tgdc", false, "Enable/Disable Telegram DC test")
|
||||
c.GoecsFlag.BoolVar(&c.WebTestStatus, "web", false, "Enable/Disable popular websites test")
|
||||
c.GoecsFlag.StringVar(&c.CpuTestMethod, "cpum", "sysbench", "Set CPU test method (supported: sysbench, geekbench, winsat)")
|
||||
c.GoecsFlag.StringVar(&c.CpuTestMethod, "cpu-method", "sysbench", "Set CPU test method (supported: sysbench, geekbench, winsat)")
|
||||
c.GoecsFlag.StringVar(&c.CpuTestThreadMode, "cput", "multi", "Set CPU test thread mode (supported: single, multi)")
|
||||
c.GoecsFlag.StringVar(&c.CpuTestThreadMode, "cpu-thread", "multi", "Set CPU test thread mode (supported: single, multi)")
|
||||
c.GoecsFlag.StringVar(&c.MemoryTestMethod, "memorym", "stream", "Set memory test method (supported: stream, sysbench, dd, winsat, auto)")
|
||||
c.GoecsFlag.StringVar(&c.MemoryTestMethod, "memory-method", "stream", "Set memory test method (supported: stream, sysbench, dd, winsat, auto)")
|
||||
c.GoecsFlag.StringVar(&c.DiskTestMethod, "diskm", "fio", "Set disk test method (supported: fio, dd, winsat)")
|
||||
c.GoecsFlag.StringVar(&c.DiskTestMethod, "disk-method", "fio", "Set disk test method (supported: fio, dd, winsat)")
|
||||
c.GoecsFlag.StringVar(&c.DiskTestPath, "diskp", "", "Set disk test path, e.g., -diskp /root")
|
||||
c.GoecsFlag.BoolVar(&c.DiskMultiCheck, "diskmc", false, "Enable/Disable multiple disk checks, e.g., -diskmc=false")
|
||||
c.GoecsFlag.StringVar(&c.Nt3Location, "nt3loc", "GZ", "Specify NT3 test location (supported: GZ, SH, BJ, CD, ALL for Guangzhou, Shanghai, Beijing, Chengdu and all)")
|
||||
c.GoecsFlag.StringVar(&c.Nt3Location, "nt3-location", "GZ", "Specify NT3 test location (supported: GZ, SH, BJ, CD, ALL for Guangzhou, Shanghai, Beijing, Chengdu and all)")
|
||||
c.GoecsFlag.StringVar(&c.Nt3CheckType, "nt3t", "ipv4", "Set NT3 test type (supported: both, ipv4, ipv6)")
|
||||
c.GoecsFlag.StringVar(&c.Nt3CheckType, "nt3-type", "ipv4", "Set NT3 test type (supported: both, ipv4, ipv6)")
|
||||
c.GoecsFlag.IntVar(&c.SpNum, "spnum", 2, "Set the number of servers per operator for speed test")
|
||||
c.GoecsFlag.BoolVar(&c.EnableLogger, "log", false, "Enable/Disable logging in the current path")
|
||||
c.GoecsFlag.BoolVar(&c.EnableUpload, "upload", true, "Enable/Disable upload the result")
|
||||
c.GoecsFlag.BoolVar(&c.AnalyzeResult, "analysis", false, "Enable/Disable post-test concise summary analysis")
|
||||
c.GoecsFlag.BoolVar(&c.AnalyzeResult, "analyze", false, "Enable/Disable post-test concise summary analysis")
|
||||
c.GoecsFlag.Parse(args)
|
||||
|
||||
c.GoecsFlag.Visit(func(f *flag.Flag) {
|
||||
c.UserSetFlags[f.Name] = true
|
||||
})
|
||||
}
|
||||
|
||||
// HandleHelpAndVersion handles help and version flags
|
||||
func (c *Config) HandleHelpAndVersion(programName string) bool {
|
||||
if c.Help {
|
||||
fmt.Printf("Usage: %s [options]\n", programName)
|
||||
c.GoecsFlag.PrintDefaults()
|
||||
return true
|
||||
}
|
||||
if c.ShowVersion {
|
||||
fmt.Println(c.EcsVersion)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// SaveUserSetParams saves user-set parameters
|
||||
func (c *Config) SaveUserSetParams() map[string]interface{} {
|
||||
saved := make(map[string]interface{})
|
||||
|
||||
if c.UserSetFlags["basic"] {
|
||||
saved["basic"] = c.BasicStatus
|
||||
}
|
||||
if c.UserSetFlags["cpu"] {
|
||||
saved["cpu"] = c.CpuTestStatus
|
||||
}
|
||||
if c.UserSetFlags["memory"] {
|
||||
saved["memory"] = c.MemoryTestStatus
|
||||
}
|
||||
if c.UserSetFlags["disk"] {
|
||||
saved["disk"] = c.DiskTestStatus
|
||||
}
|
||||
if c.UserSetFlags["ut"] {
|
||||
saved["ut"] = c.UtTestStatus
|
||||
}
|
||||
if c.UserSetFlags["security"] {
|
||||
saved["security"] = c.SecurityTestStatus
|
||||
}
|
||||
if c.UserSetFlags["email"] {
|
||||
saved["email"] = c.EmailTestStatus
|
||||
}
|
||||
if c.UserSetFlags["backtrace"] {
|
||||
saved["backtrace"] = c.BacktraceStatus
|
||||
}
|
||||
if c.UserSetFlags["nt3"] {
|
||||
saved["nt3"] = c.Nt3Status
|
||||
}
|
||||
if c.UserSetFlags["speed"] {
|
||||
saved["speed"] = c.SpeedTestStatus
|
||||
}
|
||||
if c.UserSetFlags["ping"] {
|
||||
saved["ping"] = c.PingTestStatus
|
||||
}
|
||||
if c.UserSetFlags["tgdc"] {
|
||||
saved["tgdc"] = c.TgdcTestStatus
|
||||
}
|
||||
if c.UserSetFlags["web"] {
|
||||
saved["web"] = c.WebTestStatus
|
||||
}
|
||||
if c.UserSetFlags["cpum"] || c.UserSetFlags["cpu-method"] {
|
||||
saved["cpum"] = c.CpuTestMethod
|
||||
}
|
||||
if c.UserSetFlags["cput"] || c.UserSetFlags["cpu-thread"] {
|
||||
saved["cput"] = c.CpuTestThreadMode
|
||||
}
|
||||
if c.UserSetFlags["memorym"] || c.UserSetFlags["memory-method"] {
|
||||
saved["memorym"] = c.MemoryTestMethod
|
||||
}
|
||||
if c.UserSetFlags["diskm"] || c.UserSetFlags["disk-method"] {
|
||||
saved["diskm"] = c.DiskTestMethod
|
||||
}
|
||||
if c.UserSetFlags["diskp"] {
|
||||
saved["diskp"] = c.DiskTestPath
|
||||
}
|
||||
if c.UserSetFlags["diskmc"] {
|
||||
saved["diskmc"] = c.DiskMultiCheck
|
||||
}
|
||||
if c.UserSetFlags["nt3loc"] || c.UserSetFlags["nt3-location"] {
|
||||
saved["nt3loc"] = c.Nt3Location
|
||||
}
|
||||
if c.UserSetFlags["nt3t"] || c.UserSetFlags["nt3-type"] {
|
||||
saved["nt3t"] = c.Nt3CheckType
|
||||
}
|
||||
if c.UserSetFlags["spnum"] {
|
||||
saved["spnum"] = c.SpNum
|
||||
}
|
||||
if c.UserSetFlags["analysis"] || c.UserSetFlags["analyze"] {
|
||||
saved["analysis"] = c.AnalyzeResult
|
||||
}
|
||||
|
||||
return saved
|
||||
}
|
||||
|
||||
// RestoreUserSetParams restores user-set parameters
|
||||
func (c *Config) RestoreUserSetParams(saved map[string]interface{}) {
|
||||
if val, ok := saved["basic"]; ok {
|
||||
if boolVal, ok := val.(bool); ok {
|
||||
c.BasicStatus = boolVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["cpu"]; ok {
|
||||
if boolVal, ok := val.(bool); ok {
|
||||
c.CpuTestStatus = boolVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["memory"]; ok {
|
||||
if boolVal, ok := val.(bool); ok {
|
||||
c.MemoryTestStatus = boolVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["disk"]; ok {
|
||||
if boolVal, ok := val.(bool); ok {
|
||||
c.DiskTestStatus = boolVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["ut"]; ok {
|
||||
if boolVal, ok := val.(bool); ok {
|
||||
c.UtTestStatus = boolVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["security"]; ok {
|
||||
if boolVal, ok := val.(bool); ok {
|
||||
c.SecurityTestStatus = boolVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["email"]; ok {
|
||||
if boolVal, ok := val.(bool); ok {
|
||||
c.EmailTestStatus = boolVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["backtrace"]; ok {
|
||||
if boolVal, ok := val.(bool); ok {
|
||||
c.BacktraceStatus = boolVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["nt3"]; ok {
|
||||
if boolVal, ok := val.(bool); ok {
|
||||
c.Nt3Status = boolVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["speed"]; ok {
|
||||
if boolVal, ok := val.(bool); ok {
|
||||
c.SpeedTestStatus = boolVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["ping"]; ok {
|
||||
if boolVal, ok := val.(bool); ok {
|
||||
c.PingTestStatus = boolVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["tgdc"]; ok {
|
||||
if boolVal, ok := val.(bool); ok {
|
||||
c.TgdcTestStatus = boolVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["web"]; ok {
|
||||
if boolVal, ok := val.(bool); ok {
|
||||
c.WebTestStatus = boolVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["cpum"]; ok {
|
||||
if strVal, ok := val.(string); ok {
|
||||
c.CpuTestMethod = strVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["cput"]; ok {
|
||||
if strVal, ok := val.(string); ok {
|
||||
c.CpuTestThreadMode = strVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["memorym"]; ok {
|
||||
if strVal, ok := val.(string); ok {
|
||||
c.MemoryTestMethod = strVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["diskm"]; ok {
|
||||
if strVal, ok := val.(string); ok {
|
||||
c.DiskTestMethod = strVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["diskp"]; ok {
|
||||
if strVal, ok := val.(string); ok {
|
||||
c.DiskTestPath = strVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["diskmc"]; ok {
|
||||
if boolVal, ok := val.(bool); ok {
|
||||
c.DiskMultiCheck = boolVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["nt3loc"]; ok {
|
||||
if c.Choice != "10" {
|
||||
if strVal, ok := val.(string); ok {
|
||||
c.Nt3Location = strVal
|
||||
}
|
||||
}
|
||||
}
|
||||
if val, ok := saved["nt3t"]; ok {
|
||||
if strVal, ok := val.(string); ok {
|
||||
c.Nt3CheckType = strVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["spnum"]; ok {
|
||||
if intVal, ok := val.(int); ok {
|
||||
c.SpNum = intVal
|
||||
}
|
||||
}
|
||||
if val, ok := saved["analysis"]; ok {
|
||||
if boolVal, ok := val.(bool); ok {
|
||||
c.AnalyzeResult = boolVal
|
||||
}
|
||||
}
|
||||
|
||||
c.ValidateParams()
|
||||
}
|
||||
|
||||
// ValidateParams validates parameter values
|
||||
func (c *Config) ValidateParams() {
|
||||
validCpuMethods := map[string]bool{"sysbench": true, "geekbench": true, "winsat": true}
|
||||
if !validCpuMethods[c.CpuTestMethod] {
|
||||
if c.Language == "zh" {
|
||||
fmt.Printf("警告: CPU测试方法 '%s' 无效,使用默认值 'sysbench'\n", c.CpuTestMethod)
|
||||
} else {
|
||||
fmt.Printf("Warning: Invalid CPU test method '%s', using default 'sysbench'\n", c.CpuTestMethod)
|
||||
}
|
||||
c.CpuTestMethod = "sysbench"
|
||||
}
|
||||
|
||||
validThreadModes := map[string]bool{"single": true, "multi": true}
|
||||
if !validThreadModes[c.CpuTestThreadMode] {
|
||||
if c.Language == "zh" {
|
||||
fmt.Printf("警告: CPU线程模式 '%s' 无效,使用默认值 'multi'\n", c.CpuTestThreadMode)
|
||||
} else {
|
||||
fmt.Printf("Warning: Invalid CPU thread mode '%s', using default 'multi'\n", c.CpuTestThreadMode)
|
||||
}
|
||||
c.CpuTestThreadMode = "multi"
|
||||
}
|
||||
|
||||
validMemoryMethods := map[string]bool{"stream": true, "sysbench": true, "dd": true, "winsat": true, "auto": true}
|
||||
if !validMemoryMethods[c.MemoryTestMethod] {
|
||||
if c.Language == "zh" {
|
||||
fmt.Printf("警告: 内存测试方法 '%s' 无效,使用默认值 'stream'\n", c.MemoryTestMethod)
|
||||
} else {
|
||||
fmt.Printf("Warning: Invalid memory test method '%s', using default 'stream'\n", c.MemoryTestMethod)
|
||||
}
|
||||
c.MemoryTestMethod = "stream"
|
||||
}
|
||||
|
||||
validDiskMethods := map[string]bool{"fio": true, "dd": true, "winsat": true}
|
||||
if !validDiskMethods[c.DiskTestMethod] {
|
||||
if c.Language == "zh" {
|
||||
fmt.Printf("警告: 磁盘测试方法 '%s' 无效,使用默认值 'fio'\n", c.DiskTestMethod)
|
||||
} else {
|
||||
fmt.Printf("Warning: Invalid disk test method '%s', using default 'fio'\n", c.DiskTestMethod)
|
||||
}
|
||||
c.DiskTestMethod = "fio"
|
||||
}
|
||||
|
||||
validNt3Locations := map[string]bool{"GZ": true, "SH": true, "BJ": true, "CD": true, "ALL": true}
|
||||
if !validNt3Locations[c.Nt3Location] {
|
||||
if c.Language == "zh" {
|
||||
fmt.Printf("警告: NT3测试位置 '%s' 无效,使用默认值 'GZ'\n", c.Nt3Location)
|
||||
} else {
|
||||
fmt.Printf("Warning: Invalid NT3 location '%s', using default 'GZ'\n", c.Nt3Location)
|
||||
}
|
||||
c.Nt3Location = "GZ"
|
||||
}
|
||||
|
||||
validNt3Types := map[string]bool{"both": true, "ipv4": true, "ipv6": true}
|
||||
if !validNt3Types[c.Nt3CheckType] {
|
||||
if c.Language == "zh" {
|
||||
fmt.Printf("警告: NT3测试类型 '%s' 无效,使用默认值 'ipv4'\n", c.Nt3CheckType)
|
||||
} else {
|
||||
fmt.Printf("Warning: Invalid NT3 check type '%s', using default 'ipv4'\n", c.Nt3CheckType)
|
||||
}
|
||||
c.Nt3CheckType = "ipv4"
|
||||
}
|
||||
|
||||
if c.SpNum < 0 {
|
||||
if c.Language == "zh" {
|
||||
fmt.Printf("警告: 测速节点数量 '%d' 无效,使用默认值 2\n", c.SpNum)
|
||||
} else {
|
||||
fmt.Printf("Warning: Invalid speed test node count '%d', using default 2\n", c.SpNum)
|
||||
}
|
||||
c.SpNum = 2
|
||||
}
|
||||
|
||||
validLanguages := map[string]bool{"zh": true, "en": true}
|
||||
if !validLanguages[c.Language] {
|
||||
fmt.Printf("Warning: Invalid language '%s', using default 'zh'\n", c.Language)
|
||||
c.Language = "zh"
|
||||
}
|
||||
}
|
||||
555
internal/runner/runner.go
Normal file
555
internal/runner/runner.go
Normal file
@@ -0,0 +1,555 @@
|
||||
package runner
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/oneclickvirt/ecs/internal/analysis"
|
||||
"github.com/oneclickvirt/ecs/internal/params"
|
||||
"github.com/oneclickvirt/ecs/internal/tests"
|
||||
"github.com/oneclickvirt/ecs/utils"
|
||||
"github.com/oneclickvirt/pingtest/pt"
|
||||
"github.com/oneclickvirt/portchecker/email"
|
||||
)
|
||||
|
||||
// RunChineseTests runs all tests in Chinese mode
|
||||
func RunChineseTests(preCheck utils.NetCheckResult, config *params.Config, wg1, wg2, wg3 *sync.WaitGroup, basicInfo, securityInfo, emailInfo, mediaInfo, ptInfo *string, output *string, tempOutput string, startTime time.Time, outputMutex *sync.Mutex, infoMutex *sync.Mutex) {
|
||||
*output = RunBasicTests(preCheck, config, basicInfo, securityInfo, *output, tempOutput, outputMutex)
|
||||
*output = RunCPUTest(config, *output, tempOutput, outputMutex)
|
||||
*output = RunMemoryTest(config, *output, tempOutput, outputMutex)
|
||||
*output = RunDiskTest(config, *output, tempOutput, outputMutex)
|
||||
if config.OnlyIpInfoCheck && !config.BasicStatus && preCheck.Connected && preCheck.StackType != "" && preCheck.StackType != "None" {
|
||||
*output = RunIpInfoCheck(config, *output, tempOutput, outputMutex)
|
||||
}
|
||||
if config.UtTestStatus && preCheck.Connected && preCheck.StackType != "" && preCheck.StackType != "None" && !config.OnlyChinaTest {
|
||||
wg1.Add(1)
|
||||
go func() {
|
||||
defer wg1.Done()
|
||||
result := tests.MediaTest(config.Language)
|
||||
infoMutex.Lock()
|
||||
*mediaInfo = result
|
||||
infoMutex.Unlock()
|
||||
}()
|
||||
}
|
||||
if config.EmailTestStatus && preCheck.Connected && preCheck.StackType != "" && preCheck.StackType != "None" {
|
||||
wg2.Add(1)
|
||||
go func() {
|
||||
defer wg2.Done()
|
||||
result := email.EmailCheck()
|
||||
infoMutex.Lock()
|
||||
*emailInfo = result
|
||||
infoMutex.Unlock()
|
||||
}()
|
||||
}
|
||||
if (config.OnlyChinaTest || config.PingTestStatus) && preCheck.Connected && preCheck.StackType != "" && preCheck.StackType != "None" {
|
||||
wg3.Add(1)
|
||||
go func() {
|
||||
defer wg3.Done()
|
||||
result := pt.PingTest()
|
||||
infoMutex.Lock()
|
||||
*ptInfo = result
|
||||
infoMutex.Unlock()
|
||||
}()
|
||||
}
|
||||
if preCheck.Connected && preCheck.StackType != "" && preCheck.StackType != "None" {
|
||||
*output = RunStreamingTests(config, wg1, mediaInfo, *output, tempOutput, outputMutex, infoMutex)
|
||||
*output = RunSecurityTests(config, *securityInfo, *output, tempOutput, outputMutex)
|
||||
*output = RunEmailTests(config, wg2, emailInfo, *output, tempOutput, outputMutex, infoMutex)
|
||||
}
|
||||
if runtime.GOOS != "windows" && preCheck.Connected && preCheck.StackType != "" && preCheck.StackType != "None" {
|
||||
*output = RunNetworkTests(config, wg3, ptInfo, *output, tempOutput, outputMutex, infoMutex)
|
||||
}
|
||||
if preCheck.Connected && preCheck.StackType != "" && preCheck.StackType != "None" {
|
||||
*output = RunSpeedTests(config, *output, tempOutput, outputMutex)
|
||||
}
|
||||
*output = AppendTimeInfo(config, *output, tempOutput, startTime, outputMutex)
|
||||
}
|
||||
|
||||
// RunEnglishTests runs all tests in English mode
|
||||
func RunEnglishTests(preCheck utils.NetCheckResult, config *params.Config, wg1, wg2, wg3 *sync.WaitGroup, basicInfo, securityInfo, emailInfo, mediaInfo, ptInfo *string, output *string, tempOutput string, startTime time.Time, outputMutex *sync.Mutex, infoMutex *sync.Mutex) {
|
||||
*output = RunBasicTests(preCheck, config, basicInfo, securityInfo, *output, tempOutput, outputMutex)
|
||||
*output = RunCPUTest(config, *output, tempOutput, outputMutex)
|
||||
*output = RunMemoryTest(config, *output, tempOutput, outputMutex)
|
||||
*output = RunDiskTest(config, *output, tempOutput, outputMutex)
|
||||
if config.OnlyIpInfoCheck && !config.BasicStatus && preCheck.Connected && preCheck.StackType != "" && preCheck.StackType != "None" {
|
||||
*output = RunIpInfoCheck(config, *output, tempOutput, outputMutex)
|
||||
}
|
||||
if preCheck.Connected && preCheck.StackType != "" && preCheck.StackType != "None" {
|
||||
if config.UtTestStatus {
|
||||
wg1.Add(1)
|
||||
go func() {
|
||||
defer wg1.Done()
|
||||
result := tests.MediaTest(config.Language)
|
||||
infoMutex.Lock()
|
||||
*mediaInfo = result
|
||||
infoMutex.Unlock()
|
||||
}()
|
||||
}
|
||||
if config.EmailTestStatus {
|
||||
wg2.Add(1)
|
||||
go func() {
|
||||
defer wg2.Done()
|
||||
result := email.EmailCheck()
|
||||
infoMutex.Lock()
|
||||
*emailInfo = result
|
||||
infoMutex.Unlock()
|
||||
}()
|
||||
}
|
||||
*output = RunStreamingTests(config, wg1, mediaInfo, *output, tempOutput, outputMutex, infoMutex)
|
||||
*output = RunSecurityTests(config, *securityInfo, *output, tempOutput, outputMutex)
|
||||
*output = RunEmailTests(config, wg2, emailInfo, *output, tempOutput, outputMutex, infoMutex)
|
||||
*output = RunEnglishNetworkTests(config, wg3, ptInfo, *output, tempOutput, outputMutex)
|
||||
*output = RunEnglishSpeedTests(config, *output, tempOutput, outputMutex)
|
||||
}
|
||||
*output = AppendTimeInfo(config, *output, tempOutput, startTime, outputMutex)
|
||||
}
|
||||
|
||||
// RunIpInfoCheck performs IP info check
|
||||
func RunIpInfoCheck(config *params.Config, output, tempOutput string, outputMutex *sync.Mutex) string {
|
||||
outputMutex.Lock()
|
||||
defer outputMutex.Unlock()
|
||||
return utils.PrintAndCapture(func() {
|
||||
var ipinfo string
|
||||
tests.IPV4, tests.IPV6, ipinfo = utils.OnlyBasicsIpInfo(config.Language)
|
||||
if ipinfo != "" {
|
||||
if config.Language == "zh" {
|
||||
utils.PrintCenteredTitle("IP信息", config.Width)
|
||||
} else {
|
||||
utils.PrintCenteredTitle("IP-Information", config.Width)
|
||||
}
|
||||
fmt.Printf("%s", ipinfo)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
}
|
||||
|
||||
// RunBasicTests runs basic system tests
|
||||
func RunBasicTests(preCheck utils.NetCheckResult, config *params.Config, basicInfo, securityInfo *string, output, tempOutput string, outputMutex *sync.Mutex) string {
|
||||
outputMutex.Lock()
|
||||
defer outputMutex.Unlock()
|
||||
return utils.PrintAndCapture(func() {
|
||||
utils.PrintHead(config.Language, config.Width, config.EcsVersion)
|
||||
if config.BasicStatus || config.SecurityTestStatus {
|
||||
if config.BasicStatus {
|
||||
if config.Language == "zh" {
|
||||
utils.PrintCenteredTitle("系统基础信息", config.Width)
|
||||
} else {
|
||||
utils.PrintCenteredTitle("System-Basic-Information", config.Width)
|
||||
}
|
||||
}
|
||||
if preCheck.Connected && preCheck.StackType == "DualStack" {
|
||||
tests.IPV4, tests.IPV6, *basicInfo, *securityInfo, config.Nt3CheckType = utils.BasicsAndSecurityCheck(config.Language, config.Nt3CheckType, config.SecurityTestStatus)
|
||||
} else if preCheck.Connected && preCheck.StackType == "IPv4" {
|
||||
tests.IPV4, tests.IPV6, *basicInfo, *securityInfo, config.Nt3CheckType = utils.BasicsAndSecurityCheck(config.Language, "ipv4", config.SecurityTestStatus)
|
||||
} else if preCheck.Connected && preCheck.StackType == "IPv6" {
|
||||
tests.IPV4, tests.IPV6, *basicInfo, *securityInfo, config.Nt3CheckType = utils.BasicsAndSecurityCheck(config.Language, "ipv6", config.SecurityTestStatus)
|
||||
} else {
|
||||
tests.IPV4, tests.IPV6, *basicInfo, *securityInfo, config.Nt3CheckType = utils.BasicsAndSecurityCheck(config.Language, "", false)
|
||||
config.SecurityTestStatus = false
|
||||
}
|
||||
if config.BasicStatus {
|
||||
fmt.Printf("%s", *basicInfo)
|
||||
} else if (config.Choice == "6" || config.Choice == "9") && config.SecurityTestStatus {
|
||||
scanner := bufio.NewScanner(strings.NewReader(*basicInfo))
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if strings.Contains(line, "IPV") {
|
||||
fmt.Println(line)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, tempOutput, output)
|
||||
}
|
||||
|
||||
// RunCPUTest runs CPU test
|
||||
func RunCPUTest(config *params.Config, output, tempOutput string, outputMutex *sync.Mutex) string {
|
||||
outputMutex.Lock()
|
||||
defer outputMutex.Unlock()
|
||||
return utils.PrintAndCapture(func() {
|
||||
if config.CpuTestStatus {
|
||||
realTestMethod, res := tests.CpuTest(config.Language, config.CpuTestMethod, config.CpuTestThreadMode)
|
||||
if config.Language == "zh" {
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("CPU测试-通过%s测试", realTestMethod), config.Width)
|
||||
} else {
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("CPU-Test--%s-Method", realTestMethod), config.Width)
|
||||
}
|
||||
fmt.Print(res)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
}
|
||||
|
||||
// RunMemoryTest runs memory test
|
||||
func RunMemoryTest(config *params.Config, output, tempOutput string, outputMutex *sync.Mutex) string {
|
||||
outputMutex.Lock()
|
||||
defer outputMutex.Unlock()
|
||||
return utils.PrintAndCapture(func() {
|
||||
if config.MemoryTestStatus {
|
||||
realTestMethod, res := tests.MemoryTest(config.Language, config.MemoryTestMethod)
|
||||
if config.Language == "zh" {
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("内存测试-通过%s测试", realTestMethod), config.Width)
|
||||
} else {
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("Memory-Test--%s-Method", realTestMethod), config.Width)
|
||||
}
|
||||
fmt.Print(res)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
}
|
||||
|
||||
// RunDiskTest runs disk test
|
||||
func RunDiskTest(config *params.Config, output, tempOutput string, outputMutex *sync.Mutex) string {
|
||||
outputMutex.Lock()
|
||||
defer outputMutex.Unlock()
|
||||
return utils.PrintAndCapture(func() {
|
||||
if config.DiskTestStatus && config.AutoChangeDiskMethod {
|
||||
realTestMethod, res := tests.DiskTest(config.Language, config.DiskTestMethod, config.DiskTestPath, config.DiskMultiCheck, config.AutoChangeDiskMethod)
|
||||
if config.Language == "zh" {
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("硬盘测试-通过%s测试", realTestMethod), config.Width)
|
||||
} else {
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("Disk-Test--%s-Method", realTestMethod), config.Width)
|
||||
}
|
||||
fmt.Print(res)
|
||||
} else if config.DiskTestStatus && !config.AutoChangeDiskMethod {
|
||||
if config.Language == "zh" {
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("硬盘测试-通过%s测试", "dd"), config.Width)
|
||||
_, res := tests.DiskTest(config.Language, "dd", config.DiskTestPath, config.DiskMultiCheck, config.AutoChangeDiskMethod)
|
||||
fmt.Print(res)
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("硬盘测试-通过%s测试", "fio"), config.Width)
|
||||
_, res = tests.DiskTest(config.Language, "fio", config.DiskTestPath, config.DiskMultiCheck, config.AutoChangeDiskMethod)
|
||||
fmt.Print(res)
|
||||
} else {
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("Disk-Test--%s-Method", "dd"), config.Width)
|
||||
_, res := tests.DiskTest(config.Language, "dd", config.DiskTestPath, config.DiskMultiCheck, config.AutoChangeDiskMethod)
|
||||
fmt.Print(res)
|
||||
utils.PrintCenteredTitle(fmt.Sprintf("Disk-Test--%s-Method", "fio"), config.Width)
|
||||
_, res = tests.DiskTest(config.Language, "fio", config.DiskTestPath, config.DiskMultiCheck, config.AutoChangeDiskMethod)
|
||||
fmt.Print(res)
|
||||
}
|
||||
}
|
||||
}, tempOutput, output)
|
||||
}
|
||||
|
||||
// RunStreamingTests runs platform unlock tests
|
||||
func RunStreamingTests(config *params.Config, wg1 *sync.WaitGroup, mediaInfo *string, output, tempOutput string, outputMutex *sync.Mutex, infoMutex *sync.Mutex) string {
|
||||
outputMutex.Lock()
|
||||
defer outputMutex.Unlock()
|
||||
return utils.PrintAndCapture(func() {
|
||||
if config.UtTestStatus && (config.Language == "zh" && !config.OnlyChinaTest || config.Language == "en") {
|
||||
wg1.Wait()
|
||||
if config.Language == "zh" {
|
||||
utils.PrintCenteredTitle("跨国平台解锁", config.Width)
|
||||
} else {
|
||||
utils.PrintCenteredTitle("Cross-Border-Platform-Unlock", config.Width)
|
||||
}
|
||||
infoMutex.Lock()
|
||||
info := *mediaInfo
|
||||
infoMutex.Unlock()
|
||||
fmt.Printf("%s", info)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
}
|
||||
|
||||
// RunSecurityTests runs security tests
|
||||
func RunSecurityTests(config *params.Config, securityInfo, output, tempOutput string, outputMutex *sync.Mutex) string {
|
||||
outputMutex.Lock()
|
||||
defer outputMutex.Unlock()
|
||||
return utils.PrintAndCapture(func() {
|
||||
if config.SecurityTestStatus {
|
||||
if config.Language == "zh" {
|
||||
utils.PrintCenteredTitle("IP质量检测", config.Width)
|
||||
} else {
|
||||
utils.PrintCenteredTitle("IP-Quality-Check", config.Width)
|
||||
}
|
||||
fmt.Printf("%s", securityInfo)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
}
|
||||
|
||||
// RunEmailTests runs email port tests
|
||||
func RunEmailTests(config *params.Config, wg2 *sync.WaitGroup, emailInfo *string, output, tempOutput string, outputMutex *sync.Mutex, infoMutex *sync.Mutex) string {
|
||||
outputMutex.Lock()
|
||||
defer outputMutex.Unlock()
|
||||
return utils.PrintAndCapture(func() {
|
||||
if config.EmailTestStatus {
|
||||
wg2.Wait()
|
||||
if config.Language == "zh" {
|
||||
utils.PrintCenteredTitle("邮件端口检测", config.Width)
|
||||
} else {
|
||||
utils.PrintCenteredTitle("Email-Port-Check", config.Width)
|
||||
}
|
||||
infoMutex.Lock()
|
||||
info := *emailInfo
|
||||
infoMutex.Unlock()
|
||||
fmt.Println(info)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
}
|
||||
|
||||
// RunNetworkTests runs network tests (Chinese mode)
|
||||
func RunNetworkTests(config *params.Config, wg3 *sync.WaitGroup, ptInfo *string, output, tempOutput string, outputMutex *sync.Mutex, infoMutex *sync.Mutex) string {
|
||||
outputMutex.Lock()
|
||||
defer outputMutex.Unlock()
|
||||
return utils.PrintAndCapture(func() {
|
||||
if config.BacktraceStatus && !config.OnlyChinaTest {
|
||||
utils.PrintCenteredTitle("上游及回程线路检测", config.Width)
|
||||
tests.UpstreamsCheck(config.Language)
|
||||
}
|
||||
if config.Nt3Status && !config.OnlyChinaTest {
|
||||
utils.PrintCenteredTitle("三网回程路由检测", config.Width)
|
||||
tests.NextTrace3Check(config.Language, config.Nt3Location, config.Nt3CheckType)
|
||||
}
|
||||
infoMutex.Lock()
|
||||
info := *ptInfo
|
||||
infoMutex.Unlock()
|
||||
if config.OnlyChinaTest && info != "" {
|
||||
wg3.Wait()
|
||||
utils.PrintCenteredTitle("PING值检测", config.Width)
|
||||
fmt.Println(info)
|
||||
}
|
||||
if config.PingTestStatus && info != "" {
|
||||
wg3.Wait()
|
||||
utils.PrintCenteredTitle("PING值检测", config.Width)
|
||||
fmt.Println(info)
|
||||
if config.TgdcTestStatus {
|
||||
fmt.Println(pt.TelegramDCTest())
|
||||
}
|
||||
if config.WebTestStatus {
|
||||
fmt.Println(pt.WebsiteTest())
|
||||
}
|
||||
}
|
||||
if !config.OnlyChinaTest && !config.PingTestStatus && (config.TgdcTestStatus || config.WebTestStatus) {
|
||||
utils.PrintCenteredTitle("PING值检测", config.Width)
|
||||
if config.TgdcTestStatus {
|
||||
fmt.Println(pt.TelegramDCTest())
|
||||
}
|
||||
if config.WebTestStatus {
|
||||
fmt.Println(pt.WebsiteTest())
|
||||
}
|
||||
}
|
||||
// 等待第三方库的输出完全刷新到标准输出
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}, tempOutput, output)
|
||||
}
|
||||
|
||||
// RunSpeedTests runs speed tests (Chinese mode)
|
||||
func RunSpeedTests(config *params.Config, output, tempOutput string, outputMutex *sync.Mutex) string {
|
||||
outputMutex.Lock()
|
||||
defer outputMutex.Unlock()
|
||||
return utils.PrintAndCapture(func() {
|
||||
if config.SpeedTestStatus {
|
||||
utils.PrintCenteredTitle("就近节点测速", config.Width)
|
||||
tests.ShowHead(config.Language)
|
||||
if config.Choice == "1" || !config.MenuMode {
|
||||
tests.NearbySP()
|
||||
tests.CustomSP("net", "global", 2, config.Language)
|
||||
tests.CustomSP("net", "cu", config.SpNum, config.Language)
|
||||
tests.CustomSP("net", "ct", config.SpNum, config.Language)
|
||||
tests.CustomSP("net", "cmcc", config.SpNum, config.Language)
|
||||
} else if config.Choice == "2" || config.Choice == "3" || config.Choice == "4" || config.Choice == "5" {
|
||||
// 中文模式:就近测速 + 三网各1个 + Other 1个(带回退)
|
||||
if config.Language == "zh" {
|
||||
tests.NearbySP()
|
||||
tests.CustomSP("net", "other", 1, config.Language)
|
||||
tests.CustomSP("net", "cu", 1, config.Language)
|
||||
tests.CustomSP("net", "ct", 1, config.Language)
|
||||
tests.CustomSP("net", "cmcc", 1, config.Language)
|
||||
} else {
|
||||
// 英文模式:保持原有逻辑,测4个global节点
|
||||
tests.CustomSP("net", "global", 4, config.Language)
|
||||
}
|
||||
} else if config.Choice == "6" {
|
||||
tests.CustomSP("net", "global", 11, config.Language)
|
||||
} else {
|
||||
// Custom menu mode and any other fallback choices.
|
||||
tests.NearbySP()
|
||||
tests.CustomSP("net", "cu", config.SpNum, config.Language)
|
||||
tests.CustomSP("net", "ct", config.SpNum, config.Language)
|
||||
tests.CustomSP("net", "cmcc", config.SpNum, config.Language)
|
||||
}
|
||||
// 等待第三方库的输出完全刷新到标准输出
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
}
|
||||
|
||||
// RunEnglishNetworkTests runs network tests (English mode)
|
||||
func RunEnglishNetworkTests(config *params.Config, wg3 *sync.WaitGroup, ptInfo *string, output, tempOutput string, outputMutex *sync.Mutex) string {
|
||||
outputMutex.Lock()
|
||||
defer outputMutex.Unlock()
|
||||
return utils.PrintAndCapture(func() {
|
||||
if config.TgdcTestStatus || config.WebTestStatus {
|
||||
utils.PrintCenteredTitle("PING-Test", config.Width)
|
||||
if config.TgdcTestStatus {
|
||||
fmt.Println(pt.TelegramDCTest())
|
||||
}
|
||||
if config.WebTestStatus {
|
||||
fmt.Println(pt.WebsiteTest())
|
||||
}
|
||||
}
|
||||
// 等待第三方库的输出完全刷新到标准输出
|
||||
time.Sleep(300 * time.Millisecond)
|
||||
}, tempOutput, output)
|
||||
}
|
||||
|
||||
// RunEnglishSpeedTests runs speed tests (English mode)
|
||||
func RunEnglishSpeedTests(config *params.Config, output, tempOutput string, outputMutex *sync.Mutex) string {
|
||||
outputMutex.Lock()
|
||||
defer outputMutex.Unlock()
|
||||
return utils.PrintAndCapture(func() {
|
||||
if config.SpeedTestStatus {
|
||||
utils.PrintCenteredTitle("Speed-Test", config.Width)
|
||||
tests.ShowHead(config.Language)
|
||||
tests.NearbySP()
|
||||
tests.CustomSP("net", "global", -1, config.Language)
|
||||
// 等待第三方库的输出完全刷新到标准输出
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
}, tempOutput, output)
|
||||
}
|
||||
|
||||
// AppendTimeInfo appends timing information
|
||||
func AppendTimeInfo(config *params.Config, output, tempOutput string, startTime time.Time, outputMutex *sync.Mutex) string {
|
||||
outputMutex.Lock()
|
||||
defer outputMutex.Unlock()
|
||||
endTime := time.Now()
|
||||
duration := endTime.Sub(startTime)
|
||||
minutes := int(duration.Minutes())
|
||||
seconds := int(duration.Seconds()) % 60
|
||||
currentTime := time.Now().Format("Mon Jan 2 15:04:05 MST 2006")
|
||||
return utils.PrintAndCapture(func() {
|
||||
utils.PrintCenteredTitle("", config.Width)
|
||||
if config.Language == "zh" {
|
||||
fmt.Printf("花费 : %d 分 %d 秒\n", minutes, seconds)
|
||||
fmt.Printf("时间 : %s\n", currentTime)
|
||||
} else {
|
||||
fmt.Printf("Cost Time : %d min %d sec\n", minutes, seconds)
|
||||
fmt.Printf("Current Time : %s\n", currentTime)
|
||||
}
|
||||
utils.PrintCenteredTitle("", config.Width)
|
||||
}, tempOutput, output)
|
||||
}
|
||||
|
||||
// AppendAnalysisSummary appends a concise bilingual summary for easier interpretation.
|
||||
func AppendAnalysisSummary(config *params.Config, output, tempOutput string, outputMutex *sync.Mutex) string {
|
||||
outputMutex.Lock()
|
||||
defer outputMutex.Unlock()
|
||||
finalOutput := output
|
||||
return utils.PrintAndCapture(func() {
|
||||
summary := analysis.GenerateSummary(config, finalOutput)
|
||||
if strings.TrimSpace(summary) == "" {
|
||||
return
|
||||
}
|
||||
if config.Language == "zh" {
|
||||
utils.PrintCenteredTitle("测试总结分析", config.Width)
|
||||
} else {
|
||||
utils.PrintCenteredTitle("Result Summary Analysis", config.Width)
|
||||
}
|
||||
fmt.Println(summary)
|
||||
}, tempOutput, output)
|
||||
}
|
||||
|
||||
// HandleSignalInterrupt handles interrupt signals
|
||||
func HandleSignalInterrupt(sig chan os.Signal, config *params.Config, startTime *time.Time, output *string, tempOutput string, uploadDone chan bool, outputMutex *sync.Mutex) {
|
||||
select {
|
||||
case <-sig:
|
||||
if !config.Finish {
|
||||
endTime := time.Now()
|
||||
duration := endTime.Sub(*startTime)
|
||||
minutes := int(duration.Minutes())
|
||||
seconds := int(duration.Seconds()) % 60
|
||||
currentTime := time.Now().Format("Mon Jan 2 15:04:05 MST 2006")
|
||||
outputMutex.Lock()
|
||||
timeInfo := utils.PrintAndCapture(func() {
|
||||
utils.PrintCenteredTitle("", config.Width)
|
||||
if config.Language == "zh" {
|
||||
fmt.Printf("花费 : %d 分 %d 秒\n", minutes, seconds)
|
||||
fmt.Printf("时间 : %s\n", currentTime)
|
||||
} else {
|
||||
fmt.Printf("Cost Time : %d min %d sec\n", minutes, seconds)
|
||||
fmt.Printf("Current Time : %s\n", currentTime)
|
||||
}
|
||||
utils.PrintCenteredTitle("", config.Width)
|
||||
}, "", "")
|
||||
*output += timeInfo
|
||||
finalOutput := *output
|
||||
outputMutex.Unlock()
|
||||
resultChan := make(chan struct {
|
||||
httpURL string
|
||||
httpsURL string
|
||||
}, 1)
|
||||
if config.EnableUpload {
|
||||
// 使用context来控制上传goroutine
|
||||
uploadCtx, uploadCancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer uploadCancel()
|
||||
|
||||
go func() {
|
||||
httpURL, httpsURL := utils.ProcessAndUpload(finalOutput, config.FilePath, config.EnableUpload, config.Language)
|
||||
select {
|
||||
case resultChan <- struct {
|
||||
httpURL string
|
||||
httpsURL string
|
||||
}{httpURL, httpsURL}:
|
||||
case <-uploadCtx.Done():
|
||||
// 上传被取消或超时,直接返回
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
select {
|
||||
case result := <-resultChan:
|
||||
uploadCancel() // 成功完成,取消context
|
||||
if result.httpURL != "" || result.httpsURL != "" {
|
||||
if config.Language == "en" {
|
||||
fmt.Printf("Upload successfully!\nHttp URL: %s\nHttps URL: %s\n", result.httpURL, result.httpsURL)
|
||||
} else {
|
||||
fmt.Printf("上传成功!\nHttp URL: %s\nHttps URL: %s\n", result.httpURL, result.httpsURL)
|
||||
}
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
|
||||
fmt.Println("Press Enter to exit...")
|
||||
fmt.Scanln()
|
||||
}
|
||||
os.Exit(0)
|
||||
case <-uploadCtx.Done():
|
||||
if config.Language == "en" {
|
||||
fmt.Println("Upload timeout, program exit")
|
||||
} else {
|
||||
fmt.Println("上传超时,程序退出")
|
||||
}
|
||||
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
|
||||
fmt.Println("Press Enter to exit...")
|
||||
fmt.Scanln()
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
} else {
|
||||
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
|
||||
fmt.Println("Press Enter to exit...")
|
||||
fmt.Scanln()
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
// HandleUploadResults handles uploading results
|
||||
func HandleUploadResults(config *params.Config, output string) {
|
||||
httpURL, httpsURL := utils.ProcessAndUpload(output, config.FilePath, config.EnableUpload, config.Language)
|
||||
if httpURL != "" || httpsURL != "" {
|
||||
if config.Language == "en" {
|
||||
fmt.Printf("Upload successfully!\nHttp URL: %s\nHttps URL: %s\n", httpURL, httpsURL)
|
||||
fmt.Println("Each Test Benchmark: https://bash.spiritlhl.net/ecsguide")
|
||||
} else {
|
||||
fmt.Printf("上传成功!\nHttp URL: %s\nHttps URL: %s\n", httpURL, httpsURL)
|
||||
fmt.Println("每项测试基准见: https://bash.spiritlhl.net/ecsguide")
|
||||
}
|
||||
}
|
||||
}
|
||||
56
internal/tests/cpu.go
Normal file
56
internal/tests/cpu.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/oneclickvirt/cputest/cpu"
|
||||
)
|
||||
|
||||
func CpuTest(language, testMethod, testThread string) (realTestMethod, res string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "[WARN] CpuTest panic: %v\n", r)
|
||||
res = fmt.Sprintf("\nCPU test failed: %v\n", r)
|
||||
realTestMethod = "error"
|
||||
}
|
||||
}()
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
if testMethod != "winsat" && testMethod != "" {
|
||||
// res = "Detected host is Windows, using Winsat for testing.\n"
|
||||
realTestMethod = "winsat"
|
||||
}
|
||||
res += cpu.WinsatTest(language, testThread)
|
||||
} else {
|
||||
switch testMethod {
|
||||
case "sysbench":
|
||||
res = cpu.SysBenchTest(language, testThread)
|
||||
if res == "" {
|
||||
// res = "Sysbench test failed, switching to Geekbench for testing.\n"
|
||||
realTestMethod = "geekbench"
|
||||
res += cpu.GeekBenchTest(language, testThread)
|
||||
} else {
|
||||
realTestMethod = "sysbench"
|
||||
}
|
||||
case "geekbench":
|
||||
res = cpu.GeekBenchTest(language, testThread)
|
||||
if res == "" {
|
||||
// res = "Geekbench test failed, switching to Sysbench for testing.\n"
|
||||
realTestMethod = "sysbench"
|
||||
res += cpu.SysBenchTest(language, testThread)
|
||||
} else {
|
||||
realTestMethod = "geekbench"
|
||||
}
|
||||
default:
|
||||
res = "Invalid test method specified.\n"
|
||||
realTestMethod = "null"
|
||||
}
|
||||
}
|
||||
if !strings.Contains(res, "\n") && res != "" {
|
||||
res += "\n"
|
||||
}
|
||||
return
|
||||
}
|
||||
51
internal/tests/disk.go
Normal file
51
internal/tests/disk.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/oneclickvirt/disktest/disk"
|
||||
)
|
||||
|
||||
func DiskTest(language, testMethod, testPath string, isMultiCheck bool, autoChange bool) (realTestMethod, res string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "[WARN] DiskTest panic: %v\n", r)
|
||||
res = fmt.Sprintf("\nDisk test failed: %v\n", r)
|
||||
realTestMethod = "error"
|
||||
}
|
||||
}()
|
||||
|
||||
switch testMethod {
|
||||
case "fio":
|
||||
res = disk.FioTest(language, isMultiCheck, testPath)
|
||||
if res == "" && autoChange {
|
||||
res += disk.DDTest(language, isMultiCheck, testPath)
|
||||
realTestMethod = "dd"
|
||||
} else {
|
||||
realTestMethod = "fio"
|
||||
}
|
||||
case "dd":
|
||||
res = disk.DDTest(language, isMultiCheck, testPath)
|
||||
if res == "" && autoChange {
|
||||
res += disk.FioTest(language, isMultiCheck, testPath)
|
||||
realTestMethod = "fio"
|
||||
} else {
|
||||
realTestMethod = "dd"
|
||||
}
|
||||
default:
|
||||
if runtime.GOOS == "windows" {
|
||||
realTestMethod = "winsat"
|
||||
res = disk.WinsatTest(language, isMultiCheck, testPath)
|
||||
} else {
|
||||
res = disk.DDTest(language, isMultiCheck, testPath)
|
||||
realTestMethod = "dd"
|
||||
}
|
||||
}
|
||||
if !strings.Contains(res, "\n") && res != "" {
|
||||
res += "\n"
|
||||
}
|
||||
return
|
||||
}
|
||||
233
internal/tests/memory.go
Normal file
233
internal/tests/memory.go
Normal file
@@ -0,0 +1,233 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/oneclickvirt/memorytest/memory"
|
||||
)
|
||||
|
||||
func MemoryTest(language, testMethod string) (realTestMethod, res string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "[WARN] MemoryTest panic: %v\n", r)
|
||||
res = fmt.Sprintf("\nMemory test failed: %v\n", r)
|
||||
realTestMethod = "error"
|
||||
}
|
||||
}()
|
||||
|
||||
testMethod = strings.ToLower(testMethod)
|
||||
if testMethod == "" {
|
||||
testMethod = "auto"
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
switch testMethod {
|
||||
case "stream":
|
||||
res = memory.StreamTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.WinsatTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.WindowsDDTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
realTestMethod = ""
|
||||
} else {
|
||||
realTestMethod = "dd"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "winsat"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "stream"
|
||||
}
|
||||
case "dd":
|
||||
res = memory.WindowsDDTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.WinsatTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.StreamTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
realTestMethod = ""
|
||||
} else {
|
||||
realTestMethod = "stream"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "winsat"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "dd"
|
||||
}
|
||||
case "sysbench":
|
||||
// Windows下不支持sysbench,使用stream → winsat → dd
|
||||
res = memory.StreamTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.WinsatTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.WindowsDDTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
realTestMethod = ""
|
||||
} else {
|
||||
realTestMethod = "dd"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "winsat"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "stream"
|
||||
}
|
||||
case "auto":
|
||||
res = memory.StreamTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.WinsatTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.WindowsDDTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
realTestMethod = ""
|
||||
} else {
|
||||
realTestMethod = "dd"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "winsat"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "stream"
|
||||
}
|
||||
case "winsat":
|
||||
res = memory.WinsatTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.StreamTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.WindowsDDTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
realTestMethod = ""
|
||||
} else {
|
||||
realTestMethod = "dd"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "stream"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "winsat"
|
||||
}
|
||||
default:
|
||||
res = memory.StreamTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.WinsatTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.WindowsDDTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
realTestMethod = ""
|
||||
} else {
|
||||
realTestMethod = "dd"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "winsat"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "stream"
|
||||
}
|
||||
}
|
||||
} else {
|
||||
switch testMethod {
|
||||
case "stream":
|
||||
res = memory.StreamTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.SysBenchTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.DDTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
realTestMethod = ""
|
||||
} else {
|
||||
realTestMethod = "dd"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "sysbench"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "stream"
|
||||
}
|
||||
case "dd":
|
||||
res = memory.DDTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.StreamTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.SysBenchTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
realTestMethod = ""
|
||||
} else {
|
||||
realTestMethod = "sysbench"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "stream"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "dd"
|
||||
}
|
||||
case "sysbench":
|
||||
res = memory.SysBenchTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.StreamTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.SysBenchTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.DDTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
realTestMethod = ""
|
||||
} else {
|
||||
realTestMethod = "dd"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "sysbench"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "stream"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "sysbench"
|
||||
}
|
||||
case "auto":
|
||||
res = memory.StreamTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.SysBenchTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.DDTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
realTestMethod = ""
|
||||
} else {
|
||||
realTestMethod = "dd"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "sysbench"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "stream"
|
||||
}
|
||||
case "winsat":
|
||||
// winsat 仅 Windows 支持,非 Windows fallback 到 stream → sysbench → dd
|
||||
res = memory.StreamTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.SysBenchTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
res = memory.DDTest(language)
|
||||
if res == "" || strings.TrimSpace(res) == "" {
|
||||
realTestMethod = ""
|
||||
} else {
|
||||
realTestMethod = "dd"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "sysbench"
|
||||
}
|
||||
} else {
|
||||
realTestMethod = "stream"
|
||||
}
|
||||
default:
|
||||
res = "Unsupported test method"
|
||||
realTestMethod = ""
|
||||
}
|
||||
}
|
||||
if !strings.Contains(res, "\n") && res != "" {
|
||||
res += "\n"
|
||||
}
|
||||
return
|
||||
}
|
||||
98
internal/tests/nexttrace.go
Normal file
98
internal/tests/nexttrace.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/oneclickvirt/nt3/nt"
|
||||
)
|
||||
|
||||
func NextTrace3Check(language, nt3Location, nt3CheckType string) {
|
||||
// 先检查 ICMP 权限
|
||||
conn, err := net.ListenPacket("ip4:icmp", "0.0.0.0")
|
||||
if err != nil {
|
||||
// 没有权限,显示友好提示并跳过
|
||||
if language == "zh" {
|
||||
fmt.Println("路由追踪测试需要 root 权限或 CAP_NET_RAW 能力,已跳过")
|
||||
fmt.Fprintf(os.Stderr, "[WARN] ICMP权限不足: %v\n", err)
|
||||
} else {
|
||||
fmt.Println("Route tracing test requires root privileges or CAP_NET_RAW capability, skipped")
|
||||
fmt.Fprintf(os.Stderr, "[WARN] Insufficient ICMP permission: %v\n", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
conn.Close()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if language == "zh" {
|
||||
fmt.Println("路由追踪测试出现错误,已跳过")
|
||||
fmt.Fprintf(os.Stderr, "[WARN] 路由追踪panic: %v\n", r)
|
||||
} else {
|
||||
fmt.Println("Route tracing test failed, skipped")
|
||||
fmt.Fprintf(os.Stderr, "[WARN] Route tracing panic: %v\n", r)
|
||||
}
|
||||
}
|
||||
}()
|
||||
resultChan := make(chan nt.TraceResult, 100)
|
||||
errorOccurred := false
|
||||
go func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
errorOccurred = true
|
||||
resultChan <- nt.TraceResult{
|
||||
Index: -1,
|
||||
ISPName: "Error",
|
||||
Output: []string{fmt.Sprintf("Route tracing error: %v", r)},
|
||||
}
|
||||
close(resultChan)
|
||||
}
|
||||
}()
|
||||
nt.TraceRoute(language, nt3Location, nt3CheckType, resultChan)
|
||||
}()
|
||||
for result := range resultChan {
|
||||
if result.Index == -1 {
|
||||
for index, res := range result.Output {
|
||||
res = strings.TrimSpace(res)
|
||||
if res != "" && index == 0 {
|
||||
fmt.Println(res)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if result.ISPName == "Error" {
|
||||
if language == "zh" {
|
||||
fmt.Println("路由追踪测试失败(可能因为权限不足),已跳过")
|
||||
} else {
|
||||
fmt.Println("Route tracing test failed (possibly due to insufficient permissions), skipped")
|
||||
}
|
||||
for _, res := range result.Output {
|
||||
res = strings.TrimSpace(res)
|
||||
if res != "" {
|
||||
fmt.Fprintf(os.Stderr, "[WARN] %s\n", res)
|
||||
}
|
||||
}
|
||||
errorOccurred = true
|
||||
continue
|
||||
}
|
||||
for _, res := range result.Output {
|
||||
res = strings.TrimSpace(res)
|
||||
if res == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(res, "ICMP") {
|
||||
fmt.Print(res)
|
||||
} else {
|
||||
fmt.Println(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
if errorOccurred {
|
||||
if language == "zh" {
|
||||
fmt.Println("提示: 路由追踪需要 root 权限或 CAP_NET_RAW 能力")
|
||||
} else {
|
||||
fmt.Println("Hint: Route tracing requires root privileges or CAP_NET_RAW capability")
|
||||
}
|
||||
}
|
||||
}
|
||||
284
internal/tests/speed.go
Normal file
284
internal/tests/speed.go
Normal file
@@ -0,0 +1,284 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/oneclickvirt/privatespeedtest/pst"
|
||||
"github.com/oneclickvirt/speedtest/model"
|
||||
"github.com/oneclickvirt/speedtest/sp"
|
||||
)
|
||||
|
||||
func ShowHead(language string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "[WARN] ShowHead panic: %v\n", r)
|
||||
}
|
||||
}()
|
||||
sp.ShowHead(language)
|
||||
}
|
||||
|
||||
func NearbySP() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "[WARN] NearbySP panic: %v\n", r)
|
||||
}
|
||||
}()
|
||||
if runtime.GOOS == "windows" || sp.OfficialAvailableTest() != nil {
|
||||
sp.NearbySpeedTest()
|
||||
} else {
|
||||
sp.OfficialNearbySpeedTest()
|
||||
}
|
||||
}
|
||||
|
||||
// formatString 格式化字符串到指定宽度
|
||||
func formatString(s string, width int) string {
|
||||
return fmt.Sprintf("%-*s", width, s)
|
||||
}
|
||||
|
||||
// printTableRow 打印表格行
|
||||
func printTableRow(result pst.SpeedTestResult) {
|
||||
location := result.City
|
||||
if result.CarrierType != "" {
|
||||
carrier := result.CarrierType
|
||||
switch carrier {
|
||||
case "Telecom":
|
||||
carrier = "电信"
|
||||
case "Unicom":
|
||||
carrier = "联通"
|
||||
case "Mobile":
|
||||
carrier = "移动"
|
||||
case "Other":
|
||||
carrier = "其他"
|
||||
}
|
||||
location = fmt.Sprintf("%s%s", carrier, result.City)
|
||||
}
|
||||
if len(location) > 15 {
|
||||
location = location[:15]
|
||||
}
|
||||
upload := "N/A"
|
||||
if result.UploadMbps > 0 {
|
||||
upload = fmt.Sprintf("%.2f Mbps", result.UploadMbps)
|
||||
}
|
||||
download := "N/A"
|
||||
if result.DownloadMbps > 0 {
|
||||
download = fmt.Sprintf("%.2f Mbps", result.DownloadMbps)
|
||||
}
|
||||
latency := fmt.Sprintf("%.2f ms", result.PingLatency.Seconds()*1000)
|
||||
packetLoss := "N/A"
|
||||
fmt.Print(formatString(location, 15))
|
||||
fmt.Print(formatString(upload, 16))
|
||||
fmt.Print(formatString(download, 16))
|
||||
fmt.Print(formatString(latency, 16))
|
||||
fmt.Print(formatString(packetLoss, 16))
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
// privateSpeedTest 使用 privatespeedtest 进行单个运营商测速
|
||||
// operator 参数:只支持 "cmcc"、"cu"、"ct"、"other"
|
||||
// 返回值:实际测试的节点数量和错误信息
|
||||
func privateSpeedTest(num int, operator string) (int, error) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "[WARN] privateSpeedTest panic: %v\n", r)
|
||||
}
|
||||
}()
|
||||
*pst.NoProgress = true
|
||||
*pst.Quiet = true
|
||||
*pst.NoHeader = true
|
||||
*pst.NoProjectURL = true
|
||||
// 加载服务器列表
|
||||
serverList, err := pst.LoadServerList()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("加载自定义服务器列表失败")
|
||||
}
|
||||
// 使用三网测速模式(每个运营商选择指定数量的最低延迟节点)
|
||||
serversPerISP := num
|
||||
if serversPerISP <= 0 || serversPerISP > 5 {
|
||||
serversPerISP = 2
|
||||
}
|
||||
// 单个运营商测速:先过滤服务器列表
|
||||
var carrierType string
|
||||
switch strings.ToLower(operator) {
|
||||
case "cmcc":
|
||||
carrierType = "Mobile"
|
||||
case "cu":
|
||||
carrierType = "Unicom"
|
||||
case "ct":
|
||||
carrierType = "Telecom"
|
||||
case "other":
|
||||
carrierType = "Other"
|
||||
default:
|
||||
return 0, fmt.Errorf("不支持的运营商类型: %s", operator)
|
||||
}
|
||||
// 过滤出指定运营商的服务器
|
||||
filteredServers := pst.FilterServersByISP(serverList.Servers, carrierType)
|
||||
// 先找足够多的候选服务器用于去重(找 serversPerISP * 3 个,确保去重后还能剩下足够的服务器)
|
||||
candidateCount := serversPerISP * 3
|
||||
if candidateCount > len(filteredServers) {
|
||||
candidateCount = len(filteredServers)
|
||||
}
|
||||
// 使用 FindBestServers 选择最佳服务器
|
||||
candidateServers, err := pst.FindBestServers(
|
||||
filteredServers,
|
||||
candidateCount, // 选择更多候选节点用于去重
|
||||
5*time.Second, // ping 超时
|
||||
true, // 显示进度条
|
||||
true, // 静默
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("分组查找失败")
|
||||
}
|
||||
// 去重:确保同一运营商内城市不重复
|
||||
seenCities := make(map[string]bool)
|
||||
var bestServers []pst.ServerWithLatencyInfo
|
||||
// 保留更多备用节点,以应对测速失败的情况(保留 serversPerISP * 2 个备用节点)
|
||||
maxBackupServers := serversPerISP * 2
|
||||
for _, serverInfo := range candidateServers {
|
||||
city := serverInfo.Server.City
|
||||
if city == "" {
|
||||
city = "Unknown"
|
||||
}
|
||||
if !seenCities[city] {
|
||||
seenCities[city] = true
|
||||
bestServers = append(bestServers, serverInfo)
|
||||
// 去重后保留足够的备用节点
|
||||
if len(bestServers) >= maxBackupServers {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(bestServers) == 0 {
|
||||
return 0, fmt.Errorf("去重后没有可用的服务器")
|
||||
}
|
||||
// 执行测速并逐个打印结果(不打印表头)
|
||||
// 统计成功输出的节点数
|
||||
successCount := 0
|
||||
for i, serverInfo := range bestServers {
|
||||
// 如果已经成功输出了足够的节点,则停止测试
|
||||
if successCount >= serversPerISP {
|
||||
break
|
||||
}
|
||||
result := pst.RunSpeedTest(
|
||||
serverInfo.Server,
|
||||
false, // 不禁用下载测试
|
||||
false, // 不禁用上传测试
|
||||
6, // 并发线程数
|
||||
12*time.Second, // 超时时间
|
||||
&serverInfo,
|
||||
false, // 不显示进度条
|
||||
)
|
||||
// 只要测试成功且有任意一个速度值有效,就输出结果(部分成功也显示)
|
||||
if result.Success && (result.UploadMbps > 0 || result.DownloadMbps > 0) {
|
||||
printTableRow(result)
|
||||
// 只有上传和下载都成功时才计入成功数
|
||||
if result.UploadMbps > 0 && result.DownloadMbps > 0 {
|
||||
successCount++
|
||||
}
|
||||
}
|
||||
// 在测试之间暂停(如果还需要继续测试的话)
|
||||
if successCount < serversPerISP && i < len(bestServers)-1 {
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
}
|
||||
// 返回实际成功输出的节点数量
|
||||
return successCount, nil
|
||||
}
|
||||
|
||||
// privateSpeedTestWithFallback 使用私有测速,如果失败则回退到 global 节点
|
||||
// 主要用于 Other 类型的测速
|
||||
func privateSpeedTestWithFallback(num int, operator, language string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "[WARN] privateSpeedTestWithFallback panic: %v\n", r)
|
||||
}
|
||||
}()
|
||||
// 先尝试私有节点测速
|
||||
testedCount, err := privateSpeedTest(num, operator)
|
||||
if err != nil || testedCount == 0 {
|
||||
// 私有节点失败,回退到 global 节点
|
||||
var url, parseType string
|
||||
url = model.NetGlobal
|
||||
parseType = "id"
|
||||
if runtime.GOOS == "windows" || sp.OfficialAvailableTest() != nil {
|
||||
sp.CustomSpeedTest(url, parseType, num, language)
|
||||
} else {
|
||||
sp.OfficialCustomSpeedTest(url, parseType, num, language)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func CustomSP(platform, operator string, num int, language string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "[WARN] CustomSP panic: %v\n", r)
|
||||
}
|
||||
}()
|
||||
// 对于三网测速(cmcc、cu、ct)和 other,优先使用 privatespeedtest 进行私有测速
|
||||
opLower := strings.ToLower(operator)
|
||||
if opLower == "cmcc" || opLower == "cu" || opLower == "ct" || opLower == "other" {
|
||||
testedCount, err := privateSpeedTest(num, opLower)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "[WARN] privatespeedtest failed: %v\n", err)
|
||||
// 全部失败,继续使用原有的公共节点兜底方案
|
||||
} else if testedCount >= num {
|
||||
// 私有节点测速成功且数量达标,直接返回
|
||||
return
|
||||
} else if testedCount > 0 {
|
||||
// 部分私有节点测速成功,但数量不足,用公共节点补充
|
||||
fmt.Fprintf(os.Stderr, "[INFO] 私有节点仅测试了 %d 个,补充 %d 个公共节点\n", testedCount, num-testedCount)
|
||||
num = num - testedCount // 只测剩余数量的公共节点
|
||||
// 继续执行下面的公共节点测速逻辑
|
||||
} else {
|
||||
// testedCount == 0,继续使用公共节点
|
||||
}
|
||||
}
|
||||
|
||||
var url, parseType string
|
||||
if strings.ToLower(platform) == "cn" {
|
||||
if strings.ToLower(operator) == "cmcc" {
|
||||
url = model.CnCMCC
|
||||
} else if strings.ToLower(operator) == "cu" {
|
||||
url = model.CnCU
|
||||
} else if strings.ToLower(operator) == "ct" {
|
||||
url = model.CnCT
|
||||
} else if strings.ToLower(operator) == "hk" {
|
||||
url = model.CnHK
|
||||
} else if strings.ToLower(operator) == "tw" {
|
||||
url = model.CnTW
|
||||
} else if strings.ToLower(operator) == "jp" {
|
||||
url = model.CnJP
|
||||
} else if strings.ToLower(operator) == "sg" {
|
||||
url = model.CnSG
|
||||
}
|
||||
parseType = "url"
|
||||
} else if strings.ToLower(platform) == "net" {
|
||||
if strings.ToLower(operator) == "cmcc" {
|
||||
url = model.NetCMCC
|
||||
} else if strings.ToLower(operator) == "cu" {
|
||||
url = model.NetCU
|
||||
} else if strings.ToLower(operator) == "ct" {
|
||||
url = model.NetCT
|
||||
} else if strings.ToLower(operator) == "hk" {
|
||||
url = model.NetHK
|
||||
} else if strings.ToLower(operator) == "tw" {
|
||||
url = model.NetTW
|
||||
} else if strings.ToLower(operator) == "jp" {
|
||||
url = model.NetJP
|
||||
} else if strings.ToLower(operator) == "sg" {
|
||||
url = model.NetSG
|
||||
} else if strings.ToLower(operator) == "global" || strings.ToLower(operator) == "other" {
|
||||
// other 类型回退到 global 节点
|
||||
url = model.NetGlobal
|
||||
}
|
||||
parseType = "id"
|
||||
}
|
||||
if runtime.GOOS == "windows" || sp.OfficialAvailableTest() != nil {
|
||||
sp.CustomSpeedTest(url, parseType, num, language)
|
||||
} else {
|
||||
sp.OfficialCustomSpeedTest(url, parseType, num, language)
|
||||
}
|
||||
}
|
||||
35
internal/tests/unlock.go
Normal file
35
internal/tests/unlock.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/oneclickvirt/UnlockTests/executor"
|
||||
"github.com/oneclickvirt/UnlockTests/utils"
|
||||
"github.com/oneclickvirt/defaultset"
|
||||
)
|
||||
|
||||
func MediaTest(language string) string {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "[WARN] MediaTest panic: %v\n", r)
|
||||
}
|
||||
}()
|
||||
|
||||
var res string
|
||||
readStatus := executor.ReadSelect(language, "0")
|
||||
if !readStatus {
|
||||
return ""
|
||||
}
|
||||
if executor.IPV4 {
|
||||
res += defaultset.Blue("IPV4:") + "\n"
|
||||
res += executor.RunTests(utils.Ipv4HttpClient, "ipv4", language, false)
|
||||
return res
|
||||
}
|
||||
if executor.IPV6 {
|
||||
res += defaultset.Blue("IPV6:") + "\n"
|
||||
res += executor.RunTests(utils.Ipv6HttpClient, "ipv6", language, false)
|
||||
return res
|
||||
}
|
||||
return ""
|
||||
}
|
||||
94
internal/tests/upstreams.go
Normal file
94
internal/tests/upstreams.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/oneclickvirt/UnlockTests/executor"
|
||||
bgptools "github.com/oneclickvirt/backtrace/bgptools"
|
||||
backtrace "github.com/oneclickvirt/backtrace/bk"
|
||||
. "github.com/oneclickvirt/defaultset"
|
||||
)
|
||||
|
||||
type IpInfo struct {
|
||||
Ip string `json:"ip"`
|
||||
City string `json:"city"`
|
||||
Region string `json:"region"`
|
||||
Country string `json:"country"`
|
||||
Org string `json:"org"`
|
||||
}
|
||||
|
||||
type ConcurrentResults struct {
|
||||
bgpResult string
|
||||
backtraceResult string
|
||||
bgpError error
|
||||
// backtraceError error
|
||||
}
|
||||
|
||||
var IPV4, IPV6 string
|
||||
|
||||
func UpstreamsCheck(language string) {
|
||||
// 添加panic恢复机制
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
if language == "zh" {
|
||||
fmt.Println("\n上游检测出现错误,已跳过")
|
||||
} else {
|
||||
fmt.Println("\nUpstream check failed, skipped")
|
||||
}
|
||||
fmt.Fprintf(os.Stderr, "[WARN] Upstream check panic: %v\n", r)
|
||||
}
|
||||
}()
|
||||
|
||||
results := ConcurrentResults{}
|
||||
var wg sync.WaitGroup
|
||||
if IPV4 != "" {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "[WARN] BGP info panic: %v\n", r)
|
||||
}
|
||||
}()
|
||||
for i := 0; i < 2; i++ {
|
||||
result, err := bgptools.GetPoPInfo(IPV4)
|
||||
results.bgpError = err
|
||||
if err == nil && result.Result != "" {
|
||||
results.bgpResult = result.Result
|
||||
return
|
||||
}
|
||||
if i == 0 {
|
||||
time.Sleep(3 * time.Second)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Fprintf(os.Stderr, "[WARN] Backtrace panic: %v\n", r)
|
||||
}
|
||||
}()
|
||||
result := backtrace.BackTrace(executor.IPV6)
|
||||
results.backtraceResult = result
|
||||
}()
|
||||
wg.Wait()
|
||||
if results.bgpResult != "" {
|
||||
fmt.Print(results.bgpResult)
|
||||
}
|
||||
if results.backtraceResult != "" {
|
||||
fmt.Printf("%s\n", results.backtraceResult)
|
||||
}
|
||||
if language == "zh" {
|
||||
fmt.Println(Yellow("准确线路自行查看详细路由,本测试结果仅作参考"))
|
||||
fmt.Println(Yellow("同一目标地址多个线路时,检测可能已越过汇聚层,除第一个线路外,后续信息可能无效"))
|
||||
} else {
|
||||
fmt.Println(Yellow("For accurate routing, check the detailed routes yourself. This result is for reference only."))
|
||||
fmt.Println(Yellow("When multiple routes share the same destination, detection may have passed the aggregation layer; only the first route is reliable."))
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
package memorytest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/oneclickvirt/memorytest/memory"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func MemoryTest(language, testMethod string) {
|
||||
var res string
|
||||
if runtime.GOOS == "windows" {
|
||||
if testMethod != "winsat" && testMethod != "" {
|
||||
res = "Detected host is Windows, using Winsat for testing.\n"
|
||||
}
|
||||
res += memory.WinsatTest(language)
|
||||
} else {
|
||||
switch testMethod {
|
||||
case "sysbench":
|
||||
res = memory.SysBenchTest(language)
|
||||
if res == "" {
|
||||
res = "sysbench test failed, switch to use dd test.\n"
|
||||
res += memory.DDTest(language)
|
||||
}
|
||||
case "dd":
|
||||
res = memory.DDTest(language)
|
||||
default:
|
||||
res = "Unsupported test method"
|
||||
}
|
||||
}
|
||||
if !strings.Contains(res, "\n") && res != "" {
|
||||
res += "\n"
|
||||
}
|
||||
fmt.Printf(res)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package memorytest
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
MemoryTest("zh", "sysbench")
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package ntrace
|
||||
|
||||
import (
|
||||
"github.com/oneclickvirt/nt3/nt"
|
||||
)
|
||||
|
||||
func TraceRoute3(language, location, checkType string) {
|
||||
nt.TraceRoute(language, location, checkType)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package ntrace
|
||||
|
||||
import "testing"
|
||||
|
||||
// https://github.com/nxtrace/NTrace-core/blob/main/fast_trace/fast_trace.go
|
||||
func TestTraceRoute(t *testing.T) {
|
||||
TraceRoute3("en", "GZ", "ipv4")
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package speedtest
|
||||
|
||||
import (
|
||||
"github.com/oneclickvirt/speedtest/model"
|
||||
"github.com/oneclickvirt/speedtest/sp"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func ShowHead(language string) {
|
||||
sp.ShowHead(language)
|
||||
}
|
||||
|
||||
func NearbySP() {
|
||||
if runtime.GOOS == "windows" || sp.OfficialAvailableTest() != nil {
|
||||
sp.NearbySpeedTest()
|
||||
} else {
|
||||
sp.OfficialNearbySpeedTest()
|
||||
}
|
||||
}
|
||||
|
||||
func CustomSP(platform, operator string, num int, language string) {
|
||||
var url, parseType string
|
||||
if strings.ToLower(platform) == "cn" {
|
||||
if strings.ToLower(operator) == "cmcc" {
|
||||
url = model.CnCMCC
|
||||
} else if strings.ToLower(operator) == "cu" {
|
||||
url = model.CnCU
|
||||
} else if strings.ToLower(operator) == "ct" {
|
||||
url = model.CnCT
|
||||
} else if strings.ToLower(operator) == "hk" {
|
||||
url = model.CnHK
|
||||
} else if strings.ToLower(operator) == "tw" {
|
||||
url = model.CnTW
|
||||
} else if strings.ToLower(operator) == "jp" {
|
||||
url = model.CnJP
|
||||
} else if strings.ToLower(operator) == "sg" {
|
||||
url = model.CnSG
|
||||
}
|
||||
parseType = "url"
|
||||
} else if strings.ToLower(platform) == "net" {
|
||||
if strings.ToLower(operator) == "cmcc" {
|
||||
url = model.NetCMCC
|
||||
} else if strings.ToLower(operator) == "cu" {
|
||||
url = model.NetCU
|
||||
} else if strings.ToLower(operator) == "ct" {
|
||||
url = model.NetCT
|
||||
} else if strings.ToLower(operator) == "hk" {
|
||||
url = model.NetHK
|
||||
} else if strings.ToLower(operator) == "tw" {
|
||||
url = model.NetTW
|
||||
} else if strings.ToLower(operator) == "jp" {
|
||||
url = model.NetJP
|
||||
} else if strings.ToLower(operator) == "sg" {
|
||||
url = model.NetSG
|
||||
} else if strings.ToLower(operator) == "global" {
|
||||
url = model.NetGlobal
|
||||
}
|
||||
parseType = "id"
|
||||
}
|
||||
if runtime.GOOS == "windows" || sp.OfficialAvailableTest() != nil {
|
||||
sp.CustomSpeedTest(url, parseType, num, language)
|
||||
} else {
|
||||
sp.OfficialCustomSpeedTest(url, parseType, num, language)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package speedtest
|
||||
|
||||
import "testing"
|
||||
|
||||
func Test(t *testing.T) {
|
||||
ShowHead("en")
|
||||
NearbySP()
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
package unlocktest
|
||||
|
||||
import (
|
||||
"github.com/oneclickvirt/UnlockTests/utils"
|
||||
"github.com/oneclickvirt/UnlockTests/uts"
|
||||
"github.com/oneclickvirt/defaultset"
|
||||
)
|
||||
|
||||
func MediaTest(language string) string {
|
||||
var res string
|
||||
readStatus := uts.ReadSelect(language, "0")
|
||||
if !readStatus {
|
||||
return ""
|
||||
}
|
||||
if uts.IPV4 {
|
||||
res += defaultset.Blue("IPV4:") + "\n"
|
||||
res += uts.RunTests(utils.Ipv4HttpClient, "ipv4", language, false)
|
||||
return res
|
||||
}
|
||||
if uts.IPV6 {
|
||||
res += defaultset.Blue("IPV6:") + "\n"
|
||||
res += uts.RunTests(utils.Ipv6HttpClient, "ipv6", language, false)
|
||||
return res
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package unlocktest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func Test(t *testing.T) {
|
||||
fmt.Printf(MediaTest("zh"))
|
||||
}
|
||||
504
utils/utils.go
504
utils/utils.go
@@ -3,22 +3,126 @@ package utils
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/oneclickvirt/UnlockTests/uts"
|
||||
"github.com/oneclickvirt/basics/system"
|
||||
. "github.com/oneclickvirt/defaultset"
|
||||
"github.com/oneclickvirt/security/network"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/imroc/req/v3"
|
||||
"github.com/oneclickvirt/UnlockTests/executor"
|
||||
bnetwork "github.com/oneclickvirt/basics/network"
|
||||
"github.com/oneclickvirt/basics/system"
|
||||
butils "github.com/oneclickvirt/basics/utils"
|
||||
. "github.com/oneclickvirt/defaultset"
|
||||
"github.com/oneclickvirt/security/network"
|
||||
)
|
||||
|
||||
// IsAndroid 检测当前是否在 Android (Termux) 环境下运行
|
||||
func IsAndroid() bool {
|
||||
// Termux 会设置 TERMUX_VERSION 或 PREFIX 环境变量
|
||||
if os.Getenv("TERMUX_VERSION") != "" {
|
||||
return true
|
||||
}
|
||||
if prefix := os.Getenv("PREFIX"); strings.Contains(prefix, "termux") {
|
||||
return true
|
||||
}
|
||||
// Android 系统标志文件
|
||||
if _, err := os.Stat("/system/build.prop"); err == nil {
|
||||
return true
|
||||
}
|
||||
// ANDROID_ROOT 环境变量 (Android 系统设置为 /system)
|
||||
if os.Getenv("ANDROID_ROOT") != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// androidDNSServers 是用于修复 Android/Termux 下 DNS 问题的备用服务器列表
|
||||
var androidDNSServers = []string{
|
||||
"nameserver 8.8.8.8",
|
||||
"nameserver 8.8.4.4",
|
||||
"nameserver 1.1.1.1",
|
||||
"nameserver 223.5.5.5",
|
||||
}
|
||||
|
||||
// CheckAndFixAndroidDNS 检测并尝试修复 Android Termux 下的 DNS 解析问题。
|
||||
// 仅在检测到 Android 环境下调用。不出现问题时无任何输出。
|
||||
// language: "zh" 或 "en"
|
||||
func CheckAndFixAndroidDNS(language string) {
|
||||
if !IsAndroid() {
|
||||
return
|
||||
}
|
||||
resolvPath := "/etc/resolv.conf"
|
||||
hasValidDNS := false
|
||||
if data, err := os.ReadFile(resolvPath); err == nil {
|
||||
for _, line := range strings.Split(string(data), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "nameserver") && len(strings.Fields(line)) >= 2 {
|
||||
hasValidDNS = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if hasValidDNS {
|
||||
// DNS 配置正常,无需处理
|
||||
return
|
||||
}
|
||||
// /etc/resolv.conf 缺失或无有效 nameserver,尝试自动创建
|
||||
content := strings.Join(androidDNSServers, "\n") + "\n"
|
||||
err := os.WriteFile(resolvPath, []byte(content), 0644)
|
||||
if err != nil {
|
||||
// 写入失败(权限不足),给出双语提示和修复建议
|
||||
fmt.Println("-----------------------------------------------------")
|
||||
fmt.Println("[Android/Termux] 检测到 DNS 解析配置缺失!")
|
||||
fmt.Println("这将导致依赖系统 DNS 的测试项目无法正常运行。")
|
||||
fmt.Println("解决方案(推荐):")
|
||||
fmt.Println(" 通过 Magisk / KernelSU 刷入以下模块后重启手机:")
|
||||
fmt.Println(" https://github.com/weigui404/resolv.conf")
|
||||
fmt.Println("解决方案(临时):")
|
||||
fmt.Println(" 以 root 身份执行: echo 'nameserver 8.8.8.8' > /etc/resolv.conf")
|
||||
fmt.Println("-----------------------------------------------------")
|
||||
fmt.Println("[Android/Termux] DNS resolver config is missing!")
|
||||
fmt.Println("This will cause DNS-dependent tests to fail.")
|
||||
fmt.Println("Fix (recommended):")
|
||||
fmt.Println(" Flash the following module via Magisk / KernelSU and reboot:")
|
||||
fmt.Println(" https://github.com/weigui404/resolv.conf")
|
||||
fmt.Println("Fix (temporary):")
|
||||
fmt.Println(" Run as root: echo 'nameserver 8.8.8.8' > /etc/resolv.conf")
|
||||
fmt.Println("-----------------------------------------------------")
|
||||
} else {
|
||||
// 写入成功
|
||||
if language == "zh" {
|
||||
fmt.Println("[Android/Termux] DNS 配置缺失,已自动写入 /etc/resolv.conf,DNS 解析已恢复。")
|
||||
} else {
|
||||
fmt.Println("[Android/Termux] DNS config was missing; auto-written to /etc/resolv.conf, DNS resolution restored.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取本程序本日及总执行的统计信息
|
||||
type StatsResponse struct {
|
||||
Counter string `json:"counter"`
|
||||
Action string `json:"action"`
|
||||
Total int `json:"total"`
|
||||
Daily int `json:"daily"`
|
||||
Date string `json:"date"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
// 获取最新的Github的仓库中的版本
|
||||
type GitHubRelease struct {
|
||||
TagName string `json:"tag_name"`
|
||||
}
|
||||
|
||||
// PrintCenteredTitle 根据指定的宽度打印居中标题
|
||||
func PrintCenteredTitle(title string, width int) {
|
||||
// 计算字符串的字符数
|
||||
@@ -34,19 +138,19 @@ func PrintHead(language string, width int, ecsVersion string) {
|
||||
if language == "zh" {
|
||||
PrintCenteredTitle("VPS融合怪测试", width)
|
||||
fmt.Printf("版本:%s\n", ecsVersion)
|
||||
fmt.Println("测评频道: https://t.me/vps_reviews\n" +
|
||||
fmt.Println("测评频道: https://t.me/+UHVoo2U4VyA5NTQ1\n" +
|
||||
"Go项目地址:https://github.com/oneclickvirt/ecs\n" +
|
||||
"Shell项目地址:https://github.com/spiritLHLS/ecs")
|
||||
} else {
|
||||
PrintCenteredTitle("VPS Fusion Monster Test", width)
|
||||
fmt.Printf("Version: %s\n", ecsVersion)
|
||||
fmt.Println("Review Channel: https://t.me/vps_reviews\n" +
|
||||
fmt.Println("Review Channel: https://t.me/+UHVoo2U4VyA5NTQ1\n" +
|
||||
"Go Project: https://github.com/oneclickvirt/ecs\n" +
|
||||
"Shell Project: https://github.com/spiritLHLS/ecs")
|
||||
}
|
||||
}
|
||||
|
||||
func CheckChina(enableLogger bool) bool {
|
||||
func CheckChina(enableLogger bool, language string) bool {
|
||||
if enableLogger {
|
||||
InitLogger()
|
||||
defer Logger.Sync()
|
||||
@@ -62,7 +166,7 @@ func CheckChina(enableLogger bool) bool {
|
||||
ipapiResp, err := client.R().Get(ipapiURL)
|
||||
if err != nil {
|
||||
if enableLogger {
|
||||
Logger.Info("无法获取IP信息:" + err.Error())
|
||||
Logger.Info("Failed to get IP info: " + err.Error())
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -70,42 +174,79 @@ func CheckChina(enableLogger bool) bool {
|
||||
ipapiBody, err := ipapiResp.ToString()
|
||||
if err != nil {
|
||||
if enableLogger {
|
||||
Logger.Info("无法读取IP信息响应:" + err.Error())
|
||||
Logger.Info("Failed to read IP info response: " + err.Error())
|
||||
}
|
||||
return false
|
||||
}
|
||||
isInChina := strings.Contains(ipapiBody, "China")
|
||||
if isInChina {
|
||||
fmt.Println("根据 ipapi.co 提供的信息,当前IP可能在中国")
|
||||
var input string
|
||||
fmt.Print("是否选用中国专项测试(无流媒体测试,有三网Ping值测试)? ([y]/n) ")
|
||||
if language == "zh" {
|
||||
fmt.Println("根据 ipapi.co 提供的信息,当前IP可能在中国")
|
||||
fmt.Print("是否选用中国专项测试(无平台解锁测试,有三网Ping值测试)? ([y]/n) ")
|
||||
} else {
|
||||
fmt.Println("According to ipapi.co, this IP may be located in China")
|
||||
fmt.Print("Use China-specific test (no platform unlock test, includes 3-network ping test)? ([y]/n) ")
|
||||
}
|
||||
fmt.Scanln(&input)
|
||||
switch strings.ToLower(input) {
|
||||
case "yes", "y":
|
||||
fmt.Println("使用中国专项测试")
|
||||
if language == "zh" {
|
||||
fmt.Println("使用中国专项测试")
|
||||
} else {
|
||||
fmt.Println("Using China-specific test")
|
||||
}
|
||||
selectChina = true
|
||||
case "no", "n":
|
||||
fmt.Println("不使用中国专项测试")
|
||||
if language == "zh" {
|
||||
fmt.Println("不使用中国专项测试")
|
||||
} else {
|
||||
fmt.Println("Not using China-specific test")
|
||||
}
|
||||
default:
|
||||
fmt.Println("使用中国专项测试")
|
||||
if language == "zh" {
|
||||
fmt.Println("使用中国专项测试")
|
||||
} else {
|
||||
fmt.Println("Using China-specific test")
|
||||
}
|
||||
selectChina = true
|
||||
}
|
||||
}
|
||||
return selectChina
|
||||
}
|
||||
|
||||
// OnlyBasicsIpInfo 仅检查和输出IP信息
|
||||
func OnlyBasicsIpInfo(language string) (string, string, string) {
|
||||
ipv4, ipv6, ipInfo, _, err := bnetwork.NetworkCheck("both", false, language)
|
||||
if err != nil {
|
||||
return "", "", ""
|
||||
}
|
||||
basicInfo := ipInfo
|
||||
if strings.Contains(ipInfo, "IPV4") && strings.Contains(ipInfo, "IPV6") && ipv4 != "" && ipv6 != "" {
|
||||
executor.IPV4 = true
|
||||
executor.IPV6 = true
|
||||
} else if strings.Contains(ipInfo, "IPV4") && ipv4 != "" {
|
||||
executor.IPV4 = true
|
||||
executor.IPV6 = false
|
||||
} else if strings.Contains(ipInfo, "IPV6") && ipv6 != "" {
|
||||
executor.IPV6 = true
|
||||
executor.IPV4 = false
|
||||
}
|
||||
basicInfo = strings.ReplaceAll(basicInfo, "\n\n", "\n")
|
||||
return ipv4, ipv6, basicInfo
|
||||
}
|
||||
|
||||
// BasicsAndSecurityCheck 执行安全检查
|
||||
func BasicsAndSecurityCheck(language, nt3CheckType string, securtyCheckStatus bool) (string, string, string) {
|
||||
func BasicsAndSecurityCheck(language, nt3CheckType string, securityCheckStatus bool) (string, string, string, string, string) {
|
||||
var wgt sync.WaitGroup
|
||||
var ipInfo, securityInfo, systemInfo string
|
||||
var err error
|
||||
var ipv4, ipv6, ipInfo, securityInfo, systemInfo string
|
||||
wgt.Add(1)
|
||||
go func() {
|
||||
defer wgt.Done()
|
||||
ipInfo, securityInfo, err = network.NetworkCheck("both", securtyCheckStatus, language)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
}
|
||||
ipv4, ipv6, ipInfo, securityInfo, _ = network.NetworkCheck("both", securityCheckStatus, language)
|
||||
// if err != nil {
|
||||
// fmt.Println(err.Error())
|
||||
// }
|
||||
}()
|
||||
wgt.Add(1)
|
||||
go func() {
|
||||
@@ -114,21 +255,21 @@ func BasicsAndSecurityCheck(language, nt3CheckType string, securtyCheckStatus bo
|
||||
}()
|
||||
wgt.Wait()
|
||||
basicInfo := systemInfo + ipInfo
|
||||
if strings.Contains(ipInfo, "IPV4") && strings.Contains(ipInfo, "IPV6") {
|
||||
uts.IPV4 = true
|
||||
uts.IPV6 = true
|
||||
if strings.Contains(ipInfo, "IPV4") && strings.Contains(ipInfo, "IPV6") && ipv4 != "" && ipv6 != "" {
|
||||
executor.IPV4 = true
|
||||
executor.IPV6 = true
|
||||
if nt3CheckType == "" {
|
||||
nt3CheckType = "ipv4"
|
||||
}
|
||||
} else if strings.Contains(ipInfo, "IPV4") {
|
||||
uts.IPV4 = true
|
||||
uts.IPV6 = false
|
||||
} else if strings.Contains(ipInfo, "IPV4") && ipv4 != "" {
|
||||
executor.IPV4 = true
|
||||
executor.IPV6 = false
|
||||
if nt3CheckType == "" {
|
||||
nt3CheckType = "ipv4"
|
||||
}
|
||||
} else if strings.Contains(ipInfo, "IPV6") {
|
||||
uts.IPV6 = true
|
||||
uts.IPV4 = false
|
||||
} else if strings.Contains(ipInfo, "IPV6") && ipv6 != "" {
|
||||
executor.IPV6 = true
|
||||
executor.IPV4 = false
|
||||
if nt3CheckType == "" {
|
||||
nt3CheckType = "ipv6"
|
||||
}
|
||||
@@ -139,7 +280,7 @@ func BasicsAndSecurityCheck(language, nt3CheckType string, securtyCheckStatus bo
|
||||
nt3CheckType = "ipv4"
|
||||
}
|
||||
basicInfo = strings.ReplaceAll(basicInfo, "\n\n", "\n")
|
||||
return basicInfo, securityInfo, nt3CheckType
|
||||
return ipv4, ipv6, basicInfo, securityInfo, nt3CheckType
|
||||
}
|
||||
|
||||
// CaptureOutput 捕获函数输出和错误输出,实时输出,并返回字符串
|
||||
@@ -161,19 +302,10 @@ func CaptureOutput(f func()) string {
|
||||
// 替换标准输出和标准错误输出为管道写入端
|
||||
os.Stdout = stdoutPipeW
|
||||
os.Stderr = stderrPipeW
|
||||
// 恢复标准输出和标准错误输出
|
||||
defer func() {
|
||||
os.Stdout = oldStdout
|
||||
os.Stderr = oldStderr
|
||||
stdoutPipeW.Close()
|
||||
stderrPipeW.Close()
|
||||
stdoutPipeR.Close()
|
||||
stderrPipeR.Close()
|
||||
}()
|
||||
// 缓冲区
|
||||
var stdoutBuf, stderrBuf bytes.Buffer
|
||||
// 并发读取 stdout 和 stderr
|
||||
done := make(chan struct{})
|
||||
done := make(chan struct{}, 2)
|
||||
go func() {
|
||||
multiWriter := io.MultiWriter(&stdoutBuf, oldStdout)
|
||||
io.Copy(multiWriter, stdoutPipeR)
|
||||
@@ -186,12 +318,22 @@ func CaptureOutput(f func()) string {
|
||||
}()
|
||||
// 执行函数
|
||||
f()
|
||||
// 确保所有输出都已经刷新
|
||||
os.Stdout.Sync()
|
||||
os.Stderr.Sync()
|
||||
// 等待一小段时间,确保后台goroutine的输出完成
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// 关闭管道写入端,让管道读取端可以读取所有数据
|
||||
stdoutPipeW.Close()
|
||||
stderrPipeW.Close()
|
||||
// 等待两个 goroutine 完成
|
||||
<-done
|
||||
<-done
|
||||
// 恢复标准输出和标准错误输出,并关闭管道读取端
|
||||
os.Stdout = oldStdout
|
||||
os.Stderr = oldStderr
|
||||
stdoutPipeR.Close()
|
||||
stderrPipeR.Close()
|
||||
// 返回捕获的输出字符串
|
||||
// stderrBuf.String()
|
||||
return stdoutBuf.String()
|
||||
@@ -271,11 +413,11 @@ func UploadText(absPath string) (string, string, error) {
|
||||
}
|
||||
|
||||
// ProcessAndUpload 创建结果文件并上传文件
|
||||
func ProcessAndUpload(output string, filePath string, enableUplaod bool) (string, string) {
|
||||
func ProcessAndUpload(output string, filePath string, enableUplaod bool, language string) (string, string) {
|
||||
// 使用 defer 来处理 panic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Printf("处理上传时发生错误: %v\n", r)
|
||||
fmt.Fprintf(os.Stderr, "[ERROR] Fatal error during upload: %v\n", r)
|
||||
}
|
||||
}()
|
||||
// 检查文件是否存在
|
||||
@@ -283,14 +425,22 @@ func ProcessAndUpload(output string, filePath string, enableUplaod bool) (string
|
||||
// 文件存在,删除文件
|
||||
err = os.Remove(filePath)
|
||||
if err != nil {
|
||||
fmt.Println("无法删除文件:", err)
|
||||
if language == "zh" {
|
||||
fmt.Println("无法删除文件:", err)
|
||||
} else {
|
||||
fmt.Println("Failed to delete file:", err)
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
// 创建文件
|
||||
file, err := os.Create(filePath)
|
||||
if err != nil {
|
||||
fmt.Println("无法创建文件:", err)
|
||||
if language == "zh" {
|
||||
fmt.Println("无法创建文件:", err)
|
||||
} else {
|
||||
fmt.Println("Failed to create file:", err)
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
defer file.Close()
|
||||
@@ -302,27 +452,47 @@ func ProcessAndUpload(output string, filePath string, enableUplaod bool) (string
|
||||
writer := bufio.NewWriter(file)
|
||||
_, err = writer.WriteString(cleanedOutput)
|
||||
if err != nil {
|
||||
fmt.Println("无法写入文件:", err)
|
||||
if language == "zh" {
|
||||
fmt.Println("无法写入文件:", err)
|
||||
} else {
|
||||
fmt.Println("Failed to write file:", err)
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
// 确保写入缓冲区的数据都刷新到文件中
|
||||
err = writer.Flush()
|
||||
if err != nil {
|
||||
fmt.Println("无法刷新文件缓冲:", err)
|
||||
if language == "zh" {
|
||||
fmt.Println("无法刷新文件缓冲:", err)
|
||||
} else {
|
||||
fmt.Println("Failed to flush file buffer:", err)
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
fmt.Printf("测试结果已写入 %s\n", filePath)
|
||||
if language == "zh" {
|
||||
fmt.Printf("测试结果已写入 %s\n", filePath)
|
||||
} else {
|
||||
fmt.Printf("Test results written to %s\n", filePath)
|
||||
}
|
||||
if enableUplaod {
|
||||
// 获取文件的绝对路径
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
fmt.Println("无法获取文件绝对路径:", err)
|
||||
if language == "zh" {
|
||||
fmt.Println("无法获取文件绝对路径:", err)
|
||||
} else {
|
||||
fmt.Println("Failed to get absolute file path:", err)
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
// 上传文件并生成短链接
|
||||
http_url, https_url, err := UploadText(absPath)
|
||||
if err != nil {
|
||||
fmt.Println("上传失败,无法生成链接")
|
||||
if language == "zh" {
|
||||
fmt.Println("上传失败,无法生成链接")
|
||||
} else {
|
||||
fmt.Println("Upload failed, unable to generate link")
|
||||
}
|
||||
fmt.Println(err.Error())
|
||||
return "", ""
|
||||
}
|
||||
@@ -330,3 +500,235 @@ func ProcessAndUpload(output string, filePath string, enableUplaod bool) (string
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
var StackType string
|
||||
|
||||
type NetCheckResult struct {
|
||||
HasIPv4 bool
|
||||
HasIPv6 bool
|
||||
Connected bool
|
||||
StackType string // "IPv4", "IPv6", "DualStack", "None"
|
||||
}
|
||||
|
||||
func makeResolver(proto, dnsAddr string) *net.Resolver {
|
||||
return &net.Resolver{
|
||||
PreferGo: true,
|
||||
Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
d := net.Dialer{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
return d.DialContext(ctx, proto, dnsAddr)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 前置联网能力检测
|
||||
func CheckPublicAccess(timeout time.Duration) NetCheckResult {
|
||||
if timeout < 2*time.Second {
|
||||
timeout = 2 * time.Second
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
resultChan := make(chan string, 8)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
checks := []struct {
|
||||
Tag string
|
||||
Addr string
|
||||
Kind string // udp4, udp6, http4, http6
|
||||
}{
|
||||
// UDP DNS
|
||||
{"IPv4", "223.5.5.5:53", "udp4"}, // 阿里 DNS
|
||||
{"IPv4", "8.8.8.8:53", "udp4"}, // Google DNS
|
||||
{"IPv6", "[2400:3200::1]:53", "udp6"}, // 阿里 IPv6 DNS
|
||||
{"IPv6", "[2001:4860:4860::8888]:53", "udp6"}, // Google IPv6 DNS
|
||||
// HTTP HEAD
|
||||
{"IPv4", "https://www.baidu.com", "http4"}, // 百度
|
||||
{"IPv4", "https://1.1.1.1", "http4"}, // Cloudflare
|
||||
{"IPv6", "https://[2400:3200::1]", "http6"}, // 阿里 IPv6
|
||||
{"IPv6", "https://[2606:4700::1111]", "http6"}, // Cloudflare IPv6
|
||||
}
|
||||
for _, check := range checks {
|
||||
wg.Add(1)
|
||||
go func(tag, addr, kind string) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// 记录panic但不影响其他检查,输出到stderr避免污染主输出
|
||||
fmt.Fprintf(os.Stderr, "[WARN] Panic in network check for %s (%s): %v\n", tag, addr, r)
|
||||
}
|
||||
}()
|
||||
switch kind {
|
||||
case "udp4", "udp6":
|
||||
dialer := &net.Dialer{
|
||||
Timeout: timeout / 4,
|
||||
}
|
||||
conn, err := dialer.DialContext(ctx, kind, addr)
|
||||
if err == nil && conn != nil {
|
||||
conn.Close()
|
||||
select {
|
||||
case resultChan <- tag:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
case "http4", "http6":
|
||||
var resolver *net.Resolver
|
||||
if kind == "http4" {
|
||||
resolver = makeResolver("udp4", "223.5.5.5:53")
|
||||
} else {
|
||||
resolver = makeResolver("udp6", "[2400:3200::1]:53")
|
||||
}
|
||||
dialer := &net.Dialer{
|
||||
Timeout: timeout / 4,
|
||||
Resolver: resolver,
|
||||
}
|
||||
transport := &http.Transport{
|
||||
DialContext: dialer.DialContext,
|
||||
MaxIdleConns: 1,
|
||||
MaxIdleConnsPerHost: 1,
|
||||
IdleConnTimeout: time.Second,
|
||||
TLSHandshakeTimeout: timeout / 4,
|
||||
ResponseHeaderTimeout: timeout / 4,
|
||||
DisableKeepAlives: true,
|
||||
}
|
||||
client := &http.Client{
|
||||
Timeout: timeout / 4,
|
||||
Transport: transport,
|
||||
CheckRedirect: func(req *http.Request, via []*http.Request) error {
|
||||
return http.ErrUseLastResponse
|
||||
},
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, "HEAD", addr, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp, err := client.Do(req)
|
||||
if err == nil && resp != nil {
|
||||
if resp.Body != nil {
|
||||
resp.Body.Close()
|
||||
}
|
||||
if resp.StatusCode < 500 {
|
||||
select {
|
||||
case resultChan <- tag:
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}(check.Tag, check.Addr, check.Kind)
|
||||
}
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
}()
|
||||
hasV4 := false
|
||||
hasV6 := false
|
||||
for {
|
||||
select {
|
||||
case res, ok := <-resultChan:
|
||||
if !ok {
|
||||
goto result
|
||||
}
|
||||
if res == "IPv4" {
|
||||
hasV4 = true
|
||||
}
|
||||
if res == "IPv6" {
|
||||
hasV6 = true
|
||||
}
|
||||
case <-ctx.Done():
|
||||
goto result
|
||||
}
|
||||
}
|
||||
result:
|
||||
stack := "None"
|
||||
if hasV4 && hasV6 {
|
||||
stack = "DualStack"
|
||||
} else if hasV4 {
|
||||
stack = "IPv4"
|
||||
} else if hasV6 {
|
||||
stack = "IPv6"
|
||||
}
|
||||
StackType = stack
|
||||
butils.CheckPublicAccess(3 * time.Second) // 设置basics检测,避免部分测试未启用
|
||||
return NetCheckResult{
|
||||
HasIPv4: hasV4,
|
||||
HasIPv6: hasV6,
|
||||
Connected: hasV4 || hasV6,
|
||||
StackType: stack,
|
||||
}
|
||||
}
|
||||
|
||||
// 获取每日/总的程序执行统计信息
|
||||
func GetGoescStats() (*StatsResponse, error) {
|
||||
client := req.C().SetTimeout(5 * time.Second)
|
||||
var stats StatsResponse
|
||||
resp, err := client.R().
|
||||
SetSuccessResult(&stats).
|
||||
Get("https://hits.spiritlhl.net/goecs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !resp.IsSuccessState() {
|
||||
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// 统计结果单位转换
|
||||
func FormatGoecsNumber(num int) string {
|
||||
if num >= 1000000 {
|
||||
return fmt.Sprintf("%.1fM", float64(num)/1000000)
|
||||
} else if num >= 1000 {
|
||||
return fmt.Sprintf("%.1fK", float64(num)/1000)
|
||||
}
|
||||
return fmt.Sprintf("%d", num)
|
||||
}
|
||||
|
||||
// 通过Github的API检索仓库最新TAG的版本
|
||||
func GetLatestEcsRelease() (*GitHubRelease, error) {
|
||||
urls := []string{
|
||||
"https://api.github.com/repos/oneclickvirt/ecs/releases/latest",
|
||||
"https://fd.spiritlhl.top/https://api.github.com/repos/oneclickvirt/ecs/releases/latest",
|
||||
"https://githubapi.spiritlhl.top/repos/oneclickvirt/ecs/releases/latest",
|
||||
"https://githubapi.spiritlhl.workers.dev/repos/oneclickvirt/ecs/releases/latest",
|
||||
}
|
||||
client := req.C().SetTimeout(3 * time.Second)
|
||||
for _, url := range urls {
|
||||
var release GitHubRelease
|
||||
resp, err := client.R().
|
||||
SetSuccessResult(&release).
|
||||
Get(url)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if resp.IsSuccessState() && release.TagName != "" {
|
||||
return &release, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("failed to fetch release from all sources")
|
||||
}
|
||||
|
||||
// 比较程序版本是否需要升级
|
||||
func CompareVersions(v1, v2 string) int {
|
||||
normalize := func(s string) []int {
|
||||
s = strings.TrimPrefix(strings.ToLower(s), "v")
|
||||
parts := strings.Split(s, ".")
|
||||
result := make([]int, 3)
|
||||
for i := 0; i < 3 && i < len(parts); i++ {
|
||||
n, _ := strconv.Atoi(parts[i])
|
||||
result[i] = n
|
||||
}
|
||||
return result
|
||||
}
|
||||
a := normalize(v1)
|
||||
b := normalize(v2)
|
||||
for i := 0; i < 3; i++ {
|
||||
if a[i] < b[i] {
|
||||
return -1
|
||||
} else if a[i] > b[i] {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
31
utils/utils_test.go
Normal file
31
utils/utils_test.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// func TestCheckPublicAccess(t *testing.T) {
|
||||
// timeout := 3 * time.Second
|
||||
// result := CheckPublicAccess(timeout)
|
||||
// if result.Connected {
|
||||
// fmt.Print("✅ 本机有公网连接,类型: %s\n", result.StackType)
|
||||
// } else {
|
||||
// fmt.Println("❌ 本机未检测到公网连接")
|
||||
// }
|
||||
// }
|
||||
|
||||
func TestBasicsAndSecurityCheck(t *testing.T) {
|
||||
timeout := 3 * time.Second
|
||||
result := CheckPublicAccess(timeout)
|
||||
if result.Connected {
|
||||
fmt.Printf("✅ 本机有公网连接,类型: %s\n", result.StackType)
|
||||
} else {
|
||||
fmt.Println("❌ 本机未检测到公网连接")
|
||||
}
|
||||
_, _, basicInfo, securityInfo, nt3CheckType := BasicsAndSecurityCheck("zh", "ipv4", false)
|
||||
fmt.Println(basicInfo)
|
||||
fmt.Println(securityInfo)
|
||||
fmt.Println(nt3CheckType)
|
||||
}
|
||||
Reference in New Issue
Block a user