任务计划程序的任务不执行、以 0x1 结束 ── 原因排查与安全的运维设计

· · 任务计划程序, Windows, PowerShell, 业务自动化, 批处理, 运维, 故障排查, 技术咨询

「想让用 PowerShell 写的汇总脚本每天早上 6 点自动运行」「手动运行明明没问题,一放进任务计划程序就跑不动」。在接业务自动化的相谈案件时,最后几乎都会遇到这个话题。

本博客也接连写过几篇自动化相关的文章,包括日志整理的自动化用 Pester 为脚本写测试从 C# 执行 PowerShell用 Power Automate 自动化业务。这些文章最终都以「用任务计划程序定期执行」为前提。然而,任务计划程序本身其实是一套个性相当强烈的机制,「手动能跑,但定期执行就失败」「不知不觉间停止运行却没人发现」这类事故一直没有断过。

本文将任务计划程序机制中直接关系到运维事故的部分──执行账户与登录类型、「不执行」时的排查方式、返回值 0x1 的典型原因、日志留存方式、多重启动的控制──按实务上容易碰壁的顺序整理出来。

1. 先下结论

  • 任务计划程序的大多数问题,并不是出在脚本本身,而是源自对「以谁的身份、在什么样的会话中运行」理解上的落差。一旦选择「不管用户是否登录都要运行」,就必须以「会在与交互式会话、登录时环境完全不同的世界中运行」为前提来设计。1
  • 排查「不运行」的问题,起点是「历史记录(History)」选项卡与事件日志。但任务历史记录默认是禁用的,因此在正式上线前,务必先启用「为所有任务历史启用」。2
  • 「上次运行结果」中显示的 0x1,并不是任务计划程序本身的错误,而是表示所启动的程序自己返回了退出代码 1。原因出在脚本那一侧,所以要先设计好退出代码,并建立自行留存日志的机制。3
  • PowerShell 脚本的基本调用方式是 -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "完整路径",脚本内部的路径则以 $PSScriptRoot 为基准。「起始于(可选)」一栏不能加引号,这个经典陷阱也常常在这里踩到。
  • 要避免因修改密码而让任务悄悄失效的事故,需要在执行账户的设计上下功夫(盘点服务账户,若在域环境则考虑采用 gMSA)。4
  • 多重启动的控制(默认为「不启动新实例」)、执行时限(默认 3 天)、电源条件(默认仅在 AC 电源时运行),都是最容易在没注意到的情况下就沿用默认值上线的代表性设置。注册任务时请务必逐一明确决定。5

2. 任务计划程序的结构 ── 触发器・操作・条件・设置

任务计划程序的任务,大致由 4 个要素构成。

要素 内容 容易引发事故的地方
触发器 何时启动(时刻、登录时、事件发生时等) 错过时刻触发时的处理方式(后述的 StartWhenAvailable)
操作 执行什么(程序、参数、起始文件夹) 参数加引号的方式、起始文件夹指定错误
条件 是否处于允许运行的状况(电源、网络、闲置) 默认启用「仅在使用 AC 电源时」
设置 运行中的行为(多重启动、时限、重试) 未确认默认值就直接上线

用 GUI(taskschd.msc)创建的任务,可以导出为 XML。如果想用 Git 管理任务定义,或是要把同一个任务部署到多台机器,建议用 XML 导出 + schtasks /Create /XML,或是用 PowerShell 的 ScheduledTasks 模块(New-ScheduledTaskAction / New-ScheduledTaskTrigger / New-ScheduledTaskSettingsSet / Register-ScheduledTask)把它写成脚本。5

$action   = New-ScheduledTaskAction -Execute 'pwsh.exe' `
    -Argument '-NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\Jobs\Cleanup-Logs.ps1"' `
    -WorkingDirectory 'C:\Jobs'
