diff options
author | Miikka Heikkinen <miikka.heikkinen@qt.io> | 2018-04-18 17:38:40 +0300 |
---|---|---|
committer | Miikka Heikkinen <miikka.heikkinen@qt.io> | 2018-04-20 13:20:00 +0000 |
commit | fda3aa98013b7e166e3ebc81f743d9f8297200f6 (patch) | |
tree | e8175e1f224b3ad94ca8eb55e99e60dd253df297 | |
parent | 3b6b637a5a7fc492014867ce3ce48c804d8978a4 (diff) |
Activate existing instance if desired presentation matches
When starting studio with a command line parameter to launch a
presentation, instead of launching a new instance, check if there is
and existing instance already showing that presentation and activate
it.
Task-number: QT3DS-888
Change-Id: I0aa74e40d9796808eb8b07228e85f9dfb382952a
Reviewed-by: Tomi Korpipää <tomi.korpipaa@qt.io>
-rw-r--r-- | src/Authoring/Studio/Qt3DStudio.pro | 1 | ||||
-rw-r--r-- | src/Authoring/Studio/_Win/Application/StudioApp.cpp | 55 | ||||
-rw-r--r-- | src/Authoring/Studio/_Win/Application/StudioApp.h | 3 | ||||
-rw-r--r-- | src/shared/README.txt | 2 | ||||
-rw-r--r-- | src/shared/qtlockedfile/qtlockedfile.cpp | 158 | ||||
-rw-r--r-- | src/shared/qtlockedfile/qtlockedfile.h | 73 | ||||
-rw-r--r-- | src/shared/qtlockedfile/qtlockedfile.pri | 13 | ||||
-rw-r--r-- | src/shared/qtlockedfile/qtlockedfile_unix.cpp | 107 | ||||
-rw-r--r-- | src/shared/qtlockedfile/qtlockedfile_win.cpp | 195 | ||||
-rw-r--r-- | src/shared/qtsingleapplication/qtlocalpeer.cpp | 191 | ||||
-rw-r--r-- | src/shared/qtsingleapplication/qtlocalpeer.h | 63 | ||||
-rw-r--r-- | src/shared/qtsingleapplication/qtsingleapplication.cpp | 190 | ||||
-rw-r--r-- | src/shared/qtsingleapplication/qtsingleapplication.h | 70 | ||||
-rw-r--r-- | src/shared/qtsingleapplication/qtsingleapplication.pri | 13 |
14 files changed, 1131 insertions, 3 deletions
diff --git a/src/Authoring/Studio/Qt3DStudio.pro b/src/Authoring/Studio/Qt3DStudio.pro index 767c0da6..501fa634 100644 --- a/src/Authoring/Studio/Qt3DStudio.pro +++ b/src/Authoring/Studio/Qt3DStudio.pro @@ -2,6 +2,7 @@ TEMPLATE = app TARGET = Qt3DStudio include(../commoninclude.pri) include($$OUT_PWD/../qtAuthoring-config.pri) +include(../../shared/qtsingleapplication/qtsingleapplication.pri) INCLUDEPATH += $$OUT_PWD/.. CONFIG += nostrictstrings diff --git a/src/Authoring/Studio/_Win/Application/StudioApp.cpp b/src/Authoring/Studio/_Win/Application/StudioApp.cpp index 35aca9a8..a2da4135 100644 --- a/src/Authoring/Studio/_Win/Application/StudioApp.cpp +++ b/src/Authoring/Studio/_Win/Application/StudioApp.cpp @@ -39,6 +39,8 @@ #include "Qt3DSStateApplication.h" #include "PlayerWnd.h" #include "DataInputDlg.h" +#include "qtsingleapplication.h" +#include "qtlocalpeer.h" #include <QtGui/qsurfaceformat.h> #include <QtCore/qfileinfo.h> @@ -48,6 +50,8 @@ #include <QtCore/qstandardpaths.h> #include <QtCore/qcommandlineparser.h> +const QString activePresentationQuery = QStringLiteral("activePresentation:"); + int main(int argc, char *argv[]) { // Hack to work around qml cache bug (QT3DS-556) @@ -57,7 +61,7 @@ int main(int argc, char *argv[]) Q_INIT_RESOURCE(res); QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling); - QApplication guiApp(argc, argv); + SharedTools::QtSingleApplication guiApp(QStringLiteral("Qt3DStudio"), argc, argv); #if defined(Q_OS_MACOS) QSurfaceFormat openGL33Format; @@ -118,6 +122,9 @@ int main(int argc, char *argv[]) exit(0); } + QObject::connect(&guiApp, &SharedTools::QtSingleApplication::messageReceived, + &g_StudioApp, &CStudioApp::handleMessageReceived); + // Load and apply stylesheet for the application QFile styleFile(":/style.qss"); styleFile.open(QFile::ReadOnly); @@ -241,10 +248,12 @@ void CStudioApp::performShutdown() } if (m_renderer) { - m_views->getMainFrame()->GetPlayerWnd()->makeCurrent(); + if (m_views->getMainFrame()) + m_views->getMainFrame()->GetPlayerWnd()->makeCurrent(); m_renderer->Close(); m_renderer = std::shared_ptr<Q3DStudio::IStudioRenderer>(); - m_views->getMainFrame()->GetPlayerWnd()->doneCurrent(); + if (m_views->getMainFrame()) + m_views->getMainFrame()->GetPlayerWnd()->doneCurrent(); } delete m_views; @@ -566,6 +575,17 @@ bool CStudioApp::blankRunApplication() */ bool CStudioApp::openAndRunApplication(const QString &inFilename) { + // First check if the desired presentation is already open on another instance + SharedTools::QtSingleApplication *app = + static_cast<SharedTools::QtSingleApplication *>(QCoreApplication::instance()); + const auto pids = app->runningInstances(); + for (const auto pid : pids) { + app->setBlock(true); + QString query = activePresentationQuery + inFilename; + if (app->sendMessage(query, true, 5000, pid)) + return true; + } + bool theSuccess = false; initCore(); // Load document. Upon failure, don't show startup dialog but exit immediately. @@ -715,6 +735,35 @@ void CStudioApp::clearGuides() SCOPED_DOCUMENT_EDITOR(*m_core->GetDoc(), QObject::tr("Clear Guides"))->ClearGuides(); } +void CStudioApp::handleMessageReceived(const QString &message, QObject *socket) +{ + if (message.startsWith(activePresentationQuery)) { + QLocalSocket *lsocket = qobject_cast<QLocalSocket *>(socket); + if (lsocket) { + // Another studio instance wants to know if specified presentation is open on this one + QFileInfo checkFile(message.mid(activePresentationQuery.size())); + QFileInfo openFile(m_core->GetDoc()->GetDocumentPath().GetAbsolutePath().toQString()); + if (checkFile == openFile) { + lsocket->write(SharedTools::QtLocalPeer::acceptReply(), + SharedTools::QtLocalPeer::acceptReply().size()); + // Since we accept active presentation query, it means the querying instance will + // shut down and this instance must be made active window. + if (m_pMainWnd) { + m_pMainWnd->setWindowState(m_pMainWnd->windowState() & ~Qt::WindowMinimized); + m_pMainWnd->raise(); + m_pMainWnd->activateWindow(); + } + } else { + lsocket->write(SharedTools::QtLocalPeer::denyReply(), + SharedTools::QtLocalPeer::denyReply().size()); + } + lsocket->waitForBytesWritten(1000); + } + } + if (socket) + delete socket; +} + void SendAsyncCommand(CDispatch &inDispatch, Q3DStudio::TCallbackFunc inFunc) { inDispatch.FireOnAsynchronousCommand(inFunc); diff --git a/src/Authoring/Studio/_Win/Application/StudioApp.h b/src/Authoring/Studio/_Win/Application/StudioApp.h index 64193573..cba3d748 100644 --- a/src/Authoring/Studio/_Win/Application/StudioApp.h +++ b/src/Authoring/Studio/_Win/Application/StudioApp.h @@ -96,6 +96,9 @@ public: Q3DStudio::IStudioRenderer &getRenderer(); void clearGuides(); +public Q_SLOTS: + void handleMessageReceived(const QString &message, QObject *socket); + protected: bool runApplication(); bool blankRunApplication(); diff --git a/src/shared/README.txt b/src/shared/README.txt new file mode 100644 index 00000000..717efd54 --- /dev/null +++ b/src/shared/README.txt @@ -0,0 +1,2 @@ +QtLockedFile and QtSingleApplication were copied from Qt Creator and +modified for Qt 3D Studio purposes. diff --git a/src/shared/qtlockedfile/qtlockedfile.cpp b/src/shared/qtlockedfile/qtlockedfile.cpp new file mode 100644 index 00000000..06195c24 --- /dev/null +++ b/src/shared/qtlockedfile/qtlockedfile.cpp @@ -0,0 +1,158 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt 3D Studio. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qtlockedfile.h" + +namespace SharedTools { + +/*! + \class QtLockedFile + + \brief The QtLockedFile class extends QFile with advisory locking functions. + + A file may be locked in read or write mode. Multiple instances of + \e QtLockedFile, created in multiple processes running on the same + machine, may have a file locked in read mode. Exactly one instance + may have it locked in write mode. A read and a write lock cannot + exist simultaneously on the same file. + + The file locks are advisory. This means that nothing prevents + another process from manipulating a locked file using QFile or + file system functions offered by the OS. Serialization is only + guaranteed if all processes that access the file use + QtLockedFile. Also, while holding a lock on a file, a process + must not open the same file again (through any API), or locks + can be unexpectedly lost. + + The lock provided by an instance of \e QtLockedFile is released + whenever the program terminates. This is true even when the + program crashes and no destructors are called. +*/ + +/*! \enum QtLockedFile::LockMode + + This enum describes the available lock modes. + + \value ReadLock A read lock. + \value WriteLock A write lock. + \value NoLock Neither a read lock nor a write lock. +*/ + +/*! + Constructs an unlocked \e QtLockedFile object. This constructor behaves in the same way + as \e QFile::QFile(). + + \sa QFile::QFile() +*/ +QtLockedFile::QtLockedFile() + : QFile() +{ +#ifdef Q_OS_WIN + m_semaphore_hnd = 0; + m_mutex_hnd = 0; +#endif + m_lock_mode = NoLock; +} + +/*! + Constructs an unlocked QtLockedFile object with file \a name. This constructor behaves in + the same way as \e QFile::QFile(const QString&). + + \sa QFile::QFile() +*/ +QtLockedFile::QtLockedFile(const QString &name) + : QFile(name) +{ +#ifdef Q_OS_WIN + m_semaphore_hnd = 0; + m_mutex_hnd = 0; +#endif + m_lock_mode = NoLock; +} + +/*! + Returns \e true if this object has a in read or write lock; + otherwise returns \e false. + + \sa lockMode() +*/ +bool QtLockedFile::isLocked() const +{ + return m_lock_mode != NoLock; +} + +/*! + Returns the type of lock currently held by this object, or \e QtLockedFile::NoLock. + + \sa isLocked() +*/ +QtLockedFile::LockMode QtLockedFile::lockMode() const +{ + return m_lock_mode; +} + +/*! + \fn bool QtLockedFile::lock(LockMode mode, bool block = true) + + Obtains a lock of type \a mode. + + If \a block is true, this + function will block until the lock is acquired. If \a block is + false, this function returns \e false immediately if the lock cannot + be acquired. + + If this object already has a lock of type \a mode, this function returns \e true immediately. + If this object has a lock of a different type than \a mode, the lock + is first released and then a new lock is obtained. + + This function returns \e true if, after it executes, the file is locked by this object, + and \e false otherwise. + + \sa unlock(), isLocked(), lockMode() +*/ + +/*! + \fn bool QtLockedFile::unlock() + + Releases a lock. + + If the object has no lock, this function returns immediately. + + This function returns \e true if, after it executes, the file is not locked by + this object, and \e false otherwise. + + \sa lock(), isLocked(), lockMode() +*/ + +/*! + \fn QtLockedFile::~QtLockedFile() + + Destroys the \e QtLockedFile object. If any locks were held, they are released. +*/ + +} // namespace SharedTools diff --git a/src/shared/qtlockedfile/qtlockedfile.h b/src/shared/qtlockedfile/qtlockedfile.h new file mode 100644 index 00000000..dc310d3c --- /dev/null +++ b/src/shared/qtlockedfile/qtlockedfile.h @@ -0,0 +1,73 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt 3D Studio. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#pragma once + +#include <QtCore/qfile.h> + +#if defined(Q_OS_WIN) +# if !defined(QT_QTLOCKEDFILE_EXPORT) && !defined(QT_QTLOCKEDFILE_IMPORT) +# define QT_QTLOCKEDFILE_EXPORT +# elif defined(QT_QTLOCKEDFILE_IMPORT) +# if defined(QT_QTLOCKEDFILE_EXPORT) +# undef QT_QTLOCKEDFILE_EXPORT +# endif +# define QT_QTLOCKEDFILE_EXPORT __declspec(dllimport) +# elif defined(QT_QTLOCKEDFILE_EXPORT) +# undef QT_QTLOCKEDFILE_EXPORT +# define QT_QTLOCKEDFILE_EXPORT __declspec(dllexport) +# endif +#else +# define QT_QTLOCKEDFILE_EXPORT +#endif + +namespace SharedTools { + +class QT_QTLOCKEDFILE_EXPORT QtLockedFile : public QFile +{ +public: + enum LockMode { NoLock = 0, ReadLock, WriteLock }; + + QtLockedFile(); + QtLockedFile(const QString &name); + ~QtLockedFile(); + + bool lock(LockMode mode, bool block = true); + bool unlock(); + bool isLocked() const; + LockMode lockMode() const; + +private: +#ifdef Q_OS_WIN + Qt::HANDLE m_semaphore_hnd; + Qt::HANDLE m_mutex_hnd; +#endif + LockMode m_lock_mode; +}; + +} // namespace SharedTools diff --git a/src/shared/qtlockedfile/qtlockedfile.pri b/src/shared/qtlockedfile/qtlockedfile.pri new file mode 100644 index 00000000..46d1f1a1 --- /dev/null +++ b/src/shared/qtlockedfile/qtlockedfile.pri @@ -0,0 +1,13 @@ +INCLUDEPATH += $$PWD +DEPENDPATH += $$PWD +HEADERS += $$PWD/qtlockedfile.h +SOURCES += $$PWD/qtlockedfile.cpp + +unix:SOURCES += $$PWD/qtlockedfile_unix.cpp +win32:SOURCES += $$PWD/qtlockedfile_win.cpp + +win32:contains(TEMPLATE, lib):contains(CONFIG, shared) { + DEFINES += QT_QTLOCKEDFILE_EXPORT=__declspec(dllexport) +} + + diff --git a/src/shared/qtlockedfile/qtlockedfile_unix.cpp b/src/shared/qtlockedfile/qtlockedfile_unix.cpp new file mode 100644 index 00000000..e12928fe --- /dev/null +++ b/src/shared/qtlockedfile/qtlockedfile_unix.cpp @@ -0,0 +1,107 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt 3D Studio. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qtlockedfile.h" + +#include <string.h> +#include <errno.h> +#include <unistd.h> +#include <fcntl.h> + +namespace SharedTools { + +bool QtLockedFile::lock(LockMode mode, bool block) +{ + if (!isOpen()) { + qWarning("QtLockedFile::lock(): file is not opened"); + return false; + } + + if (mode == NoLock) + return unlock(); + + if (mode == m_lock_mode) + return true; + + if (m_lock_mode != NoLock) + unlock(); + + struct flock fl; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 0; + fl.l_type = (mode == ReadLock) ? F_RDLCK : F_WRLCK; + int cmd = block ? F_SETLKW : F_SETLK; + int ret = fcntl(handle(), cmd, &fl); + + if (ret == -1) { + if (errno != EINTR && errno != EAGAIN) + qWarning("QtLockedFile::lock(): fcntl: %s", strerror(errno)); + return false; + } + + + m_lock_mode = mode; + return true; +} + + +bool QtLockedFile::unlock() +{ + if (!isOpen()) { + qWarning("QtLockedFile::unlock(): file is not opened"); + return false; + } + + if (!isLocked()) + return true; + + struct flock fl; + fl.l_whence = SEEK_SET; + fl.l_start = 0; + fl.l_len = 0; + fl.l_type = F_UNLCK; + int ret = fcntl(handle(), F_SETLKW, &fl); + + if (ret == -1) { + qWarning("QtLockedFile::lock(): fcntl: %s", strerror(errno)); + return false; + } + + m_lock_mode = NoLock; + remove(); + return true; +} + +QtLockedFile::~QtLockedFile() +{ + if (isOpen()) + unlock(); +} + +} // namespace SharedTools diff --git a/src/shared/qtlockedfile/qtlockedfile_win.cpp b/src/shared/qtlockedfile/qtlockedfile_win.cpp new file mode 100644 index 00000000..e1ac9abc --- /dev/null +++ b/src/shared/qtlockedfile/qtlockedfile_win.cpp @@ -0,0 +1,195 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt 3D Studio. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qtlockedfile.h" + +#include <QtCore/qt_windows.h> +#include <QtCore/qfileinfo.h> + +namespace SharedTools { + +#define SEMAPHORE_PREFIX "QtLockedFile semaphore " +#define MUTEX_PREFIX "QtLockedFile mutex " +#define SEMAPHORE_MAX 100 + +static QString errorCodeToString(DWORD errorCode) +{ + QString result; + char *data = 0; + FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, + 0, errorCode, 0, + (char*)&data, 0, 0); + result = QString::fromLocal8Bit(data); + if (data != 0) + LocalFree(data); + + if (result.endsWith(QLatin1Char('\n'))) + result.truncate(result.length() - 1); + + return result; +} + +bool QtLockedFile::lock(LockMode mode, bool block) +{ + if (!isOpen()) { + qWarning("QtLockedFile::lock(): file is not opened"); + return false; + } + + if (mode == m_lock_mode) + return true; + + if (m_lock_mode != 0) + unlock(); + + if (m_semaphore_hnd == 0) { + QFileInfo fi(*this); + QString sem_name = QString::fromLatin1(SEMAPHORE_PREFIX) + + fi.absoluteFilePath().toLower(); + + m_semaphore_hnd = CreateSemaphoreW(0, SEMAPHORE_MAX, SEMAPHORE_MAX, + (TCHAR*)sem_name.utf16()); + + if (m_semaphore_hnd == 0) { + qWarning("QtLockedFile::lock(): CreateSemaphore: %s", + errorCodeToString(GetLastError()).toLatin1().constData()); + return false; + } + } + + bool gotMutex = false; + int decrement; + if (mode == ReadLock) { + decrement = 1; + } else { + decrement = SEMAPHORE_MAX; + if (m_mutex_hnd == 0) { + QFileInfo fi(*this); + QString mut_name = QString::fromLatin1(MUTEX_PREFIX) + + fi.absoluteFilePath().toLower(); + + m_mutex_hnd = CreateMutexW(NULL, FALSE, (TCHAR*)mut_name.utf16()); + + if (m_mutex_hnd == 0) { + qWarning("QtLockedFile::lock(): CreateMutex: %s", + errorCodeToString(GetLastError()).toLatin1().constData()); + return false; + } + } + DWORD res = WaitForSingleObject(m_mutex_hnd, block ? INFINITE : 0); + if (res == WAIT_TIMEOUT) + return false; + if (res == WAIT_FAILED) { + qWarning("QtLockedFile::lock(): WaitForSingleObject (mutex): %s", + errorCodeToString(GetLastError()).toLatin1().constData()); + return false; + } + gotMutex = true; + } + + for (int i = 0; i < decrement; ++i) { + DWORD res = WaitForSingleObject(m_semaphore_hnd, block ? INFINITE : 0); + if (res == WAIT_TIMEOUT) { + if (i) { + // A failed nonblocking rw locking. Undo changes to semaphore. + if (ReleaseSemaphore(m_semaphore_hnd, i, NULL) == 0) { + qWarning("QtLockedFile::unlock(): ReleaseSemaphore: %s", + errorCodeToString(GetLastError()).toLatin1().constData()); + // Fall through + } + } + if (gotMutex) + ReleaseMutex(m_mutex_hnd); + return false; + } + if (res != WAIT_OBJECT_0) { + if (gotMutex) + ReleaseMutex(m_mutex_hnd); + qWarning("QtLockedFile::lock(): WaitForSingleObject (semaphore): %s", + errorCodeToString(GetLastError()).toLatin1().constData()); + return false; + } + } + + m_lock_mode = mode; + if (gotMutex) + ReleaseMutex(m_mutex_hnd); + return true; +} + +bool QtLockedFile::unlock() +{ + if (!isOpen()) { + qWarning("QtLockedFile::unlock(): file is not opened"); + return false; + } + + if (!isLocked()) + return true; + + int increment; + if (m_lock_mode == ReadLock) + increment = 1; + else + increment = SEMAPHORE_MAX; + + DWORD ret = ReleaseSemaphore(m_semaphore_hnd, increment, 0); + if (ret == 0) { + qWarning("QtLockedFile::unlock(): ReleaseSemaphore: %s", + errorCodeToString(GetLastError()).toLatin1().constData()); + return false; + } + + m_lock_mode = QtLockedFile::NoLock; + remove(); + return true; +} + +QtLockedFile::~QtLockedFile() +{ + if (isOpen()) + unlock(); + if (m_mutex_hnd != 0) { + DWORD ret = CloseHandle(m_mutex_hnd); + if (ret == 0) { + qWarning("QtLockedFile::~QtLockedFile(): CloseHandle (mutex): %s", + errorCodeToString(GetLastError()).toLatin1().constData()); + } + m_mutex_hnd = 0; + } + if (m_semaphore_hnd != 0) { + DWORD ret = CloseHandle(m_semaphore_hnd); + if (ret == 0) { + qWarning("QtLockedFile::~QtLockedFile(): CloseHandle (semaphore): %s", + errorCodeToString(GetLastError()).toLatin1().constData()); + } + m_semaphore_hnd = 0; + } +} + +} // namespace SharedTools diff --git a/src/shared/qtsingleapplication/qtlocalpeer.cpp b/src/shared/qtsingleapplication/qtlocalpeer.cpp new file mode 100644 index 00000000..efcaa228 --- /dev/null +++ b/src/shared/qtsingleapplication/qtlocalpeer.cpp @@ -0,0 +1,191 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt 3D Studio. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qtlocalpeer.h" + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qdatastream.h> +#include <QtCore/qdatetime.h> + +#if defined(Q_OS_WIN) +#include <QtCore/qlibrary.h> +#include <QtCore/qt_windows.h> +typedef BOOL(WINAPI *PProcessIdToSessionId)(DWORD, DWORD *); +static PProcessIdToSessionId pProcessIdToSessionId = nullptr; +#endif + +#if defined(Q_OS_UNIX) +#include <time.h> +#include <unistd.h> +#endif + +namespace SharedTools { + +// All reply strings must be of same length +static const QByteArray acceptReplyStr = QByteArrayLiteral("@A@"); +static const QByteArray denyReplyStr = QByteArrayLiteral("@D@"); + +QString QtLocalPeer::appSessionId(const QString &appId) +{ + QByteArray idc = appId.toUtf8(); + quint16 idNum = qChecksum(idc.constData(), idc.size()); + + QString res = QLatin1String("qtsingleapplication-") + + QString::number(idNum, 16); +#if defined(Q_OS_WIN) + if (!pProcessIdToSessionId) { + QLibrary lib(QLatin1String("kernel32")); + pProcessIdToSessionId = (PProcessIdToSessionId)lib.resolve("ProcessIdToSessionId"); + } + if (pProcessIdToSessionId) { + DWORD sessionId = 0; + pProcessIdToSessionId(GetCurrentProcessId(), &sessionId); + res += QLatin1Char('-') + QString::number(sessionId, 16); + } +#else + res += QLatin1Char('-') + QString::number(::getuid(), 16); +#endif + return res; +} + +QByteArray QtLocalPeer::acceptReply() +{ + return acceptReplyStr; +} + +QByteArray QtLocalPeer::denyReply() +{ + return denyReplyStr; +} + +QtLocalPeer::QtLocalPeer(QObject *parent, const QString &appId) + : QObject(parent), m_id(appId) +{ + if (m_id.isEmpty()) + m_id = QCoreApplication::applicationFilePath(); + + m_socketName = appSessionId(m_id); + m_server = new QLocalServer(this); + QString lockName = QDir(QDir::tempPath()).absolutePath() + + QLatin1Char('/') + m_socketName + + QLatin1String("-lockfile"); + m_lockFile.setFileName(lockName); + m_lockFile.open(QIODevice::ReadWrite); +} + +bool QtLocalPeer::isClient() +{ + if (m_lockFile.isLocked()) + return false; + + if (!m_lockFile.lock(QtLockedFile::WriteLock, false)) + return true; + + if (!QLocalServer::removeServer(m_socketName)) + qWarning("QtSingleCoreApplication: could not cleanup socket"); + bool res = m_server->listen(m_socketName); + if (!res) { + qWarning("QtSingleCoreApplication: listen on local socket failed, %s", + qPrintable(m_server->errorString())); + } + QObject::connect(m_server, &QLocalServer::newConnection, + this, &QtLocalPeer::receiveConnection); + return false; +} + +bool QtLocalPeer::sendMessage(const QString &message, int timeout, bool block) +{ + if (!isClient()) + return false; + + QLocalSocket socket; + bool connOk = false; + for (int i = 0; i < 2; i++) { + // Try twice, in case the other instance is just starting up + socket.connectToServer(m_socketName); + connOk = socket.waitForConnected(timeout/2); + if (connOk || i) + break; + int ms = 250; +#if defined(Q_OS_WIN) + Sleep(DWORD(ms)); +#else + struct timespec ts = {ms / 1000, (ms % 1000) * 1000 * 1000}; + nanosleep(&ts, NULL); +#endif + } + if (!connOk) + return false; + + QByteArray uMsg(message.toUtf8()); + QDataStream ds(&socket); + ds.writeBytes(uMsg.constData(), uMsg.size()); + bool res = socket.waitForBytesWritten(timeout); + res &= socket.waitForReadyRead(timeout); // wait for reply + QByteArray reply = socket.read(acceptReplyStr.size()); + res &= reply == acceptReplyStr; + if (block) // block until peer disconnects + socket.waitForDisconnected(-1); + return res; +} + +void QtLocalPeer::receiveConnection() +{ + QLocalSocket* socket = m_server->nextPendingConnection(); + if (!socket) + return; + + // Why doesn't Qt have a blocking stream that takes care of this? + while (socket->bytesAvailable() < static_cast<int>(sizeof(quint32))) { + if (!socket->isValid()) // stale request + return; + socket->waitForReadyRead(1000); + } + QDataStream ds(socket); + QByteArray uMsg; + quint32 remaining; + ds >> remaining; + uMsg.resize(remaining); + int got = 0; + char* uMsgBuf = uMsg.data(); + do { + got = ds.readRawData(uMsgBuf, remaining); + remaining -= got; + uMsgBuf += got; + } while (remaining && got >= 0 && socket->waitForReadyRead(2000)); + if (got < 0) { + qWarning() << "QtLocalPeer: Message reception failed" << socket->errorString(); + delete socket; + return; + } + QString message = QString::fromUtf8(uMsg.constData(), uMsg.size()); + // messageReceived handler must write the reply string + emit messageReceived(message, socket); // might take a long time to return +} + +} // namespace SharedTools diff --git a/src/shared/qtsingleapplication/qtlocalpeer.h b/src/shared/qtsingleapplication/qtlocalpeer.h new file mode 100644 index 00000000..d2db7366 --- /dev/null +++ b/src/shared/qtsingleapplication/qtlocalpeer.h @@ -0,0 +1,63 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt 3D Studio. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qtlockedfile.h" + +#include <QtNetwork/qlocalserver.h> +#include <QtNetwork/qlocalsocket.h> +#include <QtCore/qdir.h> + +namespace SharedTools { + +class QtLocalPeer : public QObject +{ + Q_OBJECT + +public: + explicit QtLocalPeer(QObject *parent = 0, const QString &appId = QString()); + bool isClient(); + bool sendMessage(const QString &message, int timeout, bool block); + QString applicationId() const + { return m_id; } + static QString appSessionId(const QString &appId); + static QByteArray acceptReply(); + static QByteArray denyReply(); + +Q_SIGNALS: + void messageReceived(const QString &message, QObject *socket); + +protected: + void receiveConnection(); + + QString m_id; + QString m_socketName; + QLocalServer* m_server; + QtLockedFile m_lockFile; +}; + +} // namespace SharedTools diff --git a/src/shared/qtsingleapplication/qtsingleapplication.cpp b/src/shared/qtsingleapplication/qtsingleapplication.cpp new file mode 100644 index 00000000..ae007bd7 --- /dev/null +++ b/src/shared/qtsingleapplication/qtsingleapplication.cpp @@ -0,0 +1,190 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt 3D Studio. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qtsingleapplication.h" +#include "qtlocalpeer.h" + +#include "qtlockedfile.h" + +#include <QtCore/qdir.h> +#include <QtCore/qsharedmemory.h> +#include <QtWidgets/qwidget.h> + +#if defined(Q_OS_WIN) +#include <QtCore/qlibrary.h> +#include <QtCore/qt_windows.h> +typedef BOOL(WINAPI *allowSetForegroundWindow)(DWORD); +static allowSetForegroundWindow pAllowSetForegroundWindow = nullptr; +#endif + +namespace SharedTools { + +static const int instancesSize = 1024; + +static QString instancesLockFilename(const QString &appSessionId) +{ + const QChar slash(QLatin1Char('/')); + QString res = QDir::tempPath(); + if (!res.endsWith(slash)) + res += slash; + return res + appSessionId + QLatin1String("-instances"); +} + +QtSingleApplication::QtSingleApplication(const QString &appId, int &argc, char **argv) + : QApplication(argc, argv), + m_firstPeer(-1), + m_pidPeer(0) +{ + this->m_appId = appId; + + const QString appSessionId = QtLocalPeer::appSessionId(appId); + + // This shared memory holds a zero-terminated array of active (or crashed) instances + m_instances = new QSharedMemory(appSessionId, this); + m_block = false; + + // First instance creates the shared memory, later instances attach to it + const bool created = m_instances->create(instancesSize); + if (!created) { + if (!m_instances->attach()) { + qWarning() << "Failed to initialize instances shared memory: " + << m_instances->errorString(); + delete m_instances; + m_instances = 0; + return; + } + } + + // QtLockedFile is used to workaround QTBUG-10364 + QtLockedFile lockfile(instancesLockFilename(appSessionId)); + + lockfile.open(QtLockedFile::ReadWrite); + lockfile.lock(QtLockedFile::WriteLock); + qint64 *pids = static_cast<qint64 *>(m_instances->data()); + if (!created) { + // Find the first instance that it still running + // The whole list needs to be iterated in order to append to it + for (; *pids; ++pids) { + if (isRunning(*pids)) { + m_peers.append(*pids); + if (m_firstPeer == -1) + m_firstPeer = *pids; + } + } + } + // Add current pid to list and terminate it + *pids++ = QCoreApplication::applicationPid(); + *pids = 0; + m_pidPeer = new QtLocalPeer(this, appId + QLatin1Char('-') + + QString::number(QCoreApplication::applicationPid())); + connect(m_pidPeer, &QtLocalPeer::messageReceived, + this, &QtSingleApplication::messageReceived); + m_pidPeer->isClient(); + lockfile.unlock(); + +#if defined(Q_OS_WIN) + if (!pAllowSetForegroundWindow) { + QLibrary lib(QLatin1String("user32")); + pAllowSetForegroundWindow = + (allowSetForegroundWindow)lib.resolve("AllowSetForegroundWindow"); + } +#endif +} + +QtSingleApplication::~QtSingleApplication() +{ + if (!m_instances) + return; + const qint64 appPid = QCoreApplication::applicationPid(); + QtLockedFile lockfile(instancesLockFilename(QtLocalPeer::appSessionId(m_appId))); + lockfile.open(QtLockedFile::ReadWrite); + lockfile.lock(QtLockedFile::WriteLock); + // Rewrite array, removing current pid and previously crashed ones + qint64 *pids = static_cast<qint64 *>(m_instances->data()); + qint64 *newpids = pids; + for (; *pids; ++pids) { + if (*pids != appPid && isRunning(*pids)) + *newpids++ = *pids; + } + *newpids = 0; + lockfile.unlock(); +} + +bool QtSingleApplication::isRunning(qint64 pid) +{ + if (pid == -1) { + pid = m_firstPeer; + if (pid == -1) + return false; + } + + QtLocalPeer peer(this, m_appId + QLatin1Char('-') + QString::number(pid, 10)); + return peer.isClient(); +} + +bool QtSingleApplication::sendMessage(const QString &message, bool activateOnAccept, + int timeout, qint64 pid) +{ + if (pid == -1) { + pid = m_firstPeer; + if (pid == -1) + return false; + } + +#if defined(Q_OS_WIN) + if (activateOnAccept && pAllowSetForegroundWindow) { + // In windows it is necessary to specifically allow bringing another application to + // foreground. Otherwise it will just blink in the task bar. + pAllowSetForegroundWindow(pid); + } +#else + Q_UNUSED(activateOnAccept) +#endif + + QtLocalPeer peer(this, m_appId + QLatin1Char('-') + QString::number(pid, 10)); + return peer.sendMessage(message, timeout, m_block); +} + +QString QtSingleApplication::applicationId() const +{ + return m_appId; +} + +void QtSingleApplication::setBlock(bool value) +{ + m_block = value; +} + +QVector<qint64> QtSingleApplication::runningInstances() const +{ + // Note that this is a list of running instances at the time of application startup, + // so it should only be used at application startup. + return m_peers; +} + +} // namespace SharedTools diff --git a/src/shared/qtsingleapplication/qtsingleapplication.h b/src/shared/qtsingleapplication/qtsingleapplication.h new file mode 100644 index 00000000..5f9cdade --- /dev/null +++ b/src/shared/qtsingleapplication/qtsingleapplication.h @@ -0,0 +1,70 @@ +/**************************************************************************** +** +** Copyright (C) 2018 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt 3D Studio. +** +** $QT_BEGIN_LICENSE:GPL-EXCEPT$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include <QtWidgets/qapplication.h> +#include <QtCore/qvector.h> + +QT_FORWARD_DECLARE_CLASS(QSharedMemory) + +namespace SharedTools { + +class QtLocalPeer; + +class QtSingleApplication : public QApplication +{ + Q_OBJECT + +public: + QtSingleApplication(const QString &id, int &argc, char **argv); + ~QtSingleApplication(); + + bool isRunning(qint64 pid = -1); + + QString applicationId() const; + void setBlock(bool value); + + bool sendMessage(const QString &message, bool activateOnAccept = true, + int timeout = 5000, qint64 pid = -1); + + QVector<qint64> runningInstances() const; + +Q_SIGNALS: + void messageReceived(const QString &message, QObject *socket); + +private: + QString instancesFileName(const QString &m_appId); + + qint64 m_firstPeer; + QSharedMemory *m_instances; + QtLocalPeer *m_pidPeer; + QString m_appId; + bool m_block; + QVector<qint64> m_peers; +}; + +} // namespace SharedTools diff --git a/src/shared/qtsingleapplication/qtsingleapplication.pri b/src/shared/qtsingleapplication/qtsingleapplication.pri new file mode 100644 index 00000000..dc7264d7 --- /dev/null +++ b/src/shared/qtsingleapplication/qtsingleapplication.pri @@ -0,0 +1,13 @@ +INCLUDEPATH += $$PWD +DEPENDPATH += $$PWD +HEADERS += $$PWD/qtsingleapplication.h $$PWD/qtlocalpeer.h +SOURCES += $$PWD/qtsingleapplication.cpp $$PWD/qtlocalpeer.cpp + +QT *= network widgets + +gotqtlockedfile = $$find(HEADERS, .*qtlockedfile.h) +isEmpty(gotqtlockedfile):include(../qtlockedfile/qtlockedfile.pri) + +win32:contains(TEMPLATE, lib):contains(CONFIG, shared) { + DEFINES += QT_QTSINGLEAPPLICATION_EXPORT=__declspec(dllexport) +} |