外部プログラムがフリーズしたときに強制終了させる方法

歴史のある企業だと、とっくに退職した社員が作成した古いプログラム(C言語や Fortran で作成されたMS-DOS版の計算プログラムなど)が遺産として残されていることがあると思います。そのほとんどはもう使うこともないプログラムでしょうけど、中には貴重なロジックが技術伝承されておらず、今後しばらく使いたいものが含まれているケースもあります。

ソースファイルが存在していれば、その中身を解析して別の言語にコンバートするという方法もあるものの、他人が作成した太古のプログラム(往々にして複雑怪奇なスパゲティプログラムであることが多い)の解読ほど厄介なことはありません。コンバートせずに元の言語で改良するにしても、C言語や Fortran のコンパイラーなんてとっくに捨てているということがほとんどでしょう。

このため、GUI を別に作成し、プログラム自体はそのままの形でソルバーとして使用するケースがあります。こういうシステムの流れは以下の通りです。

  1. ソルバープログラム用の入力データファイルを作成
  2. 外部プログラムとしてソルバーを実行
  3. 実行が終了したことを確認し、出力されたファイルを読み込んで処理

根本的な解決というよりは応急処置みたいなものですが、とりあえずはこういう形でも古いプログラムを継続して使うことは可能です。

ただし、ここで問題になるのが「ソルバープログラムが終了しない場合」です。どういうことかというと、こういうプログラムはエラー処理が十分に考慮されていないことが多く、入力データに問題があると無限ループに陥ってしまうことが多々あります。こうなってしまうと、実行が終了したことが確認できないため、システム側がずっと待ち続けるというフリーズ状態になります。

ソルバープログラムを改良できればいいんですが、前述の通りコンパイラーを持っていなかったり、そもそもソースファイル自体が消えてしまっているという対応不能のケースもあります。

この問題への対処方法はいくつか考えられますが、もっとも容易なのは「プログラムの実行が指定した時間以内に終了していない場合、フリーズとみなして強制終了させる」という方法です。需要があるかどうかはわかりませんが、このロジックについて以下で説明します。


まず最初に、ソルバープログラムが入力データファイルを読み込んで実行する形式の場合、プログラムを直接起動するよりもバッチファイル形式で実行するほうが安全です。例えば、ソルバープログラム “solver.exe” がフォルダー “C:\temp” に存在している場合は、以下のバッチファイルで実行します。

C:
cd "C:\temp"
solver

これは、ソルバープログラムでは基本的に同じフォルダー内にあるデータファイルを読み込むはずで、カレントフォルダーを明示的に移動させるためです。

メモ

「バッチファイルって何?」という人も多いでしょうけど、これは連続して実行させるコマンドを記述したファイルのことで、Windows 登場以前に広く使われていました。とりあえずは「1行ずつコマンドが書かれたテキストファイル」と考えてもらって構いません。ちなみに上記の2行目にある “cd” は “change directory” という意味です。

したがって、システム側では入力データファイルと実行用バッチファイルを作成し、ソルバープログラムを起動させることになります。


システム側のコードについては、VB.Net 形式で作成しています。ソルバープログラムを “C:\temp\solver.exe” とすると、以下の通りです。

'実行プログラムが存在するフォルダー名とファイル名(拡張子 .exe は付けない)
Dim Solver_FolderName As String = "C:\temp"
Dim Solver_FileName As String = "solver"

'バッチファイルを作成
Using FileWriter As New IO.StreamWriter(Solver_FolderName & "\run.bat", False, System.Text.Encoding.GetEncoding("shift_jis"))
    FileWriter.WriteLine(Solver_FolderName.Substring(0, 2))
    FileWriter.WriteLine("cd """ & Solver_FolderName & """")
    FileWriter.WriteLine(Solver_FileName)
End Using

'実行中、マウスカーソルを待機状態にする
Cursor.Current = Cursors.WaitCursor

'コマンドプロンプトを非表示で実行させる
Dim Process_Info As New ProcessStartInfo With {
    .FileName = Solver_FolderName & "\run.bat",
    .WindowStyle = ProcessWindowStyle.Hidden
}

'バッチファイルの実行
Dim Process_Exe As Process = Process.Start(Process_Info)

'10秒間待機
Process_Exe.WaitForExit(10000)

'マウスカーソルをデフォルトに戻す
Cursor.Current = Cursors.Default

'プログラムが終了していない場合、強制終了させる
If Process_Exe.HasExited = False Then
    Dim Process_Name As Process() = Process.GetProcessesByName(Solver_FileName)

    '実行中のプロセスを取得して、該当するものを強制終了
    Dim Each_Process As Process
    For Each Each_Process In Process_Name
        Each_Process.Kill()
    Next Each_Process

    'メッセージボックスを表示
    MessageBox.Show("Abnormal End", "Execution didn't finish normally.", MessageBoxButtons.OK, MessageBoxIcon.Exclamation)
    Exit Sub
End If

このコードでは「10秒経過して終了していない場合は強制終了」としていますが、この秒数に関してはソルバープログラムに合わせて適宜変更してください。なお、実行が正常に終了した場合は10秒間待機せず次のプロセスに移ります。

また、コマンドプロンプトが表示されるのは格好悪いという考えで非表示にしていますが、キーボードからの数値入力などコマンドプロンプトを表示させる必要がある場合は “ProcessWindowStyle.Hidden” を ”ProcessWindowStyle.Normal” に変えて下さい。

少し技術的な話をすると、最初は外部プログラム起動時にプロセスIDを保存しておき、フリーズしたときはそのIDのプロセスを強制終了させようとしたのですが、うまくいきませんでした。これは、起動したのがバッチファイルであり、ソルバープログラムを直接起動していないことが原因でした。

このため、実行中のプロセスをすべて取得し、ソルバープログラム名と一致するものを終了させるというロジックにしています。


以上、こんなマニアックなシステムに需要があるかどうかわかりませんが、古い遺産プログラムの扱いに悩んでいる人は参考にしてください。