Освойте обработку ошибок PowerShell: практическое руководство

Вам надоели эти надоедливые красные сообщения об ошибках в ваших скриптах PowerShell? Хотя они могут выглядеть устрашающе, правильная обработка ошибок является необходимой для создания надежной автоматизации в PowerShell. В этом уроке вы узнаете, как реализовать надежную обработку ошибок в ваших скриптах – от понимания типов ошибок до освоения блоков try/catch.

Предварительные требования

Этот урок предполагает, что у вас есть:

  • Установленный Windows PowerShell 5.1 или PowerShell 7+
  • Базовая знакомство с написанием скриптов PowerShell
  • Готовность воспринимать ошибки как возможности для обучения!

Понимание типов ошибок PowerShell

Перед тем как погрузиться в обработку ошибок, вам нужно понять два основных типа ошибок, которые может выдать PowerShell:

Критические ошибки

Это серьезные ошибки – ошибки, которые полностью останавливают выполнение скрипта. Вы столкнетесь с критическими ошибками, когда:

  • В вашем скрипте есть синтаксические ошибки, которые мешают его разбору
  • Происходят необработанные исключения в вызовах методов .NET
  • Вы явно указываете ErrorAction Stop
  • Критические ошибки времени выполнения делают невозможным продолжение

Некритические ошибки

Это более распространенные операционные ошибки, которые не остановят ваш скрипт:

  • Ошибки “файл не найден”
  • Сценарии “доступ запрещен”
  • Проблемы с сетевым подключением
  • Недопустимые значения параметров

Параметр ErrorAction: Ваша первая линия обороны

Давайте начнем с практического примера. Вот сценарий, который пытается удалить файлы, старше определенного количества дней:

param (
    [Parameter(Mandatory)]
    [string]$FolderPath,

    [Parameter(Mandatory)]
    [int]$DaysOld
)

$Now = Get-Date
$LastWrite = $Now.AddDays(-$DaysOld)
$oldFiles = (Get-ChildItem -Path $FolderPath -File -Recurse).Where{$_.LastWriteTime -le $LastWrite}

foreach ($file in $oldFiles) {
    Remove-Item -Path $file.FullName
    Write-Verbose -Message "Successfully removed [$($file.FullName)]."
}

По умолчанию Remove-Item генерирует не завершающие ошибки. Чтобы заставить его генерировать завершающие ошибки, которые мы можем перехватить, добавьте -ErrorAction Stop:

Remove-Item -Path $file.FullName -ErrorAction Stop

Блоки Try/Catch: Ваш швейцарский нож для обработки ошибок

Теперь давайте обернем удаление файла в блок try/catch:

foreach ($file in $oldFiles) {
    try {
        Remove-Item -Path $file.FullName -ErrorAction Stop
        Write-Verbose -Message "Successfully removed [$($file.FullName)]."
    }
    catch {
        Write-Warning "Failed to remove file: $($file.FullName)"
        Write-Warning "Error: $($_.Exception.Message)"
    }
}

Блок try содержит код, который может вызвать ошибку. Если происходит ошибка, выполнение переходит к блоку catch (если это завершающая ошибка), где вы можете:

  • Зарегистрировать ошибку
  • Принять корректирующие меры
  • Уведомить администраторов
  • Продолжить выполнение сценария гармонично

Работа с $Error: Ваш инструмент для анализа ошибок

PowerShell поддерживает массив объектов ошибок в автоматической переменной $Error. Думайте о $Error как о “черном ящике регистратора” в PowerShell – он отслеживает каждую ошибку, происходящую во время вашей сессии PowerShell, что делает его бесценным для устранения неполадок и отладки.

