c++ - Qt: la mejor práctica para una sola aplicación de protección de instancia
qmutex qsharedmemory (7)
QSingleApplication
? QMutex
? QSharedMemory
? Estoy buscando algo que funcione sin problemas en Windows, OSX y Linux (Ubuntu). Usando Qt 4.7.1
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 SingleApplication .
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 programa maneja correctamente los bloqueos y si no insiste en usar Qt, puede usar los otros mecanismos de bloqueo 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 share podría simplificarse aún más:
El destructor
~RunGuard()
yRunGuard::release()
pueden retirarse ya queQSharedMemory
se separará automáticamente del segmento de memoria compartida después de su destrucción, como en el documento de Qt paraQSharedMemory::~QSharedMemory()
: "El destructor borra la clave , lo que obliga al objeto de memoria compartida a separarse de su segmento de memoria compartida subyacente ".RunGuard::isAnotherRunning()
también se puede quitar también. El objetivo 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 paraQSharedMemory::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. "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 deQSharedMemory
unida a un segmento de memoria compartida particular se separa del segmento al destruir su instancia deQSharedMemory
, el kernel de Unix libera el segmento de memoria compartida. Pero si el último hilo o proceso se bloquea sin ejecutar el destructorQSharedMemory
, el segmento de memoria compartida sobrevive al choque ". Cuando se destruye "fix", el destructor debedetach()
undetach()
implícito y se liberará el segmento de memoria compartida que se haya conservado.No estoy seguro de si
QSharedMemory
es seguro para subprocesos / seguro para procesos o no. De lo contrario, el código relacionado conmemLock
se puede eliminar más siQSharedMemory
maneja internamente la seguridad de subprocesos. Por otro lado,fix
también debe estar protegido pormemLock
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 undetach()
implícito se llaman alrededor defix
.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 <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; }
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 bloqueo)- Otro proceso ProcOther que usa
RunGuard
comienza a ejecutarse. - ProcOther se ejecuta en
(tag2)
y crea con éxito la memoria compartida. - ProcOther se bloquea antes de llamar a
release()
en(tag3)
. - ProcCur continúa ejecutándose desde
(tag1)
. - 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 deQSharedMemory::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". - Finalmente,
RunGuard::tryToRun()
en ProcCur devolveráfalse
, que no es el esperado porque ProcCur es el único proceso existente que usaRunGuard
.
- Otro proceso ProcOther que usa
Estoy usando esta solución por ahora.
Sin embargo, tiene el inconveniente de que el programa 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 <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;
}
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
}
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 <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();
}
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:"<<AppList;
for(int i=0;i<AppList.size()-1;i++)
{
Commnd = "kill " + AppList.at(i);
m_prSystemCall->start(Commnd);
m_prSystemCall->waitForFinished(8000);
}
// ------------------------------------------------ -------
y para 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);
para ventanas:
HANDLE g_app_mutex = NULL;
bool check_one_app_instance () {g_app_mutex = :: CreateMutex (NULL, FALSE, L "8BD290769B404A7816985M9E505CF9AD64"); // esta cualquier tecla diferente como cadena if (GetLastError () == ERROR_ALREADY_EXISTS) {CloseHandle (g_app_mutex); falso retorno; }
return true;
}