【PowerShell】プロセスを監視して自動再起動する方法|ログ・多重起動防止

【PowerShell】特定のプロセスを定期監視して自動で再起動するスクリプト PowerShell

Windows上で常時動かしたいアプリケーションが落ちたとき、PowerShellでプロセスを監視して自動再起動する仕組みを作れます。ただし、単にGet-Process -Name appで見つからなければStart-Processするだけでは、同名プロセスの誤判定、連続再起動、ログ不足、タスクスケジューラ実行時の作業フォルダ違いで運用が不安定になります。

この記事では、実行ファイルパスまで確認して監視し、停止時だけ安全に再起動するPowerShellスクリプトを作ります。ログ、多重起動防止、再起動間隔の制御、タスクスケジューラ登録まで含めて、実務で使える形に整理します。

先に結論

  • 同名プロセス対策として、プロセス名だけでなく実行ファイルパスも確認します。
  • Start-Processには-WorkingDirectoryと必要な-ArgumentListを指定します。
  • 起動失敗を追えるよう、ログファイルへ時刻・状態・PIDを記録します。
  • 落ち続けるアプリを無限に再起動しないよう、クールダウン時間を設けます。
  • 監視スクリプト自体の多重起動を防ぎます。
  • 常駐運用はタスクスケジューラで起動し、実行ユーザーと作業フォルダを固定します。

ps1が実行できない場合はPowerShellでps1が実行できない原因と対処、バッチからPowerShellを呼ぶ場合はPowerShellをバッチファイルから呼び出す方法も参考にしてください。bat版の考え方はtasklistでプロセス監視する方法でも解説しています。

スポンサーリンク

PowerShell監視が向くケース

PowerShellによる監視は、簡易ツール、社内アプリ、GUIアプリ、常駐バッチの補助監視に向いています。一方、Windowsサービスとして作れるアプリなら、サービスの回復オプションを使う方が自然です。

  • 向いている:exeを起動するだけで復旧できる社内ツール
  • 向いている:ログを残しながら簡易的に死活監視したい処理
  • 注意が必要:ログインユーザーのデスクトップが必要なGUIアプリ
  • 別方式を検討:Windowsサービス、依存サービス、複雑な復旧手順がある処理

最小構成の監視スクリプト

まず、指定したプロセス名が存在しなければ起動する最小例です。仕組みの理解には十分ですが、実務では後述の完全版を使ってください。

minimal-process-monitor.ps1
$ProcessName = "SampleApp"
$ExePath = "C:\Apps\SampleApp\SampleApp.exe"
$IntervalSeconds = 60

while ($true) {
    $process = Get-Process -Name $ProcessName -ErrorAction SilentlyContinue

    if (-not $process) {
        Start-Process -FilePath $ExePath
    }

    Start-Sleep -Seconds $IntervalSeconds
}

この例は短い反面、同じ名前の別プロセスがいると起動済みと誤判定します。また、アプリが起動直後に落ち続けると、60秒ごとに再起動を繰り返します。原因調査のためのログもありません。

実行ファイルパスで対象プロセスを判定する

実務では、プロセス名だけでなく実行ファイルのパスも確認します。Get-Processでも多くの場合はPathを取得できますが、権限や32bit/64bitの違いで取れないことがあります。ここではWin32_Processを使ってExecutablePathを確認します。

find-process-by-path.ps1
$ExePath = "C:\Apps\SampleApp\SampleApp.exe"
$ProcessFile = Split-Path -Path $ExePath -Leaf

Get-CimInstance Win32_Process -Filter "Name = '$ProcessFile'" |
    Where-Object { $_.ExecutablePath -eq $ExePath } |
    Select-Object ProcessId, Name, ExecutablePath, CommandLine

この方法なら、別フォルダにある同名exeを誤って「起動中」と判断しにくくなります。複数ユーザーで同じexeを起動する環境では、実行ユーザーやセッションIDも確認対象に含めます。

ログ出力と多重起動防止を入れた完全版

次のスクリプトは、実行ファイルの存在確認、パスによるプロセス判定、ログ出力、再起動クールダウン、多重起動防止を含む実用版です。

monitor-process.ps1
param(
    [string]$ExePath = "C:\Apps\SampleApp\SampleApp.exe",
    [string]$WorkingDirectory = "C:\Apps\SampleApp",
    [string[]]$ArgumentList = @(),
    [int]$IntervalSeconds = 60,
    [int]$RestartCooldownSeconds = 300,
    [int]$MaxRestarts = 3,
    [int]$RestartWindowMinutes = 60,
    [int]$StartupGraceSeconds = 10,
    [string]$LogPath = "C:\Logs\process-monitor.log"
)

