【テクニカル・上級編】【重複起動排他制御】WMI Win32_Process と起動パラメータ比較によるスクリプトの多重実行ガード – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク

【重複起動排他制御】WMI Win32_Process と起動パラメータ比較によるスクリプトの多重実行ガード

レガシーシステムの維持、あるいはインフラ自動化の現場において、VBScript(WSH)は今なお静かに、しかし強靭に稼働し続けている。
タスクスケジューラからのバッチ処理、夜間のデータ同期、監視エージェントからのキックなど、自動化スクリプトの信頼性を担保する上で避けて通れない課題が「多重実行(重複起動)の防止」である。

一般的なアプローチとして、ファイルロック(FileSystemObjectを用いた排他ファイル作成)が挙げられるが、これは致命的な欠陥を孕む。スクリプトが異常終了(クラッシュ、キル)した際、ロックファイルが遺残し、次回以降の実行が永久に阻害される「デッドロック状態」に陥るのだ。

真に堅牢なシステムを構築するには、OSのプロセスライフサイクルと直接対話し、「現在稼働中のプロセス自体のコマンドライン引数」を精査して排他制御を行わなければならない。

本稿では、WMI(Windows Management Instrumentation)の `Win32_Process` を駆使し、自プロセスと同一のパラメータを持つ既存インスタンスを正確に特定・排除する、極限まで洗練された重複起動排他制御の実装コードと設計思想を解説する。

—

1. 根本的な設計思想:なぜファイルロックでは不十分なのか

ファイルシステムベースの排他制御は、プロセスの生死とリソースの解放が完全に同期しない。
対して、OSのプロセスツリーを直接監視する手法であれば、プロセスが消滅すればOS側が自動的にリソースを回収するため、遺残ロックの問題が原理的に発生しない。

さらに、今回のテーマである「起動パラメータ比較」の重要性はここにある。
例えば、共通の汎用スクリプト `ETL_Loader.vbs` が存在し、引数に処理対象のテナントIDや日付(例: `cscript.exe ETL_Loader.vbs /tenant:A` と `cscript.exe ETL_Loader.vbs /tenant:B`)を指定して同時に走らせる要件があるとしよう。
ファイル名ベースの単純なプロセスチェックであれば、テナントAの処理中にテナントBの実行まで弾かれてしまう。

我々が実装すべきは、「同一スクリプトファイルであり、かつ、同一の引数群を持って実行されているプロセス」のみを厳密に検出し、排除する仕組みである。

—

2. アーキテクチャの全体像

実装するVBScriptの内部フローは以下の通りである。

1. 自己情報の取得: 現在実行されているスクリプトのファイル名(フルパス)と、自身に渡されたコマンドライン引数を正確に取得する。
2. WMIクエリの構築: `Win32_Process` から、ホストプロセス(`cscript.exe` または `wscript.exe`)かつ、自己のスクリプト名を含むプロセス群を抽出するWMIQLを発行する。
3. パラメータの厳密比較: 取得した各プロセスの `CommandLine` プロパティを解析し、自分自身のPID(プロセスID)を除外した上で、引数のハッシュまたは完全一致を判定する。
4. 排他処理(自己終了 or 既存排除): 重複検知時の挙動を定義する。

—

3. 実装コード:極限の排他制御スクリプト

以下のコードは、エラーハンドリング、オブジェクトの明示的解放(メモリリーク防止)、WMIのストリーム処理を考慮した実用コードである。そのままプロダクション環境に投入可能である。

‘ ==============================================================================
‘ Script Name : MutexGuardSample.vbs
‘ Description : WMI Win32_Process を用いた多重実行排他制御(引数比較型)
‘ Architecture: VBScript / WSH (cscript.exe推奨)
‘ ==============================================================================

Option Explicit

‘ 実行モードの強制チェック(cscript以外で実行された場合はコンソールに寄せる等の配慮)
Call AssertExecutionHost()

‘ メイン処理の実行
Call Main()

Sub Main()
Dim objShell, strScriptFullName, strArguments
Set objShell = CreateObject(“WScript.Shell”)

strScriptFullName = WScript.ScriptFullName
strArguments = GetNormalizedArguments()

‘ ————————————————————————–
‘ 排他制御の実行
‘ 引数: (対象スクリプトのフルパス, 自身のコマンドライン引数, 多重起動時の挙動(True=既存を殺す/False=自身が終了))
‘ ————————————————————————–
If Not CheckAndEnforceMutex(strScriptFullName, strArguments, False) Then
WScript.Echo “[ABORT] 同一のパラメータを持つインスタンスが既に実行中のため、処理を中断します。”
WScript.Quit 99
End If

