2010年3月29日月曜日

WshShellのExecで実行したコマンドが終了しない

諸事情あって、Windows Scripting HostのVBScriptでスクリプトを書いたのですが、その際に困った事で軽くググッただけでは解決しなかったのでメモしておく事にしました。
VBScript とかあまり詳しくないんですけど。

そのスクリプトではCScriptでバッチとして実行しながら、途中で他のコマンドを同期をとって実行する必要があったので、普通に以下のように考えてスクリプトを書いてみた。

  1. コマンドを実行
  2. 実行したコマンドの終了待ち
  3. 実行したコマンドの標準出力の内容を自身の標準出力に出力


このまま素直に書けばとりあえずこうなるんだと思う。MSDNとかにもほぼそのまま載っている。
以下では実行するコマンドは適当に"Test.exe"としておく。もちろん実際には違う。
あと、終了ステータス(ExitCode)のチェックとかも実際はするけど、そこは本件とは関係ないので省略してます。
Option Explicit
Dim objShell, objExec

Set objShell = WScript.CreateObject("WScript.Shell")
Set objExec = objShell.Exec("Test.exe")

Do While objExec.Status = 0
  WScript.Sleep(100)
Loop

WScript.StdOut.WriteLine "StdOut"
WScript.StdOut.WriteLine objExec.StdOut.ReadAll

とりあえずテスト的に適当なコマンドを実行してみる分には問題なさそうなので、実際に実行したいコマンドに変えてみるといつまでたってもそのコマンドのプロセスが終了しない。
そのコマンドがやるべき事は全て実行されているのだけど、プロセスが終了しないせいで制御がこちらに戻ってこないように見える。
なので、タスクマネージャとかでそのプロセスを強制的に停止してやると制御が戻ってきて標準出力の内容も表示されてた。

いろいろ試行錯誤していたのだが、どうやら実行したコマンドの標準出力の量がとても大きくて、上記のようにプロセスを強制的に停止した後で表示される内容は本来表示されるべき内容全ては表示されていない事に気づいた。

その為、以下のように終了待ちしている途中でも、標準出力にあるものは読み出していく処理を追加したところ、とりあえずは制御は戻ってきてくれたのでやりたい事はできるようにはなった。
でも、1文字読んではSleepというのは、かなりイケてないとは思うけど。
実際には Line ごとに処理していくのが妥当かな、という気がします。
Option Explicit
Dim objShell, objExec, strOut

Set objShell = WScript.CreateObject("WScript.Shell")
Set objExec = objShell.Exec("Test.exe")

Do While objExec.Status = 0
  strOut = strOut & objExec.StdOut.Read(1)
  WScript.Sleep(100)
Loop

WScript.StdOut.WriteLine "StdOut"
WScript.StdOut.WriteLine strOut & objExec.StdOut.ReadAll


以下のような適当なスクリプトを実行させて確認したところ、標準出力のサイズが4096バイトを超える場合に、標準出力の読み出しをせずに終了待ちしていると止まってしまうようです。
特に子プロセスの標準出力を読んだり表示したりしていなくても同じでした。
Option Explicit
Dim i, max

If WScript.Arguments.Count = 0 then
  WScript.Quit(1)
End If
max = WScript.Arguments.Item(0)

For i = 1 to max
  WScript.StdOut.Write(Right(i,1))
  If i = Round( ( max / 2 ), 0) Then
    WScript.Sleep( 5000 )
  End If
Next

ちなみに途中に "Sleep" を入れているのは、処理時間がかかっているように見せて呼び出し側の動きを見る為です。

ただ、こうしても、もしエラー出力(StdErr)が4096バイトを超えてきたらやっぱり止まってしまうみたいで本質的な解決にはなっていないようです。

まあ、標準出力とエラー出力の両方がそんな大量になることなんて、めったにあるものでもないだろうから、実際に実行するコマンドの特性を見ながら対処するのが現実的なところなんでしょうか。

そう割り切って考えると、"Status"で終了待ちするのではなくて、標準出力を1行ごと読み込んで標準出力のストリーム終了まで処理するという方がスッキリする。
特に"Sleep"とか入れてなくても、処理時間がかかっているときは"ReadLine"で待ちになるようなので。
それに子プロセスが終了してから一気に出力するのではなくて、随時自分の標準出力に出力する事で、子プロセスの実行状況もわかるようになるし。
Option Explicit
Dim objShell, objExec

Set objShell = WScript.CreateObject("WScript.Shell")
Set objExec = objShell.Exec("Test.exe")

WScript.StdOut.WriteLine "StdOut"
Do While Not( objExec.StdOut.AtEndOfStream )
  WScript.StdOut.WriteLine objExec.StdOut.ReadLine
Loop

WScript.StdOut.WriteLine "StdErr"
WScript.StdErr.WriteLine objExec.StdErr.ReadAll


なんかもう少しスマートなやり方はないものなんでしょうか?

0 コメント: