【VBScript極限突破】WScript.CreateObject の第2引数が切り拓く「非同期COMイベント監視」の深層機構
Windowsレガシーシステムの自動化において、ポーリング(一定時間ごとのループ監視)によるCPUリソースの浪費や処理遅延は、システムアーキテクチャ上の致命的な脆弱性となり得ます。VBScriptは単体ではスクリプト言語に過ぎませんが、Windows Script Host(WSH)環境下において `WScript.CreateObject` の第2引数(イベントプレフィックス)を駆使することで、C++やC#に匹敵する「COMイベントシンク(Event Sink)」を構築することが可能です。
本稿では、レガシーアーキテクチャの最前線で求められる非同期イベント受信メカニズムの内部構造、STA(Single-Threaded Apartment)におけるメッセージループの挙動、そしてCOMオブジェクトの厳密なライフサイクル管理について解説します。
—
1. WSHにおける非同期イベント受信の内部メカニズム
通常、`CreateObject(“ProgID”)` はオブジェクトのインスタンスを生成し、その標準インターフェース(`IDispatch`)を取得するのみです。しかし、第2引数に文字列(イベントプレフィックス)を指定すると、WSHエンジンは内部で極めて高度なCOMパイプラインを構築します。
+——————————————————————-+
| WScript.exe / CScript.exe (Host Process) |
| |
| +——————-+ Event Hook +——————-+ |
| | VBScript Engine | <---------------- | WSH Internal Sink | |
| | (Prefix_EventName| | (IDispatch Inst) | |
| +-------------------+ +-------------------+ |
+-----------------------------------------------------^-------------+
|
IConnectionPoint::Advise
|
+-----------------------------------------------------v-------------+
| External COM Server (e.g., SWbemSink, IE, Network) |
| - Fires Event via IConnectionPoint |
+-------------------------------------------------------------------+
COM Connection Pointの自動ハンドシェイク
第2引数を指定した際、WSH内部では以下の処理が透過的に実行されます。
1. `IConnectionPointContainer` のクエリ: 生成したCOMオブジェクトがイベント通知機能をサポートしているかを確認します。
2. デフォルト・コネクション・ポイントの特定: COMオブジェクトのアウトゴーイング(発信)インターフェース(`IID`)を取得します。
3. WSH内部シンクの登録(`Advise`): WSH自身がイベント受け皿となる `IDispatch` を動的に生成し、`IConnectionPoint::Advise` メソッドを呼び出してイベントリスナーとして登録します。
4. イベントディスパッチの変換: 外部COMオブジェクトからイベントが発火(`IDispatch::Invoke`)されると、WSHは指定された `[Prefix]_[EventName]` という形式のVBScript関数を探索し、引数をマッピングして実行します。
—
2. 決定的な動作要件:WSHのメッセージループ(Message Pump)
VBScriptの実行スレッドは STA (Single-Threaded Apartment) で動作します。非同期COMイベントは、Windowsメッセージ(WM_USER等)やCOM呼び出しを通じてスレッドにディスパッチされます。
したがって、スクリプトのメイン処理が終了してしまうと、どれだけイベントリスナーを構築していてもプロセスは即座に停止し、イベントを受信することはできません。また、単なる `Do Loop` によるビジーループはCPUコアを100%消費し、同一スレッド上のCOMイベント配送をブロックします。
ここで重要となるのが `WScript.Sleep` です。
‘ 内部的にWindowsメッセージループ(PeekMessage / DispatchMessage相当)を回し、
‘ イベントキューに溜まったCOM通知をVBScriptエンジンに配送させる
WScript.Sleep 1000
`WScript.Sleep` は単にスレッドを停止させるだけでなく、WSH内部のメッセージポンプを駆動させ、待機時間中に届いたCOMイベントをVBScriptのイベントハンドラー関数へ安全に割り込ませる役割を果たします。
—
3. 実践コード:SWbemSinkを用いた非同期WMIプロセス監視
以下は、システム内で新しいプロセス(例: `notepad.exe` や他システムプロセス)が生成された瞬間に、ポーリングなしで即座に検知する完全な実用コードです。
‘ =========================================================================
‘ ファイル名: AsyncProcessMonitor.vbs
‘ 概要: WMI非同期シンクとWScriptイベントプレフィックスによるプロセス生成監視
‘ アーキテクチャ: COM Connection Point + WSH STA Message Loop
‘ =========================================================================
Option Explicit
‘ グローバル状態管理フラグ(実行ループ制御用)
Dim g_bKeepRunning
g_bKeepRunning = True
‘ COMオブジェクト参照の宣言
Dim objLocator, objServices, objSink
On Error Resume Next
‘ 1. WMI サービスへの接続
Set objLocator = CreateObject(“WbemScripting.SWbemLocator”)
Set objServices = objLocator.ConnectServer(“.”, “root\cimv2”)
If Err.Number <> 0 Then
WScript.Echo “[CRITICAL] WMIサービスへの接続に失敗しました: ” & Err.Description
WScript.Quit 1
End If
‘ 2. イベントシンクオブジェクトの生成(第2引数でイベントプレフィックス “WmiSink_” を指定)
‘ これにより WbemScripting.SWbemSink のイベントが WmiSink_xxxx 関数へルーティングされる
Set objSink = WScript.CreateObject(“WbemScripting.SWbemSink”, “WmiSink_”)
If Err.Number <> 0 Then
WScript.Echo “[CRITICAL] SWbemSink の生成に失敗しました: ” & Err.Description
WScript.Quit 1
End If
On Error GoTo 0
‘ 3. 非同期WMIクエリの発行 (ExecNotificationQueryAsync)
‘ WITHIN 1 : イベントのポーリング間隔(秒)。WMIプロバイダ側の通知精度を指定
Dim strWql
strWql = “SELECT FROM __InstanceCreationEvent WITHIN 1 WHERE TargetInstance ISA ‘Win32_Process'”
‘ 非同期クエリを開始。制御は即座に次の行へ返る
objServices.ExecNotificationQueryAsync objSink, strWql
WScript.Echo “==================================================”
WScript.Echo ” [INFO] 非同期プロセス監視を開始しました。”
WScript.Echo ” 監視を終了するには Ctrl+C を押すか、”
WScript.Echo ” notepad.exe を起動してください。”
WScript.Echo “==================================================”
‘ 4. WSH メッセージループ(イベント受信待ち受け)
Dim i, iMaxWaitSeconds
iMaxWaitSeconds = 60 ‘ 60秒間監視(運用環境に合わせて調整)
For i = 1 To iMaxWaitSeconds
‘ Sleep呼出により、裏側でスレッドメッセージキューが消化され、
‘ WmiSink_OnObjectReady が呼び出される
WScript.Sleep 1000
‘ フラグが倒された場合はループを即座に脱出
If Not g_bKeepRunning Then
WScript.Echo “[INFO] 終了条件が満たされたため、メインループを脱出します。”
Exit For
End If
Next
‘ ————————————————————————-
‘ 5. 安全な破棄シーケンス(メモリリーク・プロセス孤立防止の鉄則)
‘ ————————————————————————-
WScript.Echo “[INFO] クリーンアップシーケンスを実行中…”
‘ 非同期受信を明示的にキャンセル(COMサーバー側へのUnadvise要求)
On Error Resume Next
objSink.Cancel()
On Error GoTo 0
‘ 循環参照を防止するための明示的なオブジェクト解放
Set objSink = Nothing
Set objServices = Nothing
Set objLocator = Nothing
WScript.Echo “[INFO] 監視スクリプトは安全に終了しました。”
WScript.Quit 0
‘ =========================================================================
‘ イベントハンドラー群 (Prefix: WmiSink_)
‘ =========================================================================
”
‘ イベント1: 監視対象オブジェクトの到着通知
‘ @param objWmiObject 到着したWMIイベントオブジェクト (__InstanceCreationEvent)
‘ @param objAsyncContext 非同期実行コンテキスト(未使用時はNothing)
‘
Sub WmiSink_OnObjectReady(objWmiObject, objAsyncContext)
On Error Resume Next
Dim objProcess
‘ TargetInstance プロパティから生成された Win32_Process を取得
Set objProcess = objWmiObject.TargetInstance
WScript.Echo “————————————————–”
WScript.Echo “[EVENT] 新規プロセス検知: ” & Now
WScript.Echo ” PID : ” & objProcess.ProcessId
WScript.Echo ” Name : ” & objProcess.Name
WScript.Echo ” Executable : ” & objProcess.ExecutablePath
WScript.Echo “————————————————–”
‘ 特定プロセスの検知による動的制御例
If LCase(objProcess.Name) = “notepad.exe” Then
WScript.Echo “[ALERT] メモ帳 (notepad.exe) の起動を検知。監視を自動終了します。”
‘ メインループを止めるためのフラグ操作
g_bKeepRunning = False
End If
On Error GoTo 0
End Sub
”
‘ イベント2: 非同期操作の完了通知
‘ @param hResult 非同期処理の結果コード (HRESULT)
‘ @param objErrorObject エラー詳細オブジェクト (SWbemLastError)
‘ @param objAsyncContext 非同期実行コンテキスト
‘
Sub WmiSink_OnCompleted(hResult, objErrorObject, objAsyncContext)
WScript.Echo “[INFO] WmiSink_OnCompleted 受信 (HRESULT: &H” & Hex(hResult) & “)”
End Sub
—
4. ライフサイクル管理とメモリ最適化の深層知見
非同期イベント監視において、シニアアーキテクトが最も警戒すべきはメモリリークおよびCOMオブジェクトの孤立(Orphaned Process)です。
循環参照(Circular Reference)の恐怖
`WScript.CreateObject` でイベントをフックすると、WSH内部のシンクオブジェクトとCOMサーバーとの間で相互に参照カウント(Reference Count)が保持されます。
- アンハング処理(`objSink.Cancel()`)の必須性
`SWbemSink` などの非同期シンクを使用している場合、スクリプト終了前に明示的に `.Cancel()` を呼び出さないと、WMIプロバイダ(`wmiprvse.exe`)側に参照が残存し、`CScript.exe` が終了してもバックグラウンドでメモリーが開放されなかったり、ハンドルのリークを引き起こします。
- 明示的解放の順序
VBScriptのガベージコレクションは単純な参照カウント方式です。以下の順序を守って明示的に `Nothing` を代入する必要があります。
‘ 1. 非同期イベントの受信停止
objSink.Cancel()
‘ 2. イベントシンクの破棄(WSH側COMバインディングの解除)
Set objSink = Nothing
‘ 3. 上位COMサービスの破棄
Set objServices = Nothing
Set objLocator = Nothing
イベントハンドラー内での禁忌事項
STAモデルであるため、`WmiSink_OnObjectReady` などのイベントハンドラー関数の内部で時間のかかる同期処理や追加のブロック型COM呼び出しを行ってはなりません。
1. デッドロックの回避
ハンドラー内部で `objSink.Cancel()` を直接呼び出すと、COMの再帰呼び出しによる内部デッドロックを引き起こす場合があります。状態変更は必ずグローバル変数(フラグ)経由で行い、制御をメインのメッセージループへ戻してからキャンセル処理を実行してください。
2. 処理の短小化
ハンドラー内での処理が滞ると、次々に発火するCOMイベントがキューに滞留し、最終的にスタックオーバーフローやイベントの取りこぼしが発生します。
—
5. エンタープライズレガシー保守における運用規律
長年稼働しているシステム群にこのパターンを適用・保守する場合、以下の原則を厳守してください。
1. `CScript.exe` での実行強制
イベント監視スクリプトを `WScript.exe`(GUIホスト)で実行すると、エラー発生時や通知時にダイアログがポップアップし、無人バッチ処理が永久に停止します。スクリプトの先頭でホスト環境をチェックし、`CScript.exe` への再起動を強制するガードコードの挿入を推奨します。
2. エラーハンドリングの局所化
イベントハンドラー関数内での未処理例外は、WSHエンジン全体を無停止で崩壊させる可能性があります。ハンドラー内には必ず `On Error Resume Next` と適切なログ出力を配置し、メインスレッドへ例外を波及させない設計としてください。
3. セキュリティコンテキスト(UAC / DCOM)の考慮
WMIイベント監視などは、実行ユーザーの権限(管理者権限)や DCOM の認証レベルに依存します。サービスアカウントで実行する場合は、`SWbemLocator.ConnectServer` 時のインパーソネーションレベル(`wbemImpersonationLevelImpersonate` 等)を明示的に指定する設計が必須となります。
—
結論
`WScript.CreateObject` の第2引数は、VBScriptというレガシーなスクリプト言語に「イベント駆動型アーキテクチャ」を注入する極めて強力な機能です。
内部のCOM Connection PointメカニズムとSTAメッセージループ(`WScript.Sleep`)の本質を理解し、厳格なオブジェクトライフサイクル管理(`Cancel` および `Set Nothing`)を行うことで、現代の重厚なフレームワークにも引けを取らない、超軽量かつ高レスポンスなシステム監視・連携基盤を構築・維持することが可能となります。
