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

午前 2 時、プロセスの所在を把握していますか

Microsoft Scripting Guys

作業中の Doctor Scripto

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

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

過去のコラムについては、「Doctor Scripto's Script Shop archive」(英語) を参照してください。

(メモ: Dr. Scripto の新しいイラストに関して Patrick Lanfear と Tom Yamaguchi に感謝します)

トピック

午前 2 時、プロセスの所在を把握していますか
ジョブのスケジュール vs プロセスの作成
実行ファイルをテストする
プロセスに WSH を使用した場合
WMI が救う
Win32_ProcessStartup でプロセスを設定する
プロセス イベントを監視する
WMI イベント処理を VBScript でコーティングする
プロセス イベントを監視する別の方法
プロセスの実行時間を調べる

午前 2 時、プロセスの所在を把握していますか

私たちは多数のプロセスを世に送り出しますが、プロセスのその後の動作を実際に把握しているでしょうか。

Doctor Scripto はプロセスのことをたいへん気に掛けています。ヒープ上でプロセスと行動を共にしたり、一緒に実行したりさえします。Doctor Scripto は、自分のデバイスを後にしたプロセスたちが、ときどきトラブルを起こすことを承知しているのです。別にプロセスが不良だからというわけではなく、そもそもプロセスとはそういうものなのです。

気遣いのあるモニタの監視があれば、プロセスはネットワークの稼働を維持する大事な仕事をしながら、幸せで実りある生活を送ることができます。しかし、そのためには、開始後のプロセスの動作状況をときどき監視する必要があります。

Dr. Scripto は何年も前、ウイルス回復のための作業に取り組んでいるとき、難解なプロセス監視方法に興味を抱きました。そして、まもなく、あらゆるプロセスの動作を自動的に追跡する方法を発見することに取りつかれました。

ジョブのスケジュール vs プロセスの作成

最近になって、Dr. Scripto は前回のコラム記事"Windows XP Service Pack のインベントリ作成 : 第 3 部 - 展開用のスクリプト"を目にしました。そのコラムでは、ご記憶かもしれませんが、WMI クラス Win32_ScheduledJob (英語) を使ってリモートマシンごとに予定されたタスクを作成し、Windows XP Service Pack 2 のインストール実行ファイル (Xpsp2.exe) を実行することにしました。

当初、Dr. Scripto は Win32_Process (英語) の Create メソッドを使ってセットアップを実行するつもりでしたが、中央サーバーのファイルの共有から Xpsp2.exe を実行したいと思いました。しかし、そうする場合、プロセスの実行が目的にかないません。問題は、1 台のリモート マシン上にプロセスを作成した後、そのマシンからさらに別のマシン上にあるリモート実行ファイルを、共有の UNC パスを使って呼び出すという発想が WMI とは相容れないのです。これは致し方ありません。

Service Pack 2 で、Dr. Scripto は実行ファイルを各マシンにコピーし、それを Win32_Process を使って実行するという方法を採ることもできました。そうしなかったのは、Xpsp2.exe のサイズが非常に大きかったためです。

しかし、多くの場合、リモート ホストに容易にコピーできる小さな実行ファイルを実行したり、リモート ホストにあることが自明のアプリケーションを実行するのには、Win32_Process の方が簡単な方法といえます。プログラムを 1 つのプロセス内で実行する場合、Create メソッドで開始することができます。ただし、可視ウィンドウを指定しても、リモート マシン上ではプログラムは常に非表示ウィンドウで実行されます。したがって、この方法では、リモート マシンにプロセスを作成し、それをリモート マシン上のユーザーが操作することはできません。逆に利点として、呼びもしていない変なウィンドウがポップアップして、ユーザーが驚かされることはありません。

なぜ、リモート プロセスを作成する必要があるのでしょうか。まず、オペレーティング システムやアプリケーションのパッチをインストールするスクリプトを記述したい場合 (つまり、何らかの理由で Windows Update や Microsoft Systems Management Server を使用したくない場合) が考えられます。リモート プロセスを作成するスクリプトも、デスクトップ機でバックアップ プログラムを実行したり、ファイル サーバーのハード ディスク上でディスク クリーンアップまたは診断プログラムを実行するには良い方法でしょう。

: ちなみに、Windows Server 2003 でデフラグを実行する場合、WMI クラス Win32_Volume (英語) の Defrag メソッドを使用したスクリプトを使うのが簡単な方法です。詳しくは、「Windows 2003 でのディスクの最適化」を参照してください。

実行ファイルをテストする

このコラムのいくつかの例では、おなじみのメモ帳を使って、ユーザーが終了するまで実行を続けるアプリケーションをシミュレートします。なお、これらの例をリモート マシンに対して実行する場合は、ウィンドウが非表示になるため、タスク マネージャを開いて notepad.exe プロセスを確認する必要があります。

また、プロセスを一定時間実行させるスクリプトには (たとえば、セットアップ プログラム)、簡単な代用スクリプト Test.vbs を使います。このスクリプトは次の 1 行のコードだけからなります。

WScript.Sleep 30000

Windows スクリプト ホストのメソッド Sleep は、単純に指定されたミリ秒数だけ実行し、何もしないで終了します。この例では 30000 ミリ秒 (30 秒) 間実行します。スクリプト中でこのスクリプトを実行するには、次のコードを実行してください。

cscript test.vbs

スクリプト自体は実行可能ファイルではないため、スクリプト ホスト (cscript または wscript。スクリプトを実際に実行する実行可能ファイル) を指定しないと WMI は混乱して、コード 8 "不明なエラー" を返します。

Test.vbs の実行中、ターゲット マシンのタスク マネージャで [プロセス] タブを監視してください。cscript.exe プロセスが 30 秒間表示された後、消滅します。このプロセスは Tasklist.exe または Tlist.exe のコマンド ライン出力でも確認できます。

