Share via


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

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

Microsoft Scripting Guys

作業中の Doctor Scripto

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

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

このコラムは、シリーズの第 2 部です。まず、第 1 部「午前 2 時、プロセスの所在を把握していますか」を読むことをお勧めします。

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

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

トピック

複数のコンピュータでプロセスを作成し、監視する
そこにスクリプトあれ
まるで、ランダム化がまだ十分ではないかのように
復習: リモート コンピュータでプロセスを作成し、監視する
複数のコンピュータにプロセスを作成する
複数のマシンでプロセスを監視する
複数のコンピュータでプロセスを作成し、監視する
最終的なスクリプト: パッチして探知

複数のコンピュータでプロセスを作成し、監視する

前回のコラムでは、Win32_Process の Create メソッドを使用して、ローカル コンピュータまたはリモート コンピュータで実行可能ファイルを実行する方法、および WMI イベント ハンドリング機能を使用して、このプロセスによって生成されたイベントを監視する方法について説明しました。

そのコラムで指摘したように、Windows Server Update Services や Microsoft Systems Management Server を使用できない場合にアプリケーションの修正プログラムやアップグレードをインストールするには、このタイプのスクリプトを実行するといいでしょう。このテクニックは、バックアップや診断プログラムなど、直接、スクリプト化できないタスクを実行する特別なユーティリティの実行にも使用できます。

しかし、おそらくお気付きのことと思いますが、1 台のコンピュータにのみ実行できるスクリプトはナイスですが、それに 3 ドル足せば、フォームたっぷりのキャラメル マキアート (しかも、特大サイズのヴェンティ) が買えるのです。少なくとも、皆さんがシアトル出身だったら、こんなことを考えたかもしれません。

皆さんの好みが何であれ、実行可能ファイルを実行し、結果を監視するためのスクリプトをわざわざ記述するのなら、複数のコンピュータをターゲットにできるものでなければあまり得るものはないでしょう。

最近、Scripting Guys に次のようなメールが届きました。"あるバッチ ファイルを一度に 50 台のコンピュータで実行したいのですが、コンピュータに 1 台ずつ手動で接続したくありません。スクリプトで、このバッチ ファイルを呼び出して、実行する方法はありませんか。"

"十分に可能です" と、Dr. Scripto ははっきりと答えました。このバッチ ファイルを、WMI または ADSI を使用するスクリプトに書き直して、バッチ ファイルの目的を果たすことさえ可能です。しかし、使用可能なスクリプト機能を (それもはるかに) 超える特別なツールや実行可能ファイルを実行したい場合でも、スクリプトにより、その機能範囲を拡張することができます。

そこにスクリプトあれ

技術考古学における Dr. Scripto の調査によると、複数のコンピュータに対してスクリプトを実行したいというのは人間の原始的な衝動だそうです。初期のシステム管理者は、農業が考案されるとすぐに、スクリプトを書き始めました。そのころ、IT の天才の中に、貯蔵庫をすべて回って自ら穀物を数えるよりも、穀物の数え方を書いた粘土板に、結果を刻印するためのフォームを付けて、SandalNet 経由で倉庫番に送り、これらを送り返してもらう方がいいと気付いた者がいました。

私たちは大いに発展を遂げました。現在、私たちには数を数えるためのプロセスと、結果を送り返してもらうための WMI イベントがあります。しかし、こういった方法は、本当に、粘土板よりもはるかに優れているのでしょうか。そうです。実際、かなり優れています。これらの方法は、より早く、より軽く、雨にぬれてもぼろぼろになりません。というか、粘土板に刻まれたくさび形文字のデバッグをしようとしたこと、ありますか。

いつものように感傷や見せかけの郷愁を払いのけ、Dr. Scripto は、さらに先へ進み、複数のコンピュータで実行可能ファイルを動かし、これらの実行可能ファイルにより生成されたイベントがどのように動作するかをハンドリングするための革新的なアプローチを開発したのでした。しかし、彼は、髪が逆立つほどのこの離れ技を実行しようとすると、スクリプト作成において新たな難題が持ち上がるということに気づきましたが、困難に直面しながらも力強く生きている (それに、逆立つほど髪の毛は残っていない) Dr. Scripto は逆にわくわくしてきました。

最もやっかいな問題の原因は、前回のコラムにあるスクリプトで使用した SWbemServices の ExecNotificationQuery メソッドに隠された弱点にありました。この控えめで効果的なメソッドは、イベントを要求すると同時に戻されます。そこに問題はありません。しかし、イベントを利用するために、ExecNotificationQuery は SWbemEventSource オブジェクトを戻します。このオブジェクトは役立たずで、スクリプトがクエリした最初のコンピュータで、おそらく、タバコを吸ったり、安物のバーボンを飲んだり、ビリヤードをしたりして、のらりくらりと時間をつぶしながら、NextEvent メソッドがトリガされるのを待ちます。このコンピュータで、次にクエリと一致するイベントが発生すると、NextEvent はこのイベントの ID を盗みます。これがプロセスである場合、NextEvent はハンドルやプライベート ページの数と同じくらい、秘密の詳細を暴くことができます。

ExecNotificationQuery を実行する

一方、スクリプトは最初のコンピュータで NextEvent をずっと待ち続けています。このため、別のコンピュータでのイベントのクエリに進むことできません。多数のホストを監視するには実用的な方法とは言えません。

NextEvent を待つ

もっと良い方法があるはずです。そして、Dr. Scripto がその方法を思いついてくれるはずです。このコラムでは、ドクターは 2 つのスクリプトを使用するアプローチを取ります。

  • 1 つめのスクリプトは、複数のコンピュータで実行可能ファイルを実行するプロセスを作成し、各プロセスのプロセス ID を戻します。その後、このスクリプトは個々のコンピュータに対して順番に、2 つめのスクリプトを呼び出し、監視の対象となるコンピュータの名前とプロセス ID を渡します。

  • 2 つめのスクリプトは、渡されたプロセス ID と一致するプロセスが指定されたコンピュータで削除されたときに、イベントをトラップします。このイベントから、プロセスの開始時刻と終了時刻が取得され、プロセスの実行期間が計算されます。この情報は、全コンピュータの情報が記録される共通ログ ファイルに書き込まれます。このスクリプトは、コンピュータの台数と同じ回数、実行されます。

