设置窗口 - 改为独立非模态窗口(进任务栏、可最小化),不再被托盘浮窗挡住导致点不到 - 位置改为按屏幕可用区域居中裁剪,不再默认落在右下角 - 三个页签重构为卡片式布局:统一卡片内边距、页签行独立成行、正文左右留白、 帮助文字按实际换行测量高度、标题栏与底栏加分隔线 - 直连 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
68 lines
4.6 KiB
C#
68 lines
4.6 KiB
C#
using System.Xml.Linq;
|
||
using GatewayKeepalive.Windows;
|
||
|
||
namespace GatewayKeepalive.Tests;
|
||
|
||
/// <summary>
|
||
/// 登录自启任务的定义完全靠 XML 表达,错了只会在“下次登录”时才暴露。
|
||
/// 这里把生成出来的 XML 当场验一遍:启动命令、隐藏启动、最高权限、电源与超时设置。
|
||
/// </summary>
|
||
internal static class LoginStartupTests
|
||
{
|
||
private const string SchedulerNamespace = "http://schemas.microsoft.com/windows/2004/02/mit/task";
|
||
|
||
public static Task LogonTaskXmlAsync()
|
||
{
|
||
const string executable = @"D:\Program Files\网关 保活\GatewayKeepalive.exe";
|
||
var root = XDocument.Parse(LoginStartup.BuildTaskXml(executable)).Root!;
|
||
XNamespace ns = SchedulerNamespace;
|
||
|
||
Check.Equal("Task", root.Name.LocalName, "任务定义的根元素应当是 Task");
|
||
Check.Equal(SchedulerNamespace, root.Name.NamespaceName, "任务定义必须使用计划任务的命名空间");
|
||
|
||
var exec = root.Element(ns + "Actions")?.Element(ns + "Exec");
|
||
Check.True(exec is not null, "任务必须有一个启动程序的 Exec 动作");
|
||
Check.Equal(executable, exec!.Element(ns + "Command")?.Value, "动作命令必须是当前可执行文件");
|
||
Check.Contains("--minimized", exec.Element(ns + "Arguments")?.Value ?? string.Empty, "登录启动必须带 --minimized,不能弹出设置窗口");
|
||
Check.Equal(@"D:\Program Files\网关 保活", exec.Element(ns + "WorkingDirectory")?.Value, "工作目录应当跟随可执行文件");
|
||
|
||
var principal = root.Element(ns + "Principals")?.Element(ns + "Principal");
|
||
Check.True(principal is not null, "任务必须声明主体");
|
||
Check.Equal("InteractiveToken", principal!.Element(ns + "LogonType")?.Value, "必须以交互令牌运行,托盘才能出现在桌面上");
|
||
Check.Equal("HighestAvailable", principal.Element(ns + "RunLevel")?.Value, "必须是最高权限,否则每次登录都会弹 UAC");
|
||
Check.False(string.IsNullOrWhiteSpace(principal.Element(ns + "UserId")?.Value), "主体必须绑定当前用户");
|
||
|
||
var trigger = root.Element(ns + "Triggers")?.Element(ns + "LogonTrigger");
|
||
Check.True(trigger is not null, "必须由登录事件触发");
|
||
Check.Equal("true", trigger!.Element(ns + "Enabled")?.Value, "登录触发器必须启用");
|
||
Check.False(string.IsNullOrWhiteSpace(trigger.Element(ns + "UserId")?.Value), "登录触发器必须绑定当前用户");
|
||
|
||
var settings = root.Element(ns + "Settings");
|
||
Check.True(settings is not null, "任务必须带设置节");
|
||
Check.Equal("false", settings!.Element(ns + "DisallowStartIfOnBatteries")?.Value, "电池供电时也要启动");
|
||
Check.Equal("false", settings.Element(ns + "StopIfGoingOnBatteries")?.Value, "切换到电池供电时不能结束进程");
|
||
Check.Equal("true", settings.Element(ns + "StartWhenAvailable")?.Value, "错过触发时间后应当补跑");
|
||
Check.Equal("PT0S", settings.Element(ns + "ExecutionTimeLimit")?.Value, "常驻托盘程序不能设置执行时长上限");
|
||
Check.Equal("IgnoreNew", settings.Element(ns + "MultipleInstancesPolicy")?.Value, "不允许重复实例,避免两个托盘互相改写网络");
|
||
|
||
return Task.CompletedTask;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 晚绑定 COM 抛出的异常类型由 CLR 按 HRESULT 翻译:任务不存在时拿到的是 <see cref="FileNotFoundException"/>,
|
||
/// 不是 <see cref="System.Runtime.InteropServices.COMException"/>。早先只按类型抓 0x80070002,
|
||
/// 于是一次都没抓到——“关闭登录自启”永远在读回时失败,界面报「找不到文件或目录」。
|
||
/// </summary>
|
||
public static Task TaskMissingDetectionAsync()
|
||
{
|
||
Check.True(LoginStartup.IsTaskMissing(new FileNotFoundException()), "文件不存在(0x80070002)应当判定为任务不存在");
|
||
Check.True(LoginStartup.IsTaskMissing(new DirectoryNotFoundException()), "目录不存在(0x80070003)应当判定为任务不存在");
|
||
Check.True(LoginStartup.IsTaskMissing(new System.Runtime.InteropServices.COMException("任务不存在", unchecked((int)0x8004130F))),
|
||
"SCHED_E 的任务不存在代码应当被识别");
|
||
Check.False(LoginStartup.IsTaskMissing(new System.Runtime.InteropServices.COMException("拒绝访问", unchecked((int)0x80070005))),
|
||
"0x80070005 是权限问题,不能静默当成任务不存在");
|
||
Check.False(LoginStartup.IsTaskMissing(new InvalidOperationException("别的错误")), "其他异常不能被当成任务不存在");
|
||
return Task.CompletedTask;
|
||
}
|
||
}
|