【テクニカル・上級編】【無応答プロセス自己復旧】監視用子スクリプト(ウォッチドッグ)を並列起動して親スクリプトのハングアップを解消する構造 – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

【無応答プロセス自己復旧】監視用子スクリプト(ウォッチドッグ)を並列起動して親スクリプトのハングアップを解消する構造

レガシーシステムの深部、あるいは人知れず夜間バッチを回し続けるWindows Serverの片隅で、VBScriptは今なおインフラの心臓部を支えている。
COMコンポーネントの呼び出し、ADOを介したRDBとの同期、あるいは外部APIとの非同期バッチ処理。これらが一瞬のデッドロックや無限ループ、あるいはサードパーティ製ドライバの応答停止によって「無応答(Not Responding)」に陥ったとき、誰がそのプロセスを救うのか?

OS標準のタスクスケジューラ頼みでは、ポーリング間隔が粗すぎてリアルタイム性に欠ける。
我々が求めるべきは、「スクリプト自身が自律的に生死を監視し、致命的な沈黙を検知した瞬間に自らを刈り取り、再起動するデュアルプロセス・ウォッチドッグ構造」である。

今回は、VBScriptのランタイム特性とWSH(Windows Script Host)の限界を知り尽くしたアーキテクトへ向けて、極限環境でも確実に稼働する自己復旧システムの全貌を解説する。

1. なぜVBScriptはハングアップするのか?(根本原因の解剖学)

VBScriptの実行基盤である `wscript.exe` や `cscript.exe` は、シングルスレッドのメッセージループ上で動作する。以下のようなシチュエーションで、プロセスは容易に沈黙する。

1. COMオブジェクトのブロック: 外部のCOMサーバー(Excelや古い業務アプリのラッパーなど)がモーダルダイアログを出したまま待ち状態に入る。
2. ネットワークI/Oのハング: HTTPリクエストやファイル共有へのアクセスがタイムアウトを無視して無限待ちになる。
3. メモリリークとGCの限界: 不適切なオブジェクト参照の連鎖により、スクリプトエンジン内部のヒープが枯渇し、処理が極度に遅延した末に停止する。

これを防ぐためには、「処理を行う親プロセス」とは完全に独立したメモリ空間とライフサイクルを持つ「監視を行う子プロセス(ウォッチドッグ)」を非同期で常駐させなければならない。

2. デュアルプロセス・ウォッチドッグのアーキテクチャ

今回の設計思想は以下の通りである。

  • ハートビート方式: 親スクリプトは定期的に生存確認ファイル(またはレジストリ、一時変数)のタイムスタンプを更新する(心拍の鼓動)。
  • 非同期監視: 子スクリプト(ウォッチドッグ)は別プロセスとして起動し、親の生存タイムスタンプを監視する。
  • 強制回収(Kill & Respawn): 一定時間(例: 60秒)心拍が途絶えた場合、子プロセスは `WMI (Win32_Process)` を叩いて親プロセスを強制終了させ、親スクリプトを安全に再起動する。

3. 実装コード:完全自己復旧スクリプト群

現場でそのままコピー&ペーストして検証できるよう、「親スクリプト (master.vbs)」「監視子スクリプト (watchdog.vbs)」 の2つのコードを提示する。

① 親スクリプト:`master.vbs`

このスクリプトがメインの業務処理を行う。定期的に「私は生きている」というシグナル(ファイル更新)を送り続ける。

‘ ==============================================================================
‘ 業務メイン処理スクリプト (master.vbs)
‘ ==============================================================================
Option Explicit

Const HEARTBEAT_FILE = “C:\Temp\heartbeat.dat”
Const INTERVAL_SEC = 5 Wscriptのメインループ間隔
Dim fso, shell, i

Set fso = CreateObject(“Scripting.FileSystemObject”)
Set shell = CreateObject(“WScript.Shell”)