しかし、Dr. Scripto が彼の傑作をどのようにコード化するか、その詳細を明らかにする前に、この作品を思いつくまでの複雑な思考経路の一部を順を追って説明しましょう。

まるで、ランダム化がまだ十分ではないかのように

まず、ごく平凡な改善から話を始めましょう。前回のコラムでは、ミリ秒単位で指定された期間待機する、シンプルなテスト スクリプトを実行して、不確定な期間実行されるアプリケーションをシミュレーションしました。このコラムのテスト スクリプトは、もう少し洗練されたアプローチを取り、最大インターバルと最小インターバルの間でランダムな時間、待機してから終了します。修正プログラムやアップグレードなど、多くのコマンドライン アプリケーションはコンピュータによって実行時間が異なるため、このテスト スクリプトではより現実的なシミュレーションができます。また、スクリプトのテストにピリッとした隠し味も多少加えられます。このスパイスは Dr. Scripto が面白いと思ったものですが、これを見れば、かわいそうなドクターがどのくらい娯楽に飢えているかにうすうす感づくことでしょう。

リスト : ランダムな期間待機します。

intMax = 30 'seconds
intMin = 20 'seconds
Randomize
intWait = Int((intMax - intMin + 1) * Rnd + intMin)
WScript.Echo "Start: pausing for " & intWait & " seconds."
WScript.Sleep intWait * 1000
WScript.Echo "Stop"

このスクリプトは、20 秒から 30 秒の間、ランダムなインターバルで実行されます。実行インターバルの範囲を変更するには、intMax と intMin の値を変更するだけです (ただし、intMax は intMin よりも大きくなければいけません)。

VBScript でのランダム化の詳細については、「Hey, Scripting Guy! スクリプトを使用して乱数を生成する方法はありますか」を参照してください。

復習: リモート コンピュータでプロセスを作成し、監視する

万が一、前回のコラムを暗記していない場合は (そう、テストに出ますよ)、あの傑作の中で、Dr. Scripto はローカル コンピュータ、またはリモート コンピュータのどちらかで実行できるスクリプトをいくつか作成しました。それには、"このコンピュータへのネットワーク アクセスが必要です。また、このスクリプトを実行するには、このコンピュータへの管理者権限を持つ資格情報が必要です" というおなじみの警告がついていました。

すでに説明したとおり、WMI を使って、プロセスを作成し、オーバーヘッドを最小に抑えながら、リモートでイベントを監視できます。しなければならないことは、コンピュータ名を含む変数に割り当てられた値を、リモート コンピュータの名前に変更することだけです。

これは、前回のコラムにあるスクリプトだけでなく、「スクリプト一覧」にあるスクリプトの多くで有効です。次の行を

strComputer = "."

から

strComputer = "server1"

に変更します。ここで、server1 はリモート コンピュータです。

その後、変数 strComputer を WMI モニカにプラグインします。たとえば、次のようになります。

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

どうですか。あっという間にリモートです。リモート プロシージャ コール (RPC) と分散 COM (DCOM) のややこしい部分は WMI が面倒見てくれます。近頃は、ただでランチは食べられないって言ったのは誰ですか。

さて、複数のコンピュータでプロセスを作成するために必要なコードを書いていきましょう。ここから、これらのコンピュータで走るプロセスを監視する必要があるんでしたよね。仕上げとして、プロセスを作成し、作成されたこれらのプロセスの一生を見守るために、2 つのスクリプトを 1 つに併せます。スクリプト作成者はランチをおごってもらうだけでは生きていけません。最後のスクリプトをまとめる前に、ちょっと終わらせなければならない仕事があります。

複数のコンピュータにプロセスを作成する

いくつかのコンピュータにプロセスを作成することは、1 つのリモート コンピュータに作成することとさほど変わりはありません。コンピュータ名は、テキスト ファイル、Excel のスプレッドシート、データベース、Active Directory コンテナなど、いろいろなところから取得できます (これらのスクリプト方法については、これまで、「Tales From the Script: 複数のコンピュータに WMI スクリプトを実行する」などのコラムで説明してきました)。しかし、このタスクの基本アルゴリズムをデモンストレーションするために、最初の例では、コンピュータ名の配列だけを使用します。

VBScript の Array 関数を使用して配列を作成し、この配列を変数 (arrComputers) に割り当てたら、For Each ループを使用して、コンピュータ名と Win32_Process の Create メソッドの間で繰り返し処理し、配列内の個々のコンピュータ上でプロセスを実行します。この例では、メモ帳が実行されますが、これはどのような実行可能ファイルやバッチ ファイルにでも置き換えることができます。このメソッドの戻り値を確認します。0 は成功を表します。また、正の整数はすべて、特定の種類のエラーを表すコードです。

リスト : 複数のコンピュータでプロセスを作成します。

strCommand = "notepad.exe"
arrComputers = Array("client1", "client2", "server1")

WScript.Echo "Creating processes on:"

For Each strComputer In arrComputers
  WScript.Echo vbCrLf & "Computer name: " & strComputer
