Qt: la mejor práctica para una sola aplicación de protección de instancia

QSingleApplication ? QMutex ? QSharedMemory ? Estoy buscando algo que funcione sin problemas en Windows, OSX y Linux (Ubuntu). Usando Qt 4.7.1

Solución simple, que hace lo que quieres. Sin dependencia de red (como QtSingleApplication ) y sin ningún gasto QtSingleApplication .

Uso:

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

RunGuard.h

 #ifndef RUNGUARD_H #define RUNGUARD_H #include  #include  #include  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  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(); } 

Puede usar QSharedMemory con una clave específica y verificar si la memoria compartida con esa clave se pudo crear o no. Si no es capaz de crearlo, entonces ya se está ejecutando una instancia:

 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 } 

Como QtSingleApplication es relativamente obsoleto y ya no se mantiene, escribí un reemplazo llamado SingleApplication .

Se basa en QSharedMemory y utiliza un QLocalServer para notificar al proceso principal de la nueva instancia que se está generando. Funciona en todas las plataformas y es compatible con Qt 5.

El código completo y la documentación están disponibles aquí .

Estoy usando esta solución por ahora.

Sin embargo, tiene el inconveniente de que el progtwig solo puede ejecutarse una vez por el usuario, incluso si inician sesión desde varias ubicaciones al mismo tiempo.

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  #include  #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; } 

para 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:"<start(Commnd); m_prSystemCall->waitForFinished(8000); } 

// ———————————————— ——-

y para Windows:

 #include  #include  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); 

para ventanas:

 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; } 

De acuerdo con el documento de Qt, un QSystemSemaphore adquirido no se liberará automáticamente si el proceso falla sin llamar a su destructor en sistemas operativos de tipo Unix. Que podría ser la causa de un punto muerto en otro proceso tratando de adquirir el mismo semáforo. Si quiere estar 100% seguro de que su progtwig maneja correctamente los lockings y si no insiste en usar Qt, puede usar los otros mecanismos de locking que los sistemas operativos liberan automáticamente cuando el proceso muere, por ejemplo, lockf() y el indicador O_EXLOCK pasado a open() que se mencionan en ¿Cómo recupero un semáforo cuando el proceso que lo redujo a cero se bloquea? o flock() . De hecho, la creación de memoria compartida ya no es necesaria si se usa flock() . Simplemente usar flock() es suficiente para hacer que la protección de la aplicación de instancia única.

Si la recuperación del semáforo de los fallos en Unix no importa, creo que RunGuard de la respuesta de Dmitry Sazonov podría simplificarse aún más:

  1. El destructor ~RunGuard() y RunGuard::release() pueden retirarse ya que QSharedMemory se separará automáticamente del segmento de memoria compartida después de su destrucción, como en el documento de Qt para QSharedMemory::~QSharedMemory() : “El destructor borra la clave , lo que obliga al objeto de memoria compartida a separarse de su segmento de memoria compartida subyacente “.

  2. RunGuard::isAnotherRunning() también se puede quitar también. El objective es la ejecución exclusiva. Como ha mencionado @Nejat, podemos simplemente aprovechar el hecho de que podría haber como máximo un segmento de memoria compartida creado para una clave determinada en cualquier momento, como en el documento de Qt para QSharedMemory::create() : “Si una memoria compartida El segmento identificado por la clave ya existe, la operación de adjuntar no se realiza y se devuelve falso. ”

  3. Si entiendo correctamente, el propósito de “arreglar” el objeto QSharedMemory en el constructor es destruir el segmento de memoria compartida que sobrevive debido a la caída del proceso anterior, como en el documento de Qt: “Unix: … cuando el último hilo o proceso que tiene una instancia de QSharedMemory unida a un segmento de memoria compartida particular se separa del segmento al destruir su instancia de QSharedMemory , el kernel de Unix libera el segmento de memoria compartida. Pero si el último hilo o proceso se bloquea sin ejecutar el destructor QSharedMemory , el segmento de memoria compartida sobrevive al choque “. Cuando se destruye “fix”, el destructor debe detach() un detach() implícito y se liberará el segmento de memoria compartida que se haya conservado.

  4. No estoy seguro de si QSharedMemory es seguro para subprocesos / seguro para procesos o no. De lo contrario, el código relacionado con memLock se puede eliminar más si QSharedMemory maneja internamente la seguridad de subprocesos. Por otro lado, fix también debe estar protegido por memLock si la seguridad es un problema:

     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(); } 

    porque un attach() explícito y un detach() implícito se llaman alrededor de fix .

  5. La versión simplificada de RunGuard es la siguiente:

    Uso:

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

    runGuard.h:

     #ifndef RUNGUARD_H #define RUNGUARD_H #include  #include  #include  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  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. Hay una posible condición de carrera aquí:

     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; } 

    Considera el escenario:

    Cuando el proceso actual de ProcCur se ejecuta (tag1) sucede lo siguiente: (tenga en cuenta que (tag1) está fuera de la protección de locking)

    1. Otro proceso ProcOther que usa RunGuard comienza a ejecutarse.
    2. ProcOther se ejecuta en (tag2) y crea con éxito la memoria compartida.
    3. ProcOther se bloquea antes de llamar a release() en (tag3) .
    4. ProcCur continúa ejecutándose desde (tag1) .
    5. ProcCur se ejecuta (tag2) e intenta crear memoria compartida. Sin embargo, sharedMem.create() devolverá false porque ProcOther ha dejado uno creado. Como podemos ver en el documento de QSharedMemory::create() : “Si ya existe un segmento de memoria compartida identificado por la clave, la operación de adjuntar no se realiza y se devuelve falso”.
    6. Finalmente, RunGuard::tryToRun() en ProcCur devolverá false , que no es el esperado porque ProcCur es el único proceso existente que usa RunGuard .