このテスト スクリプトは、一定時間の実行後、ユーザーによる操作なしで終了する任意のアプリケーションをシミュレートできる簡単な方法です。パッチやバックアップ プログラムなど、実行時間が一定でなく不定の実行可能プログラムに対しても、Test.vbs はスクリプトをテストする際の有効な代用となります。テスト スクリプトの実行時間を可変にしたい場合は、秒数を格納する変数に乱数を代入し (VBScript 関数 Randomize と Rnd を使用)、その変数をパラメータとして Wscript.Sleep に渡してください。詳細については、「Hey, Scripting Guy! How Can I Generate Random Numbers Using a Script?」(英語) をご覧ください。

プロセスに WSH を使用した場合

ある程度、スクリプトの経験がある方なら、スクリプト内から実行可能ファイルを実行する方法の基本についてはご承知でしょう。ここでは、これまでそうした機会のなかった方のために、その方法を復習してみましょう。

実行可能ファイルを実行する最も簡単な方法は、Windows スクリプト ホストのオブジェクト モデルの一部である WshShell オブジェクトの Run メソッドを使った方法です。Run メソッドでは、アプリケーションを実行するウィンドウを制御できるため、GUI アプリケーションに有効です。

Set objShell = CreateObject("Wscript.Shell")
objShell.Run "notepad.exe"

このスクリプトを実行すると、デスクトップ上に [メモ帳] ウィンドウが開きます。

このコラムで紹介するスクリプトは、すべて Cscript.exe で実行するように設計されています。したがって、コンピュータの既定のスクリプト ホストが Cscript でない場合は、コマンド ラインのスクリプト名の前に "cscript" を挿入する必要があります (例: cscript script1.vbs)。

コマンド ライン実行可能ファイルの実行には、WshShell の Exec メソッドも使用できます。Exec メソッドは、ステータスとエラー情報が格納された WshScriptExec オブジェクトを返します。このメソッドでは、実行可能ファイルのコマンド ライン出力が入った標準ストリーム (STDOUT など) にもアクセスできます。

Set objShell = CreateObject("Wscript.Shell")
Set objWshScriptExec = objShell.Exec("ipconfig.exe")
WScript.Echo objWshScriptExec.StdOut.ReadAll

このスクリプトは Ipconfig のコマンド ライン出力を捕らえ、それをコマンド ラインにも表示します。表示内容は次のようになります。

C:\scripts>wshexec.vbs

Windows IP Configuration

Ethernet adapter Local Area Connection:

        Connection-specific DNS Suffix  . : fabrikam.com
        IP Address. . . . . . . . . . . . : 192.168.0.192
        Subnet Mask . . . . . . . . . . . : 255.255.255.0
        Default Gateway . . . . . . . . . : 192.168.0.1

スクリプトとは、たいしたものですね。単に Ipconfig を実行するだけでできることに、わざわざ 3 行も必要なのですから。皮肉はさておき、この機能は、あるツールのコマンド ライン出力に対してスクリプト内で何か操作をしたいとき、たとえば、出力を解析して特定の文字列を検索する場合などにとても便利です。

ただし、Run メソッドも Exec メソッドも、実行可能ファイルをローカル コンピュータ上でしか実行できないという制約があります。WSH にはスクリプトをリモートで実行できる WshController オブジェクトも用意されていますが、それにはローカル、リモート両方のコンピュータでいくつかのセットアップが必要になります。

このため、実行可能ファイルをリモート コンピュータで実行して結果を監視するという私たちの目的には、残念ながら、WSH の機能は使いやすいとはいえません。

WMI が救う

システム管理用スクリプトは、ほとんどの場合、Windows Management Instrumentation (WMI) に行き着きます。WMI クラス Win32_Process には Create メソッド (英語) があり、このメソッドを使うと、GUI またはコマンド ラインの実行可能ファイルをローカルまたはリモートのいずれかで実行し、実行中のプロセスの ID を取得することができます。

strCommand = "notepad.exe" 
Set objProcess = GetObject("winmgmts:root\cimv2:Win32_Process")
intReturn = objProcess.Create _
 (strCommand, Null, Null, intProcessID)

2 行目の GetObject の呼び出しで、Win32_Process クラスのインスタンスではなく、クラス定義を取得している点に注目してください (多くのスクリプトでは ExecQuery でインスタンスの方を取得します)。これは、Create メソッドは Win32_Process クラス上で呼び出されるのであって、そのインスタンスで呼び出されるのではないためです。Create メソッドは、呼び出されると Win32_Process のインスタンスを作成します。

Create メソッドの呼び出し時には、4 つのパラメータを次の順序で渡します。

  • CommandLine (strCommand): 実行可能ファイルの名前とそれに渡す任意のコマンド ライン引数からなる文字列。

  • CurrentDirectory: 生成するプロセスのカレント パス。ここではカレント ディレクトリを使用するため、Null を指定しました。

  • ProcessStartupInformation: プロセスのスタートアップ設定を格納した Win32_ProcessStartup 型のオブジェクト。ここでは Null を指定しましたが、後述のスクリプトではこのパラメータを使用します。

  • ProcessID (intProcessID): 生成されたプロセスの ID 番号。最初の 3 つのパラメータは入力パラメータですが、ProcessID は出力パラメータです。つまりメソッドが呼び出し元に返すパラメータです。この第 4 パラメータには変数名を指定します。すると、Create メソッドにより、生成されたプロセスの PID を示す整数がその変数に代入されます。その値をスクリプト内で利用することができます。

プロセスをリモート コンピュータで実行するには、次のスクリプトのように strComputer 変数にコンピュータ名を代入してください。この変数が GetObject に渡すモニカ文字列でマシン名に置換されます (3 行目)。

strComputer = "server1"
strCommand = "notepad.exe"
Set objProcess = GetObject("winmgmts:\\" & strComputer & _
 "\root\cimv2:Win32_Process")
intReturn = objProcess.Create _
 (strCommand, Null, Null, intProcessID)

