PowerShell 的错误处理与重试设计 ── 从 try/catch 失效的陷阱到 exit code、重试的实务定式

· 更新日期: · · PowerShell, Windows, 错误处理, 重试, 自动化, 运维改善, 脚本, 任务计划程序

「夜间批处理明明失败了,但任务计划程序上却显示成功(0x0),谁都没有察觉」「明明写了 try/catch,却始终没有进入 catch」「因为一次网络瞬断,每月只掉线一次」── 只要把 PowerShell 脚本投入实际运维,这类咨询就必然会找上门来。手动运行时令人满意的脚本,和无人值守、每晚持续运行的脚本之间,横亘着一堵墙 ── 那就是错误处理与重新执行(重试)的设计。

麻烦的是,PowerShell 的错误模型和一般编程语言的异常模型存在微妙的差异。「明明出现了错误,处理却还在继续」「明明应该被 catch 到,却还是溜走了」── 这些现象大多不是 bug,而是 PowerShell 按照规范运行的结果。如果不了解这套机制就动手写代码,就会大量产出「把失败悄悄吞掉、却以正常状态结束」的脚本。

本文面向用 PowerShell 自动化公司内部定型工作的信息系统负责人与开发者,以官方文档为依据,系统整理终止错误与非终止错误的区别、原生命令的成败判断、能让任务计划程序与监控系统识别成败的 exit code 设计,以及能够承受临时性错误的重试模式。

1. 先说结论

  • PowerShell 的错误分为「非终止错误」与「终止错误(语句终止 / 脚本终止)」。非终止错误只会显示消息并让管道继续执行,默认情况下不会进入 try/catch。1
  • 想让 try/catch 捕获的命令,标准做法是加上 -ErrorAction StopStop 会把非终止错误提升为终止错误,使其能被 catch 处理。也可以在脚本开头将 $ErrorActionPreference(默认为 Continue)设为 Stop。12
  • -ErrorAction 只针对该条命令本身覆盖 $ErrorActionPreference不过两者并非完全对称 ── -ErrorAction 能控制的仅限于非终止错误。1
  • 原生命令(robocopy、git、外部 EXE)的失败,默认不会成为 PowerShell 的错误。非零退出代码会把 $? 置为 $false 并写入 $LASTEXITCODE,但不会生成 ErrorRecord,也不会进入 catch。成败请用 $LASTEXITCODE 判断。1
  • 在 PowerShell 7.4 中,$PSNativeCommandUseErrorActionPreference 已成为正式功能。设为 $true 后,非零退出代码会触发非终止错误,配合 $ErrorActionPreference = 'Stop' 就能被 try/catch 捕获(默认为 $false)。32
  • 在 catch 内部,$_ 就是 ErrorRecord。$_.Exception 是异常本体,若是被提升的错误,可通过 $_.Exception.ErrorRecord 追溯到原始错误信息。指定异常类型的 catch 块,可以只对预期内的错误单独处理。14
  • 成败必须通过 exit code 传达给外部。exit 关键字设置退出代码,若以 pwsh -File / powershell.exe -File 方式启动,该值就会直接成为进程的退出代码。没有 exit 时,正常结束为 0,未处理异常为 1。56
  • 重试要遵循三项原则:仅限临时性错误、设有上限、具备幂等性。不要用重试来蒙混业务错误,用指数退避拉开间隔,并将处理设计成即使重新执行也不会造成重复处理。三者齐备,才称得上是「可以安全重新执行的脚本」。

2. 两种错误 ── 为什么 try/catch 不起作用

PowerShell 的错误分为 3 类:非终止错误(不停止管道,仅报告)、语句终止错误(只停止该语句,继续执行下一条)、脚本终止错误(回退整个调用堆栈)。1

在实务中容易踩坑的正是非终止错误。像 Get-ContentGet-ChildItem 这类 cmdlet,在处理某个输入失败时,通常抛出的都是非终止错误 ── 红色的错误消息会显示出来,但处理仍会继续,既不会进入 try/catch,也不会进入 trap1

