VBScriptの限界を超えろ:`WScript.CreateObject` 第2引数による「極限の非同期イベント駆動アーキテクチャ」
現場の業務自動化において、未だに「プロセスが終了するまで1秒ごとにループして監視する(ポーリング)」という泥臭いコードを書いてはいないだろうか?
ループ処理によるCPUリソースの浪費、応答性の低下、そしてタイマ精度に依存した不確定な挙動——これらはすべて、VBScriptにおける「イベント駆動(Event-Driven)プログラミング」の理解不足から生じる欠陥だ。
VBScriptはシングルスレッド言語であり、一見すると非同期処理とは無縁に思える。しかし、WSH(Windows Script Host)環境が備える `WScript.CreateObject` の第2引数(イベントプレフィックス)を正しく理解し、COMオブジェクトのコネクションポイント(`IConnectionPoint`)と連結させることで、VBScriptは「真の非同期イベントレシーバー」へと変貌する。
本記事では、大企業の実務自動化プロジェクトを統括するアーキテクトの視点から、バグを一切排除した堅牢な非同期イベント監視アーキテクチャを伝授する。
—
1. 非同期イベント受信のメカニズム:内部で何が起きているのか?
なぜ `WScript.CreateObject` の第2引数を指定するだけで、非同期イベントが受け取れるのか。その内部構造を理解せずにコードを書いてはならない。
‘ 単なるインスタンス生成(同期処理のみ)
Set obj = CreateObject(“ProgID”)
‘ イベント同期を有効化したインスタンス生成(非同期受信が可能)
Set obj = WScript.CreateObject(“ProgID”, “EventPrefix_”)
第2引数に `”EventPrefix_”` という文字列を与えると、WSH環境(`wscript.exe` または `cscript.exe`)はCOMの `IConnectionPointContainer` インターフェースを介して、対象COMオブジェクトのイベントソースに接続(Advise)する。
外部COMオブジェクト側で何らかの状態変化(プロセスの起動・終了、ファイル生成、WMIのイベント発生など)が起きると、COM内部の別スレッドからWSHに対して通知が飛ぶ。WSHのメッセージループはそれをキャッチし、`EventPrefix_イベント名` という命名規則に合致するVBScript内のサブルーチンへ動的にディスパッチする。
【極めて重要な鉄則】メインスレッドを殺してはならない
VBScriptのエンジン自体はシングルスレッド(STA: Single-Threaded Apartment)で動作している。外部COMが非同期でイベントを発行しても、VBScript側のメインスレッドが処理を終了してスクリプトが停止してしまえば、イベントを受けることは永久にできない。
したがって、非同期イベント受信を実現するための全体構造は以下の3要素で設計しなければならない。
1. イベントソースの初期化: 第2引数を指定してCOMオブジェクトを生成。
2. イベントハンドラーの実装: `プレフィックス_イベント名` のプロシージャを定義。
3. メッセージディスパッチループ(制御された待機): メインスレッドを終了させず、かつCPU使用率を0%近辺に維持しながらイベントを静観する構造。
—
2. 失敗する実装 vs 勝利する設計(アンチパターンの排除)
実務で絶対にやってはならない悪手と、その解決策を提示する。
❌ アンチパターン1:CPUを焼き切るビジーウェイト(Busy Wait)
‘ 【絶対厳禁】CPU利用率が100%に張り付き、PC全体をフリーズさせる
Do Until g_bCompleted
‘ 何もしないビジーウェイト
Loop
❌ アンチパターン2:例外未処理によるイベントスレッドのサイレントクラッシュ
イベントハンドラー関数内部でエラー(0除算や存在しないファイルへのアクセスなど)が発生した場合、適切なエラーハンドリング(`On Error Resume Next` とエラー評価)が無いと、イベントディスパッチ自体が沈黙し、スクリプトがハングアップする。
⭕ 勝利する設計:`WScript.Sleep` によるタイムスライス譲渡とフラグ制御
‘ 【推奨】OSへタイムスライスを譲渡し、CPU負荷を最小化しながら非同期イベントを待つ
Do While Not g_bCompleted
WScript.Sleep 100 ‘ メッセージループを維持しつつCPUを休ませる
Loop
`WScript.Sleep 100` を挟むことで、WSHはイベントキューを消化する余裕を持ち、CPU使用率は実質0%となる。これがエンタープライズクオリティの「待機」だ。
—
3. プロダクショングレードの完全コード例:WMI非同期プロセス監視
ここからは、実務でそのまま利用できるコードを提示する。
今回は「特定のプロセス(例: `notepad.exe`)の起動と終了を、WMIの `SWbemSink` を用いて完全に非同期で監視・ログ記録する」ツールを構築する。
※ポーリングは一切行わず、Windowsカーネルからの通知をリアルタイムに受信する。
‘===============================================================================
‘ Program: AsyncProcessMonitor.vbs
‘ Description: WMI (SWbemSink) と WScript.CreateObject を使用した
‘ 完全非同期プロセスイベント監視スクリプト
‘ Author: Production Automation Architecture Team
‘===============================================================================
Option Explicit
‘ — グローバル状態管理構造体(VBScriptではグローバル変数でカプセル化)—
Private g_bKeepRunning
Private g_objFS
Private g_objLogStream
Private g_strLogPath
‘ — メイン処理エントリポイント —
Call Main()
Sub Main()
On Error Resume Next
‘ 初期化処理
g_bKeepRunning = True
g_strLogPath = “C:\Logs\ProcessEvents.log”
‘ ログ書き込み用のFileSystemObject初期化
Set g_objFS = CreateObject(“Scripting.FileSystemObject”)
‘ ファイルが存在しない場合は追記モード(8)で作成
Set g_objLogStream = g_objFS.OpenTextFile(g_strLogPath, 8, True)
If Err.Number <> 0 Then
WScript.Echo “[CRITICAL ERROR] ログファイルのオープンに失敗しました: ” & Err.Description
WScript.Quit 1
End If
WriteLog “システム”, “非同期プロセス監視サービスを起動しました。”
‘ 1. WMI サービスへの接続
Dim objWMIService
Set objWMIService = GetObject(“winmgmts:\\.\root\cimv2”)
If Err.Number <> 0 Then
WriteLog “ERROR”, “WMIへの接続に失敗しました: ” & Err.Description
WScript.Quit 2
End If
‘ 2. 非同期イベント受信用 SINK オブジェクトの生成
‘ ★最重要:第2引数 “ProcessSink_” がイベントハンドラーの接頭辞となる
Dim objSink
Set objSink = WScript.CreateObject(“WbemScripting.SWbemSink”, “ProcessSink_”)
If Err.Number <> 0 Then
WriteLog “ERROR”, “SWbemSink の生成に失敗しました: ” & Err.Description
WScript.Quit 3
End If
‘ 3. WMIに対して非同期クエリを登録(Process Creation / Deletion を監視)
‘ 監視対象: notepad.exe (実務に合わせて変更可能)
Dim strTargetProcess
strTargetProcess = “notepad.exe”
Dim strQuery
strQuery = “SELECT FROM __InstanceOperationEvent WITHIN 1 ” & _
“WHERE TargetInstance ISA ‘Win32_Process’ ” & _
“AND TargetInstance.Name = ‘” & strTargetProcess & “‘”
‘ 非同期通知の開始(ExecNotificationQueryAsync を使用)
objWMIService.ExecNotificationQueryAsync objSink, strQuery
If Err.Number <> 0 Then
WriteLog “ERROR”, “WMI非同期クエリの登録に失敗しました: ” & Err.Description
WScript.Quit 4
End If
WriteLog “監視中”, “ターゲットプロセス [” & strTargetProcess & “] のイベント待機中… (Ctrl+C または外部停止フラグで終了)”
‘ 4. メッセージディスパッチループ(メインスレッドの維持)
‘ 単純な無限ループではなく、Sleepを挟んでCPU負荷を極限まで下げる
Dim nLoopCount
nLoopCount = 0
Do While g_bKeepRunning
WScript.Sleep 100 ‘ 100ミリ秒ごとにスレッドをOSに譲渡
‘ 運用上の安全弁:定期的なヘルスチェックや安全なシャットダウンロジックをここに挟む
nLoopCount = nLoopCount + 1
If nLoopCount >= 36000 Then ‘ 約1時間ごとにログフラッシュ等のメンテナンス
g_objLogStream.Flush
nLoopCount = 0
End If
Loop
‘ 5. 安全なクリーンアップ処理(リソースリークの絶対防止)
WriteLog “システム”, “終了シーケンスを開始します。”
‘ 非同期通知の停止
objSink.Cancel()
‘ COMオブジェクトの明示的解放
Set objSink = Nothing
Set objWMIService = Nothing
WriteLog “システム”, “監視サービスを安全に停止しました。”
g_objLogStream.Close
Set g_objLogStream = Nothing
Set g_objFS = Nothing
WScript.Quit 0
End Sub
‘===============================================================================
‘ イベントハンドラー群 (SWbemSink 専用)
‘ 命名規則: [プレフィックス]_[WMI Sinkイベント名]
‘===============================================================================
‘ — イベント1: オブジェクト(イベント)受信時 —
Sub ProcessSink_OnObjectReady(objWmiEvent, objAsyncContext)
On Error Resume Next
Dim strEventType
strEventType = objWmiEvent.Path_.Class
Dim objProcess
Set objProcess = objWmiEvent.TargetInstance
Select Case strEventType
Case “__InstanceCreationEvent”
WriteLog “EVENT”, “【プロセス起動検知】 Name: ” & objProcess.Name & _
” | PID: ” & objProcess.ProcessId & _
” | ExecPath: ” & objProcess.ExecutablePath
Case “__InstanceDeletionEvent”
WriteLog “EVENT”, “【プロセス終了検知】 Name: ” & objProcess.Name & _
” | PID: ” & objProcess.ProcessId & _
” | ExitCode: ” & objProcess.ExitCode
End Select
‘ エラーハンドリング:非同期コールバック内でのエラーでスクリプト全体を殺さない
If Err.Number <> 0 Then
‘ ログ記録失敗等の緊急回避
Err.Clear
End If
End Sub
‘ — イベント2: 非同期操作完了時(キャンセル時やエラー発生時) —
Sub ProcessSink_OnCompleted(hResult, objErrorObject, objAsyncContext)
On Error Resume Next
If hResult <> 0 Then
WriteLog “ERROR”, “非同期WMI監視が異常終了しました。 HRESULT: ” & Hex(hResult)
g_bKeepRunning = False ‘ メインループを破棄して安全に終了へ導く
Else
WriteLog “システム”, “非同期WMI監視セッションが正常に完了しました。”
End If
End Sub
‘===============================================================================
‘ 共通ロギングユーティリティ(スレッドセーフを意識した堅牢なファイル書き込み)
‘===============================================================================
Sub WriteLog(ByVal strCategory, ByVal strMessage)
On Error Resume Next
Dim strFormattedMsg
strFormattedMsg = “[” & Now() & “] [” & strCategory & “] ” & strMessage
‘ 標準出力への表示(cscript環境向け)
WScript.Echo strFormattedMsg
‘ ファイルへの追記
If Not g_objLogStream Is Nothing Then
g_objLogStream.WriteLine strFormattedMsg
End If
End Sub
—
4. エンタープライズ開発における最高レベルの注意点
上記コードを実際の運用環境(バッチサーバやクライアントPCの常駐監視)に投入する際、プロフェッショナルとして配慮すべき「3つの落とし穴」と設計指針を解説する。
① ファイルロックおよび書き込み競合(File I/O Gotchas)
非同期イベントは、予期せぬタイミングで爆発的な頻度で発生する可能性がある(例: 多数のプロセスが同時に起動・終了した場合)。
イベントハンドラー内で都度 `OpenTextFile` を実行して閉じるような実装を行うと、ファイルアクセス権の競合(`Permission Denied`)が発生して即座に破綻する。
- 対策: スクリプト開始時にファイルストリームを開きっぱなしにし、`WriteLine` で追記する設計(上記コードで実装済み)を採用せよ。スクリプト終了時に明示的に `.Close` を呼ぶこと。
② データベース(ADODB.Connection)連携時のコネクションリーク
イベント受信時にデータベース(SQL Server, Oracle等)へログを書き込むアーキテクチャを取る場合、グローバルなDBコネクションを1つ保持し続けるのは危険だ。ネットワークの一時的な切断によってコネクションが切断された場合、非同期ハンドラー内で例外が多発する。
- 対策: DB連携を行う場合は、イベントハンドラー内部で「接続 -> 書き込み -> 即時切断」を行うか、あるいはイベント内容を一度メモリ上の配列(キュー)にスタックし、メインスレッドのループ側で一括挿入(バルクインサート)するプロデューサー・コンシューマーパターンを採用せよ。
③ COMオブジェクトの解放漏れによるメモリリーク
VBScriptのガベージコレクションは参照カウント方式(Reference Counting)である。
`WScript.CreateObject` でイベントバインドしたオブジェクト(`SWbemSink` 等)を `Set obj = Nothing` せずにスクリプトを終了しようとすると、COMの参照カウントが残存し、WSHのプロセス(`cscript.exe`)がメモリ上にゴーストとして残り続ける現象が発生する。
- 対策: イベント停止時は、必ずCOMオブジェクト固有のキャンセルメソッド(`objSink.Cancel()` 等)を呼んだ上で、明示的に `Set objSink = Nothing` を実行すること。
—
5. 結論:真の自動化エンジニアへ
`WScript.CreateObject` の第2引数による非同期イベント受信は、単なる知識の切り売りではない。
VBScriptというレガシーかつ制限の多い環境において、OSのイベント通知メカニズムと正しく対話し、最小のリソースで最大限のパフォーマンスを引き出すための必須技術である。
- 無駄なポーリングループを排除せよ。
- メインスレッドは `WScript.Sleep` で賢く静観させよ。
- イベントハンドラー内の例外処理とリソース解放を徹底せよ。
この3原則を叩き込み、実務で「二度とバグを起こさない堅牢な自動化ツール」を構築してほしい。
