「アプリケーションの二重起動を防ぐにはどうすれば良いのか」「他のアプリケーションを起動し、そのアプリケーションの終了を待つにはどうすれば良いか?」といった質問は、大変よく見かける質問であり、また、それに対する回答のバリエーションも多々ある。回答を見れば、その人の技量が分かるといっても良いほどである。ここでは、よく見かける回答例を紹介する。
VBで二重起動を防ぐ方法として、App.PrevInstanceプロパティを使用する方法がある。App.PrevInstanceプロパティは、二重起動されている場合Trueとなり、されていない場合Falseとなる。したがって、例えばFormのLoad時にこのプロパティをチェックし、Trueならば、メッセージを表示して終了する、等のコードを書けば良い。
このように、VBでは二重起動のチェックは非常に簡易に実現できるので、通常はこの方法を使用すると良いだろう。(特殊なケースとして、VBで作った実行ファイルがコピーされてしまった場合、元の実行ファイルとコピーした実行ファイルを同時に起動できてしまう)
なお、まれにApp.PrevInstanceは16bitアプリケーションでしか有効でない、と勘違いしている人もいるようであるが、もちろん32bitでも正常に動作する。
VB以外のアプリケーションでは、App.PrevInstanceに相当する処理を自前で行わなければならない。その一つに、ミューテックスを使用する方法がある。
Win32には、「ミューテックスオブジェクト」という、名前の付けられるオブジェクトがある。あるプロセスで作成したミューテックスオブジェクトがシステムに存在する場合、これを他のプロセスでオープンすることができる。作成されていないのにオープンしようとした場合はエラーが返される。
これを利用して、次のようなステップで二重起動を防止できる:まず、Formのロード時などに、OpenMutexでミューテックスオブジェクトのオープンを試みる。オープンに成功した場合は、エラーメッセージを表示して終了する。オープンに失敗した場合、CreateMutexでミューテックスオブジェクトを作成し、後続の処理を実行する。アプリケーションの終了時には、ミューテックスを破棄するようにする。(ただしこれを行わなくとも、システムによってプロセス終了時に自動的に破棄される)
この手順でのポイントは、ミューテックスのオープンが成功してしまったということは、既に自アプリケーションが起動しているということを意味する、という点である。
残念ながら、この方法ではうまく行かない場合がある。これは、OpenMutexとCreateMutexの処理の間に、二つ目のアプリケーションが起動されてしまった場合である。
そもそもミューテックスは排他制御に使用されるものであり、正しく使用すればより適切に二重起動のチェックを行うことができる。
ミューテックスオブジェクトには「所有」という概念がある。一つのミューテックスオブジェクトは、一度に一つのプロセスにしか所有されず、複数のプロセスによって単一のミューテックスが所有されることはない。あるプロセスからミューテックスを所有するにはWaitForSingleObject関数を、また、所有権を放棄するにはReleaseMutex関数を使用する。
これにそって、ミューテックスを用いた二重起動防止コードを考えよう。まず、アプリケーション起動時にCreateMutex(NULLポインタ, FALSE, 適当な名前)として、ミューテックスオブジェクトを作成する(ミューテックスが既に存在している場合、CreateMutexは既存のミューテックスをオープンする)。次に、WaitForSingleObject(ミューテックスオブジェクトのハンドル, 0)を実行する。WAIT_TIMEOUTが返却された場合、他のプロセスによって既にミューテックスが所有されている=二重起動している、ということになるので、メッセージを表示して終了する。アプリケーション終了時には忘れずにReleaseMutexを実行しよう。
自アプリケーションからWordやExcelなどを起動し、その終了を待機する方法。以下に、良くある回答の例を示す。ただし、これらの方法を使用してペイントブラシの終了を待機しようとしても、うまく行かないという質問も良く見受けられる。これはpbrush.exeが単に、ペイントブラシの実体であるmspaint.exeを起動するだけのものであり、mspaint.exeを起動するとpbrush.exeはすぐに終了してしまうからである。
FindWindowは、指定されたタイトルのウィンドウを検索し、見つかった場合そのウィンドウのハンドルを返す関数である。従って、起動したアプリケーションのメインウィンドウのタイトルをキーにしてFindWindowを呼び出し、実行に成功した場合はまだ終了していないと判断し、実行に失敗した場合は終了した、と判断することができる。
この方法の欠点は、同一のタイトルを持つウィンドウが存在する場合に正しく動作しない、ということである。特に同一タイトルのフォルダウィンドウで誤認識する可能性がある。(なお、「ウィンドウクラス」の指定によってフォルダと区別することができるが、同一のタイトル・同一のウィンドウクラスのウィンドウが存在すれば、やはり破綻する)
Windowsでは、プロセスは終了時に「終了コード」を返すことができる。終了コードは、例えばプロセスが正常終了したのか、異常終了したのか等を表すために使用される。通常、正常終了の場合は0、異常終了の場合は0以外を終了コードとして返すのが慣習である。
GetExitCodeProcessは、指定されたプロセスの終了コードを取得する関数である。では、指定したプロセスが終了していない場合は、どのような値が返されるのか? 実は、この場合は、STILL_ACTIVE (= &H103)という値が返されることになっている。
従って、起動したアプリケーションに対するGetExitCodeProcessの戻り値がSTILL_ACITVEの間ループするようなコードを書くことにより、起動したアプリケーションの終了を待機することができる。
この方法は、FindWindowに比べればまだまし、というレベルである。例えば、あるアプリケーションは、異常終了時にWindowsで規定されているエラーコードを返す仕様になっているとする。&H2は「ファイルが見つからなかった」、&H6Fは「ファイル名が長すぎる」、&H103は「必要な個数のファイルが見つからなかった」、...ちょっと待て、&H103はSTILL_ACTIVEと同じ値だぞ。
この場合、GetExitCodeProcessは永久にSTILL_AVITVEと同じ値を返すことになり、ループは無限ループとなる。したがって、終了を待機しているアプリケーションはハングアップの状態になってしまうだろう。
「起動するアプリケーションから返される終了コードは全て把握している。&H103が返されることがないと分かっているので問題ない」という意見は一見正しそうに思えるが、実際のところ終了コードは他のプロセスからも設定することが可能なので、実は誤りである。
VBのShell関数でアプリケーションを起動した場合、「プロセスID」が返される。プロセスIDは、プロセスを識別するためにシステムが各プロセスに割り振る識別番号である。Shell関数は、起動したプロセスを識別するプロセスIDを返す。
プロセスIDを引数としてOpenProcessを実行すると、「プロセスハンドル」が返される。ファイルシステムにたとえると、プロセスIDがファイル名に対応し、プロセスのオープンがファイルのオープンに対応する。ファイルの内容を操作するには、ファイルをオープンし、ファイルハンドルを取得しなければならない。同様に、プロセスの内容を操作するには、プロセスをオープンし、プロセスハンドルを取得しなければならない。
プロセスハンドルを取得したら、そのプロセスに対する種々の操作を行うことができる。その一つに、「待機」が挙げられる。Windowsでは、あるオブジェクトが「シグナル状態」になるまでの間、WaitForSingleObjectなどの「待機関数」と呼ばれる関数で、待機することができる。ミューテックスオブジェクトの場合、そのミューテックスオブジェクトが所有されているならば「非シグナル状態」、所有されていないならば「シグナル状態」である。プロセスの場合、プロセスが実行中ならば「非シグナル状態」、終了していれば「シグナル状態」である。
従って、起動したプロセスのプロセスハンドルを引数としてWaitForSingleObjectを実行すると、そのプロセスが終了するまでの間待機することができる。
この方法は簡単で良い方法である。ただし、厳密な方法ではない。Shell関数実行後、OpenProcessを実行するまでの間に、(1)実行したプロセスが終了してしまい、(2)他のプロセスが起動され、同一のプロセスIDが割り振られた、という状況の場合、別のアプリケーションの終了を待機してしまうことになる。
CreateProcessを使用したソースでよく見かける誤りは、待機終了後にプロセスハンドル(および、同様にCreateProcessで返された「スレッドハンドル」)をクローズしていないバグである。この場合リソースが解放されていない状態になってしまうので、CreateProcessを行った際には、最後に必ずプロセスハンドルをCloseHandleでクローズしなければならない。
なおVBで上記のような実装を行う場合、ループや、WaitForSingleObjectによって待機を行う際には、適当なタイミングでDoEventsを呼び出さないと画面が「凍って」しまうので、注意が必要。