【问题标题】:Qt: Best practice for a single instance app protectionQt:单实例应用保护的最佳实践
【发布时间】:2011-06-27 18:03:18
【问题描述】:

QSingleApplication? QMutex? QSharedMemory?我正在寻找可以在 Windows、OSX 和 Linux (Ubuntu) 中顺利运行的东西。使用 Qt 4.7.1

【问题讨论】:

标签: c++ qt qmutex qsharedmemory


【解决方案1】:

简单的解决方案,可以满足您的需求。没有网络依赖(如QtSingleApplication)并且没有任何开销。

用法:

int main()
{
    RunGuard guard( "some_random_key" );
    if ( !guard.tryToRun() )
        return 0;

    QAppplication a(/*...*/);
    // ...
}

RunGuard.h

#ifndef RUNGUARD_H
#define RUNGUARD_H

#include <QObject>
#include <QSharedMemory>
#include <QSystemSemaphore>


class RunGuard
{

public:
    RunGuard( const QString& key );
    ~RunGuard();

    bool isAnotherRunning();
    bool tryToRun();
    void release();

private:
    const QString key;
    const QString memLockKey;
    const QString sharedmemKey;

    QSharedMemory sharedMem;
    QSystemSemaphore memLock;

    Q_DISABLE_COPY( RunGuard )
};


#endif // RUNGUARD_H

RunGuard.cpp

#include "RunGuard.h"

#include <QCryptographicHash>


namespace
{

QString generateKeyHash( const QString& key, const QString& salt )
{
    QByteArray data;

    data.append( key.toUtf8() );
    data.append( salt.toUtf8() );
    data = QCryptographicHash::hash( data, QCryptographicHash::Sha1 ).toHex();

    return data;
}

}


RunGuard::RunGuard( const QString& key )
    : key( key )
    , memLockKey( generateKeyHash( key, "_memLockKey" ) )
    , sharedmemKey( generateKeyHash( key, "_sharedmemKey" ) )
    , sharedMem( sharedmemKey )
    , memLock( memLockKey, 1 )
{
    memLock.acquire();
    {
        QSharedMemory fix( sharedmemKey );    // Fix for *nix: http://habrahabr.ru/post/173281/
        fix.attach();
    }
    memLock.release();
}

RunGuard::~RunGuard()
{
    release();
}

bool RunGuard::isAnotherRunning()
{
    if ( sharedMem.isAttached() )
        return false;

    memLock.acquire();
    const bool isRunning = sharedMem.attach();
    if ( isRunning )
        sharedMem.detach();
    memLock.release();

    return isRunning;
}

bool RunGuard::tryToRun()
{
    if ( isAnotherRunning() )   // Extra check
        return false;

    memLock.acquire();
    const bool result = sharedMem.create( sizeof( quint64 ) );
    memLock.release();
    if ( !result )
    {
        release();
        return false;
    }

    return true;
}

void RunGuard::release()
{
    memLock.acquire();
    if ( sharedMem.isAttached() )
        sharedMem.detach();
    memLock.release();
}

【讨论】:

  • 这对我有用,如果应用程序崩溃,它将允许重新启动(这是我的问题),谢谢@SaZ
  • @SaZ 感谢您发布此代码 - 我刚刚在我的应用程序中尝试过 - 它第一次工作:)
  • 哇。工作完美!非常感谢 :) 只需要在 main 中包含 RunGuard.h。
  • 另一个问题:你不能只使用QSharedMemory::lock()而不是单独的QSystemSemaphore吗?
  • @AoeAoe 这是一个代码示例,没有限制。即使没有任何备注也可以随意使用。
【解决方案2】:

由于QtSingleApplication 相对过时且不再维护,我写了一个替代品,称为SingleApplication

它基于QSharedMemory 并使用QLocalServer 通知父进程新实例正在生成。它适用于所有平台,并且兼容支持 Qt 5。

完整的代码和文档可在here获取。

基本示例:

int main(int argc, char *argv[])
{
    SingleApplication app( argc, argv );

    return app.exec();
}

高级示例

除其他外,它还支持在新生成的实例和主实例之间发送消息,例如:

int main(int argc, char *argv[])
{
    SingleApplication app( argc, argv, true );

    if( app.isSecondary() ) {
        app.sendMessage(  app.arguments().join(' ')).toUtf8() );
        app.exit( 0 );
    }

    return app.exec();
}

