Qtの応用 - 多重起動の禁止

提供:MochiuWiki : SUSE, EC, PCB
ナビゲーションに移動 検索に移動

概要

ソフトウェアのインスタンスを1つだけ実行(多重起動を禁止)する理由として、
メモリリークの問題を制限する、または、リソース、ファイル、データベース等をアプリケーションの2つのインスタンスが奪い合うことで起こる問題を排除するためである。
また、原則として、ユーザが使用するアプリケーションのコピーは1つだけでよい場合もある。

多重起動を禁止する方法には、以下に示す2つの方法がある。

  • QLockFileの使用
    一時的なファイルが作成されて、ソフトウェア終了時に破棄する。
    これは、ソフトウェアを多重起動された時、その一時ファイルの存在を確認して、一時ファイルが存在するならばインスタンスを自動的に閉じる。

  • QSystemSemaphoreとQSharedMemoryの使用
    共有メモリセグメントを作成して、一意の識別子を持つ既存のセグメントに接続する。
    接続に成功した場合、ソフトウェアのインスタンスが既に作成されているため、そのことをユーザに通知してソフトウェアを終了する。
    接続に失敗した場合、ソフトウェア用のメモリセグメントを作成して、最初のインスタンスを実行する。



QLockFileの使用

ソフトウェアの起動時に作成された一時ファイル(Lock File)の作成に失敗する場合、
既にソフトウェアのインスタンスが存在すると仮定して、ユーザに通知してソフトウェアを終了する。

QLockFileは、ソフトウェアの実行インスタンスの数を制限する。
ただし、QLockFileの使用は、ユーザのパーミッションに問題がある場合は使用できない可能性がある。

※注意
<uniq id>は、任意のIDに置き換えること。

 #include <QApplication>
 #include <QLockFile>
 #include <QDir>
 #include <QMessageBox>
 #include "CMainWindow.h"
 
 int main(int argc, char *argv[])
 {
    QApplication a(argc, argv);
 
    QLockFile lockFile(QDir::temp().absoluteFilePath("<uniq id>.lock"));
 
    // Trying to close the Lock File, if the attempt is unsuccessful for 100 milliseconds, 
    // then there is a Lock File already created by another process. 
    // Therefore, we throw a warning and close the program
    if(!lockFile.tryLock(100))
    {
       QMessageBox msgBox;
       msgBox.setIcon(QMessageBox::Warning);
       msgBox.setText("The application is already running.\n"
                      "Allowed to run only one instance of the application.");
       msgBox.exec();
 
       return -1;
    }
 
    CMainWindow w;
    w.show();
 
    return a.exec();
 }



QSystemSemaphoreとQSharedMemoryの使用

QSharedMemoryは、1台のPC上において、同時に作業しているすべてのユーザーに共有されるものである。
これは、システム全体として、ソフトウェアの単一インスタンスの実行を制限する場合等に使用する。
したがって、1人のユーザがソフトウェアを実行する場合、他のユーザは同一のソフトウェアを実行することはできない。

しかし、各プラットフォームによる共有メモリの違いが存在する。
Windowsの場合、共有メモリはプログラムが正常に終了した時および緊急事態が発生した時に解放される。
Linux / Unixの場合、緊急時にクラッシュしたメモリは解放されない。

以下の例では、同一のソフトウェアの複数のインスタンスが同時に起動した場合の問題を解決するために使用される。

セマフォはメータで作成されて、その最大数は1になる。
セマフォを解除する時、ソフトウェアの他の全てのインスタンスは共有メモリにアクセスできないため、完全に所有するリソースの1つのコピーを持つ。
このコピーは、ソフトウェアに対応する共有メモリセグメントの識別子の存在により、ソフトウェアの別のインスタンスの実行をチェックする。
共有メモリセグメントのコピーは、正常に起動する時かつ別のソフトウェアインスタンスの情報が見つからない場合に作成される。
その後、セマフォが下げられて、実行するソフトウェアの別のインスタンスが許可される。

