# 局域网遥控 全量验证脚本 # # 依次执行: # 0. 清理上一次测试可能残留的进程(否则发现端口会被抢占,导致假失败) # 1. Python 被控端协议回归(含屏幕镜像/截屏) # 2. 手表屏幕布局适配核算(490x602) # 3. 屏幕共享可见提示(真实界面 + Win32 窗口枚举) # 4. 小窗口 / 无托盘 / 后台常驻 / 双启动找回窗口 # 5. Java 协议层编译 # 6. Java <-> 桩服务端(含帧流、截屏、自动发现、单播探测) # 7. Java <-> 打包后的 EXE 端到端 # # 用法: powershell -ExecutionPolicy Bypass -File _tests\verify_all.ps1 $ErrorActionPreference = "Continue" $root = Split-Path -Parent $PSScriptRoot $tc = Join-Path $root "..\starklink\toolchain" $jdk = Join-Path $tc "jdk17" $java = Join-Path $jdk "bin\java.exe" $javac= Join-Path $jdk "bin\javac.exe" $out = Join-Path $root "_tests\out_java" $exe = Join-Path $root "dist_exe\ykxmz\ykxmz.exe" $androidJar = Join-Path $tc "android-sdk\platforms\android-34\android.jar" $script:results = @() # 当前会话是不是锁屏 / 没有交互桌面? # 锁屏时截屏类测试**必然**失败(拿不到桌面会话), # 但那不是代码问题。不识别的话汇总表一片红,真问题反而被淹没。 $script:desktopAvailable = $true try { Add-Type -Namespace Ykxmz -Name Desk -MemberDefinition @' [System.Runtime.InteropServices.DllImport("user32.dll", SetLastError=true)] public static extern System.IntPtr OpenInputDesktop(int flags, bool inherit, int access); [System.Runtime.InteropServices.DllImport("user32.dll")] public static extern bool CloseDesktop(System.IntPtr h); public static bool HasDesktop() { System.IntPtr h = OpenInputDesktop(0, false, 0x0001); if (h == System.IntPtr.Zero) return false; CloseDesktop(h); return true; } '@ -ErrorAction Stop $script:desktopAvailable = [Ykxmz.Desk]::HasDesktop() } catch { $script:desktopAvailable = $true # 查不出来就当可用,别把失败藏起来 } if (-not $script:desktopAvailable) { Write-Host "" Write-Host " ⚠ 检测到电脑处于锁屏状态。" -ForegroundColor Yellow Write-Host " 截屏相关的测试会失败,这是环境限制,不是代码问题。" -ForegroundColor Yellow Write-Host " 这些阶段会记为 SKIP 而不是 FAIL。解锁后重跑即可验证。" -ForegroundColor Yellow } function Record($name, $ok, $needsDesktop = $false) { # needsDesktop 的阶段在锁屏时记 SKIP —— 避免误报把真问题淹没 if ($needsDesktop -and -not $script:desktopAvailable) { $script:results += [pscustomobject]@{ 项目 = $name; 结果 = "SKIP(锁屏)" } return } $script:results += [pscustomobject]@{ 项目 = $name; 结果 = $(if ($ok) { "PASS" } else { "FAIL" }) } } function Banner($text) { Write-Host "" Write-Host ("=" * 70) -ForegroundColor DarkCyan Write-Host " $text" -ForegroundColor Cyan Write-Host ("=" * 70) -ForegroundColor DarkCyan } function Show-Fails($lines) { # 失败的阶段光看最后几行没用,得把 [FAIL] 打出来 $fails = $lines | Select-String -Pattern '\[FAIL\]' if ($fails) { $fails | Select-Object -First 8 | ForEach-Object { Write-Host " $($_.Line.Trim())" -ForegroundColor Red } } $lines | Select-Object -Last 1 | ForEach-Object { # ⚠ 这里可能是 ErrorRecord(子进程的 stderr 被塞进输出流时), # 而 ErrorRecord 没有 .Trim() 方法 —— 直接调会抛 # "does not contain a method named 'Trim'",反而把真正的 # 失败信息盖住。所以先转成字符串再 Trim。 Write-Host " $([string]$_.Trim())" } } function Close-TestWindows { # 测试会弹真实的 Tk 窗口(首次设置、投图、屏幕共享提示…)。 # 正常路径下测试自己会关;但用例失败/超时时进程还挂着, # 窗口就留在屏幕上不走了。这里统一收尾。 $closed = 0 Get-Process python, pythonw -ErrorAction SilentlyContinue | Where-Object { $_.Id -ne $PID } | ForEach-Object { $line = (Get-CimInstance Win32_Process -Filter "ProcessId=$($_.Id)" -ErrorAction SilentlyContinue).CommandLine if ($line -and ($line -match '_tests' -or $line -match 'run_agent' ` -or $line -match 'relay\.py')) { # 先好好关主窗口 try { $_.CloseMainWindow() | Out-Null } catch { } Start-Sleep -Milliseconds 250 if (-not $_.HasExited) { Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue } $closed++ } } # 被控端自己(测试起过 ykxmz.exe) Get-Process ykxmz -ErrorAction SilentlyContinue | ForEach-Object { try { $_.CloseMainWindow() | Out-Null } catch { } Start-Sleep -Milliseconds 250 if (-not $_.HasExited) { Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue } $closed++ } # Java 测试进程 Get-Process java -ErrorAction SilentlyContinue | ForEach-Object { $line = (Get-CimInstance Win32_Process -Filter "ProcessId=$($_.Id)" -ErrorAction SilentlyContinue).CommandLine if ($line -and ($line -match 'JavaClientTest' -or $line -match 'JavaRelayTest')) { Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue $closed++ } } if ($closed -gt 0) { Write-Host " 关了 $closed 个测试进程/窗口" -ForegroundColor DarkGray } Start-Sleep -Milliseconds 400 } function Stop-Strays { Get-Process ykxmz -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue } # 各测试会临时起 relay.py / stub / java。 # 只清 run_agent/run_stub 是不够的 —— 漏掉的 relay.py 如果没及时退出, # 端口就占着,下一个测试起不来,报出"假失败" # (实测:chunked 单跑 9/9、协议单跑 69/69,套件里却 FAIL)。 Get-Process python, pythonw -ErrorAction SilentlyContinue | Where-Object { $_.Id -ne $PID } | ForEach-Object { $line = (Get-CimInstance Win32_Process -Filter "ProcessId=$($_.Id)" -ErrorAction SilentlyContinue).CommandLine if ($line -and ($line -match 'run_agent' -or $line -match 'run_stub' ` -or $line -match 'relay\.py' -or $line -match '_tests')) { Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue } } # Java 测试进程(JavaClientTest / JavaRelayTest) Get-Process java -ErrorAction SilentlyContinue | ForEach-Object { $line = (Get-CimInstance Win32_Process -Filter "ProcessId=$($_.Id)" -ErrorAction SilentlyContinue).CommandLine if ($line -and ($line -match 'JavaClientTest' -or $line -match 'JavaRelayTest')) { Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue } } # 等到关键端口真的空出来(最多 6 秒),别让下一个测试撞上 for ($i = 0; $i -lt 12; $i++) { $busy = $false foreach ($p in 8765, 8766, 8799, 8130, 8140) { if (Get-NetTCPConnection -LocalPort $p -State Listen -ErrorAction SilentlyContinue) { $busy = $true } if (Get-NetUDPEndpoint -LocalPort $p -ErrorAction SilentlyContinue) { $busy = $true } } if (-not $busy) { break } Start-Sleep -Milliseconds 500 } Start-Sleep -Milliseconds 300 } # ---------------------------------------------------------------- 0 Banner "0/7 清理残留进程与端口" Stop-Strays $busy = @() foreach ($p in 8765, 8766, 8799, 8801, 8803, 8805) { if (Get-NetUDPEndpoint -LocalPort $p -ErrorAction SilentlyContinue) { $busy += "UDP $p" } if (Get-NetTCPConnection -LocalPort $p -State Listen -ErrorAction SilentlyContinue) { $busy += "TCP $p" } } if ($busy.Count -eq 0) { Write-Host "端口干净" -ForegroundColor Green } else { Write-Host ("仍被占用: " + ($busy -join ", ")) -ForegroundColor Yellow } # ---------------------------------------------------------------- 0a # 清掉上一次测试留在 received 里的文件。 # 不清的话会越攒越多,把 file_list 的前 100 个窗口挤满, # 导致"看不到刚上传的文件"这类假失败(真踩过:攒到 116 个)。 $receivedDir = Join-Path $root "_tests\received" if (Test-Path $receivedDir) { $old = (Get-ChildItem $receivedDir -File -ErrorAction SilentlyContinue).Count if ($old -gt 0) { Get-ChildItem $receivedDir -File | Remove-Item -Force -ErrorAction SilentlyContinue Write-Host " 清掉上次测试残留的 $old 个文件" -ForegroundColor DarkGray } } # ---------------------------------------------------------------- 0 # 编码审计放最前面:编码错了后面全白搭 Banner "0/7 编码审计(BOM / 行尾 / 乱码)" & python (Join-Path $root "_tests\audit_encoding.py") *> (Join-Path $env:TEMP "lr_enc.txt") $encCode = $LASTEXITCODE Get-Content (Join-Path $env:TEMP "lr_enc.txt") -Encoding UTF8 -ErrorAction SilentlyContinue | Select-Object -Last 3 Record "编码审计" ($encCode -eq 0) # ---------------------------------------------------------------- 1 Banner "1/7 Python 被控端协议回归" $stageOut = & python (Join-Path $root "_tests\test_agent_protocol.py") 2>&1 $stageCode = $LASTEXITCODE $stageOut | Select-Object -Last 4 Record "Python 被控端协议回归" ($stageCode -eq 0) $true # ---------------------------------------------------------------- 1b Banner "1b/7 防火墙阻止规则检测(真机连不上的根因)" $stageOut = & python (Join-Path $root "_tests\test_firewall.py") 2>&1 $stageCode = $LASTEXITCODE $stageOut | Select-Object -Last 3 Record "防火墙阻止规则检测" ($stageCode -eq 0) # ---------------------------------------------------------------- 1c Banner "1c/7 连不上?-> 电脑打开网络自检(UDP 链路)" Stop-Strays & python (Join-Path $root "_tests\test_selfcheck.py") *> (Join-Path $env:TEMP "lr_sc.txt") $scCode = $LASTEXITCODE Get-Content (Join-Path $env:TEMP "lr_sc.txt") -Encoding UTF8 -ErrorAction SilentlyContinue | Select-Object -Last 2 Record "自检请求链路" ($scCode -eq 0) # ---------------------------------------------------------------- 1e Banner "1e/7 新增功能(电量/磁盘/性能/进程/剪贴板历史/安全边界)" Stop-Strays & python (Join-Path $root "_tests\test_new_features.py") *> (Join-Path $env:TEMP "lr_feat.txt") $featCode = $LASTEXITCODE Get-Content (Join-Path $env:TEMP "lr_feat.txt") -Encoding UTF8 -ErrorAction SilentlyContinue | Select-Object -Last 2 Record "新增功能" ($featCode -eq 0) # ---------------------------------------------------------------- 1f Banner "1f/7 电脑功能注册表(39 个功能 + 安全边界 + 可配置)" Stop-Strays & python (Join-Path $root "_tests\test_features.py") *> (Join-Path $env:TEMP "lr_feats.txt") $featsCode = $LASTEXITCODE Get-Content (Join-Path $env:TEMP "lr_feats.txt") -Encoding UTF8 -ErrorAction SilentlyContinue | Select-Object -Last 2 Record "电脑功能注册表" ($featsCode -eq 0) # ---------------------------------------------------------------- 1g Banner "1g/7 功能模糊测试(105 个功能 × 空参数 / 垃圾参数 / 越权路径)" Stop-Strays & python (Join-Path $root "_tests\fuzz_features.py") *> (Join-Path $env:TEMP "lr_fuzz.txt") $fuzzCode = $LASTEXITCODE Get-Content (Join-Path $env:TEMP "lr_fuzz.txt") -Encoding UTF8 -ErrorAction SilentlyContinue | Select-Object -Last 3 Record "功能模糊测试" ($fuzzCode -eq 0) # ---------------------------------------------------------------- 1h Banner "1h/7 端到端(打包后的 EXE:配对 / 功能列表 / 越权 / 防爆破)" Stop-Strays & python (Join-Path $root "_tests\test_e2e_features.py") *> (Join-Path $env:TEMP "lr_e2e.txt") $e2eCode = $LASTEXITCODE Get-Content (Join-Path $env:TEMP "lr_e2e.txt") -Encoding UTF8 -ErrorAction SilentlyContinue | Select-Object -Last 3 Record "端到端(打包 EXE)" ($e2eCode -eq 0) # ---------------------------------------------------------------- 1i Banner "1i/7 静默模式(0 窗口 / 无终端 / 认证仍生效)" Stop-Strays & python (Join-Path $root "_tests\test_silent_mode.py") *> (Join-Path $env:TEMP "lr_silent.txt") $silentCode = $LASTEXITCODE Get-Content (Join-Path $env:TEMP "lr_silent.txt") -Encoding UTF8 -ErrorAction SilentlyContinue | Select-Object -Last 3 Record "静默模式" ($silentCode -eq 0) # ---------------------------------------------------------------- 1j Banner "1j/7 配一次→零窗口(令牌重连,全程无窗口)" Stop-Strays & python (Join-Path $root "_tests\test_pair_once_flow.py") *> (Join-Path $env:TEMP "lr_flow.txt") $flowCode = $LASTEXITCODE Get-Content (Join-Path $env:TEMP "lr_flow.txt") -Encoding UTF8 -ErrorAction SilentlyContinue | Select-Object -Last 3 Record "配一次→零窗口" ($flowCode -eq 0) # ---------------------------------------------------------------- 1k Banner "1k/7 首次安装设置窗口(出现一次,之后不再出现)" Stop-Strays & python (Join-Path $root "_tests\test_first_run_setup.py") *> (Join-Path $env:TEMP "lr_setup.txt") $setupCode2 = $LASTEXITCODE Get-Content (Join-Path $env:TEMP "lr_setup.txt") -Encoding UTF8 -ErrorAction SilentlyContinue | Select-Object -Last 3 Record "首次设置窗口" ($setupCode2 -eq 0) # ---------------------------------------------------------------- 1l Banner "1l/7 开机零窗口(照注册表里的自启命令真跑一遍)" Stop-Strays & python (Join-Path $root "_tests\test_boot_silent.py") *> (Join-Path $env:TEMP "lr_boot.txt") $bootCode = $LASTEXITCODE Get-Content (Join-Path $env:TEMP "lr_boot.txt") -Encoding UTF8 -ErrorAction SilentlyContinue | Select-Object -Last 3 Record "开机零窗口" ($bootCode -eq 0) # ---------------------------------------------------------------- 2 Banner "2/7 手表屏幕布局适配核算 (490x602)" $stageOut = & python (Join-Path $root "_tests\check_layout.py") 2>&1 $stageCode = $LASTEXITCODE $stageOut | Select-Object -Last 3 Record "手表布局适配" ($stageCode -eq 0) # ---------------------------------------------------------------- 1z Banner "1z/7 手表分页完整性(防白屏:方法定义了必须被调用)" & python (Join-Path $root "_tests\test_no_orphan_methods.py") 2>&1 | Select-Object -Last 6 Record "手表分页完整性" ($LASTEXITCODE -eq 0) # ---------------------------------------------------------------- 2a2 Banner "2a2/7 APK 内容(诊断已合并进主程序)" & python (Join-Path $root "_tests\test_apk_contents.py") *> (Join-Path $env:TEMP "lr_apk.txt") $apkCode = $LASTEXITCODE Get-Content (Join-Path $env:TEMP "lr_apk.txt") -Encoding UTF8 -ErrorAction SilentlyContinue | Select-Object -Last 2 Record "APK 内容" ($apkCode -eq 0) # ---------------------------------------------------------------- 2a Banner "2a/7 网络调优(MTU 修复)" # 这是可用率从 48% 提到 100% 的关键,而且容易被后人误改 # (比如"顺手把接口 MTU 也降一点" —— 实测那样会掉到 50%)。 & python (Join-Path $root "_tests\test_net_tune.py") 2>&1 | Select-Object -Last 4 Record "网络调优(MTU)" ($LASTEXITCODE -eq 0) # ---------------------------------------------------------------- 2b Banner "2b/7 投图显示方式(屏幕中间 / 全屏 / 自定义大小)" Stop-Strays $imgLog = Join-Path $env:TEMP "lr_imgmode.txt" & python (Join-Path $root "_tests\test_image_modes.py") *> $imgLog $imgCode = $LASTEXITCODE if ($imgCode -eq 0) { Get-Content $imgLog -Encoding UTF8 -ErrorAction SilentlyContinue | Select-Object -Last 3 } else { Write-Host "失败输出:" -ForegroundColor Red Get-Content $imgLog -Encoding UTF8 -ErrorAction SilentlyContinue | Where-Object { $_ -match "FAIL|通过|Traceback|Error" } } Record "投图显示方式" ($imgCode -eq 0) # ---------------------------------------------------------------- 3 Banner "3/7 屏幕共享可见提示(真实界面 + Win32 窗口枚举)" Stop-Strays $stageOut = & python (Join-Path $root "_tests\test_indicator_visible.py") 2>&1 $stageCode = $LASTEXITCODE $stageOut | Select-Object -Last 3 Record "屏幕共享强制提示" ($stageCode -eq 0) # ---------------------------------------------------------------- 2b Banner "2b/7 中转 chunked 请求体(防连接错位)" # Cloudflare 转发时可能用 Transfer-Encoding: chunked。 # 中转原来只读 Content-Length,残留的 "0\r\n\r\n" 会被当成下一个请求行, # 报 "Bad request syntax ('0')",整条连接错位 -> 隧道大面积 502。 $stageOut = & python (Join-Path $root "_tests\test_chunked_body.py") 2>&1 Show-Fails $stageOut Record "中转 chunked 请求体" ($LASTEXITCODE -eq 0) # ---------------------------------------------------------------- 3a Banner "3a/7 局域网直连路径(不经中转)" # 手表和电脑同一个 WiFi 时走这条:UDP 广播找到电脑 + TCP 直连。 # 这条路径完全不碰中转,所以单独盯一下。 $stageOut = & python (Join-Path $root "_tests\test_lan_path.py") 2>&1 Show-Fails $stageOut Record "局域网直连路径" ($LASTEXITCODE -eq 0) # ---------------------------------------------------------------- 3a2 Banner "3a2/7 局域网优化(网段掩码 / 发现重发 / 回切)" # 盯四件事: # 1. 网段判断按网卡真实掩码,不再写死 /24(/16 网络下能直连却去走中转) # 2. 广播发现重发 + 多网段(原来只发一次、只探一个网段) # 3. 走中转之后能定期回切局域网 # 4. 默认中转地址为空白时,局域网照样能配对、能执行命令 $stageOut = & python (Join-Path $root "_tests\test_lan_optimize.py") 2>&1 Show-Fails $stageOut Record "局域网优化" ($LASTEXITCODE -eq 0) # ---------------------------------------------------------------- 3a3 Banner "3a3/7 手表局域网切换逻辑(源码断言)" $stageOut = & python (Join-Path $root "_tests\test_watch_lan_switch.py") 2>&1 Show-Fails $stageOut Record "手表局域网切换" ($LASTEXITCODE -eq 0) # ---------------------------------------------------------------- 3a4 Banner "3a4/7 编码与换行(.java/.py 必须 UTF-8 无 BOM + LF)" # 踩过的坑:edit 工具会把行尾写成 CRLF,而仓库约定是 LF。 # 这个检查专门盯住,免得改动悄悄把整个文件的行尾换掉。 $stageOut = & python (Join-Path $root "_tests\check_encoding.py") 2>&1 Show-Fails $stageOut Record "编码与换行" ($LASTEXITCODE -eq 0) # ---------------------------------------------------------------- 3a5 Banner "3a5/7 局域网/中转对比基准(防测试自己骗人)" # 这个基准一度是红的,而且报告里写着「这是中转链路的产品 bug」—— # 实际是基准自己有两个 bug:两条路用了同一个配对码、 # 以及 attach 用了旧码(打到已经死掉的设备上)。 # 一个已经修好的产品因此背了黑锅,而且症状(连接超时) # 看起来完全像产品问题,极难自己发现。 $stageOut = & python (Join-Path $root "_tests\test_lan_bench_guard.py") 2>&1 Show-Fails $stageOut Record "局域网对比基准" ($LASTEXITCODE -eq 0) # ---------------------------------------------------------------- 3a6 Banner "3a6/7 管理页 JS 语法(防按钮全死)" # 踩过两次的坑:admin.py 的 ADMIN_JS 是普通三引号字符串, # 里面写 \n 会被 Python 变成真实换行;而 JS 的行注释 // 不能跨行, # 一断行整段