$ErrorActionPreference = "Stop"

if (-not (Test-Path -LiteralPath $ExePath)) {
    throw "Executable file was not found: $ExePath"
}

if (-not (Test-Path -LiteralPath $WorkingDirectory)) {
    throw "Working directory was not found: $WorkingDirectory"
}

$logDirectory = Split-Path -Path $LogPath -Parent
if ($logDirectory -and -not (Test-Path -LiteralPath $logDirectory)) {
    New-Item -Path $logDirectory -ItemType Directory -Force | Out-Null
}

$processFile = Split-Path -Path $ExePath -Leaf
$mutexName = "Global\ProcessMonitor_$($processFile -replace '[^a-zA-Z0-9_]', '_')"
$createdNew = $false
$mutex = [System.Threading.Mutex]::new($true, $mutexName, [ref]$createdNew)

if (-not $createdNew) {
    "Another monitor is already running. Mutex: $mutexName" |
        Out-File -FilePath $LogPath -Append -Encoding UTF8
    exit 2
}

function Write-MonitorLog {
    param([string]$Message)

    $line = "{0} {1}" -f (Get-Date -Format "yyyy-MM-dd HH:mm:ss"), $Message
    $line | Out-File -FilePath $LogPath -Append -Encoding UTF8
}

function Get-TargetProcess {
    Get-CimInstance Win32_Process -Filter "Name = '$processFile'" |
        Where-Object { $_.ExecutablePath -eq $ExePath }
}

function Start-TargetProcess {
    try {
        $startParams = @{
            FilePath = $ExePath
            WorkingDirectory = $WorkingDirectory
            PassThru = $true
        }

        if ($ArgumentList.Count -gt 0) {
            $startParams.ArgumentList = $ArgumentList
        }

        $started = Start-Process @startParams
        Write-MonitorLog "Start command succeeded. PID=$($started.Id) Path=$ExePath"

        Start-Sleep -Seconds $StartupGraceSeconds
        $verified = @(Get-TargetProcess)

        if ($verified.Count -eq 0) {
            throw "The process stopped within ${StartupGraceSeconds}s after startup."
        }

        $pids = ($verified | Select-Object -ExpandProperty ProcessId) -join ","
        Write-MonitorLog "Startup verified. PID=$pids"
        return $true
    }
    catch {
        Write-MonitorLog "Restart failed. Error=$($_.Exception.Message)"
        return $false
    }
}

$lastRestartAt = [datetime]::MinValue
$lastState = ""
$restartHistory = [System.Collections.Generic.Queue[datetime]]::new()

try {
    Write-MonitorLog "Monitor started. Target=$ExePath Interval=${IntervalSeconds}s MaxRestarts=$MaxRestarts/${RestartWindowMinutes}min"

    while ($true) {
        try {
            $now = Get-Date
            $windowStart = $now.AddMinutes(-$RestartWindowMinutes)

            while ($restartHistory.Count -gt 0 -and $restartHistory.Peek() -lt $windowStart) {
                [void]$restartHistory.Dequeue()
            }

            $processes = @(Get-TargetProcess)

            if ($processes.Count -gt 0) {
                $pids = ($processes | Select-Object -ExpandProperty ProcessId) -join ","
                if ($lastState -ne "RUNNING") {
                    Write-MonitorLog "Process is running. PID=$pids"
                    $lastState = "RUNNING"
                }
            }
            else {
                $secondsSinceRestart = ($now - $lastRestartAt).TotalSeconds

                if ($restartHistory.Count -ge $MaxRestarts) {
                    if ($lastState -ne "LIMIT_REACHED") {
                        Write-MonitorLog "Restart limit reached. Attempts=$($restartHistory.Count) Window=${RestartWindowMinutes}min"
                        $lastState = "LIMIT_REACHED"
                    }
                }
                elseif ($secondsSinceRestart -lt $RestartCooldownSeconds) {
                    if ($lastState -ne "COOLDOWN") {
                        Write-MonitorLog "Process is stopped, but restart is in cooldown."
                        $lastState = "COOLDOWN"
                    }
                }
                else {
                    Write-MonitorLog "Process is stopped. Restarting..."
                    $lastRestartAt = Get-Date
                    $restartHistory.Enqueue($lastRestartAt)

                    if (Start-TargetProcess) {
                        $lastState = "RESTARTED"
                    }
                    else {
                        $lastState = "RESTART_FAILED"
                    }
                }
            }
        }
        catch {
            Write-MonitorLog "Monitoring check failed. Error=$($_.Exception.Message)"
            $lastState = "MONITOR_ERROR"
        }

        Start-Sleep -Seconds $IntervalSeconds
    }
}
finally {
    if ($createdNew) {
        $mutex.ReleaseMutex()
    }
    $mutex.Dispose()
    Write-MonitorLog "Monitor stopped."
}

