187 lines
10 KiB
C++
187 lines
10 KiB
C++
#include "single_instance.h"
|
|
#include "debug_config.h"
|
|
#include <QApplication>
|
|
#include <QCryptographicHash>
|
|
#include <QDir>
|
|
#include <QEventLoop>
|
|
#include <QFileInfo>
|
|
#include <QLocalSocket>
|
|
#include <QTimer>
|
|
#include <QWidget>
|
|
#ifdef _WIN32
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
static const QByteArray activationRequest("ACTIVATE\n");
|
|
static const QByteArray activationReply("OK\n");
|
|
|
|
// Общий путь для повторного запуска, трея и уведомлений; showNormal потерял бы maximized.
|
|
void showAndActivateWindow(QWidget* window) {
|
|
if (!window) { DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "GUI activation: window is unavailable"); return; }
|
|
QWidget* modal = QApplication::activeModalWidget();
|
|
if (window->isMinimized()) window->setWindowState(window->windowState() & ~Qt::WindowMinimized);
|
|
window->show();
|
|
QWidget* target = modal ? modal : window;
|
|
if (target != window) {
|
|
if (target->isMinimized()) target->setWindowState(target->windowState() & ~Qt::WindowMinimized);
|
|
target->show();
|
|
}
|
|
target->raise(); target->activateWindow();
|
|
#ifdef _WIN32
|
|
if (QApplication::platformName() == QStringLiteral("windows") && !SetForegroundWindow(reinterpret_cast<HWND>(target->winId())))
|
|
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "GUI activation: Windows refused foreground pid=%lld", QCoreApplication::applicationPid());
|
|
#endif
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "GUI activation: visible=%d minimized=%d maximized=%d modal=%d active=%d",
|
|
window->isVisible(), window->isMinimized(), window->isMaximized(), target->isModal(), target->isActiveWindow());
|
|
}
|
|
|
|
// Канонический путь объединяет обращения через symlink; домашний каталог разделяет пользователей.
|
|
SingleInstance::SingleInstance(const QString& profileDirectory, QObject* parent)
|
|
: QObject(parent), m_lock(QDir(profileDirectory).filePath(QStringLiteral("vibechat.lock"))) {
|
|
QString path = QFileInfo(profileDirectory).canonicalFilePath();
|
|
if (path.isEmpty()) { m_error = QStringLiteral("Profile directory does not exist: %1").arg(profileDirectory); return; }
|
|
QString identity = QDir::homePath() + '\n' + path;
|
|
#ifdef _WIN32
|
|
identity = identity.toCaseFolded();
|
|
#endif
|
|
m_name = QStringLiteral("vibechat-") + QString::fromLatin1(
|
|
QCryptographicHash::hash(identity.toUtf8(), QCryptographicHash::Sha256).toHex().left(32));
|
|
m_server.setSocketOptions(QLocalServer::UserAccessOption);
|
|
connect(&m_server, &QLocalServer::newConnection, this, &SingleInstance::acceptConnections);
|
|
}
|
|
|
|
SingleInstance::~SingleInstance() {
|
|
if (m_lock.isLocked()) {
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "GUI instance: stopping pid=%lld server=%s",
|
|
QCoreApplication::applicationPid(), qPrintable(m_name));
|
|
m_server.close();
|
|
}
|
|
}
|
|
|
|
SingleInstance::Result SingleInstance::fail(const QString& error) {
|
|
m_error = error;
|
|
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "GUI instance: %s", qPrintable(error));
|
|
return Error;
|
|
}
|
|
|
|
// Только владелец блокировки может убирать оставшийся после аварии адрес локального канала.
|
|
SingleInstance::Result SingleInstance::start() {
|
|
if (m_started) return fail(QStringLiteral("Instance startup requested twice"));
|
|
m_started = true;
|
|
if (!m_error.isEmpty()) return fail(m_error);
|
|
if (!m_lock.tryLock()) {
|
|
if (m_lock.error() != QLockFile::LockFailedError)
|
|
return fail(QStringLiteral("Cannot lock profile %1: error=%2").arg(m_lock.fileName()).arg(int(m_lock.error())));
|
|
return activatePrimary();
|
|
}
|
|
if (!m_server.listen(m_name)) {
|
|
if (m_server.serverError() != QAbstractSocket::AddressInUseError ||
|
|
!QLocalServer::removeServer(m_name) || !m_server.listen(m_name))
|
|
return fail(QStringLiteral("Cannot listen on %1: %2").arg(m_name, m_server.errorString()));
|
|
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "GUI instance: removed stale local endpoint server=%s", qPrintable(m_name));
|
|
}
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "GUI instance: primary pid=%lld profile=%s server=%s",
|
|
QCoreApplication::applicationPid(), qPrintable(m_lock.fileName()), qPrintable(m_name));
|
|
return Primary;
|
|
}
|
|
|
|
// До появления канала владелец может ещё инициализироваться; после подключения ждём именно ACK.
|
|
SingleInstance::Result SingleInstance::activatePrimary() {
|
|
QLocalSocket socket;
|
|
QEventLoop wait;
|
|
QTimer deadline, retry;
|
|
deadline.setSingleShot(true); retry.setSingleShot(true);
|
|
bool connected = false, accepted = false;
|
|
QByteArray reply;
|
|
QString error;
|
|
connect(&deadline, &QTimer::timeout, &wait, [&] {
|
|
error = QStringLiteral("Running vibechat did not acknowledge activation within 5 seconds"); wait.quit();
|
|
});
|
|
connect(&retry, &QTimer::timeout, &socket, [&] { socket.abort(); socket.connectToServer(m_name); });
|
|
connect(&socket, &QLocalSocket::connected, &wait, [&] {
|
|
connected = true; retry.stop();
|
|
qint64 pid = 0; QString host, application;
|
|
bool ownerKnown = m_lock.getLockInfo(&pid, &host, &application);
|
|
#ifdef _WIN32
|
|
if (!ownerKnown || pid <= 0 || quint64(pid) > MAXDWORD)
|
|
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "GUI instance: cannot identify foreground recipient");
|
|
else if (!AllowSetForegroundWindow(DWORD(pid)))
|
|
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "GUI instance: foreground permission failed pid=%lld error=%lu",
|
|
pid, static_cast<unsigned long>(GetLastError()));
|
|
#endif
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "GUI instance: requesting activation owner_pid=%lld known=%d server=%s",
|
|
pid, ownerKnown, qPrintable(m_name));
|
|
if (socket.write(activationRequest) != activationRequest.size()) {
|
|
error = QStringLiteral("Cannot send activation: %1").arg(socket.errorString()); wait.quit();
|
|
}
|
|
});
|
|
connect(&socket, &QLocalSocket::readyRead, &wait, [&] {
|
|
reply += socket.readAll();
|
|
if (!activationReply.startsWith(reply)) {
|
|
error = QStringLiteral("Invalid activation acknowledgement"); wait.quit();
|
|
} else if (reply == activationReply) { accepted = true; wait.quit(); }
|
|
});
|
|
connect(&socket, &QLocalSocket::disconnected, &wait, [&] {
|
|
if (!accepted) { error = QStringLiteral("Running vibechat disconnected before activation acknowledgement"); wait.quit(); }
|
|
});
|
|
connect(&socket, &QLocalSocket::errorOccurred, &wait, [&](QLocalSocket::LocalSocketError code) {
|
|
if (accepted) return;
|
|
if (!connected && (code == QLocalSocket::ServerNotFoundError || code == QLocalSocket::ConnectionRefusedError)) {
|
|
retry.start(50); return;
|
|
}
|
|
error = QStringLiteral("Cannot activate running vibechat: %1").arg(socket.errorString()); wait.quit();
|
|
});
|
|
socket.setReadBufferSize(activationReply.size() + 1);
|
|
deadline.start(5000); retry.start(0); wait.exec();
|
|
if (!accepted) return fail(error);
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "GUI instance: activation acknowledged; secondary pid=%lld exiting",
|
|
QCoreApplication::applicationPid());
|
|
return Activated;
|
|
}
|
|
|
|
// Окно назначается до первого show/exec; отложенный показ должен примениться после начального --minimized.
|
|
void SingleInstance::setWindow(QWidget* window) {
|
|
m_window = window;
|
|
if (!window || !m_pending) return;
|
|
QTimer::singleShot(0, this, [this] {
|
|
if (m_pending && m_window) { m_pending = false; showAndActivateWindow(m_window); }
|
|
});
|
|
}
|
|
|
|
// Фиксированная маленькая команда: частичное чтение допустимо, неверный/зависший клиент закрывается.
|
|
void SingleInstance::acceptConnections() {
|
|
while (m_server.hasPendingConnections()) {
|
|
auto* socket = m_server.nextPendingConnection();
|
|
if (!socket) { DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "GUI instance: missing pending connection"); continue; }
|
|
socket->setReadBufferSize(activationRequest.size() + 1);
|
|
auto* deadline = new QTimer(socket); deadline->setSingleShot(true); deadline->start(3000);
|
|
connect(deadline, &QTimer::timeout, socket, [socket] {
|
|
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "GUI instance: activation client timed out"); socket->abort();
|
|
});
|
|
connect(socket, &QLocalSocket::errorOccurred, socket, [socket](QLocalSocket::LocalSocketError code) {
|
|
if (code != QLocalSocket::PeerClosedError)
|
|
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "GUI instance: activation connection failed: %s", qPrintable(socket->errorString()));
|
|
});
|
|
connect(socket, &QLocalSocket::disconnected, socket, [socket, deadline] {
|
|
if (deadline->isActive()) DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "GUI instance: client disconnected before sending activation");
|
|
socket->deleteLater();
|
|
});
|
|
connect(socket, &QLocalSocket::readyRead, this, [this, socket, deadline, request = QByteArray{}, done = false]() mutable {
|
|
if (done) return;
|
|
request += socket->readAll();
|
|
if (!activationRequest.startsWith(request)) {
|
|
DEBUG_WARN(DEBUG_CATEGORY_GENERAL, "GUI instance: invalid activation command bytes=%lld", qint64(request.size()));
|
|
deadline->stop(); socket->abort(); return;
|
|
}
|
|
if (request != activationRequest) return;
|
|
done = true; deadline->stop(); m_pending = true;
|
|
DEBUG_INFO(DEBUG_CATEGORY_GENERAL, "GUI instance: activation received pid=%lld window_ready=%d",
|
|
QCoreApplication::applicationPid(), !m_window.isNull());
|
|
if (m_window) { m_pending = false; showAndActivateWindow(m_window); }
|
|
if (socket->write(activationReply) != activationReply.size())
|
|
DEBUG_ERROR(DEBUG_CATEGORY_GENERAL, "GUI instance: cannot acknowledge activation: %s", qPrintable(socket->errorString()));
|
|
socket->disconnectFromServer();
|
|
});
|
|
}
|
|
}
|