Вот когда и почему вам может понадобиться использовать $Error:

  1. Устранение проблем прошлых ошибок: Даже если вы упустили красное сообщение об ошибке, $Error сохраняет историю:

    # Просмотреть подробности о последней ошибке
    $Error[0] | Format-List * -Force
    
    # Посмотреть последние 5 ошибок
    $Error[0..4] | Select-Object CategoryInfo, Exception
    
    # Поиск определенных типов ошибок
    $Error | Where-Object { $_.Exception -is [System.UnauthorizedAccessException] }
    
  2. Отладка Сценариев: Используйте $Error, чтобы понять, что пошло не так и где:

    # Получить точный номер строки и сценарий, где произошла ошибка
    $Error[0].InvocationInfo | Select-Object ScriptName, ScriptLineNumber, Line
    
    # Просмотреть полный стек вызова ошибки
    $Error[0].Exception.StackTrace
    
  3. Восстановление и Сообщения об Ошибках: Идеально подходит для создания подробных отчетов об ошибках:

    # Создать отчет об ошибке
    function Write-ErrorReport {
        param($ErrorRecord = $Error[0])
    [PSCustomObject]@{
        TimeStamp = Get-Date
        ErrorMessage = $ErrorRecord.Exception.Message
        ErrorType = $ErrorRecord.Exception.GetType().Name
        Command = $ErrorRecord.InvocationInfo.MyCommand
        ScriptLine = $ErrorRecord.InvocationInfo.Line
        ErrorLineNumber = $ErrorRecord.InvocationInfo.ScriptLineNumber
        StackTrace = $ErrorRecord.ScriptStackTrace
    }
    

    }

  4. Управление Сеансами: Очистка ошибок или проверка статуса ошибки:

    # Очистить историю ошибок (полезно в начале сценариев)
    $Error.Clear()
    
    # Подсчет общего числа ошибок (хорошо для проверки порога ошибок)
    if ($Error.Count -gt 10) {
        Write-Warning "Обнаружено высокое количество ошибок: $($Error.Count) ошибок"
    }
    

Пример из реальной жизни, объединяющий эти концепции:

function Test-DatabaseConnections {
    $Error.Clear()  # Start fresh

    try {
        # Attempt database operations...
    }
    catch {
        # If something fails, analyze recent errors
        $dbErrors = $Error | Where-Object {
            $_.Exception.Message -like "*SQL*" -or
            $_.Exception.Message -like "*connection*"
        }

        if ($dbErrors) {
            Write-ErrorReport $dbErrors[0] |
                Export-Csv -Path "C:\\Logs\\DatabaseErrors.csv" -Append
        }
    }
}

Советы профессионалов:

  • $Error поддерживается для каждой сессии PowerShell
  • У него есть предельная вместимость в 256 ошибок (контролируется $MaximumErrorCount)
  • Это массив фиксированного размера – новые ошибки вытесняют старые, когда он заполнен
  • Всегда проверяйте $Error[0] в первую очередь – это самая последняя ошибка
  • Рассмотрите возможность очистки $Error в начале важных скриптов для чистого отслеживания ошибок

Несколько блоков Catch: Целевая обработка ошибок

Так же, как вы не будете использовать один и тот же инструмент для каждого домашнего ремонта, вы не должны обрабатывать каждую ошибку PowerShell одинаково. Несколько блоков catch позволяют вам реагировать по-разному на разные типы ошибок.

Вот как это работает:

try {
    Remove-Item -Path $file.FullName -ErrorAction Stop
}
catch [System.UnauthorizedAccessException] {
    # This catches permission-related errors
    Write-Warning "Access denied to file: $($file.FullName)"
    Request-ElevatedPermissions -Path $file.FullName  # Custom function
}
catch [System.IO.IOException] {
    # This catches file-in-use errors
    Write-Warning "File in use: $($file.FullName)"
    Add-ToRetryQueue -Path $file.FullName  # Custom function
}
catch [System.Management.Automation.ItemNotFoundException] {
    # This catches file-not-found errors
    Write-Warning "File not found: $($file.FullName)"
    Update-FileInventory -RemovePath $file.FullName  # Custom function
}
catch {
    # This catches any other errors
    Write-Warning "Unexpected error: $_"
    Write-EventLog -LogName Application -Source "MyScript" -EntryType Error -EventId 1001 -Message $_
}

