use socket for single instance guard

This commit is contained in:
Le Tan 2021-01-01 17:07:47 +08:00
parent 8a1558f4da
commit 220fba09a9
3 changed files with 263 additions and 200 deletions

View File

@ -1,184 +1,225 @@
#include "singleinstanceguard.h"
#include <QDebug>
#include <QLocalServer>
#include <QLocalSocket>
#include <QDataStream>
#include <QByteArray>
#include <utils/utils.h>
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_server = tryListen();
if (m_server) {
// We are the lucky one.
qInfo() << "guard succeeds to run";
} else {
// 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;
} else {
qCritical() << "fail to create shared memory segment";
return false;
}
}
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<QLocalSocket> SingleInstanceGuard::tryConnect()
{
auto socket = QSharedPointer<QLocalSocket>::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<QLocalServer> SingleInstanceGuard::tryListen()
{
auto server = QSharedPointer<QLocalServer>::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>(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<quint32>(p_code);
out << static_cast<quint32>(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;
}
}

View File

@ -1,68 +1,83 @@
#ifndef SINGLEINSTANCEGUARD_H
#define SINGLEINSTANCEGUARD_H
#include <QSharedMemory>
#include <QStringList>
#include <QObject>
#include <QString>
#include <QSharedPointer>
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<QLocalSocket> tryConnect();
static const QString c_memKey;
static const int c_magic;
QSharedPointer<QLocalServer> 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<QLocalSocket> m_client;
QSharedPointer<QLocalServer> m_server;
bool m_ongoingConnect = false;
Command m_command;
static const QString c_serverName;
};
} // ns vnotex

View File

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