【bat】バッチファイルに実行タイムアウトを設定して自動終了する完全ガイド|PID取得・2バッチ方式・外部プロセス強制終了・経過時間チェックまで

【bat】バッチファイルに実行タイムアウトを設定して自動終了する完全ガイド|PID取得・2バッチ方式・外部プロセス強制終了・経過時間チェックまで bat

バッチファイルで「30分以上かかったら強制終了したい」「無限ループに陥っても一定時間で抜けたい」という要件はよくあります。バッチ自体にはタイムアウト機能がありませんが、自分のPIDを取得して外部から終了する・経過時間をカウントして自分で exit する・PowerShell の Job タイムアウトを使うなど複数の実装方法があります。

この記事でわかること

  • timeout + exit /b で処理時間を上限付きにする最もシンプルな方法
  • 自分のPIDをwmicで取得して、別プロセスから taskkill で強制終了する2バッチ方式
  • 外部プロセス(exe)にタイムアウトをかけて強制終了する方法
  • ループ内で経過時間を計算してソフトタイムアウトを実装する方法
  • PowerShell Job を使ったタイムアウト付き実行(確実な方式)
スポンサーリンク

タイムアウト実装方法の比較

方法 仕組み 確実性 外部プロセス対応 実装難度
timeout + exit /b 時間経過後に自分で exit △(処理がブロックすると動かない)
2バッチ方式(PID + taskkill) 別プロセスが時間後に taskkill ○(外部から強制終了)
経過時間チェック(ループ内) ループ毎に時刻差を計算して exit ○(ループが前提)
PowerShell Job Job に -TimeoutSec を指定 ◎(プロセスツリーごと終了可)
方法の使い分け
「時間が来たら自分で終わる」だけなら 経過時間チェック が最もシンプルです。外部 exe など自分でコントロールできないプロセスにタイムアウトをかけるには2バッチ方式PowerShell Job が確実です。本番環境での安定性重視なら PowerShell Job を推奨します。

方法1:timeout + exit /b でシンプルに時間制限する

処理がI/O待ちやブロッキングしない場合の最もシンプルな実装です。処理の合間に timeout を挟んでカウントし、上限に達したら exit /b します。

timeout + exit /b: 最大実行時間を設定する基本パターン
@echo off
setlocal enabledelayedexpansion

REM 最大実行時間(秒)
set MAX_SEC=30
set ELAPSED=0

:MAIN_LOOP
REM --- ここに処理を記述 ---
echo [%ELAPSED%秒] 処理中...

REM 経過時間を加算
set /a ELAPSED+=5

if %ELAPSED% geq %MAX_SEC% (
    echo タイムアウト: %MAX_SEC%秒 経過。終了します。
    exit /b 1
)

timeout /t 5 /nobreak >nul
goto MAIN_LOOP
この方式はブロッキング処理では使えない
timeout /t 5 はキー入力や /nobreak で制御できますが、外部コマンドの実行中(例: robocopy が大容量ファイルを処理中)は次の行に進まないため経過時間チェックが機能しません。外部コマンドにタイムアウトをかける場合は後述の2バッチ方式か PowerShell を使ってください。

方法2:自分のPIDを取得して2バッチ方式で強制終了する

別バッチを起動して「N秒後に taskkill /f /pid PID する」処理を担当させます。メインバッチがブロッキング処理中でも外部から確実に終了できます。

自分のPIDを wmic で取得する方法
@echo off
setlocal

REM wmic で現在の cmd.exe のプロセスIDを取得
REM %CMDCMDLINE% には現在の cmd.exe のコマンドライン文字列が含まれる

set MY_PID=
for /f "tokens=1" %%P in (
    'wmic process where "name='cmd.exe'" get ProcessId /value
    ^| findstr /r "[0-9]"
') do (
    REM WBEM_S_NO_MORE_DATA などのゴミ行を除外して最後の数値を取得
    set /a CHECK=%%P 2>nul
    if !CHECK! gtr 0 set MY_PID=%%P
)

