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 /src/shared/qtsingleapplication | |
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>
Diffstat (limited to 'src/shared/qtsingleapplication')
-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 |
5 files changed, 527 insertions, 0 deletions
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) +} |