非常に簡単ですね。WMI を使えば、ほとんど苦もなくリモート処理を実現できます。すべてのコンピュータの間を歩き回る楽しみと健康効果には反するものの、1 台の管理用ワークステーションの前に座ったまま、どんな場所にあるリモート コンピュータも操作できることが、スクリプトのもたらす最大の利点の 1 つです。これは既にご存じでしたね。単なる再確認のためです。

Win32_ProcessStartup でプロセスを設定する

上記の 2 つのスクリプトでは使いませんでしたが、既に説明したように Create メソッドの第 3 パラメータ ProcessStartupInformation には、プロセスの実行方法に関する詳細をメソッドに伝えるオブジェクトを格納することができます。

プロセスのスタートアップ設定に相当するオブジェクトを作成するには、Win32_ProcessStartup (英語) を使います。このクラスには、プロセスが実行するウィンドウ、環境変数、および優先度を扱う 14 個のプロパティがあります。ここでは、これらのプロパティの 1 つである ShowWindow に定数を渡して、ウィンドウをそのアプリケーションの通常のウィンドウとして表示するよう指定します。

Win32_ProcessStartup は、メソッドに情報を渡すためだけに使われるメソッド型定義であるため、WMI クラスとしては例外的なものです。Get メソッドで Win32_ProcessStartup のクラス定義を取得したら、SpawnInstance_ メソッドを呼び出してクラスのインスタンスを生成します。生成後、インスタンスの read/write プロパティをそれぞれ目的の値に設定します。これで、オブジェクトを Create メソッドの第 3 パラメータとして渡す準備が整いました。

通常のウィンドウと指定したいだけなのに、ずいぶん手間がかかりますね。しかし、定型コードを一度書いておけば、ウィンドウ設定を変更する場合は、定数を変更するだけで簡単に済みます。また、他のプロパティの設定も、それぞれのコード行をそのテンプレートに追加するだけで行えます。

前の 2 つのスクリプトでは、Create メソッドを呼び出して、その戻り値を変数 intReturn に代入しましたが、その変数に対して何の操作もしませんでした。しかし、次のスクリプトでは、戻り値を使ってスクリプトがコマンド ラインに出力する情報を判断します。戻り値が 0 ならば、プロセスが正常に生成されたという意味なので、プロセス ID を表示します。戻り値が 0 以外の値の場合は、メソッドが失敗したことを示します。したがって、戻り値 (WMI SDK で調べることができます) をインジケータとして表示します。

Const NORMAL_WINDOW = 1
strComputer = "."
strCommand = "notepad.exe"
Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set objStartup = objWMIService.Get("Win32_ProcessStartup")
Set objConfig = objStartup.SpawnInstance_
objConfig.ShowWindow = NORMAL_WINDOW
Set objProcess = objWMIService.Get("Win32_Process")
intReturn = objProcess.Create _
 (strCommand, Null, objConfig, intProcessID)
If intReturn = 0 Then
  Wscript.Echo "Process Created." & _
   vbCrLf & "Command line: " & strCommand & _
   vbCrLf & "Process ID: " & intProcessID
Else
  Wscript.Echo "Process could not be created." & _
   vbCrLf & "Command line: " & strCommand & _
   vbCrLf & "Return value: " & intReturn
End If

一般的なスクリプト出力は次のとおりです。

C:\scripts>process-create.vbs
Process Created.
Command line: notepad.exe
Process ID: 3464

プロセス イベントを監視する

さて、これでプロセスの生成が終わりました。これ自体なかなかエキサイティングですね (少なくも Dr. Scripto にとっては)。けれども、そのプロセスを世に送り出した後、その状態をどうやって監視すればよいのでしょうか。

単純に、そのプロセス ID を持つ Win32_Process のインスタンスを 10 秒ごとにチェックすれば、何か変化があったかどうか確認することができます。ある時点で、そのプロセス ID が見つからなくなり、それによってプロセスが終了したことがわかります。しかし、これではやや手間がかかるうえ、必要な情報が十分に得られないかもしれません。

幸いにも、WMI がより優れた手段を提供してくれます。WMI クラスとそのインスタンスに何か起こると、WMI はその内容をアプリケーションやスクリプトで利用可能な形式で示すイベントを生成します。

イベントは、どのコンピュータでも絶えず発生しています。ハード ドライブ、ディレクトリ、サービス、プロセスのあらゆる変化がイベントを生成します。WMI 単独でも、毎秒何億兆ものイベントを発生します。しかし、森の中で 1 本の木が倒れるときのように、その音を聞くにはその場にいて耳を澄まさなければなりません (近づきすぎては自分の上に木が倒れてきますが)。だからイベントを使うのです。コードで作った疑似的感覚を持つエンティティか何かでイベントに耳を澄ませていないと、イベントはむなしく虚空に消えてしまいます。幸い、イベントはそれほど重くないため、倒れてきてぶつかる心配はありません。

この議論は危険なほど哲学の方に逸れつつありますが、Dr. Scripto が認識論について熱弁を振るい始めるのはなんとか避けたいものです。WMI イベントのよりわかりやすい説明は、最新の TechNet オンデマンド Web キャスト「An Ounce of Prevention: An Introduction to WMI Events」(英語) をご覧ください。

実存主義者 Dr. Scripto

とりあえず哲学はやめにして、肝心な話をしましょう。 このアマゾン川のようなイベントの流れを調べるためには、どんなスクリプト手法が使えるのでしょうか。

WMI イベント処理を VBScript でコーティングする

これまでのスクリプトのように "winmgmts:" モニカを使って WMI に接続した場合、WMI スクリプト API で定義された SWbemServices オブジェクト (英語) が返されます。スクリプト センターにあるほとんどの WMI スクリプトでは、あるクラスのインスタンスのコレクションを取得する必要がある場合には SWbemServices の ExecQuery メソッドを使います。

しかし、WMI イベントの取得には、別のメソッド ExecNotificationQuery (英語) を使用する必要があります。イベントを検索しトラップするためのパターンが標準スクリプトとは異なるため、その方法を見てみましょう。

