今回はアプリケーションの二重起動の防止をやってみたいと思います。とは言っても、今回やるのは、起動されていたらただメッセージを表示して終了するだけの、あまり役に立たなそうなものです。
自分自身がすでに起動しているかどうかを知るためには、一般的にミューテックスオブジェクトというカーネルオブジェクトが使われるようです。ということで、まずはミューテックスオブジェクトの説明から。
Windows ではいろいろなものが『オブジェクト』というものとして扱われていて、それらは大別してカーネルオブジェクト、GDIオブジェクト、ユーザーオブジェクトの3つがあるということは、以前に説明したと思います。
また、そのカーネルオブジェクトの中には、スレッド間の同期を取ることを目的としたオブジェクト(同期オブジェクト)があり、それら同期オブジェクトは『シグナル状態』と『ノンシグナル状態』という2つの状態があることも述べましたよね。
Windowsオブジェクトについては、『共有メモリを使ってみる』の項を、また、同期オブジェクトについては『サブプロセスの終了を待つ』の項を参照してください。
Windowsの同期オブジェクトの1つに、『ミューテックス(mutex)』というものがあります。ミューテックスオブジェクトは、一言で言えば、「ある1つのスレッドに所有されている状態」と、「どのスレッドにも所有されていない状態」の2つの状態を持つオブジェクトです。この2つのうち、「所有されている状態」がノンシグナル状態であり、「所有されていない状態」がシグナル状態となります。
さて、上のように難しい説明してきましたが、残念ながら今回は、ミューテックスオブジェクトで最も重要である『被所有者』としての機能は、ここではまったく使われません。本来なら、この『被所有者』としての機能と『待機関数』を組み合わせることで、複数のプロセス間でデータアクセスの同期を取るために使われるのですが。
今回は、「システム中にただ1つだけ存在する」という、名前付きカーネルオブジェクトの特性を利用することになります。ファイルマッピングオブジェクトのときと同様に、ミューテックスオブジェクトには任意の名前をつけることが出来て、その名前を指定すれば他のどのプロセスからでも同一のオブジェクトを扱うことができるのです。この特徴のおかげで、すでに同じアプリケーションが起動しているかどうかを、指定された名前(そのアプリケーション特有の名前)のミューテックスオブジェクトがすでに存在するかどうかということから知ることができるのです。
このように考えてくると、「ミューテックスオブジェクトでなくて、他の名前付きカーネルオブジェクト(例えばファイルマッピングオブジェクト)を使ってもいいんじゃないか?」などと考えることでしょう。実はその通りで、ミューテックスオブジェクトでなくてもいいのです。でも、一般的にはミューテックスオブジェクトが最も多く使われているようです。これは筆者の推測ですが、ミューテックスオブジェクトは「所有されているかどうか」のフラグのみを管理すればいいので、他のカーネルオブジェクトと比べてオブジェクト管理のために消費されるメモリが少なくて済むためではないかと考えられます。
さて、ミューテックスオブジェクトを使って、自分自身がすでに起動しているかどうかを取得する手順を説明していくことにしましょう。
アプリケーションは、起動したらまずアプリケーション特有の名前をつけたミューテックスオブジェクトを作成します。その際に、すでに同じ名前のミューテックスが存在していれば、もうすでにアプリケーションが起動していることになります。逆に、ミューテックスを作成したときに同じ名前のミューテックスがまだ存在していなければ、それがはじめて起動されたものだということになります。
新しいミューテックスを作成するにはCreateMutex関数を使います。
HANDLE CreateMutexA( PSECURITY_ATTRIBUTES psa, // セキュリティ指定子 BOOL bInitialOwner, // 所有権指定フラグ PCTSTR pszMutexName // オブジェクトの名前 );
第1引数(psaパラメータ)にはセキュリティ関係の構造体のポインタを指定するのですが、通常は0 (NULL) を指定しておけば問題ありません。
第2引数(bInitialOwnerパラメータ)には、呼び出したスレッドがミューテックスの所有権を強制的に取得するかどうかを指定します。所有権を取得するのなら1 (TRUE) を、所有権を取得しないのなら0 (FALSE) を指定します。しかし、今回はミューテックスの所有権は問題にならないので、どちらでもかまいません。
第3引数(pszMutexNameパラメータ)には、ミューテックスオブジェクトの名前を格納した文字列変数のアドレスを指定します。ミューテックスオブジェクトが、自分のアプリケーションが作成したものかを識別するために、アプリケーション固有の名前をつけます。いくつかのミューテックスを作成したい場合は、それぞれ別の名前をつけるようにします。(今回は1つだけで十分ですが。)
この関数が成功すると、ミューテックスオブジェクトのハンドルが返ります。このハンドルは、作成されたミューテックスオブジェクトを識別するために必要になるものです。また、作成に失敗した場合には0 (NULL) が戻り値として返されます。
さて、pszMutexNameパラメータで指定された名前のミューテックスオブジェクトがすでに存在しているかどうかを取得するには、CreateMutex関数を実行した直後にGetLastError関数を呼び出します。もしもこの関数の戻り値が183 (ERROR_ALREADY_EXISTS) ならば、すでに同じ名前のミューテックスオブジェクトが存在していたことになります。これは、ファイルマッピングオブジェクトのときとまったく同じです。
ミューテックスオブジェクトは、不要になったらCloseHandle関数でハンドルをクローズします。オブジェクトハンドルはプロセス終了時に自動的にクローズされることになっているので、必ずしなければならないというわけではありませんが、クローズするように心がけましょう。
実際にスクリプトを作成してみましょう。
今回はモジュールを使ってみます。モジュール内で定義されているGetDoubleExe命令は、固有の名前を持つミューテックスオブジェクトを作成して、すでにオブジェクトが存在した場合にはstatに1を、存在しなかった場合には0を格納しています。また、QuitDoubleExe命令は、ミューテックスオブジェクトのハンドルをクローズします。この命令は、プログラム終了時に自動的に実行されるようにonexit指定されています。
また、モジュール内では、GetLastError関数を呼び出すのに、『Win32 APIのエラー処理』の項で作成したエラーコード取得モジュールapierr.asの中のgeterrcode 命令を使用しています。そのため、apierr.asをインクルードしておく必要があります。
#include "llmod.as" #include "apierr.as" ; エラーコード取得モジュール #module ;--------すでに起動されているかどうかを取得するモジュール--------- ; ミューテックスオブジェクトの名前の定義 ; (アプリケーション固有の名前にする必要があります) #define MUTEX_NAME "HSP_API_Test" ; すでに起動されているかどうかを取得する命令 #deffunc GetDoubleExe mref stt, 64 ; システム変数 stat sdim mutexname, 256 mutexname = MUTEX_NAME ; いったん別の変数に格納する ; ミューテックスオブジェクトの作成 pm.0 = 0 ; セキュリティ指定(デフォルト指定) pm.1 = 0 ; 所有権指定フラグ getptr pm.2, mutexname ; 名前を表す文字列のポインタ dllproc "CreateMutexA", pm, 3, D_KERNEL hMutex = stat ; オブジェクトハンドル ; オブジェクトが作成されていたかどうかの判別 geterrcode ; GetLastError関数によるエラーコード取得 if stat == 183 { ; ERROR_ALREADY_EXISTS stt = 1 ; すでに同じ名前のオブジェクトが存在する } else { stt = 0 ; オブジェクトが新しく作成された } return ; オブジェクトハンドルのクローズ(終了時に自動実行) #deffunc QuitDoubleExe onexit if hMutex { dllproc "CloseHandle", hMutex, 1, D_KERNEL } return #global ;------------------------モジュール終わり------------------------- GetDoubleExe if stat { dialog "すでに起動されています。" end } stop
上のスクリプトを実行させると、2度目以降に実行したものはメッセージボックスが出て終了します。ちなみに、起動している状態で、ミューテックスオブジェクトの名前(モジュール空間中で定数名MUTEX_NAMEで定義されている名前)を変えて実行させると、別のミューテックスオブジェクトが新しく作成されるので、ダイアログが出ません。
今回はすでに起動されているかどうかを知ることしかできませんでしたので、次回はもっと高度な手法による実用的な二重起動防止処理をやってみます。