二重起動の防止(2)

より高度な二重起動防止機能

前回の二重起動防止は、単に同じアプリケーションが起動されているかどうかを判定することしかできませんでした。そこで今回は、2回目以降の起動時に、すでに起動されている側のプロセスに向けて、新しいプロセスが起動されたことを通知してみます。そして、最初に起動されていた側では、通知を受け取ったら、2つ目のプロセスが起動したことを表示するようにしてみます。

メッセージによる通知

2回目以降に起動されたプロセスからすでに起動されているプロセスに対して何か通知を行うには、初回起動のプロセスが所有するウィンドウにアプリケーション固有のメッセージを送ります。以前も説明した通り、ウィンドウメッセージはいくつもの種類があってそれぞれ役割が決められていますが、以前にも説明したとおり、メッセージコード0x0400 (WM_USER) から0xBFFFまでの範囲についてはアプリケーションが自由にその役割を定義してよいということになっています。これを使用すれば、すでに起動されているプロセスがそのメッセージを取得することで再び起動されたことを知ることができます。

ここで1つ問題が出てきます。ウィンドウにメッセージを送るためのPostMessage関数やSendMessage関数は、その引数として対象となるウィンドウのハンドルを必要とします。すなわち、メッセージを送るためには対象となるウィンドウのハンドルを知らなくてはなりません。

そこで今回は、初回起動かどうかを判別するための名前付きカーネルオブジェクトとして、ミューテックスではなくファイルマッピングオブジェクトを採用し、これを使って共有メモリを作成してしまいましょう。そして、最初に起動されたプロセスのウィンドウハンドルを共有メモリに格納してしまえばよいのです。そうすれば、2回目以降に起動されたプロセスは、その共有メモリからウィンドウハンドルを取得して、そのハンドルの示すウィンドウにメッセージを送ることができます。

メッセージの送信・ポストについては、『メッセージの送信とポスト』の項を参照してください。また、ファイルマッピングオブジェクトについては『プロセス間共有メモリ』の項を参照してください。

ところで、メッセージを渡すにはPostMessage関数によるメッセージのポストSendMessage関数によるメッセージの送信があります。以前にも説明したとおり、SendMessage関数は、相手側でのメッセージ処理が完了するまで呼び出し元のプログラムの実行を停止して待機します。一方、PostMessage関数はメッセージをポストするのみで、相手のメッセージ処理を待たずに直ちに次の処理を行います。どちらを使用するかはプログラムの動きによって異なってきますが、単純に通知するのみならばPostMessage関数で問題ないでしょう。

サンプルスクリプト

大まかな処理の手順はだいたい以下のようになるかと思います。

  1. 共有メモリのためのファイルマッピングオブジェクトを作成する
  2. ファイルマッピングオブジェクトがすでに作成されていたかを調べる

処理を各部分に分けてみていくことにします。今回使用するWindows APIは以前に説明したものばかりですので、詳しい説明は省きます。

起動時(共通)

まずは以下の処理をするためのスクリプトです。

  1. 共有メモリのためのファイルマッピングオブジェクトを作成する
  2. ファイルマッピングオブジェクトがすでに作成されていたかを判定する
; ファイルマッピングオブジェクトの名前の定義
; (アプリケーション固有の名前にする必要があります)
#define MAP_OBJ_NAME  "HSP_WinAPI_Test_Mapping_Object"

; ファイルマッピングオブジェクトの作成
sharedMemSize = 1024
hMapObj = CreateFileMapping(-1,0,PAGE_READWRITE,0,sharedMemSize,MAP_OBJ_NAME)

; ファイルマッピングオブジェクトが作成されていたかどうかの判定
if (GetLastError() == ERROR_ALREADY_EXISTS) {
    ; すでに同じ名前のオブジェクトが存在する
    goto *SecondRunProc
} else {
    ; オブジェクトが新しく作成された
    goto *FirstRunProc
}

ファイルマッピングオブジェクトは、同じ名前を指定すればどのプロセスからでもそのオブジェクトを操作できてしまいます。そのため、これらの名前はアプリケーション固有のものにして、他のアプリケーションが使うオブジェクトと重ならないようにしましょう。

ファイルマッピングオブジェクト未作成時(初回起動時)

ファイルマッピングオブジェクトが作成されていなかった場合(初回起動の場合)の処理です。

  1. ウィンドウメッセージを取得するように設定する
  2. 共有メモリにメインウィンドウのハンドルを格納する
  3. メッセージが送られてきたらメッセージを表示
  4. 終了時にファイルマッピングオブジェクトをクローズ
; プログラム起動通知用ウィンドウメッセージを定義
#define MYWM_RUN_NOTIFY  (WM_USER + 1)

*FirstRunProc
; プログラム終了時処理の登録
onexit gosub *OnAppQuit

; 割り込み処理メッセージの登録
oncmd gosub *OnRunNotify, MYWM_RUN_NOTIFY

; ビューのマッピング
sharedMemPtr = MapViewOfFile(hMapObj,FILE_MAP_WRITE,0,0,sharedMemSize)

; ウィンドウハンドルを共有メモリに格納
dupptr sharedMemVal, sharedMemPtr, sharedMemSize
sharedMemVal(0) = hwnd

; ビューのマッピング解除
UnmapViewOfFile sharedMemPtr

sharedMemPtr = 0
dim sharedMemVal, 1
stop

*OnRunNotify
; 二重起動通知メッセージを受け取ったときの処理
gsel 0, 2
dialog "起動されました"
gsel 0, 1
return

