Обработка ошибок и повторные попытки в PowerShell — от ловушки нерабочего try/catch до exit code и типовых схем повтора
· Го Комура · PowerShell, Windows, Обработка ошибок, Повторные попытки, Автоматизация, Улучшение эксплуатации, Скрипт, Планировщик заданий
«Ночной пакетный процесс завершился ошибкой, но в Планировщике заданий значилось “успешно” (0x0), и никто этого не заметил». «Написал try/catch, а выполнение в catch не попадает». «Из-за кратковременного сбоя сети падает раз в месяц». Стоит только вывести скрипт PowerShell в эксплуатацию, как подобные жалобы приходят неизбежно. Между скриптом, который радует вас при запуске вручную, и скриптом, который безостановочно работает по ночам без присмотра, стоит стена — проектирование обработки ошибок и повторного выполнения (retry).
Сложность в том, что модель ошибок PowerShell тонко отличается от модели исключений, привычной по большинству языков программирования. «Ошибка есть, а обработка продолжается» и «вроде бы поймал в catch, а оно проскочило мимо» в большинстве случаев — не баг, а поведение PowerShell строго по спецификации; если писать код, не зная этого механизма, штампуются скрипты, которые проглатывают сбой и завершаются как ни в чём не бывало.
Эта статья адресована сотрудникам ИТ-отделов и разработчикам, автоматизирующим типовые внутренние процессы на PowerShell. Опираясь на официальную документацию, разберём различие между завершающими и незавершающими ошибками, определение успеха нативных команд, проектирование exit code, позволяющее судить об успехе из Планировщика заданий и систем мониторинга, а также схему повторных попыток, устойчивую к временным ошибкам.
1. Сначала вывод
- Ошибки PowerShell делятся на «незавершающие» и «завершающие» (завершающие на уровне инструкции / завершающие скрипт). Незавершающая ошибка выводит сообщение и позволяет конвейеру продолжиться, по умолчанию не попадая в try/catch.1
- Стандартное решение — добавлять
-ErrorAction Stopк любой команде, которую должен перехватить try/catch. Stop повышает статус незавершающей ошибки до завершающей, и её может обработать 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 - Три принципа повторных попыток: только временные ошибки, верхняя граница, идемпотентность. Не маскируйте retry бизнес-ошибки, увеличивайте интервал по схеме экспоненциальной задержки и проектируйте обработку так, чтобы повторный запуск не приводил к двойной обработке. Только при выполнении всех трёх условий скрипт становится «безопасным для повторного запуска».
2. Два вида ошибок — почему try/catch не срабатывает
Ошибки PowerShell делятся на три категории: незавершающие ошибки (сообщают о проблеме, не останавливая конвейер), ошибки, завершающие инструкцию (останавливают только эту инструкцию и переходят к следующей), и ошибки, завершающие скрипт (разворачивают весь стек вызовов).1
На практике ловушкой оказываются именно незавершающие ошибки. Когда командлет вроде Get-Content или Get-ChildItem не может обработать конкретный элемент входных данных, он обычно выдаёт именно незавершающую ошибку: красное сообщение об ошибке появляется, но обработка продолжается и не попадает ни в try/catch, ни в trap.1
# [Ловушка] 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 и повышает её до завершающей. Именно эта повышенная ошибка и доходит до catch внутри блока try — таков точный механизм.1 А вот исключения методов .NET (например, [int]::Parse('abc')) и ошибки разрешения имени команды с самого начала являются завершающими ошибками, поэтому попадают в catch без каких-либо дополнительных мер.1
Идея «а не поставить ли $ErrorActionPreference = 'Stop' всегда» наполовину верна. Для скрипта, работающего без присмотра, безопаснее остановиться и сообщить о сбое, чем проглотить ошибку и двигаться дальше, поэтому установка Stop в начале — хороший вариант по умолчанию. Но учтите, что $ErrorActionPreference действует на текущую область видимости и на дочерние, а значит меняет поведение вызываемых модулей и функций, а для операций уборки, которым допустимо завершаться неудачей (например, удаление временных файлов), нужно отдельно возвращать -ErrorAction SilentlyContinue.2
3. Что читать внутри catch — как устроен обход ErrorRecord
В блоке catch переменная $_ содержит ErrorRecord. Именно отсюда берётся вся информация, которую стоит фиксировать в журнале.14
try {
Copy-Item -Path $src -Destination $dest -ErrorAction Stop
}
catch [System.IO.IOException] {
# Catch с указанным типом обрабатывает отдельно только "ожидаемые сбои".
# Даже для повышенной ошибки движок сопоставляет её с исходным типом исключения
Write-Warning "Ошибка ввода-вывода: $($_.Exception.Message)"
}
catch {
# Неожиданное логируем вместе с контекстом и пробрасываем дальше (не проглатываем)
$rec = $_ # $_ - это ErrorRecord
Write-Warning ('Тип: {0} / Расположение: {1} / Объект: {2}' -f `
$rec.Exception.GetType().FullName,
$rec.InvocationInfo.PositionMessage,
$rec.TargetObject)
throw # throw без аргументов пробрасывает ту же ошибку выше
}
finally {
# finally выполняется и при успехе, и при ошибке, и при остановке через 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 и логирование при обработке исключений» (перехватывать на границе, не проглатывать ошибки, избегать двойного логирования), применимы к PowerShell без каких-либо изменений.
4. Успех нативных команд — $?, $LASTEXITCODE и новая функция 7.4
Ещё одна большая брешь — это нативные команды: robocopy, git, внутренние EXE-файлы компании. Внешние программы не участвуют в системе ошибок PowerShell и сообщают о сбое через код завершения. Поведение по умолчанию таково.1
| Событие | Поведение (по умолчанию) |
|---|---|
| Ненулевой код завершения | $? становится $false, код завершения попадает в $LASTEXITCODE |
| Создание ErrorRecord | Не происходит (не добавляется даже в $Error) |
| try/catch | Не срабатывает |
Иначе говоря, try { robocopy ... } catch { ... } (по умолчанию) вообще ничего не перехватывает. Успех нативной команды нужно проверять через $LASTEXITCODE. $? — это булево значение «успешна ли предыдущая операция», и для нативных команд оно становится $true только при коде завершения 0.1 Отметим также, что в Windows PowerShell 5.1 $? иногда становился $false просто из-за того, что нативная команда что-то писала в stderr, а в 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 и стал штатной функцией (mainstream) в 7.4.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 -File(powershell.exe -File) значение, переданное в exit, становится кодом завершения процесса как есть. Без оператора exit нормальное завершение даёт 0, а завершение из-за необработанного исключения — 1.56 - При запуске скрипта через
-Commandкод завершения вродеexit 10внутри скрипта не сохраняется. Он округляется до 0 или 1 в зависимости от успеха последней команды (если жеexit 10написан прямо в самой командной строке, возвращается именно это значение). Если эксплуатация опирается на разные коды завершения скрипта, стандартный подход — запуск через-File.6
Если положить эту спецификацию в основу, шаблон скрипта для выполнения без присмотра выглядит так.
# Invoke-NightlyExport.ps1 - каркас, позволяющий Планировщику заданий определить успех
[CmdletBinding()]
param()
$ErrorActionPreference = 'Stop' # Для выполнения без присмотра по умолчанию делаем "остановиться и сообщить"
# Ведём полную стенограмму выполнения, включая stdout и ошибки, как журнал (-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 — это командлет, который целиком записывает в текст весь ввод и вывод сессии, позволяя воспроизвести «что было на экране в тот момент» без необходимости вручную городить echo или перенаправления.10 Его стоит использовать не вместо, а вместе с собственными функциями логирования — как последний рубеж обороны. Проектирование журналов и борьба с их разрастанием разобраны в статье «Прикладной PowerShell — безопасная автоматизация анализа логов, архивирования и отчётности».
Секрет в том, чтобы не переусложнять назначение exit code. Достаточно детализации вроде: 0 = успех, 1 = постоянная ошибка (нужно смотреть человеку), диапазон 10-х = временная ошибка (можно повторить), — и на этом можно прямо строить определение успеха в «Последнем результате выполнения» Планировщика заданий или в инструментах управления заданиями. Настройки на стороне задачи (повтор при сбое, способ проверки результата выполнения) разобраны в статье «Задачи Планировщика заданий не запускаются или завершаются с 0x1 — разбор причин и безопасное проектирование эксплуатации».
6. Проектирование повторных попыток — различаем временные и бизнес-ошибки
Наконец, повторное выполнение. Ценность retry в том, чтобы автоматически поглощать временные ошибки и не будить людей посреди ночи, но если внедрить его небрежно, возникают уже другие проблемы: бесконечные повторы постоянного сбоя или порча данных из-за двойной обработки. Принципов три.
- Повторять только временные ошибки. Ограничивайтесь сбоями, которые может разрешить время: кратковременный обрыв сети, временная блокировка файла, ожидание запуска зависимой службы. Некорректный ввод, нехватку прав и ошибки настройки нужно сразу же проваливать, передавая информацию человеку через exit code и журнал.
- Спроектировать верхнюю границу и интервал. Определите предельное число попыток, а интервал увеличивайте по схеме экспоненциальной задержки (2 секунды, 4, 8…). Долбить с фиксированным интервалом собеседника, который и так испытывает сбой, только мешает его восстановлению.
- Обеспечить идемпотентность (безопасность повторного запуска). И retry, и повторный запуск из Планировщика заданий означают одно: «та же самая обработка выполняется ещё раз». Это предполагает такие приёмы, как публикация результата через временный файл с последующим переименованием или запись обработанных 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
}
# Замечание при повторе Invoke-RestMethod / Invoke-WebRequest в PowerShell 7:
# в версии 7 сбой связи приходит не как WebException времён 5.1, а как исключение
# семейства HttpRequestException, поэтому с настройками по умолчанию повтор не сработает.
# Кроме того, постоянная HTTP-ошибка вроде 404 приходит тем же типом, поэтому, получив
# ответ, нужно самостоятельно решать по коду статуса, стоит ли его повторять
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
}
Ключевой момент — объекты retry выбираются явно, по типу исключения. Если написать «повторять всё, что попало в catch», то даже постоянная ошибка вроде неверного параметра будет впустую пробовать четыре раза и ждать между попытками. После начала эксплуатации реалистичный путь развития — постепенно добавлять в $RetryableExceptions типы временных ошибок, реально замеченные в логах. Подобные общие функции стоит выносить в модуль для повторного использования (см. «Проектирование аргументов и модуляризация PowerShell»). Кроме того, логика retry и ветвления по ошибкам — это именно то место, где стоит писать тесты Pester («Настройка тестирования PowerShell через Pester — практические приёмы, снижающие риск поломки эксплуатационных скриптов»).
7. Практические рекомендации (таблица решений)
| Вопрос | Варианты | Ориентир для решения |
|---|---|---|
| Поведение при ошибке по умолчанию | Оставить Continue / задать $ErrorActionPreference = ‘Stop’ в начале | Для выполнения без присмотра безопаснее «остановиться и сообщить». Интерактивный скрипт для расследования может оставаться на Continue2 |
| Место, которое нужно перехватить | Надеяться на лучшее / явно указать -ErrorAction Stop | Командлеты чаще всего выдают незавершающие ошибки. Указывайте Stop явно на строке, которую хотите перехватить1 |
| Успех нативной команды | Не проверять / определять по $LASTEXITCODE / $PSNativeCommandUseErrorActionPreference из 7.4 | В окружениях со смешанным 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становится кодом завершения как есть, позволяя Планировщику заданий или мониторингу определить успех. - Повторные попытки стройте по трём принципам: только временные ошибки, ограниченная сверху экспоненциальная задержка и идемпотентность. Постоянные ошибки должны сразу проваливаться и передаваться человеку.
Похожие статьи
- Прикладной PowerShell — безопасная автоматизация анализа логов, архивирования и отчётности
- Настройка тестирования PowerShell через Pester — практические приёмы, снижающие риск поломки эксплуатационных скриптов
- Задачи Планировщика заданий не запускаются или завершаются с 0x1 — разбор причин и безопасное проектирование эксплуатации
- Где размещать catch и логирование при обработке исключений
- Политика выполнения и подпись скриптов PowerShell
- Проектирование аргументов и модуляризация PowerShell
Смежные области консультирования
KomuraSoft LLC (合同会社小村ソフト) занимается ревью проектирования обработки ошибок и повторных попыток для ночных пакетных процессов и типовых скриптов, расследованием прерывистых сбоев вроде «сбой, а отображается как успех» или «падает лишь раз в месяц», а также повышением эксплуатационного качества существующих скриптовых активов.
- Техническая консультация и ревью проекта
- Исследование дефектов и анализ первопричин
- Поддержка использования существующих активов и миграции
- Связаться с нами
Справочные ссылки
-
Microsoft Learn, about_Error_Handling. О трёх категориях ошибок (незавершающая, завершающая на уровне инструкции, завершающая скрипт), о том, что незавершающая ошибка по умолчанию не попадает в 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
-
Microsoft Learn, about_Preference_Variables. О том, что $ErrorActionPreference по умолчанию равен Continue, что параметр -ErrorAction имеет приоритет для отдельной команды, что настройка применяется к текущей области видимости и дочерним, что $PSNativeCommandUseErrorActionPreference по умолчанию равен $false, и о примере временного отключения параметра внутри блока скрипта для команд вроде robocopy, использующих ненулевой код завершения как информацию. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, What’s New in PowerShell 7.4. О том, что экспериментальная функция PSNativeCommandErrorActionPreference ($PSNativeCommandUseErrorActionPreference) стала штатной (mainstream) в PowerShell 7.4. ↩ ↩2 ↩3
-
Microsoft Learn, Everything you wanted to know about exceptions. О доступе к информации об исключении через $_ внутри блока catch, о том, что команда с -ErrorAction Stop и ошибка Write-Error становятся обрабатываемыми в catch, и о паттерне освобождения ресурсов через try/finally. ↩ ↩2
-
Microsoft Learn, about_Language_Keywords. О том, что ключевое слово exit задаёт код завершения и отражается в $LASTEXITCODE, что скрипт, запущенный через pwsh -File, возвращает числовой аргумент exit как код завершения, и что при отсутствии оператора exit нормальное завершение даёт 0, а необработанное исключение — 1. ↩ ↩2 ↩3
-
Microsoft Learn, about_Pwsh. О том, как определяется код завершения при запуске через -File, и о том, что запуск через -Command преобразует любой код завершения, кроме 0 и 1, в 1, поэтому для сохранения кода завершения нужен exit $LASTEXITCODE. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, about_Try_Catch_Finally. О синтаксисе try/catch/finally, о блоках catch с указанным типом и множественных catch, а также о том, что блок finally выполняется при успехе, при ошибке, при остановке через Ctrl+C и даже при exit внутри catch. ↩
-
Microsoft Learn, Differences between Windows PowerShell 5.1 and PowerShell 7.x. О том, что в PowerShell 7 изменили поведение: $? больше не становится $false только из-за записи нативной команды в stderr, а только при ненулевом коде завершения. ↩
-
Microsoft Learn, about_Automatic_Variables. О том, что $LASTEXITCODE хранит код завершения нативной программы или скрипта, и что при вызове через pwsh -File устанавливается 1 при завершении из-за исключения, значение ключевого слова exit или 0 при нормальном завершении. ↩
-
Microsoft Learn, Start-Transcript. О записи команд сессии и консольного вывода в текстовый файл, о дозаписи через -Append, о расположении и имени файла по умолчанию и об остановке через Stop-Transcript. ↩ ↩2
Похожие статьи
Недавние статьи с теми же тегами помогут подробнее изучить близкие темы.
Политика выполнения PowerShell и подпись скриптов — практическое руководство, как перестать «затыкать дыры» параметром Bypass
Политика выполнения PowerShell — это «не граница безопасности, а защитный механизм». Разбираем различия между RemoteSigned и другими поли...
Прикладной PowerShell — безопасная автоматизация анализа логов, архивирования и отчётности
Разбираем практические шаги безопасной автоматизации PowerShell-скриптами: анализ логов, CSV-отчёты, архивирование старых логов, сохранен...
Задачи планировщика заданий Windows не запускаются или завершаются с 0x1 — диагностика причин и проектирование надёжного выполнения по расписанию
Разбираем принципы проектирования надёжного выполнения задач по расписанию в Windows: учётные записи выполнения и типы входа в систему, т...
Как запустить PowerShell из C# (CSharp) и получить результат в виде объектов
Разбираем, как запустить PowerShell из C# и получать результаты не строками, а объектами PSObject, — от PowerShell SDK, AddCommand и AddP...
Тестирование PowerShell с помощью Pester — практический подход к тому, чтобы эксплуатационные скрипты было труднее сломать
Практическое руководство по тестированию PowerShell-скриптов с помощью Pester v5: безопасное покрытие обработки дат, файловых операций, л...
Связанные темы
Эти страницы показывают тему статьи в более широком контексте услуг и решений.
Технические темы Windows
Раздел о разработке Windows, расследовании сбоев и использовании существующих активов.
Услуги по этой теме
Статья напрямую связана со следующими услугами.
Разработка приложений для Windows
Бизнес-приложения, интеграция оборудования и средства связи — от требований до разработки.
Частые вопросы
Вопросы, которые часто возникают при консультациях по теме статьи.
- Я написал try/catch в PowerShell, но выполнение почему-то не попадает в catch. Почему?
- Потому что большинство ошибок, которые выдают командлеты, — незавершающие (non-terminating). try/catch перехватывает только завершающие ошибки: незавершающая ошибка выводит сообщение и позволяет обработке продолжиться, не попадая ни в catch, ни в trap. Стандартное решение — добавить -ErrorAction Stop к команде, которую нужно перехватить (или задать $ErrorActionPreference = 'Stop' в начале скрипта). Это повышает статус незавершающей ошибки до завершающей, и try/catch начинает её обрабатывать.
- Как правильно выбирать между $? и $LASTEXITCODE?
- $? — это булево значение, показывающее, успешно ли завершилась предыдущая операция; оно устанавливается и для командлетов, и для нативных команд. $LASTEXITCODE — это код завершения последней выполненной нативной программы (или скрипта, вызвавшего exit), и он не меняется при ошибках командлетов. Для определения успеха внешних команд вроде robocopy или git надёжнее ориентироваться именно на $LASTEXITCODE, поскольку по нему можно проверить и точный смысл кода завершения. Учтите, что ненулевой код завершения нативной команды по умолчанию не попадает в catch.
- Как настроить определение успеха или сбоя скрипта PowerShell из Планировщика заданий?
- В конце скрипта (и в блоках catch) явно задавайте код завершения ключевым словом exit, а задачу запускайте через pwsh -File (или powershell.exe -File), отслеживая значение «Последний результат выполнения». При запуске через -File значение, переданное в exit, становится кодом завершения процесса как есть; без exit нормальное завершение даёт 0, а необработанное исключение — 1. При запуске через -Command любой код завершения, кроме 0 и 1, преобразуется в 1, поэтому если эксплуатация строится на кодах завершения, стандартный подход — запуск через -File.
- Для каких ошибок стоит делать повторные попытки?
- Только для временных ошибок, при которых повтор действительно может изменить результат: кратковременный сбой сети, временная блокировка файла, ожидание запуска зависимой службы и тому подобное. Бизнес-ошибки и постоянные ошибки — некорректные входные данные, нехватка прав, неверные настройки — при повторе будут завершаться неудачей раз за разом, поэтому их не нужно повторять: пусть скрипт сразу завершается ошибкой и сообщает об этом человеку через журнал и exit code. Даже при повторных попытках нужно задавать верхнюю границу по количеству и интервалу, увеличивать интервал по схеме экспоненциальной задержки, а обработку заранее проектировать идемпотентной, чтобы повторный запуск не приводил к двойной обработке.
- Что делает параметр $PSNativeCommandUseErrorActionPreference в PowerShell 7.4?
- Это настройка, которая заставляет генерировать ошибку PowerShell (незавершающую) при завершении нативной команды с ненулевым кодом. Она была добавлена как экспериментальная функция в PowerShell 7.3 и стала штатной в 7.4 (значение по умолчанию — $false). При $true поведение подчиняется $ErrorActionPreference, поэтому в сочетании со Stop сбой внешней команды можно перехватывать через try/catch. Однако есть команды вроде robocopy, которые используют ненулевой код завершения как обычную информацию, поэтому на таких участках нужно временно возвращать значение $false.
Об авторе
Страница с профилем автора статьи.
Го Комура
Представитель KomuraSoft LLC
Специализируется на разработке программного обеспечения для Windows, техническом консалтинге и расследовании сбоев, особенно в проектах с унаследованными системами и трудно воспроизводимыми ошибками.
Публичные ссылки