Set colMonitorProcess = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM __InstanceOperationEvent " & _ 
 "WITHIN 1 WHERE TargetInstance ISA 'Win32_Process'")

ExecNotificationQuery メソッドはパラメータとして WQL (Windows Query Language) クエリ文字列をとります。この文字列は ExecQuery によく渡すものと似ています。ただし、このクエリ文字列には 2 つの新しい工夫があります。インスタンスの選択元となるクラスは __InstanceOperationEvent クラス (英語) で、これは WMI クラスのインスタンスの作成、変更、削除をすべて捕獲する組み込み WMI イベント クラスです。

ポーリング間隔 (スクリプトでイベントの有無を確認する頻度) の指定には、WITHIN 文と秒数を表す整数を使います。さらに、ISA 文を用いた WHERE 句でクエリにフィルタを掛け、検索するイベントの種類を ExecNotificationQuery に知らせます。ここでは、__InstanceOperationEvent の TargetInstance プロパティが Win32_Process のインスタンスであるイベントに絞り込んでいます。

このクエリで選択するクラス __InstanceOperationEvent は WMI システム クラスの一種で、他のクラスのインスタンスからイベントを受け取るすべての組み込みイベント クラスの基本クラスとなります。このクラスから派生するイベント クラスは次のとおりです。

  • __InstanceCreationEvent
    WMI クラスの新しいインスタンスが作成されたかどうかを通知します。

  • __InstanceModificationEvent
    WMI クラスの既存のインスタンスが変更されたかどうかを通知します。

  • __InstanceDeletionEvent
    WMI クラスの既存のインスタンスが削除されたかどうかを通知します。

__InstanceOperationEvent を検索した場合、返されるコレクションには 3 つの派生クラスすべてのインスタンスが含まれます。

: これらのクラスの詳細については、WMI SDK の「Determining the Type of Event to Receive」(英語) を参照してください。

言い換えれば、このクエリはこのコンピュータ上で実行されるすべてのプロセスの活動を記録し続けるよう WMI に依頼するものです。ただし、WMI イベント監視機能では、Win32_Process だけでなく、任意の WMI クラスによって起動されたイベントを捕らえることが可能です。ここでプロセスに的を絞っているのは、プロセスはなじみがあり、後からこのコラムで使用していくからです。たとえば、Win32_LogicalDisk や Win32_Service などで生成されたイベントも同じように簡単に監視することができます。

ExecNotificationQuery を実行すると、SWbemEventSource オブジェクト (英語) が返されます。このオブジェクトもまた WMI スクリプト API オブジェクト モデルの一部です。次のスクリプトでは、返されたオブジェクト参照に "colMonitorProcess" という名前を付けます。クエリで監視するプロセスの連続したコレクションだからです。

SWbemEventSource オブジェクトには、ExecNotificationQuery で定義されたクエリに一致する次の発生オブジェクトについての情報を取得できる NextEvent メソッドが用意されています。次の行によって、NextEvent を呼び出した結果をオブジェクト参照 objLatestEvent に代入します。

Set objLatestEvent = colMonitorProcess.NextEvent

NextEvent はクエリに合致するイベントが発生するまで、ひたすら待機しています。イベントが発生すると、直ちに動作を開始します。

NextEvent の監視の下でイベントが発生し終了すると、発生したイベント クラスのインスタンスが objLatestEvent に格納されます。そのイベント クラスの名前を objLatestEvent.Path_.Class で取得します。objLatestEvent を通じて、クエリで指定した __InstanceOperationEvent の TargetInstance プロパティにアクセスすることができます。このとき、TargetInstance は作成、変更、または削除された Win32_Process の最初のインスタンスを指しています。TargetInstance を使って、Win32_Process インスタンスのプロパティ、この場合は Name と ProcessID を取得します。最後に、VBScript の組み込み関数 Now を呼び出して、イベントが発生した日時を表示させます。

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colMonitorProcess = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM __InstanceOperationEvent " _ 
 & " WITHIN 1 WHERE TargetInstance ISA 'Win32_Process'")
WScript.Echo "Waiting for process change event ..."
Set objLatestEvent = colMonitorProcess.NextEvent
WScript.Echo VbCrLf & objLatestEvent.Path_.Class
Wscript.Echo "Process Name: " & objLatestEvent.TargetInstance.Name
Wscript.Echo "Process ID: " & objLatestEvent.TargetInstance.ProcessId
WScript.Echo "Time: " & Now

一般的なスクリプト出力は次のとおりです。

C:\scripts>event-single.vbs
Waiting for process change event ...

__InstanceModificationEvent
Process Name: System Idle Process
Process ID: 0
Time: 5/17/2005 4:21:05 PM

単純に NextEvent を呼び出すスクリプトでは、次に発生して終了するイベントは捕らえることができます。しかし、ほとんどの場合、監視したいのは一連のイベントであって、直後のイベント 1 つだけではないでしょう。継続的にイベントを監視するには、NextEvent を呼び出すコードを無限の Do ループに入れるだけです。次のスクリプトは、ルールによってすべてのプロセス作成、変更、削除の各イベントを連続的にトラップする方法を示しています。

ちなみに、このようないつまでも実行し続けるスクリプトを終了させるには、Ctrl キーを押しながら C キーを押してください。

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colMonitorProcess = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM __InstanceOperationEvent " _ 
 & " Within 1 WHERE TargetInstance ISA 'Win32_Process'")
WScript.Echo "Waiting for process to start or stop ..."
Do
  Set objLatestEvent = colMonitorProcess.NextEvent
  WScript.Echo VbCrLf & objLatestEvent.Path_.Class
  Wscript.Echo "Process Name: " & objLatestEvent.TargetInstance.Name
  Wscript.Echo "Process ID: " & objLatestEvent.TargetInstance.ProcessId
  WScript.Echo "Time: " & Now
Loop

一般的なスクリプト出力は次のとおりです。

C:\scripts>event-loop.vbs
Waiting for process change event ...

__InstanceModificationEvent
Process Name: System Idle Process
Process ID: 0
Time: 5/17/2005 5:13:48 PM

__InstanceModificationEvent
Process Name: scardsvr.exe
Process ID: 1100
Time: 5/17/2005 5:13:48 PM

__InstanceModificationEvent
Process Name: wmiprvse.exe
Process ID: 1416
Time: 5/17/2005 5:13:48 PM

__InstanceModificationEvent
Process Name: wmiprvse.exe
Process ID: 1788
Time: 5/17/2005 5:13:48 PM

__InstanceModificationEvent
Process Name: wdfmgr.exe
Process ID: 1868
Time: 5/17/2005 5:13:48 PM

__InstanceModificationEvent
Process Name: WINWORD.EXE
Process ID: 1944
Time: 5/17/2005 5:13:48 PM
^C

これは困りました。ご覧のように、上のスクリプトを実行すると、__InstanceModificationEvent があまりに頻繁に発生するため、出力のノイズ比率が高すぎます。プロセスの中には、生成後、絶え間なく変化を続けるものもあるのです。

より使用に適した出力を得るため、次のスクリプトではプロセスの作成イベントと削除イベントだけに絞り込み、変更イベントは無視しています。

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colMonitorProcess = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM __InstanceOperationEvent " _ 
 & " Within 1 WHERE TargetInstance ISA 'Win32_Process'")
WScript.Echo "Waiting for process to start or stop ..."
Do
  Set objLatestEvent = colMonitorProcess.NextEvent
  If objLatestEvent.Path_.Class = "__InstanceCreationEvent" _
   Or objLatestEvent.Path_.Class = "__InstanceDeletionEvent" Then
    WScript.Echo VbCrLf & objLatestEvent.Path_.Class
    Wscript.Echo "Process Name: " & objLatestEvent.TargetInstance.Name
    Wscript.Echo "Process ID: " & _
     objLatestEvent.TargetInstance.ProcessId
    WScript.Echo "Time: " & Now
  End If
Loop

一般的なスクリプト出力は次のとおりです。

C:\scripts\processes>col4-event2.vbs
Waiting for process to start or stop ...

__InstanceCreationEvent
Process Name: notepad.exe
Process ID: 3656
Time: 5/17/2005 5:19:26 PM

__InstanceCreationEvent
Process Name: calc.exe
Process ID: 2644
Time: 5/17/2005 5:19:33 PM

__InstanceDeletionEvent
Process Name: calc.exe
Process ID: 2644
Time: 5/17/2005 5:19:36 PM

__InstanceDeletionEvent
Process Name: notepad.exe
Process ID: 3656
Time: 5/17/2005 5:19:40 PM

__InstanceCreationEvent
Process Name: SMSCliUI.exe
Process ID: 2316
Time: 5/17/2005 5:19:43 PM
^C

メモ帳などの簡単なアプリケーションを開始したり終了したりしてみてください。プロセスが開始または終了するたびに、それについての情報が表示されます。これで、イベントに関する有効な情報が [コマンド プロント] ウィンドウに次々と表示されるようになりました。

TargetInstance には Win32_Process のインスタンスが含まれているため、それを使えば、そのクラスの任意のプロパティを調べることができます。この例では、クラスの名称とプロセス ID だけを取得しましたが、Win32_Process が提供できる他の情報や、場合によっては Win32_Process 自身では提供できない情報を取得したいこともあるでしょう。

プロセス イベントを監視する別の方法

特に Win32_Process イベントを監視する場合には、__InstanceOperationEvent クラスよりもさらに簡単な方法があります。賢明にも、WMI にはプロセス イベントの監視を容易にする特殊なイベント クラスが 3 つ用意されています。Win32_ProcessTrace とその派生クラス、Win32_ProcessStartTrace 、および Win32_ProcessStopTrace です。

次のスクリプトでは、プロセス削除イベントを取得する少し簡単な方法を示します。

On Error Resume Next
strComputer = "."
Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colProcessStopTrace = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM Win32_ProcessStopTrace")
WScript.Echo "Waiting for process to stop ..."
Do
  Set objLatestEvent = colProcessStopTrace.NextEvent
  WScript.Echo
  Wscript.Echo "Process Name: " & objLatestEvent.ProcessName
  Wscript.Echo "Process ID: " & objLatestEvent.ProcessId
  Wscript.Echo "Time: " & objLatestEvent.TIME_CREATED
'Property exists only on Windows Server 2003.
  Wscript.Echo "Exit Code: " & objLatestEvent.ExitStatus
Loop

一般的なスクリプト出力は次のとおりです。

C:\scripts>event-trace.vbs
Waiting for process to stop ...

Process Name: notepad.exe
Process ID: 3904
Time: 127608497739906527
Exit Code: 0

Process Name: calc.exe
Process ID: 308
Time: 127608497793389831
Exit Code: 0
^C

Win32_ProcessStopTrace クラスは独自のプロパティを持っていますが、残念ながら Win32_Process のすべてのプロパティを含んでいるわけではありません。その代わり、Win32_Process にはない便利なプロパティが 1 つ 用意されています。ExitStatus です。このプロパティから、プロセスで実行可能ファイルの実行が成功したか失敗したか示す終了コードを取得できます。このプロパティは Windows Server 2003 でしか利用できないため、プロパティを取得する前に OS のバージョンを確認するコードをスクリプトに追加した方がよいかもしれません。この例では、単純に On Error Resume Next を使用しました。

Win32_ProcessStopTrace のプロパティはさまざまな目的にかなっているようですが、プロセスの実行時間を調べる場合はどうでしょうか。Win32_ProcessStopTrace のプロパティの 1 つに TIME_CREATED があります。想像力の乏しい方は、プロセスが生成された時刻だと思うかもしれません。しかし、発生したイベントを監視しているのはプロセスの終了時です。したがって、この場合の TIME_CREATED は終了イベントが発生した時刻、つまりプロセスが終了した時刻を指します。やや論理がねじれているため、おそらく TIME_OCCURRED のような名前の方がより直感的でわかりやすかったでしょう。

