using System.Xml.Linq; using GatewayKeepalive.Windows; namespace GatewayKeepalive.Tests; /// /// 登录自启任务的定义完全靠 XML 表达,错了只会在“下次登录”时才暴露。 /// 这里把生成出来的 XML 当场验一遍:启动命令、隐藏启动、最高权限、电源与超时设置。 /// 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; } /// /// 晚绑定 COM 抛出的异常类型由 CLR 按 HRESULT 翻译:任务不存在时拿到的是 , /// 不是 。早先只按类型抓 0x80070002, /// 于是一次都没抓到——“关闭登录自启”永远在读回时失败,界面报「找不到文件或目录」。 /// 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; } }