'Connect to WMI.
  Set objWMIService = GetObject("winmgmts:" _
   & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
'Create a new process.
  Set objProcess = objWMIService.Get("Win32_Process")
  intReturn = objProcess.Create _
   (strCommand, Null, Null, intProcessID)
  If intReturn = 0 Then
    WScript.Echo "Created process number " & intProcessID
  Else
    WScript.Echo "Unable to create process. Return code: " & intReturn
  End If
Next

典型的な出力は次のとおりです。

C:\scripts\events-column5>col5-process-multi-1.vbs
Creating processes on:

Computer name: client1
Created process number 586

Computer name: client2
Unable to create process. Return code: 3

Computer name: server-d1
Created process number 1852

Create メソッドを使用して、リモート コンピュータで実行可能ファイルを実行する場合、プロセスが自動的に実行されるウィンドウは非表示で、このウィンドウを表示することはできないという点に注意してください。したがって、このサンプル スクリプトはメモ帳を開きますが、実行後、タスク マネージャ、または Kill.exe や Taskkill.exe などのコマンドライン ユーティリティを使用して、これらの非表示のウィンドウを閉じる必要があります。または、そのままにしておいて、スクリプトを実行するたびに増えていく大量の隠しプロセスに立ち向かうこともできますが、ちょっと怖いですね。

非表示のウィンドウを閉じずに、このスクリプトを実行したいという場合は、上記のテスト スクリプトを使用することもできます。これを使用すると、ランダムな期間実行された後に終了します。strCommand の値 "notepad.exe" を "cscript test.vbs" (または、サンプル スクリプトにつけたい名前) に置き換えるだけです。または、タスクを実行すると終了するその他のユーティリティを使用することも可能です。

そういえば、もし、ちょっと冒険がしたい気分なら、このスクリプトに後片付けをさせることだってできます。それには、Create メソッドが戻したプロセス ID を使って、個々のコンピュータで Win32_Process の Terminate メソッドを呼び出すだけです。でも、この方法は時間のあるときに自分で考えてください。ここではメインのスクリプトに進みます。

複数のマシンでプロセスを監視する

プロセスの作成は簡単でした。しかし、複数のマシンでプロセス イベントを監視することとなると、ちょっと話がややこしくなります。先ほど、問題の原因は、ExecNotificationQuery により戻された SWbemEventSource オブジェクトが、スクリプトの実行対象である最初のコンピュータで何もしないでイベントを待っていることにあると指摘しました。この制約を回避して、残りのコンピュータすべてを監視するにはどうすればいいでしょうか。

Dr. Scripto は沈思黙考の末、スクリプトはその内部から 2 つめのスクリプトを生み出すことができるという特徴をうまく利用して、2 つのスクリプトを使おうと決意しました。このために Win32_Process.Create を使うこともできましたが、これらのスクリプトはすべてローカル コンピュータで実行されるため、ローカルでのみ実行可能で、コード化がやや簡単な、WScript.Shell の Exec メソッドを使用しました。

このモデルでは、前のスクリプト同様、1 つめのスクリプトはコンピュータ名の配列を最初から最後までループし、監視の対象となるコンピュータそれぞれについて、Exec を使って 2 つめのスクリプトを呼び出して、パラメータの一部としてコンピュータ名を Exec に渡します。不恰好ですが、マルチスレッドのスクリプトとはだいたいこんなものです。実際、これはプロセスのスレッドではなく、別のプロセスを作成し、監視スクリプトの別のインスタンスを実行することにより、個々のコンピュータを監視します。

ただし、Exec に渡されるパラメータは、cmd.exe と /c スイッチで始まる文字列です。これにより、スクリプトを実行するためのコマンド ウィンドウが開かれ、スクリプトの実行が完了すると、このウィンドウが閉じられます。リダイレクト文字 (>) はログ ファイルに出力を送信します。このログ ファイルは指定されたディレクトリに保存されており、コンピュータにちなんだ名前がつけられています。

その後、2 つめのスクリプトにある各インスタンスが __InstanceDeletionEvent に対して ExecNotificationQuery を実行します。ここで、ターゲットは Win32_Process です。クエリは、2 つめのスクリプトに渡された名前を持つコンピュータに対して実行され、このクエリの条件を満たす次のプロセスを待ちます。このようなプロセスを無制限に捕捉したい場合は、Do ループに NextEvent オブジェクトをコードし、各マシンで継続的にイベントを待機することもできます。原理は同じです。これはコンピュータごとに独立したスクリプトで実行されるため、NextEvent は心ゆくまでぶらぶらと過ごします。それどころか、冷たい飲み物を 2、3 杯いただいて、イベントがあまり発生しない日だったら昼寝をすることさえできます。たとえ、どんなに怠け者であっても、他のどのコンピュータでも、イベント監視アクションは怠りません。

これら 2 つのスクリプトは、単にこのアプローチの骨格を表しているだけです。次のスクリプトで、この骨格を具体的に説明します。

リスト : 1 つめのスクリプト - 2 つめのスクリプトを実行して、複数のコンピュータでのプロセス イベントを監視します。

strEventHandlerScript = "c:\scripts\eventhandler.vbs"
strLogDir = "c:\scripts\logs\"
arrComputers = Array("client1", "client2", "server1")

WScript.Echo "Monitoring processes on:"

For Each strComputer In arrComputers
  WScript.Echo vbCrLf & "Computer name: " & strComputer
  Set WshShell = CreateObject("WScript.Shell")
  WshShell.Exec("cmd.exe /c cscript " & strEventHandlerScript & " " & _
   strComputer & " > " & strLogDir & strComputer & "-log.txt")
Next

次のスクリプトは、メイン スクリプトにより (この例では eventhandler.vbs として) 呼び出され、唯一の引数として、監視対象となるコンピュータの名前を渡します。この引数は、組み込みの WScript.Arguments コレクションにより処理されます。残りのコードは、前回のコラムでおなじみです。

このスクリプトは単にプロセス名と ID を取得しますが、最後のスクリプトではプロセスの実行継続期間を計算します。前回のコラムで説明したとおり、Win32_Process、Win32_ProcessStartTrace、または Win32_ProcessStopTrace からプロパティを提供されるプロセスに関する情報をすべて取得できます。

リスト : 2 つめのスクリプト - コンピュータ上の最初のプロセス削除イベントを監視します。

strComputer = WScript.Arguments(0)

WScript.Echo "Computer Name: " & strComputer

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
Set colMonitorProcess = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM __InstanceDeletionEvent " & _ 
 "WITHIN 1 WHERE TargetInstance ISA 'Win32_Process'")

