登录任务本身没问题(InteractiveToken + HighestAvailable + --minimized), ICACLS 也没有拒绝 ACE;真因是 exe 所在的 D:\dsh workspace 带 Low 强制标签, Windows 用低完整性令牌运行这里的 exe,它写不了中完整性的 %LOCALAPPDATA%, 于是第一步写日志就 UnauthorizedAccessException,托盘起不来。 - FileLog 改为 fail-safe:首选目录失败依次回退 LocalLow / TEMP / ProgramData, 缓存待写行,永不抛异常,暴露 ActiveDirectory 与 LastFailure; - 启动自检增加数据目录可写性探测,致命对话框写出真实日志目录与低完整性判断; - 新增 --logcheck 只读自检:输出程序路径、数据目录可写性、实际日志目录与结论; - README 增加放置目录说明,validation.md 记录第八轮; - 新增两条 FileLog 回归测试(本地 49/49,探测 16/16,Release 0 警告 0 错误)。
17 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,重启后登录自启报“Access to the path … is denied”)
症状:重启电脑后没有托盘图标,只弹一个对话框「程序无法继续:Access to the path 'C:\Users\emm\AppData\Local\GatewayKeepalive\logs\gateway-2026-10-24.log' is denied.」。
排查与真因:
- 计划任务本身没问题:
schtasks /Query /XML显示UserId=S-1-5-21-1629374972-835245509-3346384658-1001、LogonType=InteractiveToken、RunLevel=HighestAvailable、动作GatewayKeepalive.exe --minimized,触发器等登录用户。schtasks /Run能复现同样的对话框。 - 权限表也没问题:日志目录、日志文件、以及
C:\Users\emm\AppData\Local三层icacls全都有DESKTOP\emm:(F),没有被拒绝的 ACE;同样的脚本用同一个账号可以写。 - 真因在所在目录的完整性级别:
D:\dsh workspace被打了 Low 强制标签(icacls→Mandatory Label\Low Mandatory Level:(OI)(CI)(NW)),存在这里的 exe 会被 Windows 用低完整性令牌运行。低完整性进程写不了中完整性的%LOCALAPPDATA%,于是第一步写日志就抛UnauthorizedAccessException。把同一个 exe 复制到%LOCALAPPDATA%\Programs\GatewayKeepalive\再跑,--logcheck立刻变成“数据目录 可写=True / 结论=写入成功”。 - 也就是说:这不是提权问题、不是任务配置问题、不是 ACL 问题,而是“程序放在哪运行”。修了两头——给出正确的放置位置,并让程序不再因为写不了日志而整个起不来。
修法:
FileLog改成 fail-safe:写入先进缓冲区,按「首选目录 →%LOCALAPPDATA%Low→%TEMP%→%ProgramData%」依次尝试,任何一步失败都不抛异常;ActiveDirectory/LastFailure暴露真实落点与失败原因,切换目录时在日志里写一行“日志目录改为 …”。- 启动时探测数据目录是否真的可写(建临时文件再删),自检行里带上「数据目录=… 可写=… 日志目录=…」;致命对话框不再只说一句错误,而是写出实际生效的日志目录与“程序可能放在低完整性目录里”的判断。
- 新增
--logcheck:只用当前身份验证“能不能写日志/数据目录”,不起托盘、不提权,输出「程序路径 / 数据目录 可写 / 首选目录 / 实际目录 / 结论」,方便用户自查。
本轮验证:
- 新增两条回归测试:首选目录可写时就用首选且不记失败;首选目录不可用(被同名文件占住)时不抛异常、回退到可用目录并把失败原因写进
LastFailure。本地测试总数 47 → 49,全绿;探测离线测试 16/16;Release 构建 0 警告 0 错误。 - 实测三种落点:
%LOCALAPPDATA%\Programs\GatewayKeepalive\GatewayKeepalive.exe --logcheck→ 可写=True、实际目录=…\AppData\Local\GatewayKeepalive\logs、写入成功;artifacts\publish(低完整性)下的同一 exe → 可写=False、实际目录=…\AppData\LocalLow\GatewayKeepalive\logs、写入成功(已回退),日志里确有该行。 - 未验证:桌面托盘与登录任务仍需把 exe 换到用户目录后由用户重启确认一次(本机命令行没有提权能力,不能改计划任务的动作路径)。
第七轮修复(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 核对计划,最后通过托盘界面保存并手动启用自动保活。