开发者

Qt: Best practice for a single instance app protection

开发者 https://www.devze.com 2023-02-10 10:09 出处:网络
QSingleApplication? QMutex? QSharedMemory? I\'m looking for somet开发者_如何学Gohing that will work smoothly in Windows, OSX and Linux (Ubuntu). Using Qt 4.7.1Simple solution, that does what you want.

QSingleApplication? QMutex? QSharedMemory? I'm looking for somet开发者_如何学Gohing that will work smoothly in Windows, OSX and Linux (Ubuntu). Using Qt 4.7.1


Simple solution, that does what you want. Without network dependency (as QtSingleApplication) and without any overhead.

Usage:

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


As QtSingleApplication is relatively obsolete and not maintained anymore, I wrote a replacement, called SingleApplication.

It is based on QSharedMemory and uses a QLocalServer to notify the parent process of the new instance being spawn. It works on all platforms and is compatible supports Qt 5.

The full code and documentation are available here.

Basic Example:

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

    return app.exec();
}

Advanced Example

Among other things it supports sending messages between the newly spawned instance and the primary instance, for example:

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


You can use QSharedMemory with a specific key and check if the shared memory with that key could be created or not. If it is nor able to create it, then an instance is already run:

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
}


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


I am using this solution for now.

However it has the drawback that the program can only be run once by the user, even if they login from multiple locations at the same time.

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


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

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

and for 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);


According to Qt's doc, an acquired QSystemSemaphore won't be automatically released if the process crashes without calling its destructor under Unix-like OSes. Which could be the cause of a deadlock in another process trying to acquire the same semaphore. If you want to be 100% sure that your program properly handles crashes and if you don't insist on using Qt, you may want to use the other locking mechanisms which the operating systems do automatically release when the process dies - for example, lockf() and the O_EXLOCK flag passed to open() which are mentioned in How do I recover a semaphore when the process that decremented it to zero crashes? or flock(). In fact, creating of shared memory is no longer needed if flock() is used. Simply using flock() is enough to make single instance app protection.

If recovering semaphore from crashes in Unix doesn't matter, I think that RunGuard from Dmitry Sazonov's answer could still be somewhat simplified:

  1. The destructor ~RunGuard() and RunGuard::release() may be taken off since QSharedMemory will automatically detach from the shared memory segment upon its destruction, as in Qt's doc for QSharedMemory::~QSharedMemory(): "The destructor clears the key, which forces the shared memory object to detach from its underlying shared memory segment.".

  2. RunGuard::isAnotherRunning() may also be taken off, too. The goal is exclusive execution. As @Nejat has mentioned, we can merely take advantage of the fact there could be at most one shared memory segment being created for a given key at any time, as in Qt's doc for QSharedMemory::create(): "If a shared memory segment identified by the key already exists, the attach operation is not performed and false is returned."

  3. If I understand correctly, the purpose of "fix" QSharedMemory object in the constructor is to destroy the shared memory segment which survives due to the previous process crash, as in Qt's doc: "Unix: ... When the last thread or process that has an instance of QSharedMemory attached to a particular shared memory segment detaches from the segment by destroying its instance of QSharedMemory, the Unix kernel release the shared memory segment. But if that last thread or process crashes without running the QSharedMemory destructor, the shared memory segment survives the crash.". When "fix" gets destructed, an implicit detach() should be called by its destructor and the surviving shared memory segment, if any, will be released.

  4. Not sure if QSharedMemory is thread-safe/process-safe or not. Otherwise, the code related to memLock may be further removed if thread-safety is handled internally by QSharedMemory. On the other hand, fix should also be protected by memLock if the safety is an issue:

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

    because an explicit attach() and an implicit detach() are called around fix.

  5. The simplified version of RunGuard is as follows:

    Usage:

    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. There is a possible race condition here:

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

    Consider the scenario:

    When the current process ProcCur runs to (tag1) the following happens: (note that (tag1) is outside of lock protection)

    1. Another process ProcOther using RunGuard starts to run.
    2. ProcOther runs to (tag2) and successfully creates the shared memory.
    3. ProcOther crashes before it calls release() at (tag3).
    4. ProcCur continues running from (tag1).
    5. ProcCur runs to (tag2) and attempts to create shared memory. However, sharedMem.create() will return false because ProcOther have left a created one. As we can see in the doc of QSharedMemory::create(): "If a shared memory segment identified by the key already exists, the attach operation is not performed and false is returned."
    6. Finally, RunGuard::tryToRun() in ProcCur will return false, which is not as expected because ProcCur is the only existing process using RunGuard.
0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号