WScript.Echo "Waiting for process to stop ..."

Set objLatestEvent = colMonitorProcess.NextEvent
Wscript.Echo VbCrLf & "  Process Name: " & _
 objLatestEvent.TargetInstance.Name
Wscript.Echo "  Process ID: " & objLatestEvent.TargetInstance.ProcessId

複数のコンピュータでプロセスを作成し、監視する

プロセスを作成し、イベントを監視する方法をいろいろと聞かされて、そろそろ、心の中で "誰かドクターを止めてくれぇ" と叫んでいるのではないでしょうか (実際、Dr. Scripto がスクリプトの作成ソングを歌っていたとき、Web キャストの参加者がそう書いたことがあります)。しかし、ドクターは 2 つのバージョンのスクリプト ペアを推進するという任務を負っています。ドクターはその英知から、この任務を少しずつ、段階を追って進め、ある段階と次の段階の違いがわかるようにすることに決めました。

前の 2 つのスクリプトでは、複数のコンピュータにプロセスを 1 つ作成し、複数のコンピュータでプロセス イベントとその継続期間を監視するという潜在的に関連している 2 つのタスクに対して別々のコードを使用しました。最終的には、これらを 1 つのスクリプトにまとめましょう。

スクリプトのロジック

Dr. Scripto は、過去に長いスクリプトで行ったように、このスクリプトをプロシージャに分解しました。このモジュール アプローチにより、すでに作成したプロシージャを書き直すことなく、機能を構築し、追加することができます。

このスクリプトは、これまでのスクリプトと同様、配列からコンピュータの一覧を取得します。フィナーレを待ってから、コンピュータ名を取得するためのより実用的な方法を追加しましょう。

繰り返しますが、このロジックは、For Each ループを使って、コンピュータの配列の最初から最後までループし、個々のコンピュータで順番に WMI サービスへの接続を試みます。次に、スクリプトはエラーをチェックします。バインドに失敗した場合は、スクリプトはぞっとするような任務に対応するため、別のサブルーチン HandleError を呼び出します。WMI 接続が正常に確立された場合、スクリプトは先に進められ、CreateProcess 関数が呼び出されます。この関数には、strCommand 変数にカプセル化されたコマンド文字列が渡されます。

CreateProcess に聞き覚えがある気がするという場合、それはおそらく、前回のコラムに出てきたからでしょう。自分で自分の作品を盗用しているって言われればそのとおりなのですが、Dr. Scripto は自分が説明したことを実践していると言ってくれというでしょう。ドクターは Web キャストやワークショップで権威をかさに着て、自分の愛すべきファンに、スクリプト コードを請い、拝借し、盗用することを熱心に勧めることがよくあります。そうです。これこそスクリプトの文化です。スクリプトを作成するたびに新しいコードを発明しようとしているスクリプトの神様というのはいったいどのようなものでしょう。前回のコラムでは、CreateProcess はうまく動作しましたから、このコラムでももちろんうまく動作します。というわけで、これをコード ライブラリに追加しましょう。

ただし、配列 (または、スクリプトの最終バージョンではテキスト ファイル) に含まれる各コンピュータには、CreateProcess により実行される実行可能ファイルやスクリプト (この例では、c:\scripts\test.vbs ) が必要です。ツールやアプリケーションが、これらのコンピュータに展開されているかどうかまだわからない場合は、これらの存在を確認するためのコードをスクリプトに追加し、見つからない場合は、コンピュータにコピーする必要がありますが、これは最終的なスクリプトで行います。

皆様からのご要望により、HandleError サブルーチンも帰ってきました。しかし、最終的なスクリプトでは、これを少しパワーアップして、マルチホスト スクリプトでも快適に動作するようにします。

このスクリプトに盛り込まれる新しいサブルーチンは、ExecMonitorScript です。CreateProcess を呼び出した後、スクリプトのメイン ロジックは、この関数の戻り値を確認します。戻り値 -1 はプロセスを作成できなかったことを表します。関数からディスプレイへの出力には、これはすでに示されています。他の数値が戻された場合、これはここで作成されたプロセスのプロセス ID (PID) を表す正の整数です。したがって、PID とコンピュータ名をパラメータとして ExecMonitorScript に渡します。

ExecMonitorScript は、WshShell オブジェクトを作成し、そこで Exec メソッドを呼び出します。このスクリプトは Exec に、5 つの変数を連結した長い文字列を渡します。このコマンド文字列は、コマンドプロンプト ウィンドウを開きます。このウィンドウでは、監視スクリプトの名前を使って、cscript が実行されます。実行対象となるコンピュータの名前、および監視イベントで使用されるプロセス ID は、パラメータとしてこのスクリプトに渡され、このコマンド文字列で使用されます。その後、先ほどの監視スクリプトと同様、Exec コマンド文字列はスクリプトの出力を、適切なログ ディレクトリにある、コンピュータ名にちなんだ名前を持つテキスト ファイルにリダイレクトします。

ここまできても、各コンピュータに別々にログ ファイルが作成されています。これは比較的簡単ですが、最善の解決策とはいえないでしょう。Dr. Scripto がより良い代替案を思いついたかどうか、最終的なスクリプトを見てみましょう。

リスト : 1 つめのスクリプト - 複数のコンピュータでプロセスを作成し、監視スクリプトを実行します。

On Error Resume Next

g_strMonScript = "c:\scripts\procmon.vbs"
g_strLogDir = "c:\scripts\logs\"
strCommand = "cscript c:\scripts\test.vbs"

arrComputers = Array("fictional", "localhost", "server-d1")
WScript.Echo "Monitoring processes on:"