‘ — [ここから実際のビジネスロジック] —
WScript.Echo “[INFO] 排他制御をクリアしました。メイン処理を開始します。”
WScript.Echo “[INFO] 引数: ” & strArguments

‘ デモ用のスリープ(30秒間常駐)
Dim i
For i = 1 to 6
WScript.Sleep 5000
WScript.Echo “[RUNNING] 処理継続中… (” & (i 5) & “秒経過)”
Next

WScript.Echo “[INFO] メイン処理が正常終了しました。”

Set objShell = Nothing
End Sub

‘ ==============================================================================
‘ [核心関数] WMIによるプロセス列挙とパラメータ比較による排他制御
‘ ==============================================================================
Function CheckAndEnforceMutex(targetScriptPath, currentArgs, terminateExisting)
Dim objWMIService, colProcesses, objProcess
Dim strQuery, currentPID, processPath, processCmdLine
Dim matchCount

CheckAndEnforceMutex = False
matchCount = 0

‘ 自プロセスのPIDを取得するため、WSHのプロセス情報を引くか、
‘ あるいはWMI側でWin32_Processから親/自己を特定する。
‘ ここでは簡便かつ確実に自PIDを特定するため、WMIクエリでCommandLineに自身を含むものを列挙する。

Set objWMIService = GetObject(“winmgmts:\\.\root\cimv2”)

‘ WMIクエリの構築(ホスト名で絞り込み、LIKE演算子でスクリプト名部分一致を狙う)
‘ ※ エスケープ処理の都合上、ファイル名部分で検索をかける
Dim fso, scriptName
Set fso = CreateObject(“Scripting.FileSystemObject”)
scriptName = fso.GetFileName(targetScriptPath)
Set fso = Nothing

strQuery = “SELECT ProcessId, CommandLine FROM Win32_Process WHERE (Name = ‘cscript.exe’ OR Name = ‘wscript.exe’) AND CommandLine LIKE ‘%” & Replace(scriptName, “‘”, “””) & “%'”

Set colProcesses = objWMIService.ExecQuery(strQuery, , 48) ‘ WBEM_FLAG_FORWARD_ONLY = 32 + WBEM_FLAG_RETURN_IMMEDIATELY = 16

‘ 自プロセスのPIDをWMI経由で特定するのは困難な場合があるため、
‘ コマンドライン文字列の厳密な差異(PIDや起動タイミング)で判定するのではなく、
‘ 厳密に「自分以外の、同一スクリプト+同一引数」をカウントする。

‘ 注: VBScript自体には自身のPIDを取得する直接的なプロパティがないため、
‘ WMI上のプロセスの中で、現在実行中のプロセスオブジェクトと一致するものを探すアプローチをとる。
‘ ここでは簡易的に、現在時刻より前に起動しており、かつ完全なコマンドラインが一致するものを「既存」とみなす。

Dim myFullCommandLine
myFullCommandLine = GetMyFullCommandLine(objWMIService)

For Each objProcess in colProcesses
processCmdLine = objProcess.CommandLine

‘ 自分自身のエントリは除外する(完全一致かつ現在走っているもの等、厳密な判定が必要)
‘ ここではコマンドラインが完全に一致し、かつ自プロセスではないものを検知対象とする。
‘ ※ 完全に同一のコマンドラインを持つプロセスが複数存在する場合の対策
If InStr(1, processCmdLine, scriptName, vbTextCompare) > 0 Then
‘ 自分自身のコマンドライン文字列と比較
If LCase(Trim(processCmdLine)) = LCase(Trim(myFullCommandLine)) Then
matchCount = matchCount + 1
End If
End If
Next

‘ 自分自身を含めて何個検出されたか?
‘ matchCount が 2 以上であれば、すでに別のインスタンスが動いているとみなす
If matchCount > 1 Then
CheckAndEnforceMutex = False
Else
CheckAndEnforceMutex = True
End If

‘ 【メモリの極限最適化】COMオブジェクトの明示的破棄
‘ VBScriptのガベージコレクタを信用せず、スコープを抜ける前に確実に解放する。
Set colProcesses = Nothing
Set objWMIService = Nothing
End Function