# 【陷阱】catch 一次都不会进入,"完成" 也会照常显示
try {
    Get-Content -Path 'C:\Data\不存在的文件.txt'   # 非终止错误
    Write-Host '完成'                               # 仍会被执行
}
catch {
    Write-Host '不会执行到这里'
}

# 【定式】用 -ErrorAction Stop 提升为终止错误,使其能被 catch 处理
try {
    Get-Content -Path 'C:\Data\不存在的文件.txt' -ErrorAction Stop
    Write-Host '完成'                               # 出错时不会执行
}
catch {
    Write-Host "已捕获: $($_.Exception.Message)"
}

-ErrorAction Stop$ErrorActionPreference = 'Stop' 生效时,引擎会用 ActionPreferenceStopException 包裹非终止错误,将其提升为终止错误。在 try 块内部,被 catch 捕获到的正是这个被提升后的错误 ── 这才是准确的运作机制。1 另一方面,.NET 方法抛出的异常(如 [int]::Parse('abc'))以及命令名解析失败,从一开始就是终止错误,因此不需要额外处理就会进入 catch。1

「那么干脆一直把 $ErrorActionPreference 设为 ‘Stop’ 不就好了」── 这个想法只对了一半。对于无人值守执行的脚本,比起吞掉错误继续往下走,停下来报告失败要安全得多,所以在脚本开头设为 Stop 是一个不错的默认值。不过要注意,$ErrorActionPreference 会对当前作用域及其子作用域生效,因此调用的模块或函数的行为也会随之改变;对于允许失败的后续清理处理(比如删除临时文件),需要单独重新加上 -ErrorAction SilentlyContinue2

3. catch 中该看什么 ── 梳理 ErrorRecord

catch 块中的 $_ 就是 ErrorRecord。应该记录到日志中的信息,都可以从这里获取。14