echo 自分のPID: %MY_PID%
endlocal
より確実な PID 取得方法
wmic で cmd.exe の PID を取得するとき、複数の cmd.exe が起動していると複数の PID が返ります。最も確実なのは %ERRORLEVEL% の代わりに PowerShell を1行使って自分の PID を取得する方法です(後述)。または wmic process get ProcessId,ParentProcessId,Name で親プロセスIDを辿る方法もあります。
2バッチ方式: main.bat + killer.bat
REM ===== main.bat =====
@echo off
setlocal enabledelayedexpansion

set TIMEOUT_SEC=30
set LOGFILE=C:\logs\timeout_test.log

REM PowerShell で自分の PID を確実に取得
for /f %%P in (
    'powershell -NoProfile -Command "$pid"'
) do set MY_PID=%%P

echo [INFO] 開始 PID=%MY_PID% タイムアウト=%TIMEOUT_SEC%秒

REM killer.bat を別プロセスで起動(タイムアウト後に自分を終了させる)
start "" /min cmd /c "timeout /t %TIMEOUT_SEC% /nobreak >nul && taskkill /f /pid %MY_PID%"

REM --- メイン処理(長時間かかる想定)---
:WORK_LOOP
echo 処理中... %TIME%
timeout /t 5 /nobreak >nul
goto WORK_LOOP
killer.bat: タイムアウト後に指定PIDを終了させる汎用キラーバッチ
REM ===== killer.bat =====
@echo off
REM 引数: %1=対象PID  %2=待機秒数

set TARGET_PID=%1
set WAIT_SEC=%2

if "%TARGET_PID%"=="" (
    echo 使い方: killer.bat [PID] [秒数]
    exit /b 1
)

echo [killer] %WAIT_SEC%秒後に PID=%TARGET_PID% を終了します
timeout /t %WAIT_SEC% /nobreak >nul

tasklist /FI "PID eq %TARGET_PID%" 2>nul | findstr /i "cmd.exe" >nul
if %ERRORLEVEL% equ 0 (
    echo [killer] タイムアウト: PID=%TARGET_PID% を終了します
    taskkill /f /pid %TARGET_PID%
) else (
    echo [killer] PID=%TARGET_PID% はすでに終了しています
)
$pid は PowerShell 自身の PID ではなく呼び出し元の PID を返す
バッチから powershell -Command "$pid" を実行すると、PowerShell プロセスの PID が返ります。バッチ自身(cmd.exe)の PID が必要な場合は powershell -Command "(Get-Process -Id $pid).Parent.Id" で親プロセス(cmd.exe)の PID を取得できます。

方法3:外部プロセスにタイムアウトをかけて強制終了する

自分自身ではなく、起動した外部コマンドや exe にタイムアウトをかける場合のパターンです。

外部コマンドをバックグラウンド起動してタイムアウト後に終了
@echo off
setlocal

set EXE=C:\apps\long_task.exe
set TIMEOUT_SEC=60
set LOGFILE=C:\logs\ext_timeout.log
if not exist "C:\logs" mkdir "C:\logs"

REM 外部プロセスをバックグラウンド起動
start "" "%EXE%"

REM 起動直後の PID を wmic で特定(プロセス名で検索)
timeout /t 2 /nobreak >nul
set EXE_NAME=long_task.exe
set EXT_PID=
for /f "tokens=2 delims==" %%P in (
    'wmic process where "name='%EXE_NAME%'" get ProcessId /value ^| findstr "="'
) do set EXT_PID=%%P

echo [INFO] %EXE_NAME% PID=%EXT_PID% タイムアウト=%TIMEOUT_SEC%秒

REM タイムアウト待機
timeout /t %TIMEOUT_SEC% /nobreak >nul