*OnAppQuit
; ファイルマッピングオブジェクトのクローズ(終了時)
CloseHandle hMapObj
end

今回はアプリケーション固有のウィンドウメッセージコードとしてMYWM_RUN_NOTIFYという定数名を定義しています。これは、プログラム起動を通知する際に使用するものです。ここで使用する値は0x0400 (WM_USER) から0xBFFFの範囲にする必要があります。それ以外のものを使うと、動作がおかしくなる可能性があるので注意してください。

マッピングオブジェクト作成時(2回目以降起動)

マッピングオブジェクトが作成されていた場合(2回目以降の起動の場合)の処理です。

  1. 共有メモリからウィンドウハンドルを読み込む
  2. メッセージを送る
  3. ファイルマッピングオブジェクトをクローズして終了
*SecondRunProc
; ビューのマッピング
sharedMemPtr = MapViewOfFile(hMapObj,FILE_MAP_READ,0,0,sharedMemSize)

; 共有メモリからウィンドウハンドル取得
dupptr sharedMemVal, sharedMemPtr, sharedMemSize
hwndTarget = sharedMemVal(0)

; ビューのマッピング解除
UnmapViewOfFile sharedMemPtr
sharedMemPtr = 0
dim sharedMemVal, 1

; メッセージをポスト
PostMessage hwndTarget, MYWM_RUN_NOTIFY, 0, 0

; ファイルマッピングオブジェクトのクローズ
CloseHandle hMapObj
end

サンプルスクリプト(まとめ)

; Windows API 関数定義
#uselib "kernel32.dll"
#func  CloseHandle  "CloseHandle"  sptr
#cfunc CreateFileMapping  "CreateFileMappingA" sptr,sptr,sptr,sptr,sptr,sptr
#cfunc GetLastError  "GetLastError"
#cfunc MapViewOfFile  "MapViewOfFile" sptr,sptr,sptr,sptr,sptr
#func  UnmapViewOfFile  "UnmapViewOfFile" sptr

#uselib "user32.dll"
#func PostMessage  "PostMessageA" sptr,sptr,sptr,sptr
#func SetForegroundWindow  "SetForegroundWindow" sptr

; Windows API 定数定義
#define FILE_MAP_WRITE        2
#define FILE_MAP_READ         4
#define PAGE_READWRITE        4
#define ERROR_ALREADY_EXISTS  183
#define WM_USER               0x0400

; プログラム起動通知用ウィンドウメッセージを定義
#define MYWM_RUN_NOTIFY  (WM_USER + 1)

; ファイルマッピングオブジェクトの名前の定義
; (アプリケーション固有の名前にする必要があります)
#define MAP_OBJ_NAME "HSP_WinAPI_Test_Mapping_Object"

; ファイルマッピングオブジェクトの作成
sharedMemSize = 1024
hMapObj = CreateFileMapping(-1,0,PAGE_READWRITE,0,sharedMemSize,MAP_OBJ_NAME)

; ファイルマッピングオブジェクトが作成されていたかどうかの判別
if (GetLastError() == ERROR_ALREADY_EXISTS) {
    ; すでに同じ名前のオブジェクトが存在する
    goto *SecondRunProc
} else {
    ; オブジェクトが新しく作成された
    goto *FirstRunProc
}

*FirstRunProc
; プログラム終了時処理の登録
onexit gosub *OnAppQuit

; 割り込み処理メッセージの登録
oncmd gosub *OnRunNotify, MYWM_RUN_NOTIFY

; ビューのマッピング
sharedMemPtr = MapViewOfFile(hMapObj,FILE_MAP_WRITE,0,0,sharedMemSize)

; ウィンドウハンドルを共有メモリに格納
dupptr sharedMemVal, sharedMemPtr, sharedMemSize
sharedMemVal(0) = hwnd

; ビューのマッピング解除
UnmapViewOfFile sharedMemPtr

sharedMemPtr = 0
dim sharedMemVal, 1
stop

*OnRunNotify
; 二重起動通知メッセージを受け取ったときの処理
gsel 0, 2
dialog "起動されました"
gsel 0, 1
return

*OnAppQuit
; ファイルマッピングオブジェクトのクローズ(終了時)
CloseHandle hMapObj
end

*SecondRunProc
; ビューのマッピング
sharedMemPtr = MapViewOfFile(hMapObj,FILE_MAP_READ,0,0,sharedMemSize)

; 共有メモリからウィンドウハンドル取得
dupptr sharedMemVal, sharedMemPtr, sharedMemSize
hwndTarget = sharedMemVal(0)

; ビューのマッピング解除
UnmapViewOfFile sharedMemPtr
sharedMemPtr = 0
dim sharedMemVal, 1

; メッセージをポスト
PostMessage hwndTarget, MYWM_RUN_NOTIFY, 0, 0

; ファイルマッピングオブジェクトのクローズ
CloseHandle hMapObj

; 対象ウィンドウを前面に表示
SetForegroundWindow hwndTarget

end

上のスクリプトをいくつか同時に起動させてみましょう。2つ目、3つ目と起動されていくたびに、最初に起動されたプロセスのウィンドウに、起動されたことを示す文字列が表示されていくことと思います。

今回はメッセージを送るだけでしたが、メッセージを送る際には2つの4バイトデータをwParamパラメータ、lParamパラメータとして送ることができますし、それ以上のサイズのデータ(例えばファイル名など)も共有メモリを使えば送ることができるでしょう。いろいろと各自で試してみてください。