‘ 1. 起動時にウォッチドッグ(子プロセス)を並列起動する
‘ ※ cscript.exeを指定することでGUIの介入を防ぎ、バックグラウンドで完全非同期実行
shell.Run “cscript.exe //nologo “”C:\Temp\watchdog.vbs”” ” & DblQuote(WScript.ScriptFullName), 0, False

WScript.Echo “親プロセス開始 (PID: ” & GetCurrentProcessIdDummy() & “)”

‘ 2. メインループ(業務処理の模擬)
For i = 1 to 1000
‘ — 【業務ロジックの実行ゾーン】 —
Call UpdateHeartbeat(fso)

‘ テスト用:もし特定の条件でフリーズさせたい場合はコメントアウトを解除
‘ If i = 10 Then Do: Loop

WScript.Sleep INTERVAL_SEC 1000
Next

‘ 正常終了時はウォッチドッグを無効化するためハートビートファイルを削除
If fso.FileExists(HEARTBEAT_FILE) Then
fso.DeleteFile HEARTBEAT_FILE, True
End If

Set shell = Nothing
Set fso = Nothing

‘ — 補助関数:ハートビート更新 —
Sub UpdateHeartbeat(fsoObj)
On Error Resume Next
Dim ts
Set ts = fsoObj.CreateTextFile(HEARTBEAT_FILE, True)
ts.WriteLine Now()
ts.Close
Set ts = Nothing
On Error GoTo 0
End Sub

‘ — 文字列クォート付与関数 —
Function DblQuote(str)
DblQuote = Chr(34) & str & Chr(34)
End Function

Function GetCurrentProcessIdDummy()
GetCurrentProcessIdDummy = “Active”
End Function

② 監視子スクリプト:`watchdog.vbs`

親プロセスの脈拍を監視し、停止を検知した瞬間に蘇生措置を行う。

‘ ==============================================================================
‘ 監視用子スクリプト(ウォッチドッグ) (watchdog.vbs)
‘ ==============================================================================
Option Explicit

Const HEARTBEAT_FILE = “C:\Temp\heartbeat.dat”
Const THRESHOLD_SEC = 15 ‘ この秒数を超えて更新がない場合、ハングとみなす
Const CHECK_INTERVAL_MS = 5000 ‘ 監視のポーリング間隔 (5秒)

Dim fso, shell, targetScriptPath, targetFileName
Dim lastModTime, currentTime, diffSeconds

Set fso = CreateObject(“Scripting.FileSystemObject”)
Set shell = CreateObject(“WScript.Shell”)

‘ 引数から監視対象の親スクリプトパスを取得
If WScript.Arguments.Count = 0 Then
WScript.Quit 1
End If
targetScriptPath = WScript.Arguments(0)
targetFileName = fso.GetFileName(targetScriptPath)

‘ メインの監視ループ
Do While True
WScript.Sleep CHECK_INTERVAL_MS

‘ 親が生きていてハートビートファイルを更新しているか確認
If fso.FileExists(HEARTBEAT_FILE) Then
On Error Resume Next
Dim fileObj
Set fileObj = fso.GetFile(HEARTBEAT_FILE)
lastModTime = fileObj.DateLastModified
Set fileObj = Nothing

If Err.Number = 0 Then
currentTime = Now()
‘ 経過秒数を計算
diffSeconds = DateDiff(“s”, lastModTime, currentTime)

‘ 閾値を超えている場合(=親がフリーズしている可能性)
If diffSeconds > THRESHOLD_SEC Then
Call EmergencyRecovery(fso, shell, targetFileName, targetScriptPath)
End If
End If
On Error GoTo 0
Else
‘ ファイルが存在しない場合、親が正常終了したとみなしてウォッチドッグも終了
Exit Do
End If
Loop

Set shell = Nothing
Set fso = Nothing
WScript.Quit 0

‘ — 緊急復帰ルーチン —
Sub EmergencyRecovery(fsoObj, shellObj, procName, scriptPath)
On Error Resume Next