※注意
<uniq id 1>および<uniq id 2>は、任意のIDに置き換えること。

 #include <QApplication>
 #include <QSystemSemaphore>
 #include <QSharedMemory>
 #include <QMessageBox>
 #include "CMainWindow.h"
 
 int main(int argc, char *argv[])
 {
    QApplication a(argc, argv);
 
    QSystemSemaphore semaphore("<uniq id 1>", 1);  // create semaphore
    semaphore.acquire(); // Raise the semaphore, barring other instances to work with shared memory
 
 #ifndef Q_OS_WIN32
    // in linux / unix shared memory is not freed when the application terminates abnormally,
    // so you need to get rid of the garbage
    QSharedMemory nix_fix_shared_memory("<uniq id 2>");
    if(nix_fix_shared_memory.attach())
    {
       nix_fix_shared_memory.detach();
    }
 #endif
 
    QSharedMemory sharedMemory("<uniq id 2>");  // Create a copy of the shared memory
    bool is_running;            // variable to test the already running application
 
    if(sharedMemory.attach())
    {  // We are trying to attach a copy of the shared memory
       // To an existing segment
       is_running = true;      // If successful, it determines that there is already a running instance
    }
    else
    {
       sharedMemory.create(1); // Otherwise allocate 1 byte of memory
       is_running = false;     // And determines that another instance is not running
    }
    semaphore.release();       
 
    // If you already run one instance of the application, then we inform the user about it
    // and complete the current instance of the application
    if(is_running)
    {
       QMessageBox msgBox;
       msgBox.setIcon(QMessageBox::Warning);
       msgBox.setText("The application is already running.\n" 
                      "Allowed to run only one instance of the application.");
       msgBox.exec();
 
        return -1;
    }
 
    CMainWindow w;
    w.show();
 
    return a.exec();
 }



QLocalServer / QLocalSocketの使用

まず、QSharedMemoryクラスを使用して、アプリケーションの起動時に単一のインスタンスのみが実行されることを保証する。

次に、アプリケーションが起動する時、まず共有メモリへのアタッチを試みる。
成功した場合は、これは既に別のインスタンスが実行中であることを意味するため、新しいインスタンスは即座に終了する。
一方、アタッチに失敗した場合は、アプリケーションは新しい共有メモリセグメントを生成しようとする。
新しい共有メモリセグメントの生成が成功した場合は、現在のインスタンスが最初の実行であると判断する。

また、単に多重起動を防ぐだけでなく、既存のインスタンスと新規インスタンス間でコミュニケーションを取ることも重要である。
これを、QLocalServerQLocalSocketを使用して行う。

  1. 最初のインスタンスは、QLocalServerを使用してローカルサーバを起動する。
  2. このローカルサーバは、特定のキー (例: "/tmp/MyAppServer") で識別される。
  3. その後、新規インスタンスが起動しようとした場合、QLocalSocketを使用して既存のインスタンスに接続を試みる。
  4. 接続が確立された場合、新しいインスタンスは既存のインスタンスにメッセージを送信する。
    例えば、「新しいインスタンスが起動を試みました」といったメッセージを送信することができる。
  5. 既存のインスタンス側では、このメッセージを受信して、適切に対応することができる。
    例えば、アプリケーションウィンドウを前面に持ってくる等の動作が考えられる。


QLocalServer / QLocalSocketを使用したメリットは、単に多重起動を防ぐだけでなく、ユーザの意図 (新しいインスタンスを起動しようとした) を既存のインスタンスに伝達できる点である。
既存のインスタンスと通信する機能により、ユーザエクスペリエンスを向上させることができ、より柔軟で拡張性の高い設計になっている。

処理の流れを以下に示す。

  1. QSharedMemoryを使用して、アプリケーションの単一インスタンスを保証する。
  2. QLocalServerQLocalSocketを使用して、既存のインスタンスと新しいインスタンス間で通信を行う。
  3. SingleInstanceクラスのインスタンスを生成して、単一インスタンス制御のロジックをカプセル化する。
  4. SingleInstanceクラスのtryToRunメソッド実行して、アプリケーションが実行可能かどうかを確認する。
  5. 既存のインスタンスが検出された場合、sendMessagスロットを使用してそのインスタンスにメッセージを送信する。
    ロック向け一時ファイルが、/tmp/MyAppServerファイルが自動的に生成される。
  6. 既存のインスタンスは、messageReceivedシグナルを通じてメッセージを受信して、適切に対応する。
    (例: ウインドウのアクティブ化等)


必要に応じて、messageReceivedシグナルのハンドラを拡張して、既存のウインドウをアクティブにする等の処理を追加する。

QLocalServerQLocalSocketを使用するには、Qt Networkモジュールが必要となる。

  • Qtプロジェクトファイル (.pro) を使用する場合
 QT += network


  • CMakeLists.txtファイルを使用する場合
 find_package(Qt6 REQUIRED COMPONENTS Network)
 
 target_link_libraries(<ターゲット名> PRIVATE
    Qt6::Network
 )


 // SingletonInstance.hファイル
 
 #include <QLocalServer>
 #include <QLocalSocket>
 #include <QSharedMemory>
 #include <QDataStream>
 
 class SingletonInstance : public QObject
 {
    Q_OBJECT
 
 private:
    QString         m_key;
    QSharedMemory   m_sharedMemory;
    QLocalServer    *m_pServer;
 
 public:
    SingletonInstance(const QString &key) : m_key(key), m_sharedMemory(key), m_pServer(new QLocalServer(this))
    {
       connect(m_pServer, &QLocalServer::newConnection, this, &SingletonInstance::handleConnection);
    }
 
    bool tryToRun()
    {
       if (m_sharedMemory.attach()) {
          return false;  // 別のインスタンスが既に実行中
       }
 
       if (!m_sharedMemory.create(1)) {
          return false;  // 共有メモリの作成に失敗
       }
 
       if (!m_pServer->listen(m_key)) {
          return false;  // サーバの起動に失敗
       }
 
       return true;
    }
 
 public slots:
    void sendMessage(const QString &message)
    {
       QLocalSocket socket;
       socket.connectToServer(m_key);
       if (socket.waitForConnected(1000)) {
          QByteArray block;
          QDataStream out(&block, QIODevice::WriteOnly);
          out << message;
 
          socket.write(block);
          socket.flush();
          socket.waitForBytesWritten();
          socket.disconnectFromServer();
       }
    }
 
 signals:
    void messageReceived(const QString &message);
 
 private slots:
    void handleConnection()
    {
       QLocalSocket *pSocket = m_pServer->nextPendingConnection();
       if (pSocket) {
          connect(pSocket, &QLocalSocket::readyRead, this, [this, pSocket]() {
              QDataStream in(pSocket);
              QString message;
              in >> message;
 
              emit messageReceived(message);
              pSocket->deleteLater();
          });
       }
    }
 };


 // main.cppファイル
 
 #include <QApplication>
 #include <QMessageBox>
 #include "mainwindow.h"
 #include "SingletonInstance.h"
 
 int main(int argc, char *argv[])
 {
    QApplication app(argc, argv);
 
    SingletonInstance instance("/tmp/MyAppServer");
 
    if (!instance.tryToRun()) {
       // 別のインスタンスが既に実行中の場合、メッセージを送信して終了
       QMessageBox::warning(nullptr, "多重起動エラー", "多重起動を検出しました", QMessageBox::Ok);
       instance.sendMessage("新しいインスタンスが起動を試みました");
 
       return -1;
    }
 
    // 最初のインスタンスの場合、メインウィンドウをセットアップしてアプリケーションを実行
    QObject::connect(&instance, &SingletonInstance::messageReceived, [](const QString &message) {
       qDebug() << "受信したメッセージ: " << message;
       // ここでメッセージを処理する
       // 例: ウインドウを前面に表示する等
    });
 
    // メインウインドウのセットアップをここに記述
    MainWindow mainWindow;
    mainWindow.show();
 
    return app.exec();
 }