REM プロセスがまだ存在するか確認して終了
tasklist /FI "PID eq %EXT_PID%" 2>nul | findstr /i "%EXE_NAME%" >nul
if %ERRORLEVEL% equ 0 (
    echo [INFO] タイムアウト: %EXE_NAME% PID=%EXT_PID% を強制終了 >> "%LOGFILE%"
    taskkill /f /t /pid %EXT_PID%
) else (
    echo [INFO] %EXE_NAME% は正常終了済み >> "%LOGFILE%"
)
endlocal
taskkill /t でプロセスツリーごと終了させる
taskkill /f /t /pid PID/t オプションを付けると、そのプロセスが起動した子プロセスもまとめて終了させます。外部プロセスが子プロセスを起動している場合(例: bat から起動した robocopy など)に有効です。/f は強制終了、/t はツリー終了です。

方法4:ループ内で経過時間を計算するソフトタイムアウト

ループ処理の中で開始時刻との差分を計算し、上限を超えたら exit /b する方式です。外部コマンドを呼び出さずに実装できます。

開始時刻との差分を計算するソフトタイムアウト
@echo off
setlocal enabledelayedexpansion

set MAX_SEC=120

REM 開始時刻をセンチ秒で記録
set START_TIME=%TIME%
call :to_cs "%START_TIME%" START_CS

echo 処理開始 (最大 %MAX_SEC%秒)

:LOOP
REM --- 処理本体 ---
echo [処理中] %TIME%
timeout /t 10 /nobreak >nul

REM 経過時間チェック
call :to_cs "%TIME%" NOW_CS
set /a ELAPSED_CS=NOW_CS - START_CS
if !ELAPSED_CS! lss 0 set /a ELAPSED_CS+=8640000
set /a ELAPSED_S=ELAPSED_CS / 100

echo 経過: !ELAPSED_S!秒 / %MAX_SEC%秒

if !ELAPSED_S! geq %MAX_SEC% (
    echo タイムアウト: !ELAPSED_S!秒 経過。終了します。
    exit /b 1
)
goto LOOP

:to_cs
REM 時刻文字列をセンチ秒に変換してセット
setlocal
set T=%~1
set T=%T: =0%
for /f "tokens=1-4 delims=:." %%a in ("%T%") do (
    set /a _cs = %%a*360000 + %%b*6000 + %%c*100 + %%d
)
endlocal & set %~2=%_cs%
goto :eof
センチ秒計算で精度の高い経過時間チェック
%TIME%HH:MM:SS.cc をセンチ秒(1/100秒)に変換して差分を計算します。日付をまたぐ場合(例: 23:59に開始 → 00:01に確認)は差分が負になるため、+8640000(1日のセンチ秒数)を加算して補正します。時刻計算の詳細は実行時刻・経過時間をログに記録する完全ガイドも参照してください。

方法5:PowerShell Job でタイムアウト付き実行(最も確実)

PowerShell の Start-Job + Wait-Job -Timeout を使うと、タイムアウト時間を秒単位で指定してコマンドを実行できます。バットコードの限界を超える複雑なタイムアウトが必要な場合に最も確実な方法です。

PowerShell Job でタイムアウト付きコマンド実行(バッチから呼び出し)
@echo off
setlocal

set TIMEOUT_SEC=30
set TARGET_CMD=ping -n 100 8.8.8.8

REM PowerShell Job でタイムアウト付き実行
powershell -NoProfile -Command "
    $job = Start-Job -ScriptBlock { %TARGET_CMD% }
    $completed = Wait-Job $job -Timeout %TIMEOUT_SEC%
    if ($completed) {
        Receive-Job $job
        Write-Host "[OK] 正常完了"
        Remove-Job $job
        exit 0
    } else {
        Stop-Job $job
        Remove-Job $job
        Write-Host "[NG] タイムアウト: %TIMEOUT_SEC%秒 経過"
        exit 1
    }
"

set PS_RC=%ERRORLEVEL%
if %PS_RC% equ 0 (
    echo 処理完了
) else (
    echo タイムアウト終了
)
endlocal
PowerShell で外部 exe をタイムアウト付きで実行する
@echo off
setlocal

