summaryrefslogtreecommitdiffstats
path: root/src/shared/qtsingleapplication
diff options
context:
space:
mode:
authorMiikka Heikkinen <miikka.heikkinen@qt.io>2018-04-18 17:38:40 +0300
committerMiikka Heikkinen <miikka.heikkinen@qt.io>2018-04-20 13:20:00 +0000
commitfda3aa98013b7e166e3ebc81f743d9f8297200f6 (patch)
treee8175e1f224b3ad94ca8eb55e99e60dd253df297 /src/shared/qtsingleapplication
parent3b6b637a5a7fc492014867ce3ce48c804d8978a4 (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.cpp191
-rw-r--r--src/shared/qtsingleapplication/qtlocalpeer.h63
-rw-r--r--src/shared/qtsingleapplication/qtsingleapplication.cpp190
-rw-r--r--src/shared/qtsingleapplication/qtsingleapplication.h70
-rw-r--r--src/shared/qtsingleapplication/qtsingleapplication.pri13
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)
+}