他にも愚痴を言いたいことがあります。最悪なのは、TIME_CREATED が返すイベント時刻の形式です。WMI SDK によると、TIME_CREATED には "1601年 1 月 1 日からの経過時間を 100 ナノ秒間隔の数値で示す" uint64 (64 ビット) 型データが格納されます。

申し訳ありませんが、Dr. Scripto は 17 世紀に端を発するようなデータ型とかかわり合うことを信条として拒んでしまうのです (Scripting Guys の他のメンバーたちは、Antiques Roadshow で高値が付きそうだと考えています)。これをわかりやすい日付に変換する関数を書くこともできますが、プロセスの実行時間について詳しく調べるには、もっと良い方法があります。

プロセスの実行時間を調べる

私たちが知りたいのはプロセスが実行された時間の長さです。プロセスが終了した日時は既に Now を使って取得しました。では、開始時刻はどうでしょう。

前述の例では、プロセスの __InstanceCreationEvent と __InstanceDeletionEvent をトラップしました。このそれぞれについて Now を記録することができます。しかし、2 つの異なるイベントから、多数のプロセスの作成時刻と削除時刻を対応させるのは骨の折れる作業です。

より簡単にプロセス開始時刻を取得するためには、Win32_ProcessStopTrace を使うのではなく、組み込みイベント クラスと Win32_Process に戻る必要があります。

大きなコード

__InstanceDeletionEvent と Win32_Process のインスタンスを問い合わせるとき、プロセスが開始された時刻を TargetInstance.CreationDate から調べることができます。CreationDate はもちろん Win32_Process のプロパティです。このクラスには TerminationDate プロパティもありますが、これにアクセスためには、プロセスに対するハンドルをプロセス終了後も開いたままにしておく必要があります。これは VBScript では不可能です。ナノ秒単位の精度が不要な私たちの目的には、Now でも終了日時として十分です。

: このスクリプトを strComputer を変更してリモート コンピュータに対して実行する場合、プロセス作成時刻はリモート コンピュータの WMI から取得するのに対し、削除時刻はスクリプトが実行するローカル コンピュータの Now から取得する点に注意してください。つまり、秒単位まで正確な時間を取得するためには、2 台のコンピュータの時計を正確に同期させる必要があります。幸い、多くのネットワークでは、時計は自動的に同期されます。また通常、アプリケーションは数分または数時間にわたって実行されるため、秒単位の精度はそれほど重要でありません。いずれにせよ、コンピュータの時計が同期されていなくても、リモート コンピュータの時刻を取得したい場合には、Win32_LocalTime (英語) などの WMI クラスを使ったコードを追加できます。

次のスクリプトは、プロセス削除イベントをトラップし、プロセスの開始時刻と終了時刻を取得します。

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colMonitorProcess = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM __InstanceDeletionEvent " _ 
 & " Within 1 WHERE TargetInstance ISA 'Win32_Process'")
WScript.Echo "Waiting for a process to stop ..."
Do
  Set objLatestEvent = colMonitorProcess.NextEvent
  WScript.Echo VbCrLf & objLatestEvent.Path_.Class
  Wscript.Echo "Process Name: " & objLatestEvent.TargetInstance.Name
  Wscript.Echo "Process ID: " & objLatestEvent.TargetInstance.ProcessId
  Wscript.Echo "Time Created: " & _
   objLatestEvent.TargetInstance.CreationDate
  WScript.Echo "Time Deleted: " & Now
Loop

一般的な出力は次のとおりです。

C:\scripts>event-created.vbs
Waiting for a process to stop ...

__InstanceDeletionEvent
Process Name: calc.exe
Process ID: 3652
Time Created: 20050517173536.064608-420
Time Deleted: 5/17/2005 5:35:39 PM

__InstanceDeletionEvent
Process Name: notepad.exe
Process ID: 1736
Time Created: 20050517173530.135373-420
Time Deleted: 5/17/2005 5:35:42 PM
^C

ちょっと待ってください。 "Time Created:" の後ろの見苦しい出力はいったい何でしょう。しばらくじっと見つめていると、基本的に逆方向なのだと気づけば、次第に意味がわかってくるかもしれません (ただ目まいがするだけかもしれませんが)。少なくとも 17 世紀とは何の関係もありません。

CreationDate プロパティが返す、大部分が数字からなる長い文字列は、WMI で DATETIME と呼ばれる形式です。Windows Server 2003 から、WMI スクリプト API に SWbemDateTime (英語) という新しいオブジェクトが追加されました。このオブジェクトは、DATETIME、FILETIME (古いナノ秒形式)、VT_DATE (WMI でこう呼ばれる、人間にとって判読しやすい形式) という 3 種類の日時形式を操作、変換する機能を備えています。

日付変換

Windows Server 2003 が稼働するコンピュータだけで構成されたネットワークはあまり存在しないため、DATETIME を米国式の日付文字列に変換する独自の関数を記述します (他の形式に慣れている方には申し訳ありませんが、この関数を書き換えて好みの形式を返すのは、さほど難しくないはずです)。

WMIDateToString 関数はパラメータとして WMI DATETIME 文字列をとります。この文字列は左から年で始まり、グリニッジ標準時とのプラスまたはマイナスの時差で終わります。WMIDateToString 関数では、VBScript の文字列操作関数 Mid と Left を使って数字の文字列を分割して組み立て直し、"Time Deleted:" の後ろに表示されるのと同じ判読可能な日付形式にします。

Function WMIDateToString(dtmDate)

WMIDateToString = CDate(Mid(dtmDate, 5, 2) & "/" & _
                  Mid(dtmDate, 7, 2) & "/" & _
                  Left(dtmDate, 4) & " " & _
                  Mid(dtmDate, 9, 2) & ":" & _
                  Mid(dtmDate, 11, 2) & ":" & _
                  Mid(dtmDate, 13, 2))

