Doctor Scripto のスクリプト ショップ

Out of Sync: 帰ってきた非同期イベントの監視

Microsoft Scripting Guys

作業中の Doctor Scripto

Doctor Scripto のスクリプト ショップでは、読者の皆さんから寄せられる実際のシステム管理スクリプトに関する問題を取り上げて、それらを解決するために簡単なスクリプト例を組み合わせてより複雑なスクリプトを作成します。作成するスクリプトは、包括的で強固なものではありませんが、これらのスクリプトを通じて、再利用可能なコード モジュールから効率的なスクリプトを作成する方法、エラーやリターン コードを処理する方法、さまざまなソースから入出力を行う方法、複数のコンピュータに対して実行する方法など、実際のスクリプトの中で実行してみたいテクニックを紹介します。

このコラムやここで紹介するスクリプトが皆さんのお役に立つことを期待します。ご意見、ご要望のほか、皆さんが問題解決のために編み出した方法や、今後取り上げてほしい話題などがありましたら、ぜひお知らせください (英語)。

過去のコラムについては、「Doctor Scripto のスクリプト ショップ」を参照してください (この記事には英語のページへのリンクも含まれています)。

トピック

Out of Sync : 帰ってきた非同期イベントの監視
非同期イベント処理スクリプトの分析
WHERE 句を使用した WQL クエリのプロセスのフィルタ処理
Win32_ProcessStartTrace を使用したプロセス イベントの処理
戻り値の解釈の表示
SINK_OnObjectReady への objAsyncContext パラメータの使用
実行中のプロセスのスナップショットを取得して新しいプロセスに警告する
その他の非同期メソッド
詳細情報

Out of Sync : 帰ってきた非同期イベントの監視

またサーバー ルームのドアを安心して開けられると思ったまさにそのときです。

それはさらに危険になって帰ってきました。非同期イベントの監視はこのまま消え去ることを拒んだのです。Doctor Scripto はスクリプト室にこもって、またスクリプトをいじくり回しはじめました。これまでのコラム (どれも長くなりましたが) で検討していない問題があったからです。また、皆さんにご紹介したい興味深いフィードバックを読者からいくつか受け取りました。私たちが思うに、非同期イベントの監視は複数のコンピュータの監視に非常に役立てることができます。

ですからこのコラムは "インタラクティブ" なものになるでしょう。これは関連するあらゆるものの株価を 20 パーセント以上上昇させた、90 年代後半の流行語です。そうですね、このたとえは最近のコラムではもうわかりにくいかもしれません。いずれにしてもご意見ありがとうございます。結果的に、この非同期についてのコラムは "真珠の数珠" にもなるでしょう。これは、面白い話をつなぎ合わせた記事を表すジャーナリズム用語です。真珠をつなぎ合わせる糸として、非同期イベントの処理を徹底的に追究し、紹介する非同期スクリプトで詳細の一部を明らかにします。それらが珠玉の知恵であるか、見せかけだけのものかの判断は皆さんにお任せします。

非同期イベント処理スクリプトの分析

非同期イベント処理メソッドについては前回のコラムで詳しく説明しました。しかしこれらのメソッドは同期イベントや半同期イベントの場合よりもやや複雑であるため、メソッドの動作についていくつかのポイントをもう一度 (気合を入れて) 復習し、今回また皆さんを混乱に陥れることになるかを確認しましょう。

非同期メソッドの呼び出しでは、同期ルーチンとシンクという並列に機能する 2 つのルーチンが作成されます。これら 2 つのルーチンは、互いのタイミングに依存しません。これが "非同期" と呼ばれる理由です (Webster's によると、"同期" (synchronous) の意味は "完全に同時に発生、存在すること" であり、"a" は元の意味を否定します)。

非同期ルーチンではタスクをいくつか実行します。ここで紹介している例では、プロセス イベントに照会し、別のスレッドを作成します。このスレッドは、実質的にはスクリプトによってディスパッチされる独立したエージェントのように機能します。非同期呼び出しがこのスレッドを解放すると、処理がスクリプトに戻り、次の行に移ることができます。クエリが一致したものを検出すると (この場合はクエリに一致するイベントが起動されると)、この独立スレッドは待機中のシンクに報告を返します。この報告を "コールバック" と呼びます。非同期ルーチンがリモート コンピュータを対象に機能している場合でも、シンクは呼び出し元のコンピュータで実行されるので、非同期クエリ スレッドは結果的に実際の呼び出し元に報告を返すことになります。スレッドはスクリプトが実行され続ける限り、クエリに一致する新しいイベントをシンクに報告し続けることができます。

簡単でしょう。心配しないでください、非同期メソッドを使うために素粒子物理学の博士号は必要ありません。しかし、数珠の話は役に立つかもしれません、ただの VBScript の数珠ですけど。Scripting Guys は毎回喜んで同じ説明を繰り返すことにあきれてしまうかもしれませんが、ここでは必要な基本パターンのテンプレートを提示するので、皆さんのスクリプトの目的に合うように変更してください。その目標に沿って、ここでは WMI から直接提供される組み込みイベントを監視するいくつかのテンプレート コードを紹介します。

リスト 1 : 特定のプロセスを非同期に監視する

Set SINK = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")
Set objWMIService = GetObject("winmgmts:\\.\root\cimv2")
objWMIService.ExecNotificationQueryAsync _
 SINK, _
 "SELECT * FROM __InstanceCreationEvent WITHIN 1 " & _
 "WHERE TargetInstance ISA 'Win32_Process'"
Do
   WScript.Sleep 10000
Loop
Sub SINK_OnObjectReady(objLatestEvent, objAsyncContext)
  Wscript.Echo "Process Name: " & objLatestEvent.TargetInstance.Name
End Sub

このコードには、他の種類の WMI スクリプトに使用される馴染みのないものがいくつかあるので、コードの詳細を 1 行ずつ掘り下げていきましょう。

Set SINK = _
 WScript.CreateObject("WbemScripting.SWbemSink","SINK_")

この行では、非同期メソッドによるコールバック先となるイベント シンクを作成します。通常どおり、Set では変数 SINK にオブジェクト参照を代入します。変数には任意の名前を付けることができます。この名前は、この行の最後にある CreateObject の 2 つ目のパラメータと一致している必要はありません (しかし一致させておく方が役立ちます)。しかし変数に名前を付けたら、その名前を何行か先に出てくる ExecNotificationQueryAsync の 1 つ目のパラメータとして使用する必要があります。これにより、コールバック先のイベント シンクの名前が非同期クエリに指定されます (前回のコラムで説明しましたが、シンクは複数保持できます)。

WScript (Windows Script Host 親オブジェクト) の CreateObject は、VBScript の CreateObject とはやや異なります。イベント シンクを作成するには、WScript のバージョンを使用します。

WScript.CreateObject メソッドのパラメータとして、1 つ目は COM オブジェクト (この場合は WMI スクリプト API の SWbemSink オブジェクト) のプログラム ID (progID)、2 つ目はこのオブジェクトと共にイベント メソッドに使用する非同期シンクを表すプレフィックスを指定します。この 2 つ目のパラメータは任意の名前を付けることができる文字列ですが、このスクリプトの最後にある、シンクの OnObjectReady メソッドを処理する Sub の名前は、ここで付けたものと同じ名前で始まる必要があります。繰り返しますが、"SINK_" は私たちが使っている既定値です。

Set objWMIService = _
 GetObject("winmgmts:\\.\root\cimv2")

WMI サービスに接続するこの行は、どの WMI スクリプトでも使用します。ここではコードを簡単にするために、単純にローカル コンピュータ (" " で表します) の \root\cimv2 名前空間 (通常の既定値) に接続しています。

objWMIService.ExecNotificationQueryAsync _
 SINK, _
 "SELECT * FROM __InstanceCreationEvent WITHIN 1 " & _
 "WHERE TargetInstance ISA 'Win32_Process'"

この行には、非同期イベントの呼び出しと 2 つのパラメータが含まれています。

先ほどのコード行で、SWbemServices オブジェクトによって表される WMI サービスへの参照を取得しました。ここで、SWbemServices オブジェクトの ExecNotificationQueryAsync メソッドを呼び出し、次に示す 2 つのパラメータを渡します。

  • 1 行目で作成したシンク オブジェクトの名前。クエリでは、イベントが発生するとこのオブジェクトにコールバックします。

  • メソッドで実行する WQL クエリ。

クエリでは、__InstanceCreationEvent (アンダースコアは 2 つです) クラスのすべてのプロパティ ( * ) を取得するようにメソッドに指定します。このクラスは、WMI の組み込み (ビルトイン) イベント クラスの 1 つです。

WITHIN 句では、ポーリング間隔を 1 秒に指定します。ポーリング間隔は、メソッドがイベントのチェックを実行してから次のチェックを実行するまでの待ち時間です。前回のコラムで説明したように、1 秒などの短いポーリング間隔を使用すると、多くのコンピュータから大量のデータにクエリを実行するときのパフォーマンスに影響が出る可能性があります。実際にパフォーマンスが低下した場合は、ポーリング間隔を長くしてみるとよいでしょう。WITHIN 10 にすると、プロセッサ サイクルが少なくなり、使用するネットワーク帯域幅も狭くなります、10 秒間の待機中に開始および終了されたプロセスが見つからなかったり、不要なプロセスが最大 10 秒間不適切に動作する可能性があります。

Win32_ProcessStartEvent など、特殊な外部のイベント クラスを使用する場合は、ポーリング間隔の指定に WITHIN を使用する必要はありません。後半の「Win32_ProcessStartTrace を使用したプロセス イベントの処理」では、このクラスの使用方法について説明します。

"WHERE TargetInstance ISA 'Win32_Process'" では、クエリで検索するイベントをフィルタ処理しています。フィルタがなかった場合、__InstanceCreationEvent では、理論上コンピュータで作成される WMI クラスのすべてのインスタンスのイベントを返します。量としてはやや多すぎるでしょう。このフィルタ句では、Win32_Process のインスタンスを表すイベントのみを検索するように WMI に指定します。TargetInstance は、作成されたクラスのインスタンスを取得する __InstanceCreationEvent のプロパティです。

Do
   WScript.Sleep 10000
Loop

この無限ループによって、スクリプトは無期限にイベントを待機します。このコードがスクリプトにないと、スクリプトはイベント クエリ後に終了してしまうので、シンクがイベントを受信するために待機することはできません。WScript の Sleep メソッドでは、ミリ秒を表す整数が必要です。しかし、この間隔がイベント クエリのポーリング間隔に影響することはありません。通常どおり、コマンド ウィンドウでは、Ctrl キーを押しながら C キーを押すことでスクリプトを終了できます。

Sub SINK_OnObjectReady(objLatestEvent, objAsyncContext)
  Wscript.Echo objLatestEvent.TargetInstance.Name
End Sub

このサブルーチンは、非同期クエリが一致を検出したときに実行されます。SWbemSink オブジェクトには使用可能なイベントが他にもありますが、私たちが通常非同期イベント クエリで使用するのは OnObjectReady です。

ここで "SINK_" を Sub 名の先頭に使用する必要があります。これは、シンクの作成時にこの文字列を指定したためです。SWbemSink オブジェクトの OnObjectReady イベントは、イベント クエリによって解放された監視スレッドが、イベントを表すオブジェクトを使用してコールバックし、2 つのパラメータを渡すと起動されます。

  • objLatestEvent は、スクリプトがイベントの詳細確認に使用できる (SWbemObject 型の) オブジェクトを表しています。このクエリでは __InstanceCreationEvent クラスのインスタンスを返すため、オブジェクトはこのクラスのプロパティ (TargetInstance など) を保持します。サブルーチンは TargetInstance を使用して、作成されたインスタンス (この場合は Name プロパティを持つ Win32_Process のインスタンス) のプロパティにアクセスできます。

  • objAsyncContext は、必要に応じて非同期クエリに渡すことができる名前付きの値セットです。この値セットは、複数の非同期呼び出しで同じシンクを使用するときに、イベントを起動した呼び出しを特定するために使用します。このスクリプトの呼び出しではこのパラメータを指定しませんが、このパラメータを使用した例はこのコラムの後半で紹介します。

このルーチンの 1 行は、TargetInstance から取得した、インスタンス作成イベントを起動したプロセスの名前をエコー出力するものです。

Windows Server 2003 では、スクリプトが送信した非同期ルーチンからコールバックされたことを保証する承認メカニズムが提供されることに注意してください。以前のバージョンのオペレーティング システムでは、環境によってコールバックの偽装が問題になることがありました。詳細については、WMI SDK の「Calling a Method (英語)」と「Querying WMI(英語)」を参照してください。

難しかったですか。それは失礼しました。はい、何でしょう。いいえ、非同期イベント クエリを使って心的外傷後ストレス障害になったという話は聞いていませんよ。心の傷が早く癒えることを心から願っています。Doctor Scripto は、2 錠のアスピリンを服用して、朝になったらスクリプト センターにアクセスすることをお勧めしています。

WHERE 句を使用した WQL クエリのプロセスのフィルタ処理

私たちに寄せられた意見の 1 つに、前回のコラムの最初のスクリプト "プロセスを検索する" に関するものがありました。その内容は、対象プロセスをフィルタ処理するために、プロセスの一覧にある実行中の各プロセスをチェックすることで独自のフィルタ処理を実行するよりも、WQL クエリで WHERE 句を使用すべきだ、というものです。

一般的な規則としては、カスタム コードよりも WQL クエリでフィルタ処理を実行する方が効率的です。特にクエリが WMI から返すデータセットが大きいときや、ネットワーク帯域幅が問題であるときは、多くの場合このことを活用するのが理にかなっています。

この場合、実行中のプロセスの一覧と対象プロセスの一覧はどちらも短くなるため、コンピュータの数が適当であればパフォーマンスにそれほどの違いは生まれません。通常のサンプル スクリプトは、パフォーマンスを最適化しようとするのではなく、どんな処理を実行しているのかがはっきりわかるように記述しています。しかし、大量のコンピュータおよびデータを使用する場合に関しては、この意見を皆さんに紹介して心に留めておいてもらう価値があるように思いました。

ここで、前回のコラムの最初のスクリプトを示します。このスクリプトでは、入れ子になった For Next ループを使用することで、対象プロセスの配列にある実行中のプロセスをチェックしています。外側のループはクエリによって返される実行中のプロセスに対して繰り返され、内側のループは対象プロセスの配列に対して繰り返されます。このスクリプトでは、実行中の各プロセスを各対象プロセスと比較します。また、大文字小文字の違いによって誤った結果が返されないように、比較する両方の文字列を小文字にします。

リスト A ( 前回のコラムではリスト 1) : プロセスを検索する

strComputer = "."
arrTargetProcs = Array("calc.exe","notepad.exe","freecell.exe")
Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colProcesses = objWMIService.ExecQuery("SELECT * FROM Win32_Process")
For Each objProcess in colProcesses
  For Each strTargetProc In arrTargetProcs
    If LCase(objProcess.Name) = LCase(strTargetProc) Then
      WScript.Echo objProcess.Name
    End If
  Next
Next

次のスクリプトでは、同じ作業を実行するもう 1 つの方法を紹介します。このスクリプトでは、For Each ループを 1 つだけ使用し、対象プロセスの配列に対してループ処理を実行します。対象プロセスごとに、WHERE 句を使用するクエリを指定して ExecQuery を呼び出し、対象プロセスの名前をフィルタ処理して、一致したプロセス名のみを返します。ここでは WQL クエリの WHERE フィルタが、前回のスクリプトの外側の For Each ループの役割を果たしています。

リスト 2 : 特定のプロセスを検索するもう 1 つの方法

strComputer = "."
arrTargetProcs = Array("calc.exe","notepad.exe","freecell.exe")
Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
For Each strTargetProc In arrTargetProcs
  Set colProcesses = objWMIService.ExecQuery _
  ("SELECT * FROM Win32_Process WHERE Name='" & strTargetProc & "'")
  For Each objProcess in colProcesses
    WScript.Echo objProcess.Name
  Next
Next

Win32_ProcessStartTrace を使用したプロセス イベントの処理

何回か前のコラム「午前 2 時、プロセスの所在を把握していますか」では、プロセスを監視するためにデザインされた特殊イベント クラスである、Win32_ProcessTrace とその派生クラスの Win32_ProcessStartTrace および Win32_ProcessStopTrace の 3 つについて説明しました。この回のコラムでは、Win32_ProcessStopTrace を使用してプロセス削除イベントを監視する方法を紹介しました。

ある読者からのご提案のように、前回のコラムの __InstanceCreationEvent と Win32_Process を使用せずに、Win32_ProcessStartTrace を使用してプロセス作成イベントを監視することもできます。

Win32_ProcessTrace ファミリのメンバなど、組み込みではない WMI イベント クラスを使用すると、WQL イベント クエリの WITHIN 句でポーリング間隔を設定する必要がなくなります。これにより、クエリを実行するコンピュータの負荷が軽減される場合もあります。ただし、一度イベントを捕捉すると、そのイベントに対してすぐには Win32_Process プロパティやメソッドを呼び出せないというマイナス面もあります。これは、__InstanceCreationEvent クラスの TargetInstance プロパティにアクセスする権限がないためです。しかしこのセクションの 2 つ目のスクリプトが示すように、この問題には解決策があります。

このセクションの最初のサンプル スクリプトでは、Win32_ProcessStartTrace イベントを使用して特定のプロセスの作成を非同期に監視する方法を説明しています。また、不要なプロセスを終了させる場合は、このセクションの 2 つ目のスクリプトに例示されているように、Win32_ProcessStartTrace を Win32_Process と組み合わせることができます。

最初のスクリプトでは、単純にこのコラムの最初の非同期の例を使用し、特定のプロセスがあるかどうかをチェックするコードを追加しています。したがって非同期クエリでは、フィルタを使用せずに単純に Win32_ProcessStartTrace を使用します。

プロセスが開始し、イベントが起動すると、イベント シンク コードが対象プロセスの一覧に対して繰り返し実行され、Win32_ProcessStartTrace の ProcessName プロパティが各対象プロセス名に対してチェックされます。このとき、両方の文字列を小文字にします。一致が検出されると、スクリプトではその名前、プロセス ID、および時刻を表示します。

リスト 3 : Win32_ProcessStartTrace を使用して特定のプロセスを非同期に監視する

On Error Resume Next

strComputer = "."
arrTargetProcs = Array("calc.exe", "notepad.exe", "freecell.exe")

Set SINK = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
objWMIService.ExecNotificationQueryAsync SINK, _
 "SELECT * FROM Win32_ProcessStartTrace"

Wscript.Echo "Waiting for target processes ..."

Do
   WScript.Sleep 10000
Loop

'******************************************************************************

Sub SINK_OnObjectReady(objLatestEvent, objAsyncContext)

For Each strTargetProc In arrTargetProcs
  If LCase(objLatestEvent.ProcessName) = LCase(strTargetProc) Then
    Wscript.Echo VbCrLf & "Process Name: " & objLatestEvent.ProcessName
    Wscript.Echo "  Process ID: " & objLatestEvent.ProcessID
    Wscript.Echo "  Time: " & Now
  End If
Next

End Sub

次のリストでは、この同じスクリプトを使用し、対象プロセスを終了するコードを追加します。一致を検出したら、そのプロセス ID を変数 strHandle に代入します。Win32_Process の Handle プロパティには、プロセス ID に相当する文字列データ型の数値が含まれています。ただし、Handle には Key 修飾子が含まれています。これは、ProcessId が Key プロパティでなくても、Handle を使用して Win32_Process のインスタンスを一意に識別できることを意味します。

ここでは、WMI サービスの Get メソッドの呼び出しで strHandle 変数を使用します。この呼び出しでは、Handle プロパティが strHandle に含まれているプロセス ID の値に一致する Win32_Process のインスタンスを 1 つ取得します。Get メソッドによって返されるオブジェクトを使用すると、Win32_Process の Terminate メソッドを呼び出してプロセスを終了できます。この追加手順を行わなければいけない理由は、Win32_ProcessStartTrace クラスにメソッドが含まれておらず、このクラスを直接使用してプロセスを終了することができないためです。

リスト 4 : Win32_ProcessStartTrace Win32_Process を使用して特定のプロセスを終了する

On Error Resume Next

strComputer = "."
arrTargetProcs = Array("calc.exe", "notepad.exe", "freecell.exe")

Set SINK = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
objWMIService.ExecNotificationQueryAsync SINK, _
 "SELECT * FROM Win32_ProcessStartTrace"

Wscript.Echo "Waiting for target processes ..."

Do
   WScript.Sleep 10000
Loop

'******************************************************************************

Sub SINK_OnObjectReady(objLatestEvent, objAsyncContext)
'Trap asynchronous events.

For Each strTargetProc In arrTargetProcs
  If LCase(objLatestEvent.ProcessName) = LCase(strTargetProc) Then
    Wscript.Echo VbCrLf & "Process Name: " & objLatestEvent.ProcessName
    strHandle = CStr(objLatestEvent.ProcessID)
    Wscript.Echo "  Process ID: " & strHandle
    Wscript.Echo "  Time: " & Now
    Set objProcess = objWMIService.Get _
     ("Win32_Process.Handle='" & strHandle & "'")
    intReturn = objProcess.Terminate
    If intReturn = 0 Then
      Wscript.Echo "  Terminated"
    Else
      Wscript.Echo "  Unable to terminate"
    End If
  End If
Next

End Sub

戻り値の解釈の表示

別の読者からいただいたもう 1 つの案は、メソッドからのリターン コードを解釈し、より詳細なメッセージを表示するという拡張です。前回のスクリプトでは、戻り値が 0 (成功したことを示します) であったかどうかをチェックしているだけです。0 以外の場合は、一般的なエラー メッセージを表示します。

しかし、WMI クラスの多くのメソッドには、問題のトラブルシューティングに役立つことがある、より具体的な戻り値が用意されています。たとえば、Win32_Process.Terminate には、0 以外にも特定の意味を持つリターン コードが 5 種類あります。したがって前回のスクリプトの If intReturn = 0 Then ... Else ... End If ステートメントの代わりに、メソッドの有効な各戻り値を解釈する Select Case 構造を使用できます。

リスト 5 : Win32_Process.Terminate のリターンコードを解釈する

Select Case intReturn
  Case 0 Wscript.Echo "  Terminated"
  Case 2 Wscript.Echo "  Access denied"
  Case 3 Wscript.Echo "  Insufficient privilege"
  Case 8 Wscript.Echo "  Unknown failure"
  Case 9 Wscript.Echo "  Path not found"
  Case 21 Wscript.Echo "  Invalid parameter"
  Case Else Wscript.Echo "  Unable to terminate for undetermined reason"
End Select

SINK_OnObjectReady への objAsyncContext パラメータの使用

簡単な非同期イベントの監視スクリプトを分析したとき、SINK_OnObjectReady Sub に渡す objAsyncContext パラメータの使い方の例を紹介すると書きました。objAsyncContext では、SWbemNamedValueSet 型 (WMI スクリプト API の一部) のオブジェクトを参照します。

SWbemNamedValueSet オブジェクトは、SWbemNamedValue オブジェクトのコレクションです。各オブジェクトは単なる名前と値の組み合わせです。お聞き覚えがあるとすれば、Script Runtime の Dictionary オブジェクトを使ったことがあるからかもしれません。とてもよく似ていますからね。ここでは、OnObjectReady イベントに必要な SWbemNamedValueSet を使用する必要があります。

このセットを次の行で作成します。

Set objContext = CreateObject("WbemScripting.SWbemNamedValueSet")
objContext.Add "hostname", strComputer

これで、名前が "hostname"、値が strComputer の内容という唯一のメンバを持つ、SWbemNamedValueSet オブジェクトが用意されました。For Each ループを繰り返すたびに、処理対象のコンピュータと同じ名前の新しいコンテキスト オブジェクトが作成されます。

次に示す、シンクに対する非同期のコールバックでは、その行の 2 つ目のパラメータとしてこのオブジェクトを渡します。

Sub SINK_OnObjectReady(objLatestEvent, objAsyncContext)

その後、次の行でこのオブジェクトを使用して、非同期のコールバック元コンピュータ名を表示します。

WScript.Echo VbCrLf & "Computer Name: " & objAsyncContextItem.Value

Win32_ProcessStartTrace を使用している場合、この方法はイベントの発生元コンピュータの名前を取得するのに便利です。その理由は、Win32_ProcessStartTrace クラスが、イベントの発生元コンピュータの名前を直接取得できるプロパティを持っていないためです。また、スクリプトの実行先コンピュータ名を直接取得できる Win32_Process の CSName のようなプロパティを持たない他のクラスを使用する場合にも、この方法が役立つことがあります。

objAsyncContext パラメータは、同じコンピュータに対する異なる非同期メソッドの呼び出しが、同じシンクにコールバックしている場合に、呼び出し元を識別する手段としても使用できます。

リスト 6 : 同じシンクを使用する複数のコンピュータのプロセス開始イベントを非同期に監視する

On Error Resume Next

arrComputers = Array("sea-wks-1","sea-wks-2","sea-srv-1","sea-srv-2")
strQuery = "SELECT * FROM Win32_ProcessStartTrace"

Set SINK = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")

For Each strComputer In arrComputers
  Set objContext = CreateObject("WbemScripting.SWbemNamedValueSet")
  objContext.Add "hostname", strComputer
  Set objWMIService = GetObject("winmgmts:" _
   & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
  If Err = 0 Then
    objWMIService.ExecNotificationQueryAsync SINK, strQuery, , , , objContext
    Wscript.Echo "Waiting for processes to start on " & strComputer & " ..."
  Else
    Wscript.Echo "Computer Name: " & strComputer & vbCrLf & _
     "ERROR: " & Err.Number & vbTab & Err.Description
    Err.Clear
  End If
Next

Wscript.Echo "In monitoring mode. Press Ctrl+C to exit."

Do
   WScript.Sleep 10000
Loop

'******************************************************************************
        
Sub SINK_OnObjectReady(objLatestEvent, objAsyncContext)
'Trap asynchronous events.

Set objAsyncContextItem = objAsyncContext.Item("hostname")
WScript.Echo VbCrLf & "Computer Name: " & objAsyncContextItem.Value
Wscript.Echo "  Process Name: " & objLatestEvent.ProcessName
Wscript.Echo "  Process ID: " & objLatestEvent.ProcessID
Wscript.Echo "  Time: " & Now

End Sub

実行中のプロセスのスナップショットを取得して新しいプロセスに警告する

数人の読者の方から、不要な実行可能ファイルの一覧を維持することの難しさが指摘されました。実行可能ファイルによっては、複数のファイル名を持つものや、ファイル名を変更できるものもあるためです。またある読者の方から、実行中のサービスとプロセスの一覧を作成し、ソフトウェアに修正プログラムが適用されたかアップグレードされた場合だけ一覧を訂正して、一覧にないサービスとプロセスを締め出すことを提案していただきました。同様に別の読者の方から、現在実行中のプロセスをベンチマークとして取得してから新しいプロセスをそれぞれチェックすることを提案していただきました。プロセスが一覧にない場合、スクリプトでは、そのプロセスを終了するか実行するかをユーザーに確認し、実行が選択された場合はプロセスを一覧に追加します。

ベンチマーク コンピュータを、通常実行しているすべてのサービスとプロセスが実行されていたときの状態にして、完全な一覧を取得することは難しいかもしれません。この方法は、2、3 個の特定のアプリケーションのみを実行する、ある機能に特化したサーバーや小規模のクライアントに対してならば簡単に実行できるでしょう。

いずれにせよ、ベンチマーク コンピュータを一度適切な状態にすれば、そのコンピュータで実行されているプロセスをテキスト ファイルにダンプ (1 行に 1 プロセス) するのは簡単です。以前のコラムでも、同様の FileSystemObject コードを既に何回か使用しています。

リスト 7 : 実行中のプロセスの一覧をテキストファイルに書き込む

Const FOR_WRITING = 2
strComputer = "."
strOutputFile = "processes.txt"
Set objFSO = Createobject("Scripting.FilesystemObject")
Set objTextStream = objFSO.CreateTextFile(strOutputFile)
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colProcesses = objWMIService.ExecQuery("SELECT * FROM Win32_Process")
For Each objProcess In colProcesses
  objTextStream.WriteLine objProcess.Name
Next
objTextStream.Close

このスクリプトでは、実行中のすべてのプロセスをダンプしますが、最低限の処理しか行いません。プロセスをアルファベット順に並べ替えたり、重複している名前を削除したりはしません。これらの処理は手動で実行できます。また、これらのタスクを実行する関数を簡単にスクリプトに追加できます。

これで許容されるプロセスの一覧ができたので、実行中のプロセスとベンチマークの一覧を比較し、一覧にないプロセスを表示するスクリプトを実行します。

ベンチマークの一覧に対してプロセスをチェックするロジックには、再び入れ子になった For Each ループのペアを使用します。実行中の各プロセスに対する繰り返し処理は、カウンタを 0 に設定して開始します。次に実行中のプロセスをベンチマークの各プロセスと比較し、一致が検出されたらカウンタの数値を 1 増やします。一覧のすべてのプロセスに対するチェックが完了した後もカウンタが 0 である場合、現在のプロセスは一覧にないため、そのプロセスの名前とプロセス ID を表示します。objProcess の Terminate メソッドを呼び出す別の行を追加することで、一覧にないプロセスを中止することもできます。

リスト 8 : プロセスをテキストファイル内の一覧と比較する

Const FOR_READING = 1
strInputFile = "processes.txt"
Set objFSO = Createobject("Scripting.FilesystemObject")
Set objTextStream = objFSO.OpenTextFile(strInputFile, FOR_READING)
arrProcesses = Split(objTextStream.ReadAll, vbCrLf)
objTextStream.Close
Set objWMIService = GetObject("winmgmts:")
Set colProcesses = objWMIService.ExecQuery _
 ("SELECT * FROM Win32_Process")
WScript.Echo "Processes not on list"
For Each objProcess In colProcesses
  i = 0
  For Each strProcess In arrProcesses
    If LCase(objProcess.Name) = LCase(strProcess) Then
      i = 1
    End If
  Next
  If i = 0 Then
    WScript.Echo VbCrLf & "Process Name: " & objProcess.Name
    WScript.Echo "  Process ID: " & objProcess.ProcessId
  End If
Next

ユーザー操作なしで実行されるスクリプトの場合は、もちろんプロセスを終了するかどうかを確認するメッセージが表示されないことを望むでしょう。この場合は、スクリプトにその情報を組み込みます。しかし、このスクリプトがユーザーの存在を考慮したスキャナとして実行される場合は、ユーザーに確認メッセージを表示するように簡単に変更できます。

前回のスクリプトの

WScript.Echo "  Process ID: " & objProcess.ProcessId

の後と

End If

の前に次のコードを挿入できます。

WScript.Echo "Terminate process? Press y or n."
    strYesOrNo = WScript.StdIn.ReadLine
    If "y" = LCase(Left(strYesOrNo, 1)) Then
      objProcess.Terminate
      WScript.Echo "  Process terminated."
    Else
      WScript.Echo "  Process not terminated."
    End If

これにより、スクリプトが実行されているコマンド シェル ウィンドウに確認メッセージが表示されます。

同じ目的でポップアップ メッセージ ボックスを開くには、次のコードを挿入します。

intReturn = MsgBox("Terminate process?", 4, "Process Decision")
    If intReturn = 6 Then
      objProcess.Terminate
      WScript.Echo "  Process terminated."
    Else
      WScript.Echo "  Process not terminated."
    End If

VBScript の MsgBox 関数の 1 つ目のパラメータは、ボックスに表示されるメッセージです。2 つ目のパラメータの 4 により、メッセージ ボックスに [はい] と [いいえ] ボタンが表示されます。3 つ目のパラメータは、ボックスのタイトル バーに表示されるタイトルです。

また、ご提案のように、終了されなかったプロセスをベンチマークの一覧に追加することもできます。この場合、別のメッセージ ボックスやプロンプトを表示して、実行を許可されたすべてのプロセスが一覧に追加されるようにします。

それほど時間をかけずに、一覧内のプロセスではなく一覧にないプロセスにより生成されたイベントがあるかどうかを監視する関数を作成し、前回のコラムの最後に記載したスクリプトに追加できます。

次のスクリプトでは、この関数で使用する基本的なコードを示します。ただし、これが関数として公開されることはなく、出力もコマンド ラインで行います。このコードでは、ExecNotificationQueryAsync を呼び出して Win32_ProcessStartTrace クラスにクエリを実行します。次に OnObjectReady イベントを処理する Sub で、カウンタを含む前回のスクリプトに似たコードを使用し、現在のプロセスが一覧に存在するかどうかを判断します。一覧にない場合、このスクリプトでは同じ呼び出しを使用してプロセス オブジェクトを取得し、上記で説明したように、そのオブジェクトに対して Terminate を呼び出します。

このスクリプトでは、一覧にない起動中のすべてのプロセスを終了しようとするため、テスト用コンピュータでこのスクリプトを実行するようにしてください。

リスト 9 : 非同期にプロセスを監視し、一覧にないプロセスを終了する

On Error Resume Next

strComputer = "."

Const FOR_READING = 1
strinputfile = "proclist.txt"
Set objFSO = Createobject("Scripting.FilesystemObject")
Set objTextStream = objFSO.OpenTextFile(strInputFile, FOR_READING)
arrTargetProcs = Split(objTextStream.ReadAll, vbCrLf)
objTextStream.Close

Set SINK = WScript.CreateObject("WbemScripting.SWbemSink","SINK_")

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
objWMIService.ExecNotificationQueryAsync SINK, _
 "SELECT * FROM Win32_ProcessStartTrace"

Wscript.Echo "Waiting for processes not on list ..."

Do
   WScript.Sleep 10000
Loop

'******************************************************************************

Sub SINK_OnObjectReady(objLatestEvent, objAsyncContext)
'Trap asynchronous events.

i = 0
For Each strTargetProc In arrTargetProcs
  If LCase(objLatestEvent.ProcessName) = LCase(strTargetProc) Then
    i = 1 
  End If
Next
If i = 0 Then
  Wscript.Echo VbCrLf & "Process Name: " & objLatestEvent.ProcessName
  strHandle = CStr(objLatestEvent.ProcessID)
  Wscript.Echo "  Process ID: " & strHandle
  Wscript.Echo "  Time: " & Now
  Set objProcess = objWMIService.Get _
   ("Win32_Process.Handle='" & strHandle & "'")
  intReturn = objProcess.Terminate
  Select Case intReturn
    Case 0 Wscript.Echo "  Terminated"
    Case 2 Wscript.Echo "  Access denied"
    Case 3 Wscript.Echo "  Insufficient privilege"
    Case 8 Wscript.Echo "  Unknown failure"
    Case 9 Wscript.Echo "  Path not found"
    Case 21 Wscript.Echo "  Invalid parameter"
    Case Else Wscript.Echo "  Unable to terminate for undetermined reason"
  End Select
End If

End Sub

その他の非同期メソッド

このコラムでは非同期イベントの処理についてのみ説明してきましたが、ExecNotificationQueryAsync 以外にも非同期メソッドがあることを覚えておいてください。前回のスクリプト ショップで指摘したように、他のいくつかの WMI 同期メソッドには、対応する非同期メソッドもあります。明らかなものとして、Scripting Guys のサンプル スクリプトによく登場する、支持率の高いメソッドである ExecQuery には、非同期型の二卵性双生児である ExecQueryAsync がいます。

なぜイベント クエリではない WMI クエリを非同期に実行するのでしょうか。特にさまざまなコンピュータから大量のデータが返されることが予想される場合は、非同期クエリを使う方が効率的な場合があります。たとえば、イベント ログにクエリを実行したり、大きいファイルの一覧を取得したりするときは、非同期にクエリを実行するといいかもしれません。

何か使えるものがありましたか。この URL で非同期について耳にするのはおそらくこれが最後ではありません、Dr. Scripto が話すことがあればですが。来月アクセスして確かめてみてください。改めて、ご意見ありがとうございました。今後も何かありましたらこちらまで英語でお寄せください。

詳細情報