try {
    Copy-Item -Path $src -Destination $dest -ErrorAction Stop
}
catch [System.IO.IOException] {
    # 用指定类型的 catch 只单独处理「预期内的失败」。
    # 即便是被提升后的错误,引擎在匹配时看的仍是原始异常类型
    Write-Warning "I/O 错误: $($_.Exception.Message)"
}
catch {
    # 预期外的错误连同上下文一起记录到日志,然后重新抛出(不要吞掉)
    $rec = $_   # $_ 就是 ErrorRecord
    Write-Warning ('类型: {0} / 位置: {1} / 对象: {2}' -f `
        $rec.Exception.GetType().FullName,
        $rec.InvocationInfo.PositionMessage,
        $rec.TargetObject)
    throw       # 不带参数的 throw 会把同一个错误继续向上传播
}
finally {
    # finally 无论 try 成功、出错,还是被 Ctrl+C 中断都会执行,后续清理放在这里
    if ($tempFile -and (Test-Path $tempFile)) { Remove-Item $tempFile -ErrorAction SilentlyContinue }
}

有三个要点值得关注。

  • $_.Exception 是异常本体。被 -ErrorAction Stop 提升的错误会被包裹进 ActionPreferenceStopException,但 catch 在做类型匹配时,引擎看的是原始异常类型(例如 ItemNotFoundException),所以指定类型的 catch 可以照常书写。原始的 ErrorRecord 可以通过 $_.Exception.ErrorRecord 追溯到。1
  • $_.InvocationInfo.PositionMessage 中包含「是哪个文件、第几行、哪条命令」的信息,在无人值守执行的日志中,有没有这一项会让排查时间相差一个数量级。
  • finally 块无论 try 成功、出错,还是被 Ctrl+C 中断都会执行。关闭连接、删除临时文件之类的收尾工作应放在 finally 中。7

该在哪一层 catch、日志写在什么位置,这类设计问题是跨语言通用的。在《异常处理时 catch 与日志该放在哪里》中整理的原则(在边界处 catch、不要吞掉错误、避免重复记录日志)同样可以直接套用到 PowerShell 上。

4. 原生命令的成败 ── $? 与 $LASTEXITCODE,以及 7.4 的新功能

另一个大坑是 robocopy、git、公司内部 EXE 这类原生命令。外部程序不参与 PowerShell 的错误体系,而是通过退出代码来报告失败。默认行为如下。1

事件 行为(默认)
非零退出代码 $? 变为 $false,退出代码写入 $LASTEXITCODE
生成 ErrorRecord 不会(也不会追加到 $Error 中)
try/catch 不会进入

也就是说,try { robocopy ... } catch { ... }(在默认情况下)什么都捕获不到。原生命令的成败判断要写成 $LASTEXITCODE$? 是「上一步操作是否成功」的布尔值,对原生命令而言,只有退出代码为 0 时才会是 $true1 另外,在 Windows PowerShell 5.1 中,原生命令只要向 stderr 写入内容,$? 就可能变为 $false;而在 PowerShell 7 中,这一行为已改为仅在非零退出代码时才变为 $false。这是一项更贴合实际情况的改动 ── 向 stderr 输出不应被视为失败。8

# 原生命令用 $LASTEXITCODE 判断
robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR /R:2 /W:5
if ($LASTEXITCODE -ge 8) {
    # robocopy 的 0~7 属于成功系(是否发生复制等信息),8 以上为失败
    throw "robocopy 执行失败 (ExitCode=$LASTEXITCODE)"
}

从 PowerShell 7.4 开始,可以用 $PSNativeCommandUseErrorActionPreference 来改变这一行为。它在 7.3 中作为实验性功能加入,并在 7.4 中转为正式功能(mainstream)3 设为 $true 后,退出代码非零的原生命令会触发一个明确标注退出代码的非终止错误,该错误遵循 $ErrorActionPreference 的设置。也就是说,配合 Stop 使用,外部命令的失败也能被纳入 try/catch。12

# PowerShell 7.4 及以后: 外部命令的失败也能用 try/catch 处理(默认为 $false)
$PSNativeCommandUseErrorActionPreference = $true
$ErrorActionPreference = 'Stop'

try {
    git.exe fetch origin
}
catch {
    Write-Warning "git 执行失败: $($_.Exception.Message)"
    throw
}

& {
    # 像 robocopy 这种「非零 ≠ 失败」的命令,在脚本块内临时禁用该设置,
    # 按以往方式用 $LASTEXITCODE 判断(退出脚本块后会恢复原状)
    $PSNativeCommandUseErrorActionPreference = $false
    robocopy.exe 'D:\Reports' '\\fileserver\reports' /MIR
    if ($LASTEXITCODE -ge 8) { throw "robocopy 执行失败 (ExitCode=$LASTEXITCODE)" }
}

正如官方文档中 robocopy 的例子所示,确实存在把非零退出代码当作正常信息使用的命令,因此如果要统一启用,就需要针对这些例外区间单独设计。2 在只能使用 Windows PowerShell 5.1 的现场,并不存在这项功能,请统一用 $LASTEXITCODE 判断。5.1 与 7 之间的行为差异在迁移时很容易成为陷阱,也请一并参考《Windows PowerShell 5.1とPowerShell 7の違いと移行》。

5. exit code 设计 ── 让任务计划程序与监控系统可以判断成败

捕获到错误之后,接下来就是向外部报告。任务计划程序或监控工具用来了解脚本成败的手段,实际上只有进程的退出代码这一种。下面来准确掌握相关规范。

  • exit <数值> 可以明确设置脚本的退出代码。exit 同时也会给 $LASTEXITCODE 设置对应的值。59
  • pwsh -Filepowershell.exe -File)方式启动时,exit 指定的值会原样成为进程的退出代码。如果没有 exit 语句,正常完成为 0,因未处理异常而终止则为 1。56
  • -Command 方式启动脚本时,脚本内 exit 10 这类退出代码不会被保留。它会根据最后一条命令的成败被归约为 0 或 1(如果直接在命令字符串中写 exit 10,则会返回该值)。如果运维中需要区分不同的脚本退出代码,标准做法是用 -File 启动。6

把这套规范落实为骨架,无人值守脚本的模板就会是下面这样。

# Invoke-NightlyExport.ps1 ── 让任务计划程序能够判断成败的骨架
[CmdletBinding()]
param()

$ErrorActionPreference = 'Stop'   # 无人值守执行时,把「停下并报告」设为默认行为

# 把包含标准输出与错误在内的执行证据作为日志保留下来(用 -Append 追加到按日期命名的文件)
Start-Transcript -Path "C:\Logs\NightlyExport_$(Get-Date -Format yyyyMMdd).log" -Append

try {
    Export-DailyData      # 业务处理主体(调用已模块化的函数)
    exit 0                # 明确表示成功
}
catch [System.Net.WebException] {
    Write-Warning "通信错误: $($_.Exception.Message)"
    exit 10               # 临时性错误 ── 为任务侧配置重新执行留出余地
}
catch {
    Write-Warning "预期外的错误: $($_.Exception.Message)"
    Write-Warning $_.InvocationInfo.PositionMessage
    exit 1                # 永久性错误 ── 不重新执行,交由人工查看
}
finally {
    Stop-Transcript       # finally 能保证即使经由 exit 离开,证据也会被正常关闭
}

Start-Transcript 是一个把会话的输入输出整体记录成文本的 cmdlet,即使不额外编写 echo 或重定向,也能重现「当时屏幕上到底显示了什么」。10 它与自己编写的日志函数并不冲突,值得作为最后一道防线并用。日志设计与体积膨胀的应对方式,参见《PowerShell 脚本进阶 ── 安全地实现日志排查、归档与报表自动化》。

exit code 的分配诀窍是不要设计得太复杂。0=成功、1=永久性错误(人工查看)、10 号段=临时性错误(可以重新执行)这种粒度就已经足够,可以直接对接任务计划程序的「上次运行结果」或作业管理工具的成败判断。任务侧的配置(失败时重新执行、如何确认运行结果)请参见《任务计划程序的任务不执行、以 0x1 结束 ── 原因排查与安全的运维设计》。

6. 重试设计 ── 区分临时性错误与业务错误

最后是重新执行。重试的价值在于「自动吸收临时性错误,不用在半夜把人叫醒」,但如果实现得草率,就会造成另一种事故 ──「对永久性失败没完没了地重试」「因重复处理而破坏数据」。原则有三条。

  • 只对临时性错误进行重试。仅限于网络瞬断、文件被临时锁定、等待依赖服务启动这类时间能够解决的失败。输入不合法、权限不足、配置错误应立即判定为失败,通过 exit code 与日志交给人工处理。
  • 设计好上限与间隔。确定重试次数的上限,并用指数退避(2 秒、4 秒、8 秒……)逐渐拉长间隔。对正处于故障中的对方以固定间隔持续敲打,只会妨碍它的恢复。
  • 保持幂等(重新执行也是安全的)。无论是重试还是任务计划程序的重新执行,本质都意味着「同一处理会再跑一次」。前提是要有相应的设计,比如输出用临时文件+改名的方式发布、记录已处理的 ID 来拒绝重复导入等。

作为一种模式,可以归纳为下面这个形态。

function Invoke-WithRetry {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [scriptblock] $Operation,
        # 传入 0 以下的值会导致一次都不执行就正常结束,因此强制要求 1 以上
        [ValidateRange(1, 100)]
        [int] $MaxAttempts = 4,
        # 负值会在重试时的 Start-Sleep 中引发另一个错误,因此在参数绑定阶段就拒绝
        [ValidateRange(0, 3600)]
        [int] $BaseDelaySeconds = 2,
        # 只列举值得重试的异常类型(默认是通信、I/O 相关)
        [Type[]] $RetryableExceptions = @([System.IO.IOException], [System.Net.WebException])
    )
    for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
        try {
            # 先把输出接到变量里,成功后再返回。如果直接 return & $Operation,
            # 一旦在输出到一半时发生异常,部分输出就会流向调用方,
            # 导致重试成功时同一份数据被重复交付
            $output = & $Operation
            return $output
        }
        catch {
            $ex = $_.Exception
            $isRetryable = $RetryableExceptions | Where-Object { $ex -is $_ }
            if (-not $isRetryable -or $attempt -eq $MaxAttempts) {
                throw   # 业务错误,或已达到重试上限 ── 直接判定为失败
            }
            # 给指数增长的等待时间设置上限(既避免重试次数多的配置等待过久,
            # 也不会超出 Start-Sleep 能接受的范围)
            $delay = [math]::Min($BaseDelaySeconds * [math]::Pow(2, $attempt - 1), 300)
            Write-Warning "失败(第 ${attempt} 次): $($ex.Message) ── ${delay} 秒后重试"
            Start-Sleep -Seconds $delay
        }
    }
}

# 用法: 目标处理要预先用 -ErrorAction Stop 转换为终止错误
Invoke-WithRetry -Operation {
    Copy-Item -Path '\\fileserver\out\daily.csv' -Destination 'D:\Work' -ErrorAction Stop
}

# 在 PowerShell 7 中重试 Invoke-RestMethod / Invoke-WebRequest 时的注意事项:
# 在 7 中,通信失败不再是 5.1 时代的 WebException,而是以 HttpRequestException
# 系列类型出现,因此按默认设置不会被重试。另外,像 404 这类永久性的 HTTP 错误
# 响应也是同一种类型,所以一旦收到响应,就需要自己根据状态码判断「是否值得重试」
Invoke-WithRetry -RetryableExceptions ([System.Net.Http.HttpRequestException]) -Operation {
    # 用 -SkipHttpErrorCheck 让错误响应也不抛异常地被接收,再根据状态码分别处理
    $r = Invoke-WebRequest -Uri 'https://api.example.co.jp/orders' -TimeoutSec 30 -SkipHttpErrorCheck
    if ($r.StatusCode -in 408, 429, 500, 502, 503, 504) {
        # 只把临时性状态码当作 HttpRequestException 抛出 → 会被重试
        throw [System.Net.Http.HttpRequestException]::new("临时性 HTTP 错误: $($r.StatusCode)")
    }
    if ($r.StatusCode -ge 400) {
        throw "永久性 HTTP 错误: $($r.StatusCode)"   # 类型不同,不会被重试
    }
    $r.Content | ConvertFrom-Json
}

关键在于,重试对象是通过异常类型明确选定的。如果写成「只要 catch 到就全部重试」,那么连参数错误这类永久性错误也会被无谓地尝试 4 次、白白等待。投入运维之后,比较现实的做法是根据实际日志中观测到的临时性错误类型,逐步把它们添加到 $RetryableExceptions 中。把这类通用函数抽取成模块,可以提高复用性(参见《PowerShellの引数設計とモジュール化》)。另外,重试与错误分支的逻辑,正是值得用 Pester 编写测试的部分(参见《用 Pester 完善 PowerShell 测试 ── 让运维脚本不易损坏的实务方法》)。

7. 实务定式(判断表)

论点 选项 判断标准
错误的默认行为 保持 Continue / 在开头设置 $ErrorActionPreference = ‘Stop’ 无人值守执行时「停下并报告」更安全。用于交互式排查的脚本保持 Continue 即可2
想要 catch 的位置 听天由命 / 显式加上 -ErrorAction Stop cmdlet 大多抛出非终止错误。想捕获的那一行要显式加上 Stop1
原生命令的成败 放任不管 / 用 $LASTEXITCODE 判断 / 7.4 的 $PSNativeCommandUseErrorActionPreference 混用 5.1 的环境统一用 $LASTEXITCODE 判断。仅限 7.4 以上则用新功能 + robocopy 等例外区间32
向外部报告成败 仅靠日志 / 设计 exit code 并以 -File 启动 日志给人看,exit code 给机器用,两者都需要。以 -Command 启动会导致退出代码被压缩6
执行证据 仅靠自有日志 / 并用 Start-Transcript 可以连自有日志抓不到的输出(如外部命令的标准输出)一并保留下来的保险10
重试 所有错误都重试 / 仅限临时性错误 + 指数退避 + 幂等 重试业务错误是事故的根源。上限、间隔、幂等三件套缺一不可

8. 总结

  • PowerShell 的错误分为非终止错误与终止错误,非终止错误默认不会进入 try/catch。想要捕获的命令,标准做法是显式加上 -ErrorAction Stop
  • $ErrorActionPreference 的默认值是 Continue。无人值守脚本应在开头将其设为 Stop,从结构上防止「把失败悄悄吞掉、却以正常状态结束」这类事故。
  • 原生命令的失败默认不会进入 catch。请用 $LASTEXITCODE 判断,或者在 PowerShell 7.4 及以后善用 $PSNativeCommandUseErrorActionPreference
  • 在 catch 中,把异常的类型、消息、位置信息从 $_(ErrorRecord)中取出并记入日志,收尾工作放在 finally 中。finally 即便经由 Ctrl+C 或 exit 离开也会执行。
  • 成败要通过 exit code 报告给外部。以 -File 方式启动时,exit 的值会直接成为退出代码,任务计划程序或监控系统据此就能判断成败。
  • 重试遵循三项原则:仅限临时性错误、带上限的指数退避、幂等。永久性错误应立即判定为失败并交给人工处理。

相关文章

相关咨询领域

小村软件有限公司提供夜间批处理、定型处理脚本的错误处理与重试设计评审,「明明失败却被当作成功」「每月只掉线一次」这类间歇性故障的调查,以及既有脚本资产运维质量的改善服务。

参考链接

  1. Microsoft Learn,about_Error_Handling。关于非终止错误・语句终止错误・脚本终止错误这 3 种分类,非终止错误默认不会进入 catch/trap,通过 -ErrorAction Stop 提升错误的机制(ActionPreferenceStopException 与 $_.Exception.ErrorRecord),指定类型的 catch 会按原始异常类型匹配,$? 与 $LASTEXITCODE 的规范,原生命令的非零退出代码默认不会生成 ErrorRecord,以及 $PSNativeCommandUseErrorActionPreference 的行为。  2 3 4 5 6 7 8 9 10 11 12 13 14 15

  2. Microsoft Learn,about_Preference_Variables。关于 $ErrorActionPreference 的默认值为 Continue,-ErrorAction 参数会在单条命令上优先生效,该设置作用于当前作用域及其子作用域,$PSNativeCommandUseErrorActionPreference 的默认值为 $false,以及针对像 robocopy 这类把非零退出代码当作信息使用的命令,在脚本块内临时禁用该设置的示例。  2 3 4 5 6 7

  3. Microsoft Learn,What’s New in PowerShell 7.4。关于实验性功能 PSNativeCommandErrorActionPreference($PSNativeCommandUseErrorActionPreference)在 PowerShell 7.4 中转为正式功能(mainstream)。  2 3

  4. Microsoft Learn,Everything you wanted to know about exceptions。关于在 catch 块内可以通过 $_ 访问异常信息,加了 -ErrorAction Stop 的命令与 Write-Error 的错误会变得可以在 catch 中处理,以及 try/finally 的资源释放模式。  2

  5. Microsoft Learn,about_Language_Keywords。关于 exit 关键字会设置退出代码并同步反映到 $LASTEXITCODE,以 pwsh -File 启动的脚本会把 exit 的数值参数作为退出代码返回,以及没有 exit 语句时正常完成为 0、未处理异常为 1。  2 3

  6. Microsoft Learn,about_Pwsh。关于 -File 启动时退出代码的确定方式,以及 -Command 启动时 0 与 1 以外的退出代码会被转换为 1,因此若要保留退出代码需要 exit $LASTEXITCODE。  2 3 4

  7. Microsoft Learn,about_Try_Catch_Finally。关于 try/catch/finally 的语法、指定类型的 catch 块与多重 catch,以及 finally 块在成功、出错之外,遇到 Ctrl+C 中断或 catch 内 exit 时也会执行。 

  8. Microsoft Learn,Differences between Windows PowerShell 5.1 and PowerShell 7.x。关于 PowerShell 7 中,原生命令仅向 stderr 写入内容时 $? 不会变为 $false,仅在非零退出代码时才会变为 $false 这一改动。 

  9. Microsoft Learn,about_Automatic_Variables。关于 $LASTEXITCODE 会保存原生程序或脚本的退出代码,以及在 pwsh -File 调用时,因异常终止会被设为 1、exit 关键字的值,或正常完成时被设为 0。 

  10. Microsoft Learn,Start-Transcript。关于把会话的命令与控制台输出记录到文本文件、用 -Append 追加写入、默认的保存位置与文件名,以及用 Stop-Transcript 停止记录。  2

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

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

常见问题

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

在 PowerShell 中写了 try/catch,为什么却不会进入 catch?
这是因为 cmdlet 抛出的错误大多是非终止错误(non-terminating error)。try/catch 只能捕获终止错误,非终止错误只会显示消息并继续处理,不会进入 catch。标准的应对方法是给想要捕获的命令加上 -ErrorAction Stop(或者在脚本开头把 $ErrorActionPreference 设为 'Stop')。这样一来,非终止错误就会被提升为终止错误,从而能被 try/catch 处理。
$? 和 $LASTEXITCODE 应该如何区分使用?
$? 是表示上一步操作是否成功的布尔值,cmdlet 和原生命令都会设置它。$LASTEXITCODE 是最后一次执行的原生程序(或已 exit 的脚本)的退出代码,cmdlet 的错误不会改变它。在判断 robocopy、git 等外部命令是否成功时,用可以进一步确认退出代码具体含义的 $LASTEXITCODE 来判断更可靠。请注意,原生命令的非零退出代码默认不会进入 catch。
怎样才能让任务计划程序判断出 PowerShell 脚本的成败?
做法是在脚本末尾(以及各个 catch 块)用 exit 关键字明确设置退出代码,任务侧则以 pwsh -File(或 powershell.exe -File)方式启动,并监控「上次运行结果」这个值。以 -File 启动时,exit 指定的值会原样成为进程的退出代码;没有 exit 时,正常结束为 0,未处理异常为 1。以 -Command 启动会把 0 和 1 以外的退出代码转换为 1,因此如果要围绕退出代码构建运维流程,标准做法就是用 -File 启动。
应该针对哪些错误进行重试?
应仅限于重试后结果可能改变的临时性错误(如网络瞬断、文件被临时锁定、等待服务启动等)。输入数据不合法、权限不足、配置错误这类业务错误・永久性错误,无论重试多少次都会失败,因此不应重试,而应立即判定为失败,通过日志与 exit code 通知人工处理。即使要重试,前提也是要给次数与间隔设置上限、用指数退避拉开间隔,并把处理设计成即使重新执行也不会造成重复处理(幂等)。
PowerShell 7.4 中的 $PSNativeCommandUseErrorActionPreference 是做什么用的设置?
这是一项设置:当原生命令以非零退出代码结束时,触发一个 PowerShell 错误(非终止错误)。它在 PowerShell 7.3 中作为实验性功能加入,在 7.4 中转为正式功能(默认为 $false)。设为 $true 后会遵循 $ErrorActionPreference 的设置,因此配合 Stop 使用,就能用 try/catch 捕获外部命令的失败。不过由于像 robocopy 这样把非零退出代码当作正常信息使用的命令也存在,需要注意只在那部分区间内把它临时改回 $false。

作者简介

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

Go Komura

小村软件有限公司 代表

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

返回博客列表