From 220fba09a93c57e7c1e32e49d1a3f616aa2abe96 Mon Sep 17 00:00:00 2001 From: Le Tan Date: Fri, 1 Jan 2021 17:07:47 +0800 Subject: [PATCH] use socket for single instance guard --- src/core/singleinstanceguard.cpp | 331 +++++++++++++++++-------------- src/core/singleinstanceguard.h | 103 ++++++---- src/main.cpp | 29 ++- 3 files changed, 263 insertions(+), 200 deletions(-) diff --git a/src/core/singleinstanceguard.cpp b/src/core/singleinstanceguard.cpp index daeaeed6..b7b9e97b 100644 --- a/src/core/singleinstanceguard.cpp +++ b/src/core/singleinstanceguard.cpp @@ -1,184 +1,225 @@ #include "singleinstanceguard.h" + #include +#include +#include +#include +#include #include using namespace vnotex; -const QString SingleInstanceGuard::c_memKey = "vnotex_shared_memory"; -const int SingleInstanceGuard::c_magic = 376686683; +const QString SingleInstanceGuard::c_serverName = "vnote"; SingleInstanceGuard::SingleInstanceGuard() - : m_online(false), - m_sharedMemory(c_memKey) { + qInfo() << "guarding is on"; +} + +SingleInstanceGuard::~SingleInstanceGuard() +{ + qInfo() << "guarding is off"; + exit(); } bool SingleInstanceGuard::tryRun() { - m_online = false; + Q_ASSERT(!m_online); - // If we can attach to the sharedmemory, there is another instance running. - // In Linux, crashes may cause the shared memory segment remains. In this case, - // this will attach to the old segment, then exit, freeing the old segment. - if (m_sharedMemory.attach()) { - qInfo() << "another instance is running"; +#if defined(Q_OS_WIN) + // On Windows, multiple servers on the same name are allowed. + m_client = tryConnect(); + if (m_client) { + // There is one server running and we are now connected, so we could not continue. return false; } - // Try to create it. - bool ret = m_sharedMemory.create(sizeof(SharedStruct)); - if (ret) { - // We created it. - m_sharedMemory.lock(); - SharedStruct *str = (SharedStruct *)m_sharedMemory.data(); - str->m_magic = c_magic; - str->m_filesBufIdx = 0; - str->m_askedToShow = false; - m_sharedMemory.unlock(); - - m_online = true; - return true; + m_server = tryListen(); + if (m_server) { + // We are the lucky one. + qInfo() << "guard succeeds to run"; } else { - qCritical() << "fail to create shared memory segment"; - return false; + // We still allow the guard to run. There maybe a bug need to fix. + qWarning() << "failed to connect to an existing instance or establish a new local server"; } +#else + m_server = tryListen(); + if (m_server) { + // We are the lucky one. + qInfo() << "guard succeeds to run"; + } else { + // Here we are sure there is another instance running. But we still use a socket to connect to make sure. + m_client = tryConnect(); + if (m_client) { + // We are sure there is another instance running. + return false; + } + + // We still allow the guard to run. There maybe a bug need to fix. + qWarning() << "failed to connect to an existing instance or establish a new local server"; + } +#endif + + setupServer(); + + m_online = true; + return true; } -void SingleInstanceGuard::openExternalFiles(const QStringList &p_files) +void SingleInstanceGuard::requestOpenFiles(const QStringList &p_files) { if (p_files.isEmpty()) { return; } - - if (!m_sharedMemory.isAttached()) { - if (!m_sharedMemory.attach()) { - qCritical() << "fail to attach to the shared memory segment" - << (m_sharedMemory.error() ? m_sharedMemory.errorString() : ""); - return; - } - } - - int idx = 0; - int tryCount = 100; - while (tryCount--) { - m_sharedMemory.lock(); - SharedStruct *str = (SharedStruct *)m_sharedMemory.data(); - V_ASSERT(str->m_magic == c_magic); - for (; idx < p_files.size(); ++idx) { - if (p_files[idx].size() + 1 > FilesBufCount) { - // Skip this long long name file. - continue; - } - - if (!appendFileToBuffer(str, p_files[idx])) { - break; - } - } - - m_sharedMemory.unlock(); - - if (idx < p_files.size()) { - Utils::sleepWait(500); - } else { - break; - } - } } -bool SingleInstanceGuard::appendFileToBuffer(SharedStruct *p_str, const QString &p_file) +void SingleInstanceGuard::requestShow() { - if (p_file.isEmpty()) { - return true; + Q_ASSERT(!m_online); + if (!m_client || m_client->state() != QLocalSocket::ConnectedState) { + qWarning() << "failed to request show" << m_client->errorString(); + return ; } - int strSize = p_file.size(); - if (strSize + 1 > FilesBufCount - p_str->m_filesBufIdx) { - return false; - } - - // Put the size first. - p_str->m_filesBuf[p_str->m_filesBufIdx++] = (ushort)strSize; - const QChar *data = p_file.constData(); - for (int i = 0; i < strSize; ++i) { - p_str->m_filesBuf[p_str->m_filesBufIdx++] = data[i].unicode(); - } - - return true; -} - -QStringList SingleInstanceGuard::fetchFilesToOpen() -{ - QStringList files; - - if (!m_online) { - return files; - } - - Q_ASSERT(m_sharedMemory.isAttached()); - m_sharedMemory.lock(); - SharedStruct *str = (SharedStruct *)m_sharedMemory.data(); - V_ASSERT(str->m_magic == c_magic); - Q_ASSERT(str->m_filesBufIdx <= FilesBufCount); - int idx = 0; - while (idx < str->m_filesBufIdx) { - int strSize = str->m_filesBuf[idx++]; - Q_ASSERT(strSize <= str->m_filesBufIdx - idx); - QString file; - for (int i = 0; i < strSize; ++i) { - file.append(QChar(str->m_filesBuf[idx++])); - } - - files.append(file); - } - - str->m_filesBufIdx = 0; - m_sharedMemory.unlock(); - - return files; -} - -void SingleInstanceGuard::showInstance() -{ - if (!m_sharedMemory.isAttached()) { - if (!m_sharedMemory.attach()) { - qCritical() << "fail to attach to the shared memory segment" - << (m_sharedMemory.error() ? m_sharedMemory.errorString() : ""); - return; - } - } - - m_sharedMemory.lock(); - SharedStruct *str = (SharedStruct *)m_sharedMemory.data(); - V_ASSERT(str->m_magic == c_magic); - str->m_askedToShow = true; - m_sharedMemory.unlock(); -} - -bool SingleInstanceGuard::fetchAskedToShow() -{ - if (!m_online) { - return false; - } - - Q_ASSERT(m_sharedMemory.isAttached()); - m_sharedMemory.lock(); - SharedStruct *str = (SharedStruct *)m_sharedMemory.data(); - V_ASSERT(str->m_magic == c_magic); - bool ret = str->m_askedToShow; - str->m_askedToShow = false; - m_sharedMemory.unlock(); - - return ret; + sendRequest(m_client.data(), OpCode::Show, QString()); } void SingleInstanceGuard::exit() { - if (!m_online) { + m_online = false; + + if (m_client) { + m_client->disconnectFromServer(); + m_client.clear(); + } + + if (m_server) { + m_server->close(); + m_server.clear(); + } +} + +QSharedPointer SingleInstanceGuard::tryConnect() +{ + auto socket = QSharedPointer::create(); + socket->connectToServer(c_serverName); + if (socket->waitForConnected(200)) { + // Connected. + qDebug() << "socket connected to server" << c_serverName; + return socket; + } else { + qDebug() << "socket connect timeout"; + return nullptr; + } +} + +QSharedPointer SingleInstanceGuard::tryListen() +{ + auto server = QSharedPointer::create(); + bool ret = server->listen(c_serverName); + if (!ret && server->serverError() == QAbstractSocket::AddressInUseError) { + // On Unix, a previous crash may leave a server running. + // Clean up and try again. + QLocalServer::removeServer(c_serverName); + ret = server->listen(c_serverName); + } + + if (ret) { + qDebug() << "local server listening on" << c_serverName; + return server; + } else { + qDebug() << "failed to start local server"; + return nullptr; + } +} + +void SingleInstanceGuard::setupServer() +{ + if (!m_server) { return; } - Q_ASSERT(m_sharedMemory.isAttached()); - m_sharedMemory.detach(); - m_online = false; + connect(m_server.data(), &QLocalServer::newConnection, + this, [this]() { + auto socket = m_server->nextPendingConnection(); + if (socket) { + qInfo() << "local server receives new connect" << socket; + if (m_ongoingConnect) { + qWarning() << "drop the connection since there is one ongoing connect"; + socket->disconnectFromServer(); + socket->deleteLater(); + return; + } + + m_ongoingConnect = true; + m_command.clear(); + + connect(socket, &QLocalSocket::disconnected, + this, [this, socket]() { + Q_ASSERT(m_ongoingConnect); + socket->deleteLater(); + m_ongoingConnect = false; + }); + connect(socket, &QLocalSocket::readyRead, + this, [this, socket]() { + receiveCommand(socket); + }); + } + }); +} + +void SingleInstanceGuard::receiveCommand(QLocalSocket *p_socket) +{ + QDataStream inStream; + inStream.setDevice(p_socket); + inStream.setVersion(QDataStream::Qt_5_12); + + if (m_command.m_opCode == OpCode::Null) { + // Relies on the fact that QDataStream serializes a quint32 into + // sizeof(quint32) bytes. + if (p_socket->bytesAvailable() < (int)sizeof(quint32) * 2) { + return; + } + + quint32 opCode = 0; + inStream >> opCode; + m_command.m_opCode = static_cast(opCode); + inStream >> m_command.m_size; + } + + if (p_socket->bytesAvailable() < m_command.m_size) { + return; + } + + qDebug() << "op code" << m_command.m_opCode << m_command.m_size; + + switch (m_command.m_opCode) { + case OpCode::Show: + Q_ASSERT(m_command.m_size == 0); + emit showRequested(); + break; + + default: + qWarning() << "unknown op code" << m_command.m_opCode; + break; + } +} + +void SingleInstanceGuard::sendRequest(QLocalSocket *p_socket, OpCode p_code, const QString &p_payload) +{ + QByteArray block; + QDataStream out(&block, QIODevice::WriteOnly); + out.setVersion(QDataStream::Qt_5_12); + out << static_cast(p_code); + out << static_cast(p_payload.size()); + if (p_payload.size() > 0) { + out << p_payload; + } + p_socket->write(block); + if (p_socket->waitForBytesWritten(3000)) { + qDebug() << "request sent" << p_code << p_payload.size(); + } else { + qWarning() << "failed to send request" << p_code; + } } diff --git a/src/core/singleinstanceguard.h b/src/core/singleinstanceguard.h index d5a169a0..ffa1acad 100644 --- a/src/core/singleinstanceguard.h +++ b/src/core/singleinstanceguard.h @@ -1,68 +1,83 @@ #ifndef SINGLEINSTANCEGUARD_H #define SINGLEINSTANCEGUARD_H -#include -#include +#include +#include +#include + +class QLocalServer; +class QLocalSocket; namespace vnotex { - class SingleInstanceGuard + class SingleInstanceGuard : public QObject { + Q_OBJECT public: SingleInstanceGuard(); - // Return ture if this is the only instance of VNote. + ~SingleInstanceGuard(); + + // Try to run. Return true on success. bool tryRun(); - // There is already another instance running. - // Call this to ask that instance to open external files passed in - // via command line arguments. - void openExternalFiles(const QStringList &p_files); - - // Ask another instance to show itself. - void showInstance(); - - // Fetch files from shared memory to open. - // Will clear the shared memory. - QStringList fetchFilesToOpen(); - - // Whether this instance is asked to show itself. - bool fetchAskedToShow(); - + // Server API. + public: // A running instance requests to exit. void exit(); + // Clients API. + public: + void requestOpenFiles(const QStringList &p_files); + + void requestShow(); + + signals: + void openFilesRequested(const QStringList &p_files); + + void showRequested(); + private: - // The count of the entries in the buffer to hold the path of the files to open. - enum { FilesBufCount = 1024 }; - - struct SharedStruct { - // A magic number to identify if this struct is initialized - int m_magic; - - // Next empty entry in m_filesBuf. - int m_filesBufIdx; - - // File paths to be opened. - // Encoded in this way with 2 bytes for each size part. - // [size of file1][file1][size of file2][file 2] - // Unicode representation of QString. - ushort m_filesBuf[FilesBufCount]; - - // Whether other instances ask to show the legal instance. - bool m_askedToShow; + enum OpCode + { + Null = 0, + Show }; - // Append @p_file to the shared struct files buffer. - // Returns true if succeeds or false if there is no enough space. - bool appendFileToBuffer(SharedStruct *p_str, const QString &p_file); + struct Command + { + void clear() + { + m_opCode = OpCode::Null; + m_size = 0; + } - bool m_online; + OpCode m_opCode = OpCode::Null; + int m_size = 0; + }; - QSharedMemory m_sharedMemory; + QSharedPointer tryConnect(); - static const QString c_memKey; - static const int c_magic; + QSharedPointer tryListen(); + + void setupServer(); + + void receiveCommand(QLocalSocket *p_socket); + + void sendRequest(QLocalSocket *p_socket, OpCode p_code, const QString &p_payload); + + // Whether succeeded to run. + bool m_online = false; + + QSharedPointer m_client; + + QSharedPointer m_server; + + bool m_ongoingConnect = false; + + Command m_command; + + static const QString c_serverName; }; } // ns vnotex diff --git a/src/main.cpp b/src/main.cpp index 970cebd6..948afc0c 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -30,12 +30,6 @@ void initWebEngineSettings(); int main(int argc, char *argv[]) { - SingleInstanceGuard guard; - bool canRun = guard.tryRun(); - if (!canRun) { - return 0; - } - QTextCodec *codec = QTextCodec::codecForName("UTF8"); if (codec) { QTextCodec::setCodecForLocale(codec); @@ -75,12 +69,22 @@ int main(int argc, char *argv[]) initWebEngineSettings(); - const QString iconPath = ":/vnotex/data/core/icons/vnote.ico"; - // Make sense only on Windows. - app.setWindowIcon(QIcon(iconPath)); + { + const QString iconPath = ":/vnotex/data/core/icons/vnote.ico"; + // Make sense only on Windows. + app.setWindowIcon(QIcon(iconPath)); - app.setApplicationName(ConfigMgr::c_appName); - app.setOrganizationName(ConfigMgr::c_orgName); + app.setApplicationName(ConfigMgr::c_appName); + app.setOrganizationName(ConfigMgr::c_orgName); + } + + // Guarding. + SingleInstanceGuard guard; + bool canRun = guard.tryRun(); + if (!canRun) { + guard.requestShow(); + return 0; + } try { app.setApplicationVersion(ConfigMgr::getInst().getConfig().getVersion()); @@ -125,6 +129,9 @@ int main(int argc, char *argv[]) window.show(); VNoteX::getInst().getThemeMgr().setBaseBackground(window.palette().color(QPalette::Base)); + QObject::connect(&guard, &SingleInstanceGuard::showRequested, + &window, &MainWindow::showMainWindow); + window.kickOffOnStart(); int ret = app.exec();