【讨论】:

  • 感谢您发布此信息。您是否愿意接受添加“命令”以与连接一起发送的拉取请求?我想有选择地执行“打开文件”之类的命令。
  • signal( SIGILL, SingleApplicationPrivate::terminate ); // 4 LOL,你试过这个吗?我想没有。这东西不行。
  • @LtWorf 这到底是什么问题?
  • 对不起,我把它误读为 SIGKILL。无论如何它已经坏了,因为在 sigkill 的情况下它不会做任何清理。
  • 我知道我无法处理 SIGKILL,但我使用了另一个 hack 来处理这种情况。通过初始化然后显式删除QSharedMemory 实例,如果没有附加任何活动进程,内核将清除该块。
【解决方案3】:

您可以将QSharedMemory 与特定密钥一起使用,并检查是否可以创建具有该密钥的共享内存。如果它也不能创建它,那么一个实例已经在运行:

QSharedMemory sharedMemory;
sharedMemory.setKey("MyApplicationKey");

if (!sharedMemory.create(1))
{
    QMessageBox::warning(this, tr("Warning!"), tr("An instance of this application is running!") );

    exit(0); // Exit already a process running
}

【讨论】:

  • 解决方案不完整。有很多可能的故障。当应用程序崩溃时,您不会检查案例。您可以查看我的答案完整解决方案。
  • 这个解决方案确实非常不完整。 -1
【解决方案4】:

对于 Windows:

HANDLE g_app_mutex = NULL;

bool check_one_app_instance()
{
    g_app_mutex = ::CreateMutex(NULL, FALSE, L"8BD290769B404A7816985M9E505CF9AD64"); // this any different key as string
    if(GetLastError() == ERROR_ALREADY_EXISTS)
    {
        CloseHandle(g_app_mutex);
        return false;
    }

    return true;
}

【讨论】:

  • 问题是关于跨平台解决方案。
【解决方案5】:

我现在正在使用这个解决方案。

但是它的缺点是程序只能由用户运行一次,即使他们同时从多个位置登录。

singleinstance.h

#ifndef SINGLEINSTANCE_H
#define SINGLEINSTANCE_H

typedef enum {
    SYSTEM,
    SESSION,
} scope_t;

class SingleInstance
{
public:
    static bool unique(QString key, scope_t scope);
};

#endif // SINGLEINSTANCE_H

singleinstance.cpp

#include <QLockFile>
#include <QProcessEnvironment>

#include "singleinstance.h"

/**
 * @brief filename
 * @param key
 * @param scope
 * @return a fully qualified filename
 *
 * Generates an appropriate filename for the lock
 */
static QString filename(QString key, scope_t scope) {

    QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
    QString tmp = env.value("TEMP", "/tmp") + "/";
    QString user = env.value("USER", "alfio");


    QString r;                                                                                                                                                                         
    switch (scope) {                                                                                                                                                                   
        case SYSTEM:                                                                                                                                                                   
            r = tmp;                                                                                                                                                                   
            break;
        case SESSION:
            //FIXME this will prevent trabucco to run in multiple X11 sessions
            r = env.value("XDG_RUNTIME_DIR", tmp + user) + "/";
            break;
    }
    return r + key + ".lock";
}

/**
 * @brief SingleInstance::unique
 * @param key the unique name of the program
 * @param scope wether it needs to be system-wide or session-wide
 * @return true if this is the only instance
 *
 * Make sure that this instance is unique.
 */
bool SingleInstance::unique(QString key, scope_t scope) {
    QLockFile* lock = new QLockFile(filename(key, scope));
    bool r = lock->tryLock();
    if (!r)
        delete lock;
    return r;
}

【讨论】:

  • 如果你没有临时文件夹的写权限怎么办?而且性能更差。
  • 在一台正常工作的机器上,你确实可以访问它。如果你没有/tmp,你甚至不能进行图形登录。性能几乎无关紧要,因为它是在启动程序时完成的一次检查。
  • 这取决于您的平台。我为 Windows XP 创建了一个访问非常受限的应用程序。在运行时只提供了一个用于应用程序需求的可写文件夹和该文件夹的路径。所以检查锁定的文件是非常昂贵的操作。
  • 谁是 QLockFile* 锁的所有者?它似乎是一个悬空资源
【解决方案6】:

对于 Linux:

//------------------------------------------------

QProcess *m_prSystemCall;
m_prSystemCall = new QProcess();

QString Commnd = "pgrep  " + qApp->applicationDisplayName();
m_prSystemCall->start(Commnd);
m_prSystemCall->waitForFinished(8000);
QString output(m_prSystemCall->readAllStandardOutput());
QStringList AppList = output.split("\n", QString::SkipEmptyParts);
qDebug() <<"pgrep out:"<<AppList;
for(int i=0;i<AppList.size()-1;i++)
{
    Commnd = "kill " + AppList.at(i);
    m_prSystemCall->start(Commnd);
    m_prSystemCall->waitForFinished(8000);
}

