设置窗口 - 改为独立非模态窗口(进任务栏、可最小化),不再被托盘浮窗挡住导致点不到 - 位置改为按屏幕可用区域居中裁剪,不再默认落在右下角 - 三个页签重构为卡片式布局:统一卡片内边距、页签行独立成行、正文左右留白、 帮助文字按实际换行测量高度、标题栏与底栏加分隔线 - 直连 DNS 复选框、下拉框、滚动条改为自绘控件,配色与整体统一 页签 - PanelChip 的选中/强调改为值变化即重绘,修掉“切换页签后旧页签仍显示为激活、 鼠标移上去才更新”的陈旧绘制 滚轮 - 输入框滚不动自己时把滚轮交给外层页面,单行输入框不再吞掉滚轮事件 登录自启 - 改用任务 XML + schtasks 写入,COM 失败时用 schtasks 复核;写入后读回校验 - 任务不存在按 HRESULT 判定(0x80070002 / 0x80070003 / 0x8004130F),修掉 “关闭登录自启必定报错并回滚”(晚绑定 COM 抛的是 FileNotFoundException) - 状态读取 COM 失败时回退到 schtasks,失败按异常类型 + HResult 去重记录 诊断 - 界面报错统一写日志;控制器记录自启读写结果;启动自检记录管理员/自启/进程/参数 发布 - 去掉 -SelfContained 分支,只保留单文件运行时依赖包,脚本与文档同步 - 新增 tools/unblock-publish.ps1,清除发布产物的网络来源标记 测试 - 新增登录自启任务 XML 用例与“任务缺失按 HRESULT 判定”回归测试(46 → 47),探测 16/16
14 KiB
验证记录
日期:2026-10-23(初版)、2026-10-24(第二轮修复)。环境:Windows 10/11 x64,开发机网卡 以太网(索引 16),IPv4 地址 192.168.6.119/24,主路由 192.168.6.1。
第七轮修复(2026-10-24,关闭登录自启必报错:按 HRESULT 而不是按异常类型判断)
症状:点「登录后自动启动」必弹「开机自启设置失败:找不到文件或目录:系统找不到指定的文件。(0x80070002) 已回滚到原设置。」,这个开关因此永远关不掉。
日志给出的因果链(01:20:55):SetEnabled(false) → RemoveTask() 删掉任务 → 读回状态 QueryEnabled() → 抛 System.IO.FileNotFoundException 0x80070002 → 控制器记失败 → 托盘回滚成“已启用” → 界面报错、任务依旧在。
真因:晚绑定 COM(dynamic)抛出的异常类型是 CLR 按 HRESULT 翻译过的——任务不存在时拿到的是 FileNotFoundException,不是 COMException。原代码三处 catch (COMException) when (HResult == 0x80070002) 因此一次都没命中,「任务不存在」被当成真错误抛到了界面。本机实测确认:GetFolder('\').GetTask('不存在的名字') → System.IO.FileNotFoundException / 0x80070002。
修法:新增 LoginStartup.IsTaskMissing(Exception),只认 HResult(0x80070002 / 0x80070003 / 0x8004130F),三处捕获统一改成 catch (Exception) when (IsTaskMissing(e)):删任务、读任务动作、读回状态。
本轮验证:
- 新增回归测试「missing-task detection follows HRESULT, not exception type」:0x80070002 / 0x80070003 / 0x8004130F 三种判定为“任务不存在”,0x80070005(拒绝访问)必须继续上报。本地测试总数 46 → 47,全绿;探测离线测试 16/16。
- Release 构建 0 警告 0 错误(
TreatWarningsAsErrors开启);重新发布artifacts\publish\GatewayKeepalive.exe(545,472 字节)。 - 未验证:真正的“关 → 开 → 关”需要提权点击,命令行侧没有管理员权限,只能由用户在托盘里点一次确认。
第六轮修复(2026-10-24,页签不重绘的真因、自启报错可诊断)
- 页签“同时激活”的真因:
PanelChip.Selected/Emphasized是自动属性,赋值时既不Invalidate()也不UpdateStyles()。切页只是把布尔值改了,像素还停在上一次:看上去两个页签都亮着,直到鼠标移进旧页签触发悬停重绘才“追上”状态——这正是“鼠标放上去才更新”。第五轮以为是配色太接近,只改了外观,所以问题照旧。现在两个属性改成显式字段 + 值变化时Invalidate(),SelectTab再对全部页签补一次重绘,切页即刻同步。 - 自启报错为什么一直查不到:
ToggleAutostart失败只弹提示、不写日志,日志里没有任何证据。现在四处留痕:控制器AutostartEnabled/SetAutostart记录成功与失败(含异常全文)、ReportError把界面上每条错误写进日志、SetEnabled入口记录「启用方向 + 是否管理员 + 进程路径」、启动时写一行启动自检:管理员=… 登录自启=… 进程=… 参数=…。下次再报错,日志里直接就有原因。 --preview输出目录:目标目录不存在时先创建,不再静默地什么都不生成。
本轮验证:
- Release 构建 0 警告 0 错误(
TreatWarningsAsErrors开启);核心测试 46/46、探测离线测试 16/16。 - 重新发布
artifacts\publish\GatewayKeepalive.exe(545,472 字节)+diagnostics/;--preview指向一个不存在的目录也能正常产出 10 张图。 - 未验证:页签切换需要真人在新进程里点一次——离屏渲染每次整窗重绘,天生看不到“该重绘却没重绘”这类问题;自启报错的具体原因要等下一次点击后看日志。
第五轮修复(2026-10-24,页签单选语义、滚轮归属、自启状态读回)
- 页签“同时激活”:选中态原来是半透明强调底 + 彩色描边、深色字没变,和悬停态(只换底色、文字变亮)几乎一样——鼠标一经过,就有两个页签看起来都是“当前页”。现在选中项是实心 Accent 底 + 深色字(和“保存”主按钮同一套),悬停只给一层很浅的中性底、永不用强调色,未选中项既无底色也无描边。单选语义靠形状和颜色一起表达,不靠深浅差别。注意:配色不是真因,见第六轮。
- 鼠标停在输入框上页面滚不动:滚轮让页逻辑原来只挂在多行框上,单行框(网关、DNS、端口、最少成功数)照样把滚轮吃掉。现在任何输入框“自己滚不动”时都把滚轮让给外层页面;页面本来不需要滚动时不吃事件,继续向上冒泡。
- 自启状态读错:任务计划 COM 偶尔读不到刚注册好的任务(同一台机器上
schtasks却看得见),界面就把“已启用”显示成关闭,用户再保存一次又会重复注册任务。现在 COM 读不到时用schtasks /Query /TN … /XML复核;两条路都读不到才报错,并且同一个失败签名只往日志里写一次,不再每个检查周期刷屏。 - 启动时的两次提示:第一次是 exe 带“来自网络”标记时 Windows 的“未知发布者”安全警告——当前发布目录里的 exe 已无该标记(
Get-Item -Stream *只有:$DATA),发布脚本产出的是本机新建文件,不会再带;第二次是 UAC,改路由和 DNS 必须提权,系统强制,程序无法绕过。
本轮验证:
- Release 构建 0 警告 0 错误(
TreatWarningsAsErrors开启)。 - 核心测试 46/46、探测离线测试 16/16。本次运行环境不再限制文件写入,此前因沙箱拒绝
File.Replace而失败的 24 项全部通过。 - 重新发布
artifacts\publish\GatewayKeepalive.exe(544,960 字节)+diagnostics/;--preview出 10 张图,三页设置与托盘面板逐张核对,docs/screenshots/已更新。 - 未验证:页签悬停态与输入框上的滚轮需要真人用鼠标确认(离屏渲染只能看到未悬停的画面)。
第四轮修复(2026-10-24,独立复查后加固)
一次只读代码复查(未看本轮改动过程)在本轮改动里找出了四处真实缺陷,已全部修掉:
- 保存失败会留下一个关不掉的窗口:
SettingsForm.SaveAsync出错时把_closing留在true,之后的关闭按钮和 Esc 都不再响应。改为保存失败时复位标志;窗口/页面已释放时(保存回调可能在Dispose之后触发)所有 UI 更新都先判IsDisposed。 - 自绘画笔泄漏:
PanelChip选中态背景的SolidBrush没释放,每次重绘漏一个 GDI 句柄(面板常驻,长时间运行会累积)。 - 滚轮“跳一格再拖会弹回”:
DarkScrollBar页跳后没有重置拖动基准,拖动被算成从跳转前的位置开始。页跳后同步基准值。 - 多行输入框吞掉滚轮:文本框能滚就归它,滚不动时把滚轮让给外层页面(
FieldBox→DarkScrollPanel,同一档步长),不会再出现“鼠标停在多行框上页面滚不动”。 - 另外一并加固:
schtasks改为并发读取 stdout/stderr 后再等退出(顺序ReadToEnd会被写满的 stderr 缓冲卡住,20 秒超时形同虚设);读取任务动作时释放全部 COM RCW;RepairIfStale除了比对路径也比对--minimized参数;校正任务改为后台线程执行,不再拖慢托盘点亮;页面栈在布局时清理已释放的项;WrapTextLabel改用TextRenderer绘制并用TextRenderer.MeasureText量高(绘制与测量同一条路径)。
本轮验证:
- Release 构建 0 警告、0 错误(
TreatWarningsAsErrors开启)。 - 新增回归测试「登录自启任务 XML」:断言动作命令、
--minimized、工作目录、InteractiveToken、HighestAvailable、电源与超时设置、单实例策略。本地测试总数 45 → 46。 - 探测离线测试 16/16;核心测试在本机沙箱内 22 通过 / 24 失败,失败项与第二轮完全相同(沙箱拒绝
File.Replace等文件写入),新增用例通过。 - 发布:单文件约 543 KB +
diagnostics/;已发布 exe 的--preview输出 10 张界面图,只读诊断--diagnose返回readOnly=true、网关192.168.6.1、接口索引 16。 - 界面:三页设置与托盘面板重新渲染后逐张核对,截图已更新到
docs/screenshots/。
第三轮修复(2026-10-24,界面细节与启动体验)
- 页签误判“同时激活”:
PanelChip未选中时改成既无底色也无描边(只有悬停/按下才浮现),选中项才有强调底色与强调色文字,单选语义在视觉上一眼可辨。 - 设置窗口进入任务栏:改为非模态独立窗口(
Show()+ShowInTaskbar=true),不再依附托盘浮窗;打开设置也不再让整个程序处于忙碌状态。 - 启动提示与启动反馈:手动启动(非
--minimized)一定弹出面板,避免“以为没启动→反复双击→反复弹 UAC”;新增tools/unblock-publish.ps1清除发布产物的“来自网络”标记,README 说明两次系统提示的来源(未签名安全警告 + UAC 提权)。 - 多行输入框与滚轮:
FieldBox只在内容真的放不下时才挂竖条;DarkScrollPanel命中“有竖条的多行框”时把WM_MOUSEWHEEL转发给它,页面不需要滚动时不再吞掉滚轮事件。 - 复查:Release 构建 0 警告 0 错误;预览重新渲染后逐张核对(三页页签、探测目标多行框不再出现残留方块)。
第二轮修复(2026-10-24)
针对反馈的五个问题:
- 设置界面重做:左侧分段标签改成自适应宽度的小页签,卡片圆角/内边距/行距统一,输入框改为自绘圆角容器(去掉系统浅色边框与原生滚动条),复选框改为自绘勾选行,标题区与底栏各有一条分隔线,窗口默认 720×620 并随 DPI 缩放。
- 设置窗口位置:不再
ShowDialog(owner)走CenterParent(原先会被托盘浮窗挤到屏幕右下角并压在浮窗下面),改为按鼠标所在屏幕居中,OnShown再校正一次。 - 设置打开时可以退出:点“退出程序”会先请求设置窗口关闭,
OpenSettingsAsync结束后自动继续退出流程;保存进行中则等保存完成后再退出。 - 登录自启重写:任务 XML 交
schtasks /Create /XML创建,失败回退任务计划 COM;两条路径都在创建后读回Enabled校验,任一失败都会带上系统返回原文报错并写日志。启动时若发现任务指向的程序路径与当前可执行文件不一致,会自动改写(需管理员)。 - 发布只看依赖运行时版:
build.ps1去掉-SelfContained,只产出单文件 +diagnostics/,发布命令加-p:NuGetAudit=false(本项目零外部包,离线也能发布)。
本轮验证:
- Release 构建 0 警告、0 错误。
- 探测离线测试 16/16 通过。
- 核心测试在本机沙箱内 21 通过 / 24 失败:失败项均为
UnauthorizedAccessException/IOException发生在File.Replace等文件写入路径上,是当前受限运行环境拒绝文件替换导致,与本轮改动无关;在正常桌面会话下需复跑确认。 - 发布:
artifacts/publish/GatewayKeepalive.exe(约 524 KB 单文件)与diagnostics/GatewayKeepalive.Cli.exe均正常;托盘 exe--preview输出 10 张界面图,只读诊断--help正常。 - 界面:三页设置与托盘面板四态均重新渲染并逐张检查,截图已更新到
docs/screenshots/。
已实际验证(第一轮,2026-10-23)
- 解决方案 Debug/Release 构建:0 警告、0 错误(
TreatWarningsAsErrors已开启)。 - 核心离线测试:45/45 通过,覆盖状态机计数与冷却、切换故障补偿、外部修改保护、跨网络恢复拒绝、无变化跳过写入、持久的暂停状态、Stop 恢复基线、写入前现场复核、原子 JSON 持久化与原生结构布局。
- 探测离线测试:16/16 通过,覆盖 SOCKS5 分片/截断/慢读取消/多种地址类型、DNS 压缩与 TCP 回退、畸形响应、指定 DNS 与系统 DNS 分离、旁路由候选不依赖当前出口、按实际网关选择出口探测目标。
- 只读诊断:成功读取网卡、IPv4 默认路由、DNS 配置和有效选路;
NativeLayout为 104/168/64 字节,路由 metric 偏移 84,接口 metric 偏移 148,与 x64 SDK 结构一致。运行前后系统网关保持192.168.6.1。 - 切换计划预演(
--dry-run --proxy):生成目标 metric 1 的旁路由默认路由,原主路由降为 metric 101,DNS 指向192.168.6.2,未写系统。 - 真实分层探测:主路由 TLS 出口 2/2 通过;
192.168.6.2的 TCP/DNS/SOCKS 探测超时,判为旁路由不可用,符合当前设备实际状态。 - 发布包:单文件约 524 KB(依赖 .NET 10 桌面运行时);托盘
--preview能启动并输出 10 张界面图,无异常输出;只读诊断通过。 - 界面预览渲染:主面板正常/故障回退/未配置、诊断展开,设置三页及滚动后状态,均在 200% 缩放下检查过无裁切、无浅色系统控件。
未验证,需真机确认
以下都需要真实管理员的网络写入,本次刻意未执行,以免改变用户当前网络:
- 实际切换到旁路由并读回路由/DNS/出口;旁路由断开后的自动回退;恢复后回切旁路由。
- 旁路由 DNS 正常但默认转发失效、SOCKS5 正常但转发失效等不对称故障。
- VPN 或其他网卡同时持有默认路由、DHCP 续租、拔插网线、休眠唤醒后的行为。
- 登录后最高权限计划任务的创建、注销/重启后启动(本轮重写后仍需要真机管理员账号确认一次)。
- 崩溃后重启识别未完成切换日志的实际恢复流程。
首次在目标机启用前,建议先运行发布的 diagnostics/GatewayKeepalive.Cli.exe --diagnose,确认网卡 GUID、主/旁路由和 DNS 与计划一致,再用 --dry-run 核对计划,最后通过托盘界面保存并手动启用自动保活。