set EXE=C:\apps\long_task.exe
set TIMEOUT_SEC=60

powershell -NoProfile -Command "
    $proc = Start-Process -FilePath '%EXE%' -PassThru
    $exited = $proc.WaitForExit(%TIMEOUT_SEC% * 1000)
    if ($exited) {
        Write-Host "[OK] 終了コード: " + $proc.ExitCode
        exit $proc.ExitCode
    } else {
        $proc.Kill()
        Write-Host "[NG] タイムアウト: %TIMEOUT_SEC%秒 経過 強制終了"
        exit 1
    }
"

echo PowerShell RC=%ERRORLEVEL%
endlocal
PowerShell の WaitForExit はミリ秒指定
$proc.WaitForExit(60000) の引数はミリ秒です。60秒なら 60 * 1000 = 60000 を渡します。バッチ変数 %TIMEOUT_SEC% を使う場合は %TIMEOUT_SEC% * 1000 のように PowerShell 内で計算できます。プロセスの終了を「待つ」のではなくタイムアウトを設けない場合のパターンはプロセスの終了を待つ全方法まとめも参照してください。

まとめ

  • シンプルなループ: カウンタを加算して上限到達で exit /b 1(ブロッキング処理には不適)
  • 2バッチ方式: powershell -Command "$pid" で PID 取得 → 別プロセスで taskkill /f /pid
  • 外部プロセスのタイムアウト: start → wmic で PID 取得 → timeouttaskkill /t(/t でツリーごと)
  • 経過時間チェック: %TIME% をセンチ秒変換して差分計算(日付またぎも補正)
  • PowerShell Job: Start-Job + Wait-Job -Timeout(最も確実・外部 exe にも対応)
  • 外部 exe 精密制御: Start-Process -PassThru + WaitForExit(ms)(ミリ秒精度)

関連記事: プロセスの終了を待つ全方法まとめ / プロセスを監視して異常終了を検知する完全ガイド / エラー時に処理を中断・終了する方法

よくある質問(FAQ)

Qkiller.bat で taskkill してもバッチが終了しません。
Ataskkill は通常 cmd.exe プロセスを終了しますが、対象の PID が間違っていると別のプロセスを誤終了させる危険があります。まず tasklist /FI "PID eq [PID]" で対象プロセスを確認してから実行してください。また taskkill /f(強制)を付けないと応答待ちになる場合があります。
Q%~n0 を PID として使おうとしましたが動きません。
A%~n0 はバッチファイルの名前(拡張子なし)を返す修飾子です。プロセスID(PID)とは全く別のものです。バッチ自身の PID を取得するには本記事で紹介している powershell -Command "(Get-Process -Id $pid).Parent.Id"wmic process where "name='cmd.exe'" を使ってください。
Q2バッチ方式で killer が先に終了してしまいます。
Akiller バッチは timeout /t N /nobreak >nul の待機中にCtrl+C などで中断されると PID が空のまま taskkill を実行してしまいます。tasklist /FI "PID eq %TARGET_PID%" ... | findstr ... で終了前に対象プロセスの存在確認をしてから taskkill する実装(本記事の killer.bat)で防げます。
QPowerShell Job の標準出力をバッチ側で受け取れません。
AStart-Job で実行したコマンドの出力は Job のバッファに蓄積され、Receive-Job で取得します。Receive-Job $job の結果を変数に格納するか、ファイルにリダイレクト(Receive-Job $job | Out-File result.txt)してバッチ側から読み込む方法が確実です。
Qタイムアウト時に終了コードを 1 にして呼び出し元に伝えたいです。
Aバッチから exit /b 1 で終了するとその値が呼び出し元の %ERRORLEVEL% に返ります。call で呼び出した場合は if %ERRORLEVEL% neq 0 echo タイムアウトで終了 で判定できます。ただし taskkill で強制終了された場合は終了コードを制御できません。終了コードの設計についてはエラー時に処理を中断・終了する方法も参照してください。