For Each strComputer In arrComputers
  WScript.Echo vbCrLf & strComputer
'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
      ExecMonitorScript strComputer, intPID
    End If
  End If
Next

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

Function CreateProcess(strCL)
'Create a process.

On Error Resume Next

Set objProcess = objWMIService.Get("Win32_Process")
intReturn = objProcess.Create _
 (strCL, Null, Null, 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 ExecMonitorScript(strBox, intProcessID)
'Run script to monitor process deletion events

On Error Resume Next

Set WshShell = CreateObject("WScript.Shell")
WshShell.Exec("cmd.exe /k cscript " & g_strMonScript & " " & _
 strBox & " " & intProcessID & " > " & g_strLogDir & strBox & "-log.txt")

End Sub

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

'Handle errors.
Sub HandleError

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

End Sub

先ほどのスクリプトでは、ExecMonitorScript は、各コンピュータでプロセスを監視する 2 つめのスクリプトを起動しました。前述の最初の監視スクリプトでは、テクニックを説明するためだけに、ターゲットを Win32_Process プロセスとし、このイベントに対応する、最初の __InstanceDeletionEvent をトラップしました。しかし、これらのスクリプトの最終的な目的は、最初のスクリプトで開始された特定のプロセスに何が起こったかを発見することです。

2 つめのスクリプトは、どのようにして監視対象となるプロセスを判断するのでしょう。答えは簡単です。最初のスクリプトは、監視対象となるマシンの名前と、検索するプロセス ID の 2 つのパラメータを渡します。したがって、このスクリプトのコマンドライン構文は次のようになります。

procmon.vbs <MachineName> <processID>

1 つめのスクリプトを実行せずに、2 つめのスクリプトを単独でテストすることもできます。

  • メモ帳を使って、プロセスを開始します。

  • タスク マネージャ (Tlist.exe または Tasklist.exe) からプロセス ID を取得します。

  • 使用しているコンピュータの名前と、このプロセス ID を使って、このスクリプトを実行します。

  • メモ帳のウィンドウを閉じます。

このスクリプトにより、今、停止したメモ帳のプロセスの詳細が表示されます。

このイベント監視スクリプトは、WSH でスクリプトを実行すると必ず作成される、組み込みの WScript.Arguments コレクションを使用して起動します。このスクリプトに、スペースと引数が続いている場合、Arguments はこの引数を先頭項目 (要素 0) としてコレクションに追加します。さらに、別のスペースと別の引数が続いている場合、Arguments はこの引数を 2 番目の項目 (要素 1) として追加します。スクリプトは、これらを WScript.Arguments(0) および WScript.Arguments(1) として取得します。

ユーザーによって実行されるように設計されたスクリプトでは、通常、これらの引数がコマンドライン上に存在していること、存在しない場合は、使用方法を説明するメッセージが表示されることを確認します。しかし、Dr. Scripto が工夫してくれたおかげで、このスクリプトは、別のスクリプトにより実行されるので、2 つめのスクリプトに正しい引数を渡すことを 1 つめのスクリプトに任せることができます。スクリプトをデバッグするとき、これらのスクリプトがまるで意志を持っているように感じられたとしても、この場合、それは Dr. Scripto の信念が狙ったとおりの効果なのです。

その後、strComputer の値としてコンピュータの名前を、intProcessID の値としてプロセスの数を取得したら、これら 2 つの値を使用できるようになります。このコンピュータの WMI に接続するために、WMI への接続用モニカ文字列に strComputer を連結します。また、ExecNotificationQuery に渡される WMI Query Language クエリの一部として intProcessID を使用し、どのイベントを監視するかを知らせます。

このスクリプトでは、リモート コンピュータ上の WMI への接続を試みた後、多少のエラー チェックを行います。WMI のバインドは数秒で終わりますが、これがタイム アウトした場合は、リモート コンピュータが使用できないというエラー メッセージが戻されます。多数のコンピュータについて、これを最適化する必要がある場合は、WMI への接続を試みる前に、まず、リモート コンピュータを ping することができます。ping は WMI GetObject よりも早く帰ってくるため、数秒を節約できます。余分なコーディングが必要ですが、これにより節約できる時間は、コンピュータの台数が多くなればなるほど貴重になります。

監視しているプロセスが終了したとき、NextEvent にはこのプロセスに関する情報が含まれています。この情報を取り出して、プロセスの実行継続時間を計算します。先ほどのスクリプトから引数を取得することを除き、このスクリプトは、前回のコラムにある最終的な監視スクリプトとほぼ同じです。

最初のコンピュータでプロセスが終了するのを SWbemEventSource が待っている間にスクリプトがハングしてしまうため、For Each ループでは、各コンピュータの名前を順番に使うことはできなかったことを思い出してください。ここでは、各コンピュータで、この 2 つめのスクリプトを別々に呼び出すことにより、この落とし穴を回避しています。Dr. Scripto は、これについては本当に新しい観点から考えたのです。

リスト : 2 つめのスクリプト - 指定されたコンピュータで特定の PID を持つプロセスを監視し、その継続時間を出力します。

On Error Resume Next

strComputer = WScript.Arguments(0)
intProcessID = WScript.Arguments(1)

WScript.Echo "Computer Name: " & strComputer

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
If Err.Number <> 0 Then
  HandleError
  WScript.Quit
End If

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"

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

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

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

Sub HandleError
'Handle errors.

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

End Sub

最終的なスクリプト: パッチして探知

最終的な 2 つのスクリプトは、この前の 2 つとはそれほど大きく違いませんが、ここでは I/O メソッドを使用しています。これは、コンピュータの数が増加していく場合に、より実用的です。

1 つめのスクリプトは、コンピュータのリストを取得します。これらのコンピュータに対して、テキスト ファイルからプロセスが作成されます。このスクリプトは、ReadTextFile 関数で、Script Runtime の一部、私たちの親友である FileSystemObject を使用します。ReadTextFile 関数に実行対象となるホスト名が記載されたテキスト ファイルへのパスを渡すと、この関数からコンピュータ名の配列が戻されます。この関数は、以前、このコラムで使用したので、愛読者の皆様はすでにおなじみでしょう。コンピュータのリストをテキスト ファイルに保存したくないという場合は、似たような関数を使用して、スプレッドシートやデータベース、Active Directory コンテナから名前を取得することもできます。

これらのスクリプトは、大量のコンピュータ名が記載された長いリストに対してはまだテストされていないことを忘れないでください。コンピュータの数が増えると、個々のマシンに対して、独立したインスタンスとして 2 つめのスクリプトを実行することにより、メモリの消費が急速に進む可能性があります。10 台のコンピュータ名が記載されたリストを使って、Windows XP でこのスクリプトを実行した場合、実行中に監視スクリプトのインスタンス 1 つにつき、約 4 ~ 6 MB のメモリが使用されましたが、消費された CPU リソースは取るに足らない量でした。その反面、使用しているワークステーションでリスト サイズについて、現実的な上限を発見した場合は、コンピュータ名を複数のリストに分割してまとめ、これらが 1 つずつ、時間を変えて実行されるようにスケジュールを設定することができます。このためには、実行ごとに、strInputFile の値を別のテキスト ファイルに変更するだけです。数行のコードを追加するだけで、スケジュール ジョブにより実行されるコマンドのパラメータとして、ファイル名を渡すことができるようになります。

最終的なスクリプトに対するもう 1 つの重要な機能強化として、実行される実行可能ファイルまたはスクリプトをリモート コンピュータにコピーするか、それともリモート コンピュータに既に存在するファイルを実行するかを選択できるようにしました。前者は修正プログラムで、後者はツールで好まれる方法です。このオプションは、Change Block の変数に設定します。blnCopyFile を True に設定すると、スクリプトのメイン ロジックにより FileCopy 関数が呼び出され、パラメータとして、コピーされる実行可能ファイルまたはスクリプトファイルの名前とパス、およびコピー先のリモート ディレクトリが渡されます。ファイルが正常にコピーされた場合のみ、ロジックは CreateProcess メソッドの呼び出しに進みます。blnCopyFile を False に設定すると、スクリプトにより、実行するファイルは各リモート マシンの目的のフォルダにすでに存在するとみなされ、FileCopy 関数はスキップされます。

FileCopy 関数は、FileSystemObject の CopyFile メソッドを使用して、実行の対象となる実行可能ファイルまたはスクリプトを、各リモート コンピュータの指定されたディレクトリにコピーします。指定されたリモート ディレクトリが存在しない場合は作成されます。ファイルが既に存在する場合は上書きされます。ちょっとした注意を 1 つ。このシナリオではありそうもないことですが、CopyFile では、読み取り専用ファイルは上書きされません。

Dr. Scripto は、FileSystemObject からのメソッドを使用するために、FileCopy 関数を書きました。これは、Script Runtime の一部で、ローカル コンピュータでのみ実行されます。しかし、FileSystemObject は、たとえば、\\server1\c$\update のような UNC パスをパラメータとして受け付けます。このため、あるコンピュータから別のコンピュータへファイルをコピーできます。また、Dr. Scripto は、WMI クラス CIM_DataFile の Copy メソッドを使用するアプローチも考慮しました。この Copy メソッドからの戻り値は便利ですが、このアプローチでは、WSH を使用して、リモート フォルダをローカル ネットワーク ドライブにマップし、作業が完了したら、マップを解除する必要があります。これはやや複雑な感じがします。

これらの 2 つの最終的なスクリプトでは、先ほどお約束しましたとおり、Dr. Scripto は、マルチホスト スクリプトでよりよく動作するように、HandleError サブルーチンも強化しています。エラーが発生したコンピュータの名前を渡すと、HandleError により、この名前が出力に組み込まれます。複数のコンピュータでスクリプトを実行している場合、どのコンピュータでエラーが発生しているかを知る必要があります。

リスト : 1 つめのスクリプト - テキスト ファイルにリストされた複数のコンピュータでプロセスを作成し、各コンピュータに対して監視スクリプトを実行します。

'******************************************************************************
'Change block
strInputFile = "c:\scripts\hosts.txt"
g_strMonScript = "c:\scripts\procmon-ex.vbs"
strCommand = "cscript c:\update\test-random.vbs"
blnCopyFile = True 'Set to True if exe must be copied to remote machines.
strFileToCopy = "c:\scripts\test-random.vbs" 'Executable or script
strRemoteDir = "\c$\update"
'******************************************************************************

On Error Resume Next

arrComputers = ReadTextFile(strInputFile)
WScript.Echo "Monitoring processes on:"

For Each strComputer In arrComputers
  WScript.Echo vbCrLf & strComputer
'Connect to WMI.
  Set objWMIService = GetObject("winmgmts:" _
   & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
  If Err.Number <> 0 Then
    HandleError strComputer
  Else
    If blnCopyFile Then
      strTarget = "\\" & strComputer & strRemoteDir
      intFC = FileCopy(strFileToCopy, strTarget)
      If intFC = 0 Then
        intPID = CreateProcess(strCommand)
        If intPID <> -1 Then
          ExecMonitorScript strComputer, intPID
        End If
      End If
    Else
      intPID = CreateProcess(strCommand)
      If intPID <> -1 Then
        ExecMonitorScript strComputer, intPID
      End If
    End If
  End If
Next
'******************************************************************************

Function ReadTextFile(strFileName)
'Get list of computers from text file.

On Error Resume Next

Const FOR_READING = 1
Dim arrLines()

Set objFSO = CreateObject("Scripting.FileSystemObject")
If objFSO.FileExists(strFilename) Then
  Set objTextStream = objFSO.OpenTextFile(strFilename, FOR_READING)
Else
  WScript.Echo "Input text file " & strFilename & " not found."
  WScript.Quit
End If

If objTextStream.AtEndOfStream Then
  WScript.Echo "Input text file " & strFilename & " is empty."
  WScript.Quit
End If

Do Until objTextStream.AtEndOfStream
  intLineNo = objTextStream.Line
  ReDim Preserve arrLines(intLineNo - 1)
  arrLines(intLineNo - 1) = objTextStream.ReadLine
Loop

objTextStream.Close

ReadTextFile = arrLines

End Function

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

Function FileCopy(strSourceFile, strTargetFolder)
'Copy executable or script to remote machine.
'If remote folder does not exist, creates it.
'Overwrites file if it exists in remote folder.

On Error Resume Next

Set objFSO = CreateObject("Scripting.FileSystemObject")
If Not objFSO.FileExists(strSourceFile) Then
  WScript.Echo "Error: File " & strSourceFile & " not found."
  WScript.Quit
End If
If Not objFSO.FolderExists(strTargetFolder) Then
  objFSO.CreateFolder(strTargetFolder)
End If
If Err = 0 Then
  objFSO.CopyFile strSourceFile, strTargetFolder & "\"
  If Err = 0 Then
    WScript.Echo "Copied file " & strSourceFile & " to folder " & _
     strTargetFolder
    FileCopy = 0
  Else
    WScript.Echo "Unable to copy file " & strSourceFile & " to folder " & _
     strTargetFolder
    FileCopy = 2
    HandleError strHost
  End If
Else
  WScript.Echo "Unable to create folder " & strTargetFolder
  FileCopy = 1
  HandleError strHost
End If

End Function
 '******************************************************************************

Function CreateProcess(strCL)
'Create a process.

On Error Resume Next

Set objProcess = objWMIService.Get("Win32_Process")
intReturn = objProcess.Create _
 (strCL, Null, Null, 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 ExecMonitorScript(strHost, intProcessID)
'Launch second script to monitor process deletion events

On Error Resume Next

strCommandLine = "cscript " & g_strMonScript & " " & _
 strHost & " " & intProcessID
Set WshShell = CreateObject("WScript.Shell")
WshShell.Exec(strCommandLine)
If Err.Number <> 0 Then
  HandleError strHost
Else
  WScript.Echo "Running command line:" & vbCrLf & _
   strCommandLine
End If

End Sub

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

Sub HandleError(strHost)
'Handle errors.

strError = "Computer Name: " & strHost & VbCrLf & _
 "ERROR " & Err.Number & VbCrLf & _
 "Description: " & Err.Description & VbCrLf & _
 "Source: " & Err.Source
WScript.Echo strError
Err.Clear

End Sub

典型的な入力テキストファイル、hosts.txt は次のとおりです。管理者特権を持つネットワークにある、アクセス可能なコンピュータを置き換えています。

client1
client2
server1
server2

典型的なコマンドライン出力は次のとおりです。

client1
Process created.
Command line: cscript c:\update\test-random.vbs
Process ID: 2952
Running command line:
cscript c:\scripts\procmon-ex.vbs client1 1044

client2
Process created.
Command line: cscript c:\update\test-random.vbs
Process ID: 944
Running command line:
cscript c:\scripts\procmon-ex.vbs client2 1540

server1
Process created.
Command line: cscript c:\update\test-random.vbs
Process ID: 3748
Running command line:
cscript c:\scripts\procmon-ex.vbs server1 2384

server2
Process created.
Command line: cscript c:\update\test-random.vbs
Process ID: 3192
Running command line:
cscript c:\scripts\procmon-ex.vbs server2 3460

先ほどの 2 つのスクリプト同様、メイン スクリプトにより呼び出されるイベント監視スクリプトは、指定されたコンピュータで、指定されたプロセス ID を持つ最初のイベントをトラップしています。1 つめのスクリプトは、プロセスを実行している各コンピュータについて、次のコマンドラインを呼び出します。

procmon-ex.vbs <MachineName> <processID>

1 つめのスクリプトでは、コンピュータのリストのソースはテキスト ファイルでした。(Dr. Scripto が高く評価する品質である) 調和感と統一感を維持するため、1 つめのスクリプトにより実行される 2 つめのスクリプトでも、結果はテキスト ファイルに出力されます。

しかし、先ほどの監視スクリプトではコンピュータごとに、別々のテキスト ファイルに出力をリダイレクトしていましたが、これとは対照的に、ここではこの監視スクリプトのインスタンスごとに、共通テキスト ファイルが開かれ、プロセス削除イベントの継続期間やその他の詳細情報がこのファイルにログされます。結果をすべて 1 つのファイルに記録することにより、検索、比較、レポート作成などに使用しやすくなります。

多数のコンピュータが同時にログ ファイルに書き込もうとすると、書き込み操作が重なる可能性があります。しかし、書き込み操作は非常に速く行われるため、コンピュータの台数が適切であれば、この解決策はうまく動作すると思われます。多数のコンピュータが扱えるようにスケールアップするには、データベースに結果を記録するか、または多くの書き込み操作に対応できるその他のアプリケーションに結果を記録するといいでしょう。

ログに記録するジョブを実行する WriteTextFile サブルーチンは、ログ ファイルの名前 (strFileName) と、終了したばかりのプロセスの情報を含む出力文字列 (strOutput) という 2 つのパラメータを使用します。このサブルーチンは FileSystemObject を使用して、strFileName で指定されたファイルが存在するかどうかをチェックします。ファイルが存在する場合、このファイルが開かれ、情報が追加されます。存在しない場合はログ ファイルが作成されます。このサブルーチンは、ファイルに情報を追加するため、書き込みを行うと、上書きは行われず、既存の情報に新しい情報が追加されます。ログ ファイルに書き込むだけではなく、このスクリプトは、同じ結果を cmd.exe ウィンドウに表示することもできます。

この監視スクリプトには、もう 1 つ、以前のスクリプトと比べて強化された機能があります。それは、少なくとも 2 つの引数が渡されていることを確認し、もしどちらかがない場合には、エラー メッセージを表示するというものです。これにより、空白行など、入力ファイルの問題を捕捉しやすくなります。

リスト : 2 つめのスクリプト - 指定されたコンピュータで特定の ID を持つプロセスを監視し、テキスト ログ ファイルに、その継続時間とその他の詳細情報を出力します。

On Error Resume Next

strOutputFile = "c:\scripts\logs\proclog.txt"

strComputer = WScript.Arguments(0)
intProcessID = WScript.Arguments(1)

'Check to make sure arguments are not empty.
If IsEmpty(strComputer) Or IsEmpty(intProcessID) Then
  strArgError = _
   "Computer Name: " & strComputer & vbCrLf & _
   "  Process ID: " & intProcessID & vbCrLf & _
   "  " & Now & vbCrLf & _
   "  ERROR: Computer name and process ID not passed as arguments."
  WScript.Echo strArgError
  WriteTextFile strOutputFile, strArgError
  WScript.Quit
End If

Set objWMIService = GetObject("winmgmts:" _
 & "{impersonationLevel=impersonate}!\\" & strComputer & "\root\cimv2")
If Err.Number <> 0 Then
  HandleError strComputer
  WScript.Quit
End If

Set colMonitorProcess = objWMIService.ExecNotificationQuery _
 ("SELECT * FROM __InstanceDeletionEvent " & _ 
 "WITHIN 1 WHERE TargetInstance ISA 'Win32_Process' " & _
 "AND TargetInstance.ProcessId = '" & intProcessID & "'")
If Err.Number <> 0 Then
  HandleError strComputer
  WScript.Quit
End If

WScript.Echo "Waiting for process to stop ..."

Set objLatestEvent = colMonitorProcess.NextEvent
strProcDeleted = Now
strProcCreated = _
 WMIDateToString(objLatestEvent.TargetInstance.CreationDate)
strProcessName = objLatestEvent.TargetInstance.Name
strPID = objLatestEvent.TargetInstance.ProcessId
strCSName = objLatestEvent.TargetInstance.CSName
intSecs = DateDiff("s", strProcCreated, strProcDeleted)
arrHMS = SecsToHours(intSecs)

strData = "Computer Name: " & strCSName & VbCrLf & _
 "  Process Name: " & strProcessName & VbCrLf & _
 "  Process ID: " & strPID & VbCrLf & _
 "  Time Created: " & strProcCreated & VbCrLf & _
 "  Time Deleted: " & strProcDeleted & VbCrLf & _
 "  Duration: " & arrHMS(2) & " hours, " & _
 arrHMS(1) & " minutes, " & arrHMS(0) & " seconds"

Wscript.Echo strData
WriteTextFile strOutputFile, strData
WScript.Echo "Data written to " & strOutputFile


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

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(strHost)

strError = "Computer Name: " & strHost & VbCrLf & _
 "ERROR " & Err.Number & VbCrLf & _
 "Description: " & Err.Description & VbCrLf & _
 "Source: " & Err.Source
WScript.Echo strError
WriteTextFile strOutputFile, strError
Err.Clear

End Sub

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

'Write or append data to text file.
Sub WriteTextFile(strFileName, strOutput)

On Error Resume Next

Const FOR_APPENDING = 8

'Open text file for output.
Set objFSO = CreateObject("Scripting.FileSystemObject")
If objFSO.FileExists(strFileName) Then
  Set objTextStream = objFSO.OpenTextFile(strFileName, FOR_APPENDING)
Else
  Set objTextStream = objFSO.CreateTextFile(strFileName)
End If

'Write data to file.
objTextStream.WriteLine strOutput
objTextStream.WriteLine

objTextStream.Close

End Sub

典型的な出力ファイルの内容は次のとおりです。

Computer Name: CLIENT1
  Process Name: cscript.exe
  Process ID: 2952
  Time Created: 6/24/2005 5:05:49 PM
  Time Deleted: 6/24/2005 5:06:18 PM
  Duration: 0 hours, 0 minutes, 29 seconds

Computer Name: CLIENT2
  Process Name: cscript.exe
  Process ID: 944
  Time Created: 6/24/2005 5:12:00 PM
  Time Deleted: 6/24/2005 5:12:22 PM
  Duration: 0 hours, 0 minutes, 22 seconds

Computer Name: SERVER1
  Process Name: cscript.exe
  Process ID: 3748
  Time Created: 6/24/2005 5:11:57 PM
  Time Deleted: 6/24/2005 5:12:25 PM
  Duration: 0 hours, 0 minutes, 28 seconds

Computer Name: SERVER2
  Process Name: cscript.exe
  Process ID: 3192
  Time Created: 6/24/2005 5:12:01 PM
  Time Deleted: 6/24/2005 5:12:26 PM
  Duration: 0 hours, 0 minutes, 25 seconds

さて、プロセスの実行とそのイベントの監視についての攻略は完璧ですね。コードの詰め込みすぎで頭が痛み、目がぼーっとかすんでいる皆様にとって、粘土板と SandalNet は、いえ、GUI と SneakerNet すら、非常に魅力的に聞こえませんか。これらは、オフィス 1 か所にある数台のコンピュータにとっては確かにシンプルです。しかし、より大きく、より広域にわたるネットワークでこのようなタスクを実行する必要がある場合は、私たちのコラムで説明したアイディアの一部をスクリプトに組み込んで、修正プログラムの適用作業や管理作業を自動化していただきたいと思います。

それでは、このテーマはこの辺で一休みとして、さらに青々としたスクリプトの草原に進みましょうか。それとも、プロセス畑やイベント畑は肥沃で、まだ耕すところが残っているでしょうか。Dr. Scripto は来月も皆さんにお目にかかることを楽しみにしております。