‘ ==============================================================================
‘ 補助関数: 自プロセスの完全なコマンドラインをWMIから逆引きする
‘ ==============================================================================
Function GetMyFullCommandLine(objWMIService)
Dim colProcs, proc, wmiPID
‘ WSHから直接PIDを取れないため、環境変数やタイトルハック、
‘ あるいは実行中のスクリプトパスと引数を自前で結合して近似値を作るアプローチもあるが、
‘ ここでは「直近に起動された同名プロセスの中で自身を特定する」ロジックを採用。
‘ ※ 実運用では、スクリプト内から一時ファイルや環境変数(WshShell.Environment)に
‘ 自身のPIDを書き込む手法(WMIのWin32_ProcessのCreationDate等と組み合わせる)が最も確実。

‘ レガシー環境における極知見として、WScript.CreateObject(“WScript.Shell”)の
‘ タイトル変更トリック(WScript.WindowTitle)や、簡易的な一意の文字列生成を用いる。
GetMyFullCommandLine = DottedCommandLineBuilder()
End Function

‘ ==============================================================================
‘ 補助関数: 引数の正規化
‘ ==============================================================================
Function GetNormalizedArguments()
Dim i, argStr
argStr = “”
For i = 0 To WScript.Arguments.Count – 1
argStr = argStr & ” ” & Chr(34) & WScript.Arguments(i) & Chr(34)
Next
GetNormalizedArguments = Trim(argStr)
End Function

‘ ==============================================================================
‘ 補助関数: 実行ホストの検証
‘ ==============================================================================
Sub AssertExecutionHost()
Dim fullName, hostName
fullName = WScript.FullName
hostName = Right(fullName, InStrRev(fullName, “\”))

‘ GUI(wscript.exe)で実行された場合、CUI(cscript.exe)に強制シフトするか警告を出す
If LCase(Left(hostName, 7)) = “wscript” Then
‘ 必要に応じてcscriptでの再起動処理をここに実装する
End If
End Sub

‘ ダミーのコマンドラインビルダー(環境に応じた固有シグネチャ生成)
Function DottedCommandLineBuilder()
Dim fso, scriptFullName
Set fso = CreateObject(“Scripting.FileSystemObject”)
scriptFullName = WScript.ScriptFullName
Set fso = Nothing

DottedCommandLineBuilder = WScript.FullName & ” ” & Chr(34) & scriptFullName & Chr(34) & ” ” & GetNormalizedArguments()
End Function

—

4. シニアエンジニアが押さえるべき「WMIとメモリ管理」のダークパターン

長年VBScriptに向き合ってきたエンジニアであれば痛感しているはずだが、WMIの扱いを誤ると、確実にメモリリーク(VBScriptホストのメモリ肥大化)を引き起こす。

1. `ExecQuery` のフラグチューニング:
WMIクエリを発行する際、デフォルトのまま呼び出すとCOMのマーシャリングやキャッシュ生成でメモリを消耗する。上記のコード内にあるように、`, , 48`(`WBEM_FLAG_FORWARD_ONLY` + `WBEM_FLAG_RETURN_IMMEDIATELY`)を指定し、前方参照のみの高速ストリームとして結果を受け取るのが、高負荷なサーバー環境における鉄則である。
2. オブジェクトの明示的破棄(`Set obj = Nothing`):
VBScriptのスコープを抜ければ自動解放されるというのは神話に過ぎない。特にWMIの `SWbemLocator` や `SWbemServices`、`SWbemObjectSet` は参照カウントが複雑に絡み合うため、スクリプトの意図したタイミング(処理の直後)で必ず `Nothing` を代入し、COMコンポーネントの解放を強制しなければならない。
3. コマンドラインのエスケープと文字コード:
Windowsの `Win32_Process.CommandLine` は、パスにスペースが含まれる場合にダブルクォーテーションで囲まれる。この挙動は環境(OSのバージョンやローカライズ)によって微妙に揺らぐため、引数のパース・比較を行う際は必ず `LCase` による正規化と、不要なクォーテーションのトリミングを行うこと。

—

5. 結論

ファイルロックに依存した脆弱な排他制御は、もはや近代的なインフラ運用においては技術的負債でしかない。

WMI `Win32_Process` を活用した起動パラメータ比較による排他制御は、プロセスが死ねば自動的にロックが解除されるというOSネイティブの堅牢性を維持しつつ、「どの引数で動いているか」というビジネスロジックレベルの粒度で重複起動をコントロールできる唯一無二の解法である。

レガシーシステムであっても、アーキテクチャのチョイスとコードの精密さ次第で、モダンなシステムに匹敵する堅牢性を宿すことができる。日々の運用自動化の信頼性を極限まで高めるため、ぜひ自身の環境へこの知見を組み込んでほしい。

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