//--------------------------------------------- ----------

对于 Windows:

#include <tlhelp32.h>
#include <comdef.h>

QString pName = qApp->applicationDisplayName();
pName += ".exe";
PROCESSENTRY32 entry;
entry.dwSize = sizeof(PROCESSENTRY32);

HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, NULL);

if (Process32First(snapshot, &entry) == TRUE)
{
    DWORD myPID =  GetCurrentProcessId();
    while (Process32Next(snapshot, &entry) == TRUE)
    {
        const WCHAR* wc = entry.szExeFile ;
        _bstr_t b(wc);
        const char* c = b;

        if (stricmp(c, pName.toStdString().c_str()) == 0)
        {
            HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, entry.th32ProcessID);

            qDebug() <<"myPID: "<< myPID << "entry.th32ProcessID" << entry.th32ProcessID;
            if(myPID != entry.th32ProcessID)
                TerminateProcess(hProcess,0);
            QThread::msleep(10);
            CloseHandle(hProcess);
        }
    }

}

CloseHandle(snapshot);

【讨论】:

    【解决方案7】:

    根据 Qt 的文档,如果进程在未调用其析构函数的情况下在类 Unix 操作系统下崩溃,则不会自动释放获得的 QSystemSemaphore。这可能是另一个试图获取相同信号量的进程出现死锁的原因。如果你想 100% 确定你的程序能正确处理崩溃并且你不坚持使用 Qt, 您可能希望使用操作系统在进程终止时自动释放的其他锁定机制——例如,lockf() 和传递给open()open() 标志,在How do I recover a semaphore when the process that decremented it to zero crashes?flock() 中提到。事实上,如果使用flock(),则不再需要创建共享内存。只需使用flock() 即可实现单实例应用保护。

    如果在 Unix 中从崩溃中恢复信号量无关紧要,我认为 RunGuard from Dmitry Sazonov's answer 仍然可以稍微简化:

    1. 析构函数 ~RunGuard()RunGuard::release() 可能会被取消,因为 QSharedMemory 在销毁时会自动从共享内存段中分离出来,就像在 Qt 的 QSharedMemory::~QSharedMemory() 文档中一样:“析构函数清除了密钥,这会强制共享内存对象与其底层共享内存段分离。”。

    2. RunGuard::isAnotherRunning() 也可以取消。目标是独家执行。正如@Nejat 所提到的,我们只能利用这样一个事实,即在任何时候都可以为给定的键创建最多一个共享内存段,就像在 Qt 的QSharedMemory::create() 文档中一样:“如果一个共享内存段由key已经存在,不执行attach操作,返回false。”

    3. 如果我理解正确,在构造函数中“修复”QSharedMemory 对象的目的是破坏由于先前进程崩溃而幸存的共享内存段,如 Qt 的文档中所示:“Unix: .. . 当最后一个拥有QSharedMemory 实例的线程或进程连接到特定共享内存段时,通过销毁其QSharedMemory 实例与该段分离,Unix 内核释放共享内存段。但如果最后一个线程或进程在没有运行QSharedMemory 析构函数的情况下崩溃,共享内存段在崩溃中幸存下来。”。当“修复”被破坏时,一个隐式的detach() 应该被它的析构函数调用并且幸存的共享内存段(如果有的话)将被释放。

    4. 不确定QSharedMemory 是否是线程安全/进程安全的。否则,如果线程安全由QSharedMemory 在内部处理,则与memLock 相关的代码可能会被进一步删除。另一方面,如果安全问题,fix 也应该由memLock 保护:

      RunGuard::RunGuard( const QString& key )
          : key( key )
          , memLockKey( generateKeyHash( key, "_memLockKey" ) )
          , sharedMemKey( generateKeyHash( key, "_sharedMemKey" ) )
          , sharedMem( sharedMemKey )
          , memLock( memLockKey, 1 )
      {
          memLock.acquire();
          {
              QSharedMemory fix( sharedMemKey ); // Fix for *nix: http://habrahabr.ru/post/173281/
              fix.attach();
          }
          memLock.release();
      }
      

      因为在 fix 周围调用了显式 attach() 和隐式 detach()

    5. RunGuard的简化版如下:

      用法:

      int main()
      {
          RunGuard guard( "some_random_key" );
          if ( !guard.tryToRun() )
              return 0;
      
          QAppplication a(/*...*/);
          // ...
      }
      

      runGuard.h:

      #ifndef RUNGUARD_H
      #define RUNGUARD_H
      
      #include <QObject>
      #include <QSharedMemory>
      #include <QSystemSemaphore>
      
      class RunGuard
      {
      
      public:
          RunGuard( const QString& key );
          bool tryToRun();
      
      private:
          const QString key;
          const QString memLockKey;
          const QString sharedMemKey;
      
          QSharedMemory sharedMem;
          QSystemSemaphore memLock;
      
          Q_DISABLE_COPY( RunGuard )
      };
      
      
      #endif // RUNGUARD_H
      

      runGuard.cpp:

      #include "runGuard.h"
      #include <QCryptographicHash>
      
      namespace
      {
      
          QString generateKeyHash( const QString& key, const QString& salt )
          {
              QByteArray data;
              data.append( key.toUtf8() );
              data.append( salt.toUtf8() );
              data = QCryptographicHash::hash( data, QCryptographicHash::Sha1 ).toHex();
              return data;
      }
      
      }
      
      RunGuard::RunGuard( const QString& key )
          : key( key )
          , memLockKey( generateKeyHash( key, "_memLockKey" ) )
          , sharedMemKey( generateKeyHash( key, "_sharedMemKey" ) )
          , sharedMem( sharedMemKey )
          , memLock( memLockKey, 1 )
      {
          QSharedMemory fix( sharedMemKey ); // Fix for *nix: http://habrahabr.ru/post/173281/
          fix.attach();
      }
      
      bool RunGuard::tryToRun()
      {
          memLock.acquire();
          const bool result = sharedMem.create( sizeof( quint64 ) );
          memLock.release();
          if ( !result )
              return false;
      
          return true;
      }
      
    6. 这里可能存在竞争条件:

      bool RunGuard::tryToRun()
      {
          if ( isAnotherRunning() )   // Extra check
              return false;
                                                                     // (tag1)
          memLock.acquire();
          const bool result = sharedMem.create( sizeof( quint64 ) ); // (tag2)
          memLock.release();
          if ( !result )
          {
              release();                                             // (tag3)
              return false;
          }
      
          return true;
      }
      

      考虑场景:

      当当前进程 ProcCur 运行到(tag1) 时,会发生以下情况:(注意(tag1) 超出了锁定保护)

      1. 另一个使用RunGuard的进程ProcOther开始运行。
      2. ProcOther 运行到(tag2) 并成功创建共享内存。
      3. ProcOther 在调用 release() (tag3) 之前崩溃。
      4. ProcCur 继续从 (tag1) 运行。
      5. ProcCur 运行到(tag2) 并尝试创建共享内存。但是,sharedMem.create() 将返回 false,因为 ProcOther 已经创建了一个。在QSharedMemory::create()的文档中可以看到:“如果key标识的共享内存段已经存在,则不执行attach操作,返回false。”
      6. 最后,ProcCur 中的RunGuard::tryToRun() 将返回false,这与预期不同,因为ProcCur 是唯一使用RunGuard 的现有进程。

    【讨论】:

    • 1.当您想要做一些额外的逻辑并且当您需要直接释放 RunGuard 时,这是必要的。 2. 如果需要 IPC,此检查可用于其他情况。 3. 是的,这是针对您的情况对 *nix 的修复。 4. QSystemSemaphore 用作跨进程“互斥锁”。有必要防止某些竞争条件。它不是“服务员”。 RunGuard 不是线程安全的,因为这里不需要线程安全。但 RunGuard 必须是过程安全的。
    • QSharedMemory 是否是进程安全/线程安全的仍然未知。因此,通过 QSystemSemaphore 保护 QSharedMemory 的成员函数调用可能是微不足道的。应该有文档提到 QSharedMemory 不是进程安全/线程安全的,但我找不到任何此类文档。因此,我说“memLock 可能被进一步删除”。
    • 你错了。您是说,线程不安全和可重入类之间没有区别。在 Qt 中,reentrant 意味着您可以在不同的线程中使用一个实例,但您应该使用一些守卫来保护对对象的访问。您需要提高理解多线程以及理解线程和进程之间差异的技能。
    • 顺便说一句,您需要仔细阅读文档。再读一遍:“一个可重入函数也可以从多个线程同时调用,但前提是每次调用都使用自己的数据。”没有任何关于实例的说明。仅关于调用。
    • 代码中可能存在竞争。见上文 6.。
    猜你喜欢
    • 2020-09-21
    • 2017-11-16
    • 2013-01-08
    • 2020-01-16
    • 2017-07-12
    • 2022-08-13
    • 2015-06-11
    • 2016-10-20
    • 1970-01-01
    相关资源
    最近更新 更多