Files
Gateway-keepaliveForDotnet/docs/validation.md
T
chuzhongzai 545d96174d 修复登录自启的权限问题:低完整性目录、日志兜底与数据目录自检
登录任务本身没问题(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 错误)。
2026-10-24 11:18:09 +08:00

136 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 验证记录
日期: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` 核对计划,最后通过托盘界面保存并手动启用自动保活。