logo
Published on

出力が止まったら再起動 #PowerShell

Authors

PowerShellでのコマンド・プログラム実行において、標準出力が停止(フリーズ・ハング)してしまうような状況があると思う。これを検知し、その場合に該当のプログラムを停止、再実行するような実装を行った。

例えば、以下は5秒ごとにffmpegコマンドを実行するPowerShellスクリプトである。

ffmpeg -f dshow -i audio="Microphone (USB Microphone)" -y -t 00:00:05 "tmp.mp3"

なお、本紹介においてはスタックオーバーフローの回答を参考にした。

I need to get the current state of the last line of the command output stream, then if the line remains unchanged (staled/freezing/hanging) over 5 seconds log "warning: the program is freezing. trying to restart...", then stop the process and re-start the command. command line - Restart the process if its output unchanged in PowerShell - Super User

(日本語訳) コマンドの出力ストリームの最後の行の現在の状態を取得し、行が5秒間変化しない(停止/フリーズ/ハング)場合は「プログラムがフリーズしています。再起動を試みています...」とログ出力し、プロセスを停止してコマンドを再起動したい。方法はありませんか?

上記のコマンドは5秒間の実行をし終了するが、シンプルな実装であれば、以下のようにループして半永久的に実行させ続けることができる。

for ($i = 0; $i -gt -1; $i++) {
  $date = Get-Date -Format "yyyy-MM-ddTHH-mm-ss"
  $dir = "C:\_documents\WindowsPowerShell\data\recording\$($date.Substring(0,4))\$($date.Substring(5,2))\$($date.Substring(8,2))"
  $path = "$dir\$($date.Substring(11,2))-$($date.Substring(14,2))-$($date.Substring(17,2)).mp3"
  New-Item -ItemType Directory -Force -Path $dir
  ffmpeg -f dshow -i audio="Microphone (USB Microphone)" -y -t 00:10:00 -b:a 128k $path
}

しかしここで問題なのが、上記のような実行途中でハング・フリーズしてしまうようなプログラムは、そうなった際に自動でそれに対処することのできるようなメカニズムを用意してやる必要があるということだ。

これがWindowsでなくLinuxなら、tmuxやscreenを活用することでターミナルコンソールの出力や入力に簡単に干渉することができるが、WindowsでのPowerShellにおいては、tmuxほど開発が進み汎用的な使用に耐えうるようなものはないと思われる。

ちなみに、本記事で取り扱っているのはあくまで"ハング・出力の停止・フリーズ"に対する対処であり、プログラムの強制終了についてではない。コマンドがエラーコードを伴って終了するならば、別の(よりシンプルな)実装方法が存在する。

例えばNode.jsプログラムでは以下のようにプログラム部分をラッピングすることで、エラーが発生しコマンドが終了した場合にプログラムを再度実行させることができる。

while ($true) {
    try {
        node .\notify_app\amazon_scrape.js
    } catch {
        Write-Host "An error occurred: $_"
        Write-Host "Restarting the program..."
    }
    # Optional delay between restarts (in seconds)
    Start-Sleep -Seconds 1
}

Javaの場合でもほとんど同様の実装が可能である。

while ($true) {
    try {
        # Start your Java application using the java -jar command
        Start-Process -FilePath "java" -ArgumentList "-jar", $jarPath -NoNewWindow
        # Wait for the Java application to finish
        Wait-Process -Name "java" -ErrorAction SilentlyContinue
    }
    catch {
        Write-Host "An error occurred: $($_.Exception.Message)"
    }
}

さて、話を戻すと、PowerShellにおいては、以下のようにStart-Processを用いてコマンドを実行し、当該のバックグラウンドプロセスの出力ログに干渉してやることで、必要に応じた処理を行うことができる。

$log = "C:\_documents\WindowsPowerShell\data\recording\output.log";
$micName = "Microphone (USB Microphone)";

$process = $null;

Function run-ffmpeg {param([string]$Log,[string]$MicName)
    $date = Get-Date -Format "yyyy-MM-ddTHH-mm-ss"
    $dir = "C:\_documents\WindowsPowerShell\data\recording\$($date.Substring(0,4))\$($date.Substring(5,2))\$($date.Substring(8,2))"
    $path = "$dir\$($date.Substring(11,2))-$($date.Substring(14,2))-$($date.Substring(17,2)).mp3"
    New-Item -ItemType Directory -Force -Path $dir
    Write-Host "Saving to file path: $path";

    $ffmpegCommand = "ffmpeg -f dshow -i audio='$MicName' -y -t 00:10:00 -b:a 128k $path 2>> $Log"

    $process = Start-Process powershell -ArgumentList "-NoProfile -ExecutionPolicy Bypass -Command $($ffmpegCommand)" -WindowStyle Hidden -PassThru;

    Write-Host "Process ID: $($process.Id)";
    # Stop-Process -Id $process.Id

    Start-Sleep -Seconds 4;
    logchecks $Log;
};

Function tail {Get-Content -Path $args[0] -Tail 1};
Function check {tail $args[0]};

Function logchecks {
    do {
        $checklog = check $args[0];
        Start-Sleep -Seconds 5;
        $rechecklog = check $args[0];
        if ($checklog -eq $rechecklog) {
            $message = "Restarting triggered. [reason: the last line of the log file has not changed after 5 seconds]";
            Write-Host $message;
            Write-Host "Stopping process with ID: $($process.Id)";
            
            # json, {type: "separator", date: date, message: message}
            $now = Get-Date -Format "yyyy-MM-ddTHH-mm-ss";
            Write-Output "{`"type`":`"separator`",`"date`":`"$now`",`"message`":`"$message`"}" 2>&1 | Tee-Object -FilePath $Log -Append

            # try to prevent the program from being quit
            try {
                Stop-Process -Id $process.Id;
            } catch {
                Write-Host "Process with ID: $($process.Id) has already been stopped";
            }
        }
    } while ($checklog -ne $rechecklog)

    run-ffmpeg -Log $log -MicName $micName;  
};

logchecks $log;

上記のスクリプトの実行フローは以下の通りである。

  1. 初回のrun-ffmpegがlogchecks関数のより実行される
  2. run-ffmpeg関数がffmpegコマンドを実行し、標準出力をファイルにリダイレクトする
  3. logchecks関数が5秒ごとに出力ログの最終行をチェックする。もし5秒前と変化がなければ、フリーズと判断し、ffmpegコマンドを実行しているバックグラウンドプロセスを停止し、再度run-ffmpeg関数を実行する。変化があれば、そのまま5秒後に再度チェックを行い、以降同様の処理を繰り返す。

これにより、ログがフリーズした場合のハンドリングが可能となる。