PowerShellでダウンロードが失敗した場合再試行するスクリプトを試作する。

powershell コンピュータ
powershell

複数のURLを順番にダウンロードするスクリプトがあるのですが、ダウンロードするURLを配列で管理しているとエラーが発生した際、どこまで実行したか不明になってしまいます。例外処理が行われていないのが原因なのですが、問題点には目をつむってエラーが発生しても再実行可能なスクリプトになるように考えてみます。

# 
# テキストファイルを読み込み先頭一行を取り出し保存し、ファイルが空になるまで繰り返す。
# 

function Get-TopRow
{
    param ([string]$list_file)

    $bak_file = "${list_file}.bak"
    Move-Item $list_file $bak_file -Force

    $row = ""
    $writer = New-Object System.IO.StreamWriter($list_file, $false, [System.Text.Encoding]::GetEncoding("utf-8"))
    $reader = New-Object System.IO.StreamReader($bak_file, [System.Text.Encoding]::GetEncoding("utf-8"))
    while (($line = $reader.ReadLine()) -ne $null)
    {
        if ($row -eq "") {
            $row = $line
        } else {
            $writer.WriteLine($line)
        }
    }
    $reader.Close()
    $writer.Close()
    
    Remove-Item $bak_file
    return $row
}

$list_file = ".\list.txt"
# ファイルが存在しない場合作成して終了
if (-not(Test-Path $list_file))
{
    New-Item $list_file
    Exit
}

# ファイルが空になるまで繰り返す
while ($row = Get-TopRow($list_file))
{
    # 何らかの処理(とりあえずコンソールに出力)
    Write-Host $row
}

あまり良い方法が思いつきませんでした。多分探せば最適な事例があるとは思いますが、検索する用語が思いつきません。
作ってみましたが例外が発生した行が失われてしまいます。

よくよく考えてみると例外が発生した行を書き出し、書き出したファイルが存在する場合そこから再実行する方法が、もっとシンプルに出来そうな気がします。もう少し欲を言えば例外で数回リトライしてダメな場合、行を書き出してスクリプトが終了する方法良さそうです。

# 
# 再実行処理の試作
# 

# 例外が発生する関数(ダウンロード処理など)
function TestFunc
{
    param([string]$row)

    # 乱数が3の場合わざと例外を発生させる
    $n = Get-Random -Maximum 3
    if ($n -eq 0)
    {
        throw $row
    }
}


# 処理対象のリスト
$listsStr = @"
a
b
c
d
e
"@
$list = ($listsStr -split "\r\n")

# エラーファイル
$errFile = ".\err.txt"

# エラーファイルがある場合レジューム
if (Test-Path $errFile)
{
    $reader = New-Object System.IO.StreamReader($errFile, [System.Text.Encoding]::GetEncoding("utf-8"))
    $line = $reader.ReadLine()
    $reader.Close()

    # 配列の再作成
    $i = [Array]::IndexOf($list, $line)
    if ($i -gt 0) {
        $list = $list[$i..($list.Count-1)]
    }
    Write-Host("前回失敗した${line}から再開")
}


# $listが空になるまで繰り返し
while ($list.Count -ge 1)
{
    # 0番目の項目を取り出し
    $row = $list[0]
    # 1番目以降をスライスで取得し配列を再作成
    if ($list.Count -eq 1)
    {
        # 長さ0の配列を強制
        $list = @()
    }
    else
    {
        $list = $list[1..($list.Count-1)]
    }

    # 確認のため出力
    Write-Host $row

    # 再試行カウント
    $rcnt = 0
    $rmax = 3
    while($rcnt -lt $rmax)
    {
        try {
            if ($rcnt -gt 0)
            {
                Write-Host "再試行${rcnt}回目"
            }
            TestFunc($row)
            $rcnt = $rmax + 99
        }
        catch {
            $rcnt += 1
            if ($rmax -le $rcnt)
            {
                # エラーファイルを書き出してスクリプトを終了
                $writer = New-Object System.IO.StreamWriter($errFile, $false, [System.Text.Encoding]::GetEncoding("utf-8"))
                $writer.WriteLine($row)
                $writer.Close()
                throw "再試行回数の限界突破"
            }
        }
        finally {
            Write-Host "finally"
        }
    }
}
# 正常終了の場合でエラーファイルがある場合削除
if (Test-Path $errFile) {
    Remove-Item $errFile
}

実行初回

PS H:\ps1> .\retryprot.ps1
a
finally
再試行1回目
finally
再試行2回目
finally
b
finally
再試行1回目
finally
再試行2回目
finally
Exception: H:\ps1\retryprot.ps1:88:17
Line |
  88 |                  throw "再試行回数の限界突破"
     |                  ~~~~~~~~~~~~~~~~~~
     | 再試行回数の限界突破

エラーファイルが出力される
実行2回目

PS H:\ps1> .\retryprot.ps1
前回失敗したbから再開
b
finally
c
finally
d
finally
再試行1回目
finally
e
finally

エラーファイルに書かれた前回失敗した場所から再開

自分が使っているダウンロードスクリプトに組み込んで試してみました。一応動作はしましたが、例外が発生しれくれないので正しく機能しているかは謎です。404のページなどはテスト出来るかもしれませんが、タイムアウト的なものはどうやってテストすれば良いのか思いつきません。(ネットワークケーブルを抜くとかルーターの電源を落とすなど物理的な方法は思いつきますが…)。そのうち例外が発生するでしょうから気長に待ちたいと思います。

追記:20240409
例外が発生し再試行もしてくれて、再度スクリプトを実行すると前回失敗した場所から再実行する部分も上手く行きました。
ただ、何の例外だったかが握りつぶされて、再試行回数の最大を超えた例外で上書きされて、ちょっと困りました。
(ネットワークのタイムアウトを想定していましたが、まるで違う例外が発生していたり…)
再試行する例外を決めてそれ以外の例外の場合はそのまま終了するように変更したいと思います。

コメント