Start-Process-PassThruを指定すると、起動したプロセスのPIDをログへ残せます。起動後はStartupGraceSecondsだけ待ち、対象プロセスが残っているかを再確認します。起動コマンドが成功しても、設定ファイル不足などで直後に終了するケースを見逃しません。

起動失敗はcatchでログへ記録し、監視ループ自体は終了させません。プロセス情報の取得に一時的に失敗した場合もMonitoring check failedを残し、次の巡回で監視を再開します。-WorkingDirectoryを指定しないと、タスクスケジューラから実行したときに想定外のフォルダを基準に動くことがあります。

このスクリプトは、同じ監視対象に対して監視スクリプトが二重起動することをMutexで防ぎます。複数のアプリを別々に監視する場合は、対象exeごとに別のMutex名になります。

引数付きでアプリを起動する

監視対象アプリに起動引数が必要な場合は、配列で渡します。スペースを含む値も、配列に分けると扱いやすくなります。

run-monitor-with-arguments.ps1
.\monitor-process.ps1 `
  -ExePath "C:\Apps\ReportTool\ReportTool.exe" `
  -WorkingDirectory "C:\Apps\ReportTool" `
  -ArgumentList @("--config", "C:\Apps\ReportTool\prod.json") `
  -IntervalSeconds 60 `
  -RestartCooldownSeconds 300 `
  -MaxRestarts 3 `
  -RestartWindowMinutes 60 `
  -StartupGraceSeconds 10 `
  -LogPath "C:\Logs\report-tool-monitor.log"

ps1ファイルが実行できない場合は、実行ポリシーやファイルのブロック状態を確認してください。診断手順はps1が実行できない原因と対処で整理しています。

タスクスケジューラで常駐起動する

監視スクリプトを手動で開いたPowerShellウィンドウに任せると、ログオフやウィンドウ終了で監視も止まります。常駐させるならタスクスケジューラへ登録します。

task-scheduler-action.txt
プログラム:
powershell.exe

引数:
-NoProfile -ExecutionPolicy Bypass -File "C:\Scripts\monitor-process.ps1" -ExePath "C:\Apps\SampleApp\SampleApp.exe" -WorkingDirectory "C:\Apps\SampleApp" -LogPath "C:\Logs\sampleapp-monitor.log"

開始:
C:\Scripts

タスクのトリガーは「コンピューターの起動時」または「ログオン時」にします。GUIアプリを起動する場合は、実行ユーザーのデスクトップセッションが必要になることがあるため、「ユーザーがログオンしているときのみ実行する」の方が適する場合があります。

SSHやログ取得など、定期処理をタスクスケジューラで動かす考え方はPowerShellでSSH経由のログ取得をタスクスケジューラ連携する方法も参考になります。

ログの見方とローテーション

ログには監視開始、稼働検知、停止検知、再起動、停止を記録します。長期間運用する場合はログサイズが増えるため、日付別ファイルにするか、一定サイズで古いログを退避します。

daily-log-path.ps1
$today = Get-Date -Format "yyyyMMdd"
$LogPath = "C:\Logs\process-monitor-$today.log"

ログをイベントログへ書きたい場合はWrite-EventLogも使えますが、事前にイベントソースの登録が必要です。まずはファイルログで運用し、監視基盤へ連携する段階でイベントログ化を検討すると簡単です。

連続再起動を抑制する

起動してすぐ落ちるアプリを短い間隔で再起動し続けると、ログやCPUを圧迫し、原因調査も難しくなります。完全版ではRestartCooldownSecondsで試行間隔を空け、さらにMaxRestartsRestartWindowMinutesで一定時間内の再起動回数を制限しています。

  • 1回目の停止では再起動する
  • 直近の再起動から5分以内なら再起動しない
  • クールダウン中であることをログへ残す
  • 60分以内に3回試行したら、それ以前の試行が集計対象外になるまで待つ
  • 起動失敗も1回として数え、設定不備による無限再試行を防ぐ

停止と起動失敗をテストする

本番運用へ入れる前に、監視対象を手動終了して正常に再起動することを確認します。次に、検証環境で起動引数を一時的に誤った値へ変更し、Restart failedが記録されたあとも監視スクリプトが動き続けることを確認してください。

process-monitor.log
2026-06-09 09:00:00 Monitor started. Target=C:\Apps\SampleApp\SampleApp.exe Interval=60s MaxRestarts=3/60min
2026-06-09 09:01:00 Process is stopped. Restarting...
2026-06-09 09:01:00 Start command succeeded. PID=1234 Path=C:\Apps\SampleApp\SampleApp.exe
2026-06-09 09:01:10 Startup verified. PID=1234
2026-06-09 10:15:00 Restart failed. Error=The process stopped within 10s after startup.
2026-06-09 10:25:00 Restart limit reached. Attempts=3 Window=60min

Start command succeededのあとにStartup verifiedがあれば、猶予時間経過後も対象プロセスが稼働しています。Restart limit reachedが出た場合は、回数制限を安易に増やす前にアプリ側のログ、起動引数、権限、依存ファイルを確認します。

複数プロセスを監視する

複数アプリを監視したい場合は、完全版スクリプトを対象ごとに別タスクとして登録する方法が分かりやすいです。1つのスクリプトでまとめたい場合は設定配列を使います。

multiple-process-config.ps1
$targets = @(
    @{
        ExePath = "C:\Apps\AppA\AppA.exe"
        WorkingDirectory = "C:\Apps\AppA"
        LogPath = "C:\Logs\appa-monitor.log"
    },
    @{
        ExePath = "C:\Apps\AppB\AppB.exe"
        WorkingDirectory = "C:\Apps\AppB"
        LogPath = "C:\Logs\appb-monitor.log"
    }
)

foreach ($target in $targets) {
    .\monitor-process.ps1 @target
}

ただし、この例のように順番に呼ぶと最初の監視が無限ループするため、同時監視には向きません。実務では、対象ごとにタスクを分ける、または1つの監視ループ内で全対象を巡回する構成にします。

1つのループで複数対象を巡回する例

1つのPowerShellプロセスで複数対象を巡回したい場合は、各対象の状態をハッシュテーブルで持ちます。ここでは考え方だけを示します。

multi-target-loop-outline.ps1
$targets = @(
    @{ Name = "AppA"; ExePath = "C:\Apps\AppA\AppA.exe"; WorkingDirectory = "C:\Apps\AppA" },
    @{ Name = "AppB"; ExePath = "C:\Apps\AppB\AppB.exe"; WorkingDirectory = "C:\Apps\AppB" }
)

while ($true) {
    foreach ($target in $targets) {
        # 対象ごとに Get-CimInstance で存在確認し、
        # 停止していれば Start-Process する
    }

    Start-Sleep -Seconds 60
}

完全な複数対象監視はログ、クールダウン、例外処理も対象ごとに持つ必要があります。保守しやすさを優先するなら、最初はタスクを分ける構成がおすすめです。

管理者権限が必要な場合

監視対象が管理者権限を必要とする場合、監視スクリプトも同じ権限で起動する必要があります。タスクスケジューラでは「最上位の特権で実行する」を有効にします。

管理者権限の自動取得やUACの扱いは、bat記事ですが管理者権限を自動取得する方法が参考になります。権限不足で起動に失敗する場合、ログにAccess is deniedなどが残るようにしておくと切り分けやすくなります。

プロセス監視とポート監視を分けて考える

プロセスが存在していても、アプリがハングしてポート応答しないことがあります。WebサーバーやAPIなら、プロセス監視だけでなくポートやHTTPの応答確認も組み合わせます。

ポート確認はTest-NetConnectionで特定ポートをチェックする方法、ホスト監視はPing監視を自動化する方法を参考にしてください。

運用前のチェックリスト

  • exeのフルパスが正しい
  • 作業フォルダが正しい
  • 起動引数が手動実行時と同じ
  • ログフォルダへ書き込みできる
  • 同名プロセスを誤判定しない
  • 連続再起動が抑制される
  • 監視スクリプトが二重起動しない
  • タスクスケジューラの実行ユーザーが正しい
  • 必要なら最上位の特権で実行している
  • 停止方法とログ確認方法を運用手順に書いている

まとめ

PowerShellでプロセスを監視して自動再起動する場合は、プロセス名だけで判定せず、実行ファイルパスまで確認することが重要です。Start-Processでは-WorkingDirectory-ArgumentList-PassThruを適切に使い、起動結果をログへ残します。

実務運用では、ログ出力、連続再起動抑制、多重起動防止、タスクスケジューラの実行ユーザー設定が効いてきます。プロセスが生きているだけでは正常とは限らないため、必要に応じてポート監視やHTTP応答確認も組み合わせてください。