$trigger  = New-ScheduledTaskTrigger -Daily -At '06:00'
# 电源条件的默认值是「仅在使用 AC 电源时启动・切换为电池供电时停止」。
# 如果是也要在笔记本电脑或现场终端上运行的任务,就要在这里明确允许
$settings = New-ScheduledTaskSettingsSet -StartWhenAvailable `
    -MultipleInstances IgnoreNew `
    -ExecutionTimeLimit (New-TimeSpan -Hours 2) `
    -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
# 为避免密码显示在屏幕上,通过 Get-Credential 获取
$cred = Get-Credential -UserName 'DOMAIN\svc-batch' -Message '执行账户的凭据'
Register-ScheduledTask -TaskName 'KS-Cleanup-Logs' `
    -Action $action -Trigger $trigger -Settings $settings `
    -User $cred.UserName -Password $cred.GetNetworkCredential().Password

只要任务定义变成代码,就能把在验证机上测试过的内容原封不动地带到正式环境,也能避免「不知道现在是用什么设置在跑」这种状态。

如果是部署到多台机器,把在 GUI 上建好的任务导出成 XML、再用 schtasks 分发,也是实际验证过可行的方法。

rem 在验证机上导出已创建好的任务
schtasks /Query /TN "KS-Cleanup-Logs" /XML > KS-Cleanup-Logs.xml

rem 在各台机器导入(执行账户与密码在注册时指定)
schtasks /Create /TN "KS-Cleanup-Logs" /XML KS-Cleanup-Logs.xml /RU DOMAIN\svc-batch /RP *

XML 里包含了触发器、条件、设置的所有内容,只要放进代码库,就能对任务定义做审查与差异管理。反过来说,靠人工在 10 台机器上用 GUI 分别注册同一个任务,一定会产生「只有一台设置不一样」的走样任务。建议在台数还没到两位数之前,就先把它变成代码。

另外,本文的内容以 Windows 10 / 11、Windows Server 2016 之后的任务计划程序(Task Scheduler 2.0 系列)为前提。如果环境中还残留着更古老、源自 at 命令的任务,请先从盘点它们开始。

3. 执行账户与登录类型 ── 最容易出事的地方

在任务属性中选择的「仅当用户登录时才运行」「不管用户是否登录都要运行」,在内部其实对应的是登录类型(LogonType)的选择。如果对这里的理解有落差,就会在「手动能跑、定期执行却不动」这类问题上,说不出真正原因而一直绕圈子。1

3.1 三种模式的差异

选项 内部机制 特点・限制
仅当用户登录时才运行 交互式令牌(InteractiveToken) 登录期间屏幕上可看到窗口。注销期间则根本不会启动
不管用户是否登录都要运行 密码保存(Password) 注册时会保存密码。屏幕上看不到(非交互)。修改密码后会导致启动失败
同上+勾选「不保存密码」 S4U 不保存密码,但相对地无法访问网络上的资源,也无法访问加密文件(EFS)1

实务上常见的典型事故如下。

  • 用「不保存密码」(S4U)注册一个访问共享文件夹的脚本。本地测试时能跑,但生产环境中偏偏只有访问共享文件夹这一步会失败。→ 原因是 S4U 没有网络凭据。
  • 用「不管用户是否登录都要运行」注册后过了几个月,域密码到期而修改了密码。此后任务就一直卡在登录失败(0x8007052E),却没有任何人发现。
  • 用「不管登录都要运行」注册一个启动 GUI 应用程序的任务。应用程序其实已经启动,但界面完全不会显示在任何地方,因而被误以为「没有在跑」。→ 原因是它是在非交互式会话中运行。需要交互界面的处理,原则上在这种设置下是无法运行的。

另外,以 Password / S4U 运行的账户,需要具备「作为批处理作业登录」的权利(SeBatchLogonRight)。Administrators 组默认就有这项权利,但如果打算把普通用户账户当作服务账户使用,就要另外确认本地安全策略的设置。6

3.2 该用哪个账户运行

  • SYSTEM:不需要管理密码,权限强大,但也正因为权限太强而要小心。用在仅限本机的维护处理上很方便,但不应该把所有会碰触业务数据的任务都交给 SYSTEM 运行。关于是否真的需要管理员权限的思考方式,已在另一篇文章「Windows 应用程序什么时候真的需要管理员权限」中整理过。
  • 专用的服务账户(普通用户):可以做到最小权限,但相对地每次修改密码时都需要更新任务设置。请把密码有效期与任务盘点放在一起管理。
  • gMSA(组托管服务账户):在域环境中是首选。因为密码由域控制器自动管理,「修改密码导致任务失效」这个问题本身就不存在了。任务计划程序支持以 gMSA 运行任务。4

另外,勾选「使用最高权限运行」的意思是:会使用 UAC 拆分出的两个令牌中、管理员一侧(已提升权限)的令牌来运行。不需要管理员权限的任务,请不要勾选这个选项。

4. 排查「不运行」问题的步骤

4.1 先启用历史记录,再开始怀疑

任务计划程序右侧窗格中的「为所有任务历史启用」,默认是禁用的。如果历史记录一直保持禁用,连「运行失败」这个事实本身都不会留下任何记录。上线前务必先启用,并让自己能同时查看事件日志(事件查看器 → 应用程序和服务日志 → Microsoft → Windows → TaskScheduler → Operational)来确认状况。2

排查的基本步骤,直接照着 Microsoft 的故障排除指南走就已经很够用。2

  1. 先单独测试脚本 ── 在放进任务之前,先在与执行账户相同的条件下(可能的话用 runas 或验证机)确认脚本本身能够顺利跑完。
  2. 查看状态列与历史记录选项卡 ── 区分究竟是根本没被触发,还是启动了却失败。如果没有被触发,就要怀疑触发器设置或条件(电源、网络),并用手动运行(右键 → 运行)确认操作本身能不能跑。
  3. 临时改成「仅当用户登录时才运行」试试看 ── 如果这样就能跑,就可以把原因锁定在非交互式会话或凭据(前一章)上。

4.2 如何解读「上次运行结果」

显示值 含义
0x0 正常结束(启动的程序返回退出代码 0)
0x1 启动的程序返回退出代码 1(并非任务计划程序本身的错误)
0x41300 正在等待下次计划的运行(SCHED_S_TASK_READY)
0x41301 当前正在运行(SCHED_S_TASK_RUNNING)
0x41303 尚未运行过一次(SCHED_S_TASK_HAS_NOT_RUN)
0x8007010B 起始位置(「起始于(可选)」)的指定不正确。加了引号时的典型症状
0x8007052E 登录失败。可能是已保存的密码过期、权限不足等

0x413xx 系列是任务计划程序自身的状态代码,0x8007xxxx 是 Windows 的错误代码,而像 0x10x2 这种小数值,则是被启动程序自身的退出代码。3 只要能分清楚这几种,就不会从一开始就选错排查的方向(是任务设置,还是脚本本身)。

4.3 留意条件与设置的默认值

  • 电源条件:默认会启用「只有在计算机使用交流电源时才启动此任务」。如果用笔记本电脑当验证机,就会变成只有在电池供电时才不动、变成一个「无法重现的问题」。而且从 Windows 10 开始,电池节能模式启用期间,许多任务的触发器都会被延迟。7
  • 错过开始时间时:如果电脑当时处于关机状态、错过了开始时间,默认要等到下一次计划时间才会运行。要明确地启用「如果错过计划的启动时间,立即启动任务」(-StartWhenAvailable),或是先在设计上决定这是不是一个「可以被跳过也无妨」的任务。5
  • 唤醒:如果是在夜间运行、电脑平时会进入睡眠的环境,也要决定是否需要启用「唤醒计算机运行此任务」(-WakeToRun)。

4.4 触发器设计本身的注意事项

有时候「以为不运行」,其实是触发器的设计本身就和原本的意图不一致。

  • 「每月 31 日」在没有 31 日的月份就不会运行。 如果是月末处理,比较安全的做法是改成「每月最后一天」的设计意图(例如在月初处理上个月的数据,或是在脚本内自行判断日期)。
  • 时刻是注册该任务的那台机器的本地时间。 如果把同一份 XML 部署到海外分部的机器,或极少数以 UTC 设置运行的服务器上,各分部的实际运行时刻就会不一致。请事先以规格明确定义「所有分部都在日本时间早上 6 点」还是「各分部当地时间早上 6 点」。
  • 如果开始用任务计划程序做短间隔的重复运行(例如每 5 分钟),就要留意。 它作为「一天一次的批处理」工具非常优秀,但如果开始需要分钟级的轮询或常驻监控,那就已经进入常驻进程的范畴了(后述第 8 章)。
  • 事件触发器很强大,但要先确认目标事件是否真的能稳定被记录下来。 以应用程序日志中特定事件 ID 作为触发器的架构,一旦应用程序更新导致事件的出现方式改变,就会在毫无预警的情况下失效。改用时刻触发器+在脚本内自行做条件判断,结果上往往反而更容易追踪。

5. 以 0x1 结束的典型模式与 PowerShell 的正确调用方式

0x1 只是「脚本失败了」这个结果本身,原因其实在于脚本运行环境的差异。手动运行与定期运行之间,主要有以下几点不同。

  • 当前工作目录不同:如果没有指定「起始于(可选)」,就会在 C:\Windows\System32 之类的目录下运行。用相对路径写的脚本会在这里出问题。脚本内部要以 $PSScriptRoot 为基准组出路径,任务那一侧则在「起始于(可选)」一栏指定工作文件夹。这时候「起始于(可选)」一栏不能加引号,即使路径中含有空格,也要不加引号地写(加了就会以 0x8007010B 失败)。
  • 环境变量・配置文件不同:登录脚本或用户配置文件所设置的环境变量、已映射的网络驱动器(X: 等),请视为在非交互式会话中并不存在。应该直接使用 UNC 路径(\\server\share\...),并用 -NoProfile 排除配置文件造成的差异。
  • 执行策略不同:即使已经为用户设置了 RemoteSigned,服务账户那边也可能完全没有设置。要在任务的参数中明确加上 -ExecutionPolicy Bypass
  • 工具本身的退出代码约定特殊:例如 robocopy 在「有文件需要复制并成功复制」的情况下会返回 1。如果用直接回传退出代码的包装脚本,就会出现「明明正常却看起来像 0x1」,或反过来的情况。使用的外部命令,其退出代码约定请务必事先确认。

PowerShell 调用的基本形式如下。

程序或脚本:            pwsh.exe          (若为 Windows PowerShell 则为 powershell.exe)
添加参数:             -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "C:\Jobs\Cleanup-Logs.ps1"
起始于 (可选):         C:\Jobs           (不加引号)

使用 -File 而不是 -Command,除了参数转义比较简单之外,还因为脚本里的 exit n 会直接成为进程的退出代码,才能从任务计划程序的「上次运行结果」判断成败。脚本内部也要设计成:成功时明确返回 0,失败时明确返回非 0。

# 为了让 Cmdlet 的「非终止错误」也能被 catch 捕获,设置为 Stop。
# 如果不设这个,Copy-Item 等命令失败时可能会被直接放过,结果变成 exit 0
$ErrorActionPreference = 'Stop'

try {
    Main
    exit 0
}
catch {
    Write-Error $_
    exit 1
}

关于该在哪里捕获异常、又该如何记录的思考方式,也已在另一篇文章「异常的捕获与日志设计」中详细说明过。

6. 日志要自己留存

任务计划程序的历史记录,最多只能告诉你「有没有启动、退出代码是多少」。至于脚本实际做了什么、做到哪一步,就必须由脚本自己留下日志记录。

最基本的做法,不是在任务的参数栏里做重定向(因为任务计划程序的「参数」栏并不会通过 Shell 执行,重定向语法不会生效),而是在脚本内部直接获取事务日志记录,这样比较省事。

$logDir = 'C:\Jobs\logs'
New-Item -ItemType Directory -Path $logDir -Force | Out-Null
Start-Transcript -Path (Join-Path $logDir ("cleanup-{0:yyyyMMdd}.log" -f (Get-Date))) -Append
try {
    # 主要处理
}
finally {
    Stop-Transcript
}

至于日志本身持续累积的问题(版本管理、归档),正好可以直接沿用「PowerShell 脚本进阶 ── 安全地实现日志排查、归档与报表自动化」一文中的内容。

若想再进一步,也可以考虑写入 Windows 事件日志。与文件日志不同的优点是,成败结果会直接送到运维人员原本就已经在看的地方(事件查看器、现有的监控工具)。

# --- 只在安装时执行一次,且需要管理员权限(安装程序或初始化脚本)---
if (-not [System.Diagnostics.EventLog]::SourceExists('KS-Jobs')) {
    [System.Diagnostics.EventLog]::CreateEventSource('KS-Jobs', 'Application')
}

# --- 任务主体(以最小权限账户运行)只负责写入。
#     SourceExists 在搜索所有日志时可能需要管理员权限,运行时不要调用它。
#     以下假设是在 Main 的失败处理器(catch)内调用 ---
catch {
    $err = $_
    try {
        [System.Diagnostics.EventLog]::WriteEntry('KS-Jobs',
            "Cleanup-Logs failed: $($err.Exception.Message)",
            [System.Diagnostics.EventLogEntryType]::Error, 1001)
    }
    catch {
        # 不能因为写不进事件日志,就把原始的失败吞掉不管
        Write-Warning "写入事件日志失败: $_"
    }
    exit 1
}

这里补充两点。首先,事件源的注册(CreateEventSource)需要管理员权限。如果把注册处理混进任务主体,生产环境中以最小权限服务账户运行、第一次失败时就会变成「想注册却先抛出异常 → 结果连真正重要的事件都写不进去」的双重失败。请像上面那样把注册独立到安装阶段处理,运行时只负责写入。其次,如果像本文的任务注册示例一样把执行引擎设为 pwsh.exe,就无法使用 Windows PowerShell 5.1 时代的 New-EventLog / Write-EventLog Cmdlet(会因为找不到命令而让整个脚本失败)。像上面那样直接调用 .NET 类,则不论 5.1 或 7 都能运行。

在这之上,再加入一套「失败时能通知到人」的机制──例如发送邮件或 Teams / Slack 通知──就能避免「盘点时才发现已经停了好几个月」这类事故。不需要多复杂的通知架构,光是在失败时 POST 到 Webhook 的几行代码就已经很够用。反过来说,「每次成功都通知」迟早会没人看,因此建议把成功通知控制在周报级别即可,并把「失败」与「没有运行」(上次运行时间过旧)当作检测目标。至于日志该写些什么,判断标准也已在「异常的捕获与日志设计」中整理过。

7. 多重启动与长时间运行的控制

如果上一次运行还没结束,下一个计划时刻就到了,会发生什么事?这由「设置」选项卡中的「如果任务已经在运行,则应用以下规则」决定,对应到 PowerShell 中的 -MultipleInstances5

设置值 行为 适合的用途
IgnoreNew(GUI 默认:不启动新实例) 运行中则跳过新的启动 具幂等性的一般定期批处理。优先选这个
Queue 运行中则在结束后依次运行 不容许漏算的汇总类任务
Parallel 并行启动 原则上应避免。只有能保证并行安全时才用

同时建议把「停止之前的时间」(-ExecutionTimeLimit,默认 3 天)设成符合现实的数值(大约是预估运行时间的 2〜3 倍),可以避免进程卡死之后把第二天的任务也一起拖垮。5

需要注意的是,IgnoreNewQueue 保护的范围,仅限于同一个任务定义之内。如果是另一个任务调用了同一个脚本,或是人员在故障处理时手动运行造成的冲突,这些设置是防不了的。如果有多条路径会碰触同一份资源(文件、数据库、外部系统),就要在脚本这一侧也加上互斥机制。惯用做法是使用命名的 Mutex。

$mutex = New-Object System.Threading.Mutex($false, 'Global\KS-Cleanup-Logs')
if (-not $mutex.WaitOne(0)) {
    Write-Warning '因为有其他实例正在运行,所以结束。'
    exit 0   # 如果「没有运行」不算失败就用 0,若要当成失败则用非 0
}
try {
    # 主要处理
}
finally {
    $mutex.ReleaseMutex()
}

只要在名称前面加上 Global\,即使是在不同的会话之间(例如不同用户的任务与手动运行),互斥也能生效。是要等待获取锁(在 WaitOne 传入超时时间),还是要立刻放弃,请依任务的性质决定。另外要注意,Global\ 开头的命名对象在整台机器上任何人都看得到。在有多个用户登录的共享服务器上,如果因恶意或意外先抢到了同名的 Mutex,任务就会永远被跳过(而且因为是 exit 0,看起来还会像是正常结束)。在这类环境中,建议为 Mutex 设置 ACL(MutexSecurity)以限制能获取它的账户,或至少把「因获取失败而跳过」这件事,一并送进前一节提到的通知与事件日志中,让监控能检测到连续被跳过的情况。通过文件进行联动时的互斥控制,已在「文件集成的互斥控制基础」中详细说明过。

8. 任务计划程序的「该收手的时机」 ── 与常驻服务的取舍

任务计划程序并不是万能的。当需求逐渐成长时,有一条界线是与其硬撑着继续用,不如换一套机制反而更好。

需求 适合的机制
一天数次以内的定时批处理 任务计划程序
启动时机混杂了人・事件・时刻,且想呈现整个流程 Power Automate(另一篇文章
以分钟级轮询、常时监控、队列处理 Windows 服务/常驻进程
需要在多次处理之间保留状态、想精细控制重试与退避策略 Windows 服务/常驻进程

一旦开始用「每 5 分钟一次的任务」来做轮询,每次启动都要付出创建进程、加载模块的成本,还得额外做一套把上一次状态存到文件之类的机制,实质上等于是把常驻进程拆成一小段一小段重新实现。走到这个阶段,直接改用 .NET 的 Generic Host 与 BackgroundService 做成常驻服务会比较干脆。实现模式已在「在桌面应用程序中使用 Generic Host 与 BackgroundService」中说明过。

反过来说,把月结、日结这种批处理特意改成服务、还自己管理定时器,也是一种过度设计。大致的判断原则是:只要符合「运行间隔在一小时以上・处理彼此独立・不需保留状态」,就用任务计划程序;一旦开始不符合这些条件,就该考虑改成常驻化,这样的界线大致不会出错。

9. 上线前的检查清单

建议在注册任务之前,依次逐一确认以下项目。

  • 是否已确定执行账户(是不是随手选了 SYSTEM?如果是域环境,有没有考虑过 gMSA?)
  • 是否理解登录类型的限制(S4U 无法访问网络;若使用 Password,是否已确定好修改密码时的运维方式)
  • 是否已在等同于执行账户的条件下单独测试过脚本
  • 是否以 -NoProfile -NonInteractive -ExecutionPolicy Bypass -File 的形式调用
  • 脚本内的路径是否以 $PSScriptRoot / UNC 为基准(是否还依赖映射驱动器・相对路径)
  • 「起始于(可选)」一栏是否没有加引号
  • 是否已设计退出代码(成功为 0/失败为非 0,且已确认外部命令的退出代码约定)
  • 是否已启用任务历史记录?是否已有脚本自身的日志与失败通知
  • 是否已明确设置电源条件・StartWhenAvailable・多重启动・执行时限
  • 任务定义是否已用 XML 或 PowerShell 脚本的形式存进代码库

10. 总结

任务计划程序并不是「写完脚本就结束」,而是要先把执行账户、会话、默认值这三个前提都设计好,才会真正进入稳定运维的状态。反过来说,只要在注册时把本文提到的重点──登录类型的选择、启用历史记录、退出代码与日志的设计、明确指定多重启动与时限──一次确实掌握好,之后就会出乎意料地不太需要再费心。

「手动能跑、但定期执行却不动」,几乎可以确定原因就在会话与环境的差异上。在盲目调整设置之前,请先在历史记录选项卡确认运行到了哪一步,并依次试试本文提到的排查步骤。

相关文章

相关咨询领域

小村软件有限公司承接以 PowerShell、任务计划程序为基础的业务自动化设计评审,以及协助重建那些「虽然还在跑、但已经没人能修」的定期任务。

参考链接

  1. Microsoft Learn,logonType Simple Type (Task Scheduler)。登录类型的定义。关于 S4U 不保存密码、但相对地无法访问网络及加密文件。  2 3

  2. Microsoft Learn,Troubleshoot issues with scheduled tasks not running。「先单独测试脚本 → 确认状态与历史记录 → 更改安全选项」的排查步骤,以及 TaskScheduler Operational 事件日志的所在位置。  2 3

  3. Microsoft Learn,Task Scheduler error and success constants。SCHED_S_TASK_READY (0x41300)、SCHED_S_TASK_RUNNING (0x41301)、SCHED_S_TASK_HAS_NOT_RUN (0x41303) 等状态与错误代码的定义。  2

  4. Microsoft Learn,Manage group Managed Service Accounts。gMSA 的密码可由域端自动管理,以及任务计划程序支持以 gMSA 运行任务。  2

  5. Microsoft Learn,New-ScheduledTaskSettingsSet。任务设置对象的参数,包括 MultipleInstances(Parallel/Queue/IgnoreNew)、StartWhenAvailable、ExecutionTimeLimit(默认 3 天)等。  2 3 4 5

  6. Microsoft Learn,Security Contexts for Tasks。任务的安全上下文,以及以 Password / S4U 注册的任务在运行时需要「作为批处理作业登录」权利。 

  7. Microsoft Learn,What’s New in Task Scheduler。关于从 Windows 10 开始,电池节能模式启用期间,非交互式任务的触发器会被延迟。 

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

常见问题

汇总了咨询这一主题时常见的问题。

任务计划程序的「上次运行结果」中显示的 0x1 是什么意思?
0x1 并不是任务计划程序本身的错误,而是表示所启动的程序返回了退出代码 1。原因出在脚本那一侧。手动运行与定期运行之间,典型的差异在于当前工作目录、环境变量与配置文件、执行策略不同。另外也要注意像 robocopy 这类即使正常结束也会返回 1 的外部命令退出代码约定。只要知道 0x413xx 系列是任务计划程序自身的状态代码、0x8007xxxx 是 Windows 的错误代码,就不会弄错排查方向。
手动执行明明能跑,放进任务计划程序却不动,是为什么?
几乎可以确定原因在于执行账户与会话、环境的差异。选择「不管用户是否登录都要运行」时,会在非交互式会话中执行,因此映射的网络驱动器或用户配置文件中的环境变量都不存在。而且「不保存密码」(S4U)也无法访问网络上的资源。有效的排查方式是:先查看历史记录选项卡、单独测试脚本、临时改成「仅当用户登录时才运行」。由于任务历史记录默认是禁用的,正式上线前请务必先启用。
在任务计划程序中调用 PowerShell 脚本的正确写法是什么?
基本做法是在「程序」中指定 pwsh.exe(或 powershell.exe),参数写成 -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "完整路径" 的形式。使用 -File 而不是 -Command,脚本中的 exit n 会直接成为进程的退出代码,从而可以判断成败。脚本内部的路径要以 $PSScriptRoot 为基准,「起始于(可选)」一栏绝对不能加引号(加了会以 0x8007010B 失败)。脚本那一侧则要设计成:成功时明确返回 0,失败时明确返回非 0。
如何防止因修改密码而导致任务停止运行的事故?
以「不管用户是否登录都要运行」注册时会保存密码,因此修改密码后,任务会一直卡在登录失败(0x8007052E)。如果是域环境,首选是密码由域控制器自动管理的 gMSA(组托管服务账户),这个问题本身就不存在了。如果使用专用的服务账户,请把密码有效期与任务盘点放在一起管理。同时再加入一套失败时通过邮件或 Teams 通知的机制,就能避免出现「已经停了几个月都没人发现」的事故。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表