mirror of
https://gitee.com/vnotex/vnote.git
synced 2025-07-06 06:19:52 +08:00
use socket for single instance guard
This commit is contained in:
parent
8a1558f4da
commit
220fba09a9
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
19
src/main.cpp
19
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));
|
||||
|
||||
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();
|
||||
|
Loading…
x
Reference in New Issue
Block a user