Qt:单一实例应用程序保护的最佳做法
QSingleApplication
? QMutex
? QSharedMemory
? 我正在寻找能在Windows,OSX和Linux(Ubuntu)上顺利运行的东西。 使用Qt 4.7.1
简单的解决scheme,这就是你想要的。 没有networking依赖(如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(); }
您可以使用QSharedMemory
和特定的密钥,并检查是否可以创build与该密钥的共享内存。 如果它不能创build它,则已经运行一个实例:
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 }
由于QtSingleApplication
相对过时,不再维护,我写了一个名为SingleApplication的替代品。
它基于QSharedMemory
并使用QLocalServer
来通知正在产生的新实例的父进程。 它适用于所有平台,并与Qt 5兼容。
完整的代码和文档可在这里find 。
对于Windows:
HANDLE g_app_mutex = NULL;
bool check_one_app_instance(){g_app_mutex = :: CreateMutex(NULL,FALSE,L“8BD290769B404A7816985M9E505CF9AD64”); //这个任何不同的键作为stringif(GetLastError()== ERROR_ALREADY_EXISTS){CloseHandle(g_app_mutex); 返回false; }
return true;
}
我现在正在使用这个解决scheme。
但是它的缺点是程序只能由用户运行一次,即使他们同时从多个地点login。
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; }
对于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);
根据Qt的文档,如果进程崩溃而不是在类Unix操作系统下调用它的析构函数,那么获得的QSystemSemaphore
将不会被自动释放。 这可能是另一个尝试获取相同信号量的进程陷入僵局的原因。 如果你想100%确定你的程序正确地处理了崩溃,并且如果你不坚持使用Qt,你可能需要使用操作系统在进程死亡时自动释放的其他locking机制 – 例如lockf()
和传递给open()
的O_EXLOCK
标志在我如何恢复一个信号量的过程中减less到零崩溃? 或flock()
。 实际上,如果使用flock()
则不再需要创build共享内存。 只需使用flock()
就足以使单实例应用程序保护。
如果从Unix中的崩溃中恢复信号量并不重要,我认为来自Dmitry Sazonov的答案的RunGuard仍然可以被简化:
-
析构函数
~RunGuard()
和RunGuard::release()
可能会被取消,因为QSharedMemory
会在其被销毁时自动从共享内存段中分离出来,就像在Qt的QSharedMemory::~QSharedMemory()
的文档中QSharedMemory::~QSharedMemory()
:“析构函数清除键,这迫使共享内存对象从其底层共享内存段中分离出来。“ -
RunGuard::isAnotherRunning()
也可能被取消。 目标是独家执行。 正如@Nejat所提到的那样,我们只能利用这个事实,就是在任何时候最多只能为一个给定的键创build一个共享内存段,就像Qt的QSharedMemory::create()
的文档:“如果一个共享内存由密钥标识的段已经存在,则不执行附加操作,并返回false。 -
如果我理解正确,在构造函数中“修复”
QSharedMemory
对象的目的是为了销毁由于之前的进程崩溃而存活的共享内存段,如在Qt的doc中:“Unix:…当最后一个线程或进程有一个连接到特定共享内存段的QSharedMemory
实例,通过销毁它的实例QSharedMemory
,Unix内核释放共享内存段,但是如果最后一个线程或进程崩溃而没有运行QSharedMemory
析构函数,共享内存段幸存下来“。 当“修复”被破坏时,隐含的detach()
应该被析构函数调用,而幸存的共享内存段(如果有的话)将被释放。 -
不确定
QSharedMemory
是否是线程安全的/进程安全的。 否则,如果线程安全性由QSharedMemory
内部处理,memLock
可能会进一步删除与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(); }
因为一个显式的
attach()
和一个隐式的detach()
被称为fix
。 -
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; }
-
这里有一个可能的竞争条件:
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)
在locking保护之外)- 使用
RunGuard
另一个进程ProcOther开始运行。 - ProcOther运行到
(tag2)
tag2(tag2)
并成功创build共享内存。 - 在
(tag3)
调用release()
之前, ProcOther崩溃。 - ProcCur从
(tag1)
继续运行。 - ProcCur运行到
(tag2)
tag2(tag2)
并尝试创build共享内存。 但是,sharedMem.create()
将返回false
因为ProcOther已经创build了一个。 正如我们在QSharedMemory::create()
的文档中所看到的QSharedMemory::create()
:“如果由密钥标识的共享内存段已经存在,则不执行附加操作,并返回false。 - 最后,
RunGuard::tryToRun()
将返回false
,这不是预期的,因为ProcCur是使用RunGuard
的唯一现有进程。
- 使用