Общие типы ошибок, с которыми вы столкнетесь:

  • [System.UnauthorizedAccessException] – Доступ запрещен
  • [System.IO.IOException] – Файл заблокирован/используется
  • [System.Management.Automation.ItemNotFoundException] – Файл/путь не найден
  • [System.ArgumentException] – Неверный аргумент
  • [System.Net.WebException] – Проблемы с сетью/вебом

Вот реальный пример, который это иллюстрирует:

function Remove-StaleFiles {
    [CmdletBinding()]
    param(
        [string]$Path,
        [int]$RetryCount = 3,
        [int]$RetryDelaySeconds = 30
    )

    $retryQueue = @()

    foreach ($file in (Get-ChildItem -Path $Path -File)) {
        $attempt = 0
        do {
            $attempt++
            try {
                Remove-Item -Path $file.FullName -ErrorAction Stop
                Write-Verbose "Successfully removed $($file.FullName)"
                break  # Exit the retry loop on success
            }
            catch [System.UnauthorizedAccessException] {
                if ($attempt -eq $RetryCount) {
                    # Log to event log and notify admin
                    $message = "Permission denied after $RetryCount attempts: $($file.FullName)"
                    Write-EventLog -LogName Application -Source "FileCleanup" -EntryType Error -EventId 1001 -Message $message
                    Send-AdminNotification -Message $message  # Custom function
                }
                else {
                    # Request elevated permissions and retry
                    Request-ElevatedAccess -Path $file.FullName  # Custom function
                    Start-Sleep -Seconds $RetryDelaySeconds
                }
            }
            catch [System.IO.IOException] {
                if ($attempt -eq $RetryCount) {
                    # Add to retry queue for later
                    $retryQueue += $file.FullName
                    Write-Warning "File locked, added to retry queue: $($file.FullName)"
                }
                else {
                    # Wait and retry
                    Write-Verbose "File in use, attempt $attempt of $RetryCount"
                    Start-Sleep -Seconds $RetryDelaySeconds
                }
            }
            catch {
                # Unexpected error - log and move on
                $message = "Unexpected error with $($file.FullName): $_"
                Write-EventLog -LogName Application -Source "FileCleanup" -EntryType Error -EventId 1002 -Message $message
                break  # Exit retry loop for unexpected errors
            }
        } while ($attempt -lt $RetryCount)
    }

    # Return retry queue for further processing
    if ($retryQueue) {
        return $retryQueue
    }
}

Советы профессионалов для нескольких блоков Catch:

  1. Порядок имеет значение – сначала указывайте более специфичные исключения
  2. Используйте пользовательские функции для обработки каждого типа ошибок последовательно
  3. Рассмотрите логику повтора для временных ошибок
  4. Регистрируйте различные типы ошибок в различные места
  5. Используйте наиболее конкретный тип исключения возможный
  6. Тестируйте каждый блок catch, умышленно вызывая каждый тип ошибки

Использование блоков Finally: Почистите за собой

Блок finally – это ваша бригада уборщиков – он всегда выполняется, вне зависимости от наличия ошибок или нет. Это делает его идеальным для:

  • Закрытия файловых дескрипторов
  • Отключения от баз данных
  • Освобождения системных ресурсов
  • Восстановления исходных настроек

Вот практический пример:

try {
    $stream = [System.IO.File]::OpenRead($file.FullName)
    # Process file contents here...
}
catch {
    Write-Warning "Error processing file: $_"
}
finally {
    # This runs even if an error occurred
    if ($stream) {
        $stream.Dispose()
        Write-Verbose "File handle released"
    }
}

Думайте о блоке finally как о правиле ответственного кемпера: “Всегда убирайте свой лагерь перед отъездом, несмотря на все, что произошло во время поездки.”

Лучшие практики обработки ошибок

  1. Будьте конкретными с действиями по ошибке
    Вместо общего ErrorAction Stop используйте его выборочно для команд, где вам нужно перехватывать ошибки.

  2. Используйте переменные ошибок

    Remove-Item $path -ErrorVariable removeError
    if ($removeError) {
        Write-Warning "Не удалось удалить элемент: $($removeError[0].Exception.Message)"
    }
    
  3. Регистрируйте ошибки соответственно

    • Используйте Write-Warning для восстановимых ошибок
    • Используйте Write-Error для серьезных проблем
    • Рассмотрите возможность записи в журнал событий Windows для критических сбоев
  4. Очистите ресурсы
    Всегда используйте блоки finally для очистки ресурсов, таких как дескрипторы файлов и сетевые подключения.

  5. Тестирование обработки ошибок
    Намеренно вызывайте ошибки, чтобы проверить, что ваша обработка ошибок работает так, как ожидается.

Сборка всего вместе

Вот полный пример, включающий эти лучшие практики:

function Remove-OldFiles {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory)]
        [string]$FolderPath,

        [Parameter(Mandatory)]
        [int]$DaysOld,

        [string]$LogPath = "C:\\Logs\\file-cleanup.log"
    )

    try {
        # Validate input
        if (-not (Test-Path -Path $FolderPath)) {
            throw "Folder path '$FolderPath' does not exist"
        }

        $Now = Get-Date
        $LastWrite = $Now.AddDays(-$DaysOld)

        # Find old files
        $oldFiles = Get-ChildItem -Path $FolderPath -File -Recurse |
                    Where-Object {$_.LastWriteTime -le $LastWrite}

        foreach ($file in $oldFiles) {
            try {
                Remove-Item -Path $file.FullName -ErrorAction Stop
                Write-Verbose -Message "Successfully removed [$($file.FullName)]"

                # Log success
                "$(Get-Date) - Removed file: $($file.FullName)" |
                    Add-Content -Path $LogPath
            }
            catch [System.UnauthorizedAccessException] {
                Write-Warning "Access denied to file: $($file.FullName)"
                "$ErrorActionPreference - Access denied: $($file.FullName)" |
                    Add-Content -Path $LogPath
            }
            catch [System.IO.IOException] {
                Write-Warning "File in use: $($file.FullName)"
                "$(Get-Date) - File in use: $($file.FullName)" |
                    Add-Content -Path $LogPath
            }
            catch {
                Write-Warning "Unexpected error removing file: $_"
                "$(Get-Date) - Error: $_ - File: $($file.FullName)" |
                    Add-Content -Path $LogPath
            }
        }
    }
    catch {
        Write-Error "Critical error in Remove-OldFiles: $_"
        "$(Get-Date) - Critical Error: $_" |
            Add-Content -Path $LogPath
        throw  # Re-throw error to calling script
    }
}

Эта реализация:

  • Проверяет входные параметры
  • Использует конкретные блоки catch для обычных ошибок
  • Регистрирует как успешные, так и неудачные действия
  • Обеспечивает подробный вывод для устранения неполадок
  • Перехватывает критические ошибки и передает их вызывающему скрипту

Заключение

Правильная обработка ошибок имеет решающее значение для надежных скриптов PowerShell. Понимая типы ошибок и эффективно используя блоки try/catch, вы можете создавать скрипты, которые грациозно обрабатывают сбои и предоставляют значимую обратную связь. Не забудьте тщательно протестировать вашу обработку ошибок – ваше будущее “я” поблагодарит вас, когда вы будете устранять проблемы в производстве!

Теперь вперед и ловите эти ошибки! Просто помните – единственная плохая ошибка – это необработанная ошибка.

Source:
https://adamtheautomator.com/powershell-error-handling/