二重起動を防止してみる ACT-2

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

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

メッセージによる通知

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

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

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

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

ところで、メッセージを渡すにはPostMessage関数によるメッセージのポストSendMessage関数によるメッセージの送信がありますが、SendMessage関数は、メッセージ処理が完了するまでスレッドを止めてしまうという特性上、デッドロック(スレッド同士で互いのスレッドの作業が終わるのを待ってしまう結果、プログラムそのものが停止してしまうこと)が発生するプログラムを作ってしまう可能性が高くなってしまいます。これは、hsgetmsg.dllを使ってメッセージ取得を行なっている限りはありえないことですが、メッセージの割り込み処理機能を使った場合には十分にありえることです。したがって、ここでは PostMessage関数を使ってメッセージを送ることにします。

あえてメッセージ処理を待つ必要がある場合にはSendMessage関数を使うことになりますが、デッドロックを起こさないように十分注意してプログラミングを行なう必要があります。(hsgetmsg.dllでは意味がありませんが。)

手順

以上のことから、手順はだいたい以下のようになるかと思います。

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

サンプルスクリプト

今回使用するAPI関数は以前に説明したものばかりですので、詳しい説明は要らないでしょう。

処理を各部分に分けてみていくことにします。

起動時(共通)

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

  1. 共有メモリのためのファイルマッピングオブジェクトを作成する
  2. ファイルマッピングオブジェクトがすでに作成されていたかを調べる
    #include "llmod.as"
    #include "hsgetmsg.as"
    #include "apierr.as"         ; エラーコード取得モジュール

    #define MYWM_DBEXE  0x0410   ; アプリケーション固有メッセージ

    mapobjname = "urawaza_mapobj"    ; ファイルマッピングオブジェクトの名前

    ; ファイルマッピングオブジェクトの作成
    pm.0 = -1                    ; 0xFFFFFFFF
    pm.1 = 0                     ; NULL
    pm.2 = 4                     ; PAGE_READWRITE
    pm.3 = 0
    pm.4 = 1024                  ; ファイルマッピングオブジェクトのサイズ
    getptr pm.5, mapobjname      ; オブジェクト名のアドレス
    dllproc "CreateFileMappingA", pm, 6, D_KERNEL
    hmapobj = stat               ; オブジェクトのハンドル

    ; オブジェクトが作成されていたかどうかの判別
    geterrcode                   ; GetLastError関数によるエラーコード取得
    if stat == 183 {             ; ERROR_ALREADY_EXISTS
        goto *lb_exist           ; すでに同じ名前のマッピングオブジェクトが
                                 ; 存在する場合
    } else {
        goto *lb_new             ; マッピングオブジェクトが存在しない場合
    }

アプリケーション固有のメッセージコードとし0x0410MYWM_DBEXEという定数名で定義してあります。これは、あとでメッセージをやり取りする際に使用するものです。ここに指定するメッセージ別の値でもいいですが、値は0x0400以上のものにする必要があります。それ以外のものを使うと、動作がおかしくなる可能性があります。

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

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

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

  1. ウィンドウをサブクラス化して、メッセージを取得するように設定する
  2. 共有メモリにメインウィンドウのハンドルを格納する
  3. メッセージが送られてくるのを待つ
  4. 終了時にファイルマッピングオブジェクトをクローズ
*lb_new
    onexit *lb_on_exit

    ; ウィンドウのサブクラス化
    set_subclass
    hwnd = stat                  ; ウィンドウハンドル
    set_message MYWM_DBEXE       ; メッセージ設定(固有メッセージ)

    ; ビューのマッピング
    pm.0 = hmapobj               ;マッピングオブジェクトハンドル
    pm.1 = 2                     ; FILE_MAP_WRITE(読み書きアクセス)
    pm.2 = 0                     ; オフセット(上位)
    pm.3 = 0                     ; オフセット(下位)
    pm.4 = 0                     ; サイズ(オブジェクト全体を指定)
    dllproc "MapViewOfFile", pm, 5, D_KERNEL
    lpdata = stat                ; データのアドレス

    ; ウィンドウハンドルを共有メモリに格納
    ll_poke4 hwnd, lpdata 

    ; ビューをマッピング解除
    dllproc "UnmapViewOfFile", lpdata, 1, D_KERNEL

    dup msg, msgval.1
    dup wprm, msgval.2
    dup lprm, msgval.3

*mainloop
    get_message
    if msgval {
        if msg == MYWM_DBEXE {
            ; メッセージを受け取ったとき
            gsel 0, 2
            dialog "起動されました"
            gsel 0, 1
        }
    } else {
        wait 10
    }
    goto *mainloop

*lb_on_exit
    ; ファイルマッピングオブジェクトのクローズ(終了時)
    dllproc "CloseHandle", hmapobj, 1, D_KERNEL
    end

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

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

  1. 共有メモリからウィンドウハンドルを読み込む
  2. メッセージを送る
  3. ファイルマッピングオブジェクトをクローズ
*lb_exist
    ; ビューのマッピング
    pm.0 = hmapobj               ; マッピングオブジェクトハンドル
    pm.1 = 4                     ; FILE_MAP_READ(読み込み専用アクセス)
    pm.2 = 0                     ; オフセット(上位)
    pm.3 = 0                     ; オフセット(下位)
    pm.4 = 0                     ; サイズ(オブジェクト全体を指定)
    dllproc "MapViewOfFile", pm, 5, D_KERNEL
    lpdata = stat                ; データのアドレス

    ; 共有メモリからウィンドウハンドル取得
    ll_peek4 hwnd, lpdata

    ; ビューをマッピング解除
    dllproc "UnmapViewOfFile", lpdata, 1, D_KERNEL

    ; メッセージをポスト
    pm.0 = hwnd                  ; ウィンドウハンドル
    pm.1 = MYWM_DBEXE            ; メッセージコード
    pm.2 = 0                     ; wParam
    pm.3 = 0                     ; lParam
    dllproc "PostMessageA", pm, 4, D_USER

    ; ファイルマッピングオブジェクトのクローズ
    dllproc "CloseHandle", hmapobj, 1, D_KERNEL
    end

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

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