‘ 1. ハングアップした親プロセスをWMI経由で強制終了 (cscript.exe / wscript.exe の対象プロセス)
Dim wmi, processes, proc
Set wmi = GetObject(“winmgmts:{impersonationLevel=impersonate}!\\.\root\cimv2”)
Set processes = wmi.ExecQuery(“SELECT FROM Win32_Process WHERE Name LIKE ‘%script.exe’ AND CommandLine LIKE ‘%” & procName & “%'”)

For Each proc in processes
‘ ウォッチドッグ自身のPIDは殺さないようにガード
If proc.ProcessId <> CInt(CreateObject(“WScript.Shell”).Environment(“Process”)(“PID”)) Then
proc.Terminate()
End If
Next

‘ 2. 滞留しているハートビートファイルを削除
If fsoObj.FileExists(HEARTBEAT_FILE) Then
fsoObj.DeleteFile HEARTBEAT_FILE, True
End If

‘ 3. 親プロセスを再起動
shellObj.Run “cscript.exe //nologo ” & DblQuote(scriptPath), 0, False

‘ 4. ログ出力(イベントログやテキストファイルへの記録を推奨)
Dim logFile
Set logFile = fsoObj.OpenTextFile(“C:\Temp\watchdog_recover.log”, 8, True)
logFile.WriteLine “[” & Now() & “] 警告: 親プロセス [” & procName & “] の無応答を検知。強制再起動を実行しました。”
logFile.Close
Set logFile = Nothing

On Error GoTo 0

‘ 再起動後はこのウォッチドッグ自体は役目を終えたので終了
WScript.Quit 0
End Sub

Function DblQuote(str)
DblQuote = Chr(34) & str & Chr(34)
End Function

4. チーフアーキテクトが教える:本番運用における極限の最適化ポイント

レガシーなVBScript環境でこの構造を運用するにあたり、以下の「実戦の知見」を忘れてはならない。

1. メモリ管理とCOMオブジェクトのデストロイ

VBScriptは参照カウンタ方式のGC(ガベージコレクション)を採用しているが、複雑なオブジェクトの循環参照やエラー発生時のクリーンアップ漏れはメモリリーク直結する。

  • ループの内部でCOMオブジェクト(`CreateObject`)を生成する場合、必ずループの最後で `Set obj = Nothing` を明示的に呼ぶこと。 これを怠ると、数時間でVBScriptエンジンのメモリ消費量が膨れ上がり、OSのページファイルを圧迫して自滅する。

2. WMIクエリのパフォーマンスペナルティ

子スクリプト側で使用している `Win32_Process` を対象としたWMIクエリは、実行時に一定のCPUコストを支払う。ポーリング間隔をあまりに短く(例:1秒以下に)設定すると、監視プロセス自体がサーバーのリソースを食いつぶす本末転倒な事態に陥る。

  • 監視のポーリング間隔は最低でも 5秒〜10秒 を死守せよ。

3. 排他制御(ファイルアクセスの競合)

親がハートビートファイルを書き込む瞬間と、子がそれを読み取る瞬間がミリ秒単位でバッティングすると、VBScript特有の `エラー 70: 書き込み権限がありません (Permission Denied)` が発生する。

  • これを防ぐため、双方のスクリプトで `On Error Resume Next` を適切に配置し、一過性のファイルロックエラーでウォッチドッグ自体がクラッシュしない堅牢なエラーハンドリング(防御的プログラミング)を実装している。

5. 総括:レガシーの寿命を延ばすエンジニアリング

最新のコンテナ技術やマイクロサービス全盛の時代であっても、既存のWindowsインフラストラクチャには、VBScriptで書かれた「動かざること山の如し」な自動化スクリプトが眠っている。
それらを全面リプレースするには膨大なコストがかかるが、今回提示したような「デュアルプロセスによる自己復旧ウォッチドッグ構造」を外付けで構築するだけで、可用性はモダンなシステムに匹敵するレベルへと引き上げられる。

枯れた技術を極限までチューニングし、システムを泥臭く、しかし絶対に止めない――これこそが、真のインフラエンジニアの矜持である。

タイトルとURLをコピーしました