End Function

では、もう一度プロセス削除イベントをトラップしてみましょう。ただし、今度は DateDiff という VBScript 関数を使って、プロセス実行時間 (プロセスの作成時刻と削除時刻の秒数差) も計算します。DateDiff は、どちらの日付も VT_DATE 形式でなければなりません。このため、先ほどの WMIDateToString 関数を使ってプロセスの CreationDate 値を変換します。

strComputer = "."
Set objWMIService = GetObject("winmgmts:\\" & strComputer & "\root\cimv2")
Set colMonitorProcess = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM __InstanceDeletionEvent " _ 
 & " Within 1 WHERE TargetInstance ISA 'Win32_Process'")
WScript.Echo "Waiting for process to stop ..."
Do
  Set objLatestEvent = colMonitorProcess.NextEvent
  strProcDeleted = Now
  strProcCreated = _
   WMIDateToString(objLatestEvent.TargetInstance.CreationDate)
  Wscript.Echo VbCrLf & "Process Name: " & _
   objLatestEvent.TargetInstance.Name
  Wscript.Echo "Process ID: " & objLatestEvent.TargetInstance.ProcessId
  Wscript.Echo "Time Created: " & strProcCreated
  WScript.Echo "Time Deleted: " & strProcDeleted
  intSecs = DateDiff("s", strProcCreated, strProcDeleted)
  WScript.Echo "Duration: " & intSecs & " seconds"
Loop

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

'Convert WMI DATETIME format to US-style date string.
Function WMIDateToString(dtmDate)

WMIDateToString = CDate(Mid(dtmDate, 5, 2) & "/" & _
                  Mid(dtmDate, 7, 2) & "/" & _
                  Left(dtmDate, 4) & " " & _
                  Mid(dtmDate, 9, 2) & ":" & _
                  Mid(dtmDate, 11, 2) & ":" & _
                  Mid(dtmDate, 13, 2))

End Function

一般的な出力は次のとおりです。

C:\scripts>event-duration.vbs
Waiting for process to stop ...

Process Name: calc.exe
Process ID: 2068
Time Created: 5/17/2005 5:39:26 PM
Time Deleted: 5/17/2005 5:39:33 PM
Duration: 7 seconds

Process Name: notepad.exe
Process ID: 1800
Time Created: 5/17/2005 5:39:21 PM
Time Deleted: 5/17/2005 5:39:39 PM
Duration: 18 seconds
^C

プロセス イベントの監視方法がわかったところで、指定した時間だけ実行するイベントを作成し、その存続期間を監視してみましょう。ロジックを明瞭にするために、最も単純なケースから始めます。したがって、ローカル コンピュータまたはリモート コンピュータでプロセスを作成後、プロセス イベントを監視し、開始時刻と終了時刻だけを未処理のまま出力します。先に作成した Test.vbs を実行するので、プロセスは指定された時間実行されます。

はじめにプロセスを作成します。Create の戻り値が 0 以外の場合、プロセスが作成されなかったことを示します。したがって、戻り値を表示して終了します。

プロセスが正常に作成されると、出力パラメータ intProcessID にプロセス ID が返されます。これで、監視するプロセス固有の識別子が判明します。次にコードのイベント処理部分で、作成したプロセスの ID を持つ Win32_Process の __InstanceDeletionEvent イベントを問い合わせます。それが見つかったら、プロセスの情報を出力します。

Const NORMAL_WINDOW = 1
strComputer = "."
strCommand = "cscript c:\scripts\test.vbs"

'Connect to WMI.
Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

'Create process startup parameters.
Set objStartup = objWMIService.Get("Win32_ProcessStartup")
Set objConfig = objStartup.SpawnInstance_
objConfig.ShowWindow = NORMAL_WINDOW

'Create a new process.
Set objProcess = objWMIService.Get("Win32_Process")
intReturn = objProcess.Create _
 (strCommand, Null, objConfig, intProcessID)

If intReturn = 0 Then
  Wscript.Echo "Process Created." & _
   vbCrLf & "Command line: " & strCommand & _
   vbCrLf & "Process ID: " & intProcessID

'Monitor process deletion events
  Set colMonitorProcess = objWMIService.ExecNotificationQuery _
   ("SELECT * FROM __InstanceDeletionEvent " & _ 
   "WITHIN 1 WHERE TargetInstance ISA 'Win32_Process' " & _
   "AND TargetInstance.ProcessId = '" & intProcessID & "'")
  WScript.Echo "Waiting for process to stop ..."
  Set objLatestEvent = colMonitorProcess.NextEvent
  strTimeDeleted = Now
  Wscript.Echo VbCrLf & "Process Name: " & _
   objLatestEvent.TargetInstance.Name
  Wscript.Echo "Process ID: " & objLatestEvent.TargetInstance.ProcessId
  Wscript.Echo "Time Created: " & _
   objLatestEvent.TargetInstance.CreationDate
  WScript.Echo "Time Deleted: " & strTimeDeleted

Else
  Wscript.Echo "Process could not be created." & _
   vbCrLf & "Command line: " & strCommand & _
   vbCrLf & "Return value: " & intReturn
End If

一般的な出力は次のとおりです。

C:\scripts>monitor-event-simple.vbs
Process Created.
Command line: cscript c:\scripts\test.vbs
Process ID: 2052
Waiting for process to stop ...

Process Name: cscript.exe
Process ID: 2052
Time Created: 20050517164539.367041-420
Time Deleted: 5/17/2005 4:46:10 PM

このコラムの最後に、より複雑なスクリプトを紹介します。このスクリプトでは、上記のプロセス作成とイベント監視の仕組みを別々のプロシージャに分割し、小さなエラー処理コードを追加しました。今度は、プロセス実行時間を秒単位で計算した後、独立した関数 SecsToHours を使って合計秒数を時間、分、秒に分割します。バックアップ プログラムのような長いプロセスの場合、これによって大きな秒数よりも理解しやすい出力を得ることが可能です。

CreationDate の WMI DATETIME 値の変換には、前のスクリプトで使用した WMIDateToString 関数を再び利用しています。

SecsToHours、WMIDateToString のほか、最終スクリプトのコードを次の 2 つのプロシージャに分割しました。

  • CreateProcess 関数: パラメータとしてコマンド文字列をとり、成功の場合はプロセス ID を、失敗の場合は -1 を返します。

  • MonitorEvent サブルーチン: パラメータとしてプロセス ID をとります。

スクリプト本体のロジックで CreateProcess の戻り値をチェックして、値がゼロでない場合のみ MonitorEvent を呼び出します。これより、MonitorEvent に渡される値は常に、作成されたプロセスのプロセス ID になります。

プロセスの実行時間以外にも、MonitorEvent では TargetInstance を使用して、Win32_Process のプロパティがあるものなら、プロセスについて何でも調べることができます。このクラスには 45 のプロパティがあります。

On Error Resume Next

strComputer = "." 'Can change to name of remote machine.
strCommand = "cscript c:\scripts\test.vbs"

'Connect to WMI.
Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")

If Err.Number <> 0 Then
  HandleError
Else
  intPID = CreateProcess(strCommand)
  If intPID <> -1 Then
    MonitorEvent(intPID)
  End If
End If

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

Function CreateProcess(strCL)

Const NORMAL_WINDOW = 1

'Create process startup parameters.
Set objStartup = objWMIService.Get("Win32_ProcessStartup")
Set objConfig = objStartup.SpawnInstance_
objConfig.ShowWindow = NORMAL_WINDOW

'Create a new process.
Set objProcess = objWMIService.Get("Win32_Process")
intReturn = objProcess.Create _
 (strCL, Null, objConfig, intProcessID)

If intReturn = 0 Then
  Wscript.Echo "Process Created." & _
   vbCrLf & "Command line: " & strCL & _
   vbCrLf & "Process ID: " & intProcessID
  CreateProcess = intProcessID
Else
  Wscript.Echo "Process could not be created." & _
   vbCrLf & "Command line: " & strCL & _
   vbCrLf & "Return value: " & intReturn
  CreateProcess = -1
End If

End Function

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

Sub MonitorEvent(intProcessID)

'Monitor process deletion events
Set colMonitorProcess = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM __InstanceDeletionEvent " & _ 
 "WITHIN 1 WHERE TargetInstance ISA 'Win32_Process' " & _
 "AND TargetInstance.ProcessId = '" & intProcessID & "'")
WScript.Echo "Waiting for process to stop ..."
Set objLatestEvent = colMonitorProcess.NextEvent
strProcDeleted = Now
strProcCreated = _
 WMIDateToString(objLatestEvent.TargetInstance.CreationDate)
Wscript.Echo VbCrLf & "Process Name: " & _
 objLatestEvent.TargetInstance.Name
Wscript.Echo "Process ID: " & objLatestEvent.TargetInstance.ProcessId
Wscript.Echo "Time Created: " & strProcCreated
WScript.Echo "Time Deleted: " & strProcDeleted
intSecs = DateDiff("s", strProcCreated, strProcDeleted)
arrHMS = SecsToHours(intSecs)
WScript.Echo "Duration: " & arrHMS(2) & " hours, " & _
 arrHMS(1) & " minutes, " & arrHMS(0) & " seconds"

End Sub

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

Function WMIDateToString(dtmDate)
'Convert WMI DATETIME format to US-style date string.

WMIDateToString = CDate(Mid(dtmDate, 5, 2) & "/" & _
                  Mid(dtmDate, 7, 2) & "/" & _
                  Left(dtmDate, 4) & " " & _
                  Mid(dtmDate, 9, 2) & ":" & _
                  Mid(dtmDate, 11, 2) & ":" & _
                  Mid(dtmDate, 13, 2))

End Function

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

Function SecsToHours(intTotalSecs)
'Convert time in seconds to hours, minutes, seconds and return in array.

intHours = intTotalSecs \ 3600
intMinutes = (intTotalSecs Mod 3600) \ 60
intSeconds = intTotalSecs Mod 60
SecsToHours = Array(intSeconds, intMinutes, intHours)

End Function

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

'Handle errors.
Sub HandleError

WScript.Echo "ERROR " & Err.Number & VbCrLf & _
 "Description: " & Err.Description & VbCrLf & _
 "Source: " & Err.Source
Err.Clear

End Sub

一般的な出力は次のとおりです。

C:\scripts>monitor-event.vbs
Process Created.
Command line: cscript c:\scripts\test.vbs
Process ID: 2168
Waiting for process to stop ...

Process Name: cscript.exe
Process ID: 2168
Time Created: 5/17/2005 4:25:38 PM
Time Deleted: 5/17/2005 4:26:10 PM
Duration: 0 hours, 0 minutes, 32 seconds

32 秒? 変ですね。Test.vbs の実行時間は 30 秒のはずです。クエリ内の "WITHIN 1" で指定した 1 秒のポーリング間隔や、丸めのエラーと関係があるかもしれませんね。秒単位までしか測定していませんから。あるいは、プロセスが冷水器のところで Dr. Scripto とおしゃべりをしてから仕事に戻ったので、少し長くなったのでしょうか。高精度な時間が必要なら別の手法を使う必要があるでしょうが、このスクリプトの目的にはこのアルゴリズムで十分です。

さて、ここで一息いれましょう。今回はプロセスを 1 台のコンピュータで実行し、その実行時間を調べました。だからどうしたのだ、と呟いているかもしれませんね。私が管理しているのは 1 台だけじゃない。ネットワーク全体のパッチあてや保守がしたいのだ、と。皆さん、次回にご期待ください。Dr. Scripto は、ネットワーク上のすべてのコンピュータで作成した、すべてのプロセスのライフ ストーリーを聞くまで休みません。