7 changed files with 473 additions and 28 deletions
@ -0,0 +1,186 @@
|
||||
#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(); |
||||
}); |
||||
} |
||||
} |
||||
@ -0,0 +1,44 @@
|
||||
/* Один GUI на каталог профиля: QLockFile защищает данные, локальный канал передаёт
|
||||
* запрос показа владельцу. start вызывается один раз до чтения конфига/запуска ядра. |
||||
* Объект живёт до уничтожения окон; все вызовы — GUI-поток. */ |
||||
#pragma once |
||||
|
||||
#include <QObject> |
||||
#include <QString> |
||||
#include <QLockFile> |
||||
#include <QLocalServer> |
||||
#include <QPointer> |
||||
|
||||
class QWidget; |
||||
|
||||
/* Показать окно, сохранив maximized/fullscreen, и активировать его модальный диалог, если он есть. */ |
||||
void showAndActivateWindow(QWidget* window); |
||||
|
||||
class SingleInstance : public QObject { |
||||
public: |
||||
enum Result { Error = -1, Primary, Activated }; |
||||
|
||||
/* profileDirectory — существующий каталог конфига; parent владеет объектом по правилам QObject. */ |
||||
explicit SingleInstance(const QString& profileDirectory, QObject* parent = nullptr); |
||||
~SingleInstance() override; |
||||
|
||||
/* Получить блокировку и открыть канал либо дождаться ACK владельца (до 5 секунд).
|
||||
* Primary: продолжить запуск; Activated: завершить второй процесс с 0; Error: errorString(). */ |
||||
Result start(); |
||||
/* Текущее окно: мастер настройки или MainWindow. nullptr откладывает показ до следующего setWindow. */ |
||||
void setWindow(QWidget* window); |
||||
QString errorString() const { return m_error; } |
||||
QString serverName() const { return m_name; } |
||||
|
||||
private: |
||||
Result fail(const QString& error); |
||||
Result activatePrimary(); |
||||
void acceptConnections(); |
||||
|
||||
QLockFile m_lock; // Защита профиля на весь срок жизни первого экземпляра.
|
||||
QLocalServer m_server; // Только текущий пользователь; имя связано с каноническим путём профиля.
|
||||
QString m_name, m_error; |
||||
QPointer<QWidget> m_window; // Заимствованное окно; обнуляется при уничтожении мастера/GUI.
|
||||
bool m_started = false; |
||||
bool m_pending = false; // Запрос принят до создания окна; повторные запросы объединяются.
|
||||
}; |
||||
@ -0,0 +1,207 @@
|
||||
/* Настоящие процессы и локальные сокеты: один владелец профиля, ACK, startup-race,
|
||||
* аварийный выход и показ окна без запуска сетевого ядра/аудиоустройств. */ |
||||
#include "../src/single_instance.h" |
||||
#include "debug_config.h" |
||||
#include <QtTest> |
||||
#include <QApplication> |
||||
#include <QDialog> |
||||
#include <QDir> |
||||
#include <QElapsedTimer> |
||||
#include <QFileInfo> |
||||
#include <QLocalSocket> |
||||
#include <QProcess> |
||||
#include <QTemporaryDir> |
||||
#include <QTimer> |
||||
#include <QWidget> |
||||
#include <memory> |
||||
#include <vector> |
||||
|
||||
// Дочерний процесс создаёт окно только после определения роли.
|
||||
static int peerMain(QApplication& app, const QString& profile) { |
||||
SingleInstance instance(profile); |
||||
const auto role = instance.start(); |
||||
if (role == SingleInstance::Error) return 2; |
||||
if (role == SingleInstance::Activated) { qInfo("PEER_FORWARDED"); return 0; } |
||||
QWidget window; |
||||
instance.setWindow(&window); |
||||
qInfo("PEER_PRIMARY"); |
||||
QTimer watch; |
||||
QObject::connect(&watch, &QTimer::timeout, &window, [&] { |
||||
if (window.isVisible()) { qInfo("PEER_VISIBLE"); watch.stop(); } |
||||
}); |
||||
watch.start(10); |
||||
QTimer::singleShot(15000, &app, &QCoreApplication::quit); |
||||
return app.exec(); |
||||
} |
||||
|
||||
// Процесс всегда завершается до удаления временного каталога, включая ранний выход QVERIFY.
|
||||
struct Peer { |
||||
QProcess process; |
||||
QByteArray output; |
||||
explicit Peer(const QString& profile) { |
||||
process.setProcessChannelMode(QProcess::MergedChannels); |
||||
process.start(QCoreApplication::applicationFilePath(), {"--peer", profile}); |
||||
} |
||||
~Peer() { |
||||
if (process.state() != QProcess::NotRunning) { process.kill(); process.waitForFinished(3000); } |
||||
} |
||||
bool saw(const QByteArray& marker) { output += process.readAll(); return output.contains(marker); } |
||||
}; |
||||
|
||||
class InstanceTest : public QObject { |
||||
Q_OBJECT |
||||
private slots: |
||||
void forwardsToExistingProcess() { |
||||
QTemporaryDir profile; QVERIFY(profile.isValid()); |
||||
QString path = profile.filePath(QString::fromUtf8("profile space-кириллица")); QVERIFY(QDir().mkpath(path)); |
||||
Peer first(path); QTRY_VERIFY(first.saw("PEER_PRIMARY")); |
||||
Peer second(path); QTRY_COMPARE(second.process.state(), QProcess::NotRunning); |
||||
QCOMPARE(second.process.exitCode(), 0); QVERIFY(second.saw("PEER_FORWARDED")); |
||||
QVERIFY(!second.output.contains("PEER_PRIMARY")); QTRY_VERIFY(first.saw("PEER_VISIBLE")); |
||||
QVERIFY(!QFileInfo::exists(QDir(path).filePath("vibechat.cfg"))); |
||||
} |
||||
|
||||
void simultaneousStartup() { |
||||
QTemporaryDir profile; QVERIFY(profile.isValid()); |
||||
std::vector<std::unique_ptr<Peer>> peers; |
||||
for (int i = 0; i < 4; i++) peers.push_back(std::make_unique<Peer>(profile.path())); |
||||
for (const auto& peer : peers) QTRY_VERIFY(peer->saw("PEER_PRIMARY") || peer->saw("PEER_FORWARDED")); |
||||
unsigned primary = 0, forwarded = 0; |
||||
for (const auto& peer : peers) { |
||||
if (peer->output.contains("PEER_PRIMARY")) { primary++; QTRY_VERIFY(peer->saw("PEER_VISIBLE")); } |
||||
else { |
||||
QTRY_COMPARE(peer->process.state(), QProcess::NotRunning); |
||||
QCOMPARE(peer->process.exitCode(), 0); forwarded++; |
||||
} |
||||
} |
||||
QCOMPARE(primary, 1U); QCOMPARE(forwarded, 3U); |
||||
} |
||||
|
||||
void differentProfiles() { |
||||
QTemporaryDir a, b; QVERIFY(a.isValid() && b.isValid()); |
||||
Peer first(a.path()), second(b.path()); |
||||
QTRY_VERIFY(first.saw("PEER_PRIMARY")); QTRY_VERIFY(second.saw("PEER_PRIMARY")); |
||||
QVERIFY(!first.output.contains("PEER_FORWARDED") && !second.output.contains("PEER_FORWARDED")); |
||||
} |
||||
|
||||
void recoversAfterCrash() { |
||||
QTemporaryDir profile; QVERIFY(profile.isValid()); |
||||
Peer first(profile.path()); QTRY_VERIFY(first.saw("PEER_PRIMARY")); |
||||
first.process.kill(); QVERIFY(first.process.waitForFinished(3000)); |
||||
Peer replacement(profile.path()); QTRY_VERIFY(replacement.saw("PEER_PRIMARY")); |
||||
Peer secondary(profile.path()); QTRY_COMPARE(secondary.process.state(), QProcess::NotRunning); |
||||
QCOMPARE(secondary.process.exitCode(), 0); QTRY_VERIFY(replacement.saw("PEER_VISIBLE")); |
||||
} |
||||
|
||||
void queuesActivationUntilWindowExists() { |
||||
QTemporaryDir profile; QVERIFY(profile.isValid()); |
||||
SingleInstance instance(profile.path()); QCOMPARE(instance.start(), SingleInstance::Primary); |
||||
Peer secondary(profile.path()); QTRY_COMPARE(secondary.process.state(), QProcess::NotRunning); |
||||
QCOMPARE(secondary.process.exitCode(), 0); |
||||
QWidget window; QVERIFY(!window.isVisible()); |
||||
instance.setWindow(&window); window.showMinimized(); |
||||
QTRY_VERIFY(window.isVisible() && !window.isMinimized()); |
||||
} |
||||
|
||||
void waitsForServerStartup() { |
||||
QTemporaryDir profile; QVERIFY(profile.isValid()); |
||||
QLockFile lock(profile.filePath("vibechat.lock")); QVERIFY(lock.tryLock()); |
||||
SingleInstance identity(profile.path()); QLocalServer server; |
||||
QTimer::singleShot(250, &server, [&] { QVERIFY(server.listen(identity.serverName())); }); |
||||
connect(&server, &QLocalServer::newConnection, &server, [&] { |
||||
auto* socket = server.nextPendingConnection(); QVERIFY(socket); |
||||
connect(socket, &QLocalSocket::readyRead, socket, [socket, request = QByteArray{}]() mutable { |
||||
request += socket->readAll(); |
||||
if (!request.endsWith('\n')) return; |
||||
QCOMPARE(request, QByteArray("ACTIVATE\n")); |
||||
QCOMPARE(socket->write("OK\n"), 3LL); socket->disconnectFromServer(); |
||||
}); |
||||
}); |
||||
Peer secondary(profile.path()); QTRY_COMPARE(secondary.process.state(), QProcess::NotRunning); |
||||
QCOMPARE(secondary.process.exitCode(), 0); QVERIFY(secondary.saw("PEER_FORWARDED")); QVERIFY(lock.isLocked()); |
||||
} |
||||
|
||||
void liveOwnerWithoutServerTimesOut() { |
||||
QTemporaryDir profile; QVERIFY(profile.isValid()); |
||||
QLockFile lock(profile.filePath("vibechat.lock")); QVERIFY(lock.tryLock()); |
||||
QElapsedTimer time; time.start(); |
||||
Peer secondary(profile.path()); QTRY_COMPARE_WITH_TIMEOUT(secondary.process.state(), QProcess::NotRunning, 7000); |
||||
QCOMPARE(secondary.process.exitCode(), 2); QVERIFY(secondary.saw("did not acknowledge")); |
||||
QVERIFY(time.elapsed() < 7000 && lock.isLocked()); QVERIFY(!secondary.output.contains("PEER_PRIMARY")); |
||||
} |
||||
|
||||
void rejectsMissingAndInvalidAcknowledgement_data() { |
||||
QTest::addColumn<QByteArray>("reply"); |
||||
QTest::newRow("invalid") << QByteArray("NO\n"); |
||||
QTest::newRow("disconnected") << QByteArray(); |
||||
QTest::newRow("truncated") << QByteArray("O"); |
||||
} |
||||
|
||||
void rejectsMissingAndInvalidAcknowledgement() { |
||||
QFETCH(QByteArray, reply); |
||||
QTemporaryDir profile; QVERIFY(profile.isValid()); |
||||
QLockFile lock(profile.filePath("vibechat.lock")); QVERIFY(lock.tryLock()); |
||||
SingleInstance identity(profile.path()); QLocalServer server; QVERIFY(server.listen(identity.serverName())); |
||||
connect(&server, &QLocalServer::newConnection, &server, [&] { |
||||
auto* socket = server.nextPendingConnection(); QVERIFY(socket); |
||||
connect(socket, &QLocalSocket::readyRead, socket, [socket, reply] { |
||||
socket->readAll(); |
||||
if (!reply.isEmpty()) QCOMPARE(socket->write(reply), qint64(reply.size())); |
||||
socket->disconnectFromServer(); |
||||
}); |
||||
}); |
||||
Peer secondary(profile.path()); QTRY_COMPARE(secondary.process.state(), QProcess::NotRunning); |
||||
QCOMPARE(secondary.process.exitCode(), 2); QVERIFY(!secondary.saw("PEER_PRIMARY")); QVERIFY(lock.isLocked()); |
||||
} |
||||
|
||||
void validatesCommandsAndPartialReads() { |
||||
QTemporaryDir profile; QVERIFY(profile.isValid()); |
||||
SingleInstance instance(profile.path()); QCOMPARE(instance.start(), SingleInstance::Primary); |
||||
QWidget window; instance.setWindow(&window); |
||||
QLocalSocket bad; bad.connectToServer(instance.serverName()); QVERIFY(bad.waitForConnected(1000)); |
||||
QCOMPARE(bad.write("BAD\n"), 4LL); QVERIFY(bad.waitForBytesWritten(1000)); |
||||
QTRY_COMPARE(bad.state(), QLocalSocket::UnconnectedState); QVERIFY(!window.isVisible()); |
||||
QLocalSocket good; good.connectToServer(instance.serverName()); QVERIFY(good.waitForConnected(1000)); |
||||
QCOMPARE(good.write("ACT"), 3LL); QVERIFY(good.waitForBytesWritten(1000)); |
||||
QTest::qWait(30); QVERIFY(!window.isVisible()); QCOMPARE(good.bytesAvailable(), 0LL); |
||||
QCOMPARE(good.write("IVATE\n"), 6LL); QVERIFY(good.waitForBytesWritten(1000)); |
||||
QTRY_VERIFY(window.isVisible()); QTRY_COMPARE(good.bytesAvailable(), 3LL); QCOMPARE(good.readAll(), QByteArray("OK\n")); |
||||
} |
||||
|
||||
void missingDirectoryFails() { |
||||
QTemporaryDir profile; QVERIFY(profile.isValid()); |
||||
SingleInstance instance(profile.filePath("missing")); QCOMPARE(instance.start(), SingleInstance::Error); |
||||
QVERIFY(instance.errorString().contains("does not exist")); |
||||
QVERIFY(!QFileInfo::exists(profile.filePath("missing"))); |
||||
} |
||||
|
||||
void restoresWindowState() { |
||||
QWidget window; |
||||
showAndActivateWindow(&window); QVERIFY(window.isVisible()); QVERIFY(!window.isMinimized()); |
||||
window.setWindowState(Qt::WindowMaximized | Qt::WindowMinimized); |
||||
showAndActivateWindow(&window); QVERIFY(window.isVisible()); QVERIFY(window.isMaximized()); QVERIFY(!window.isMinimized()); |
||||
window.hide(); showAndActivateWindow(&window); QVERIFY(window.isVisible() && window.isMaximized()); |
||||
} |
||||
|
||||
void activatesModalDialog() { |
||||
QTemporaryDir profile; QVERIFY(profile.isValid()); |
||||
SingleInstance instance(profile.path()); QCOMPARE(instance.start(), SingleInstance::Primary); |
||||
QWidget window; instance.setWindow(&window); window.show(); |
||||
QDialog modal(&window); modal.setModal(true); modal.show(); |
||||
QTRY_COMPARE(QApplication::activeModalWidget(), &modal); |
||||
Peer secondary(profile.path()); QTRY_COMPARE(secondary.process.state(), QProcess::NotRunning); |
||||
QCOMPARE(secondary.process.exitCode(), 0); QVERIFY(modal.isVisible()); |
||||
QCOMPARE(QApplication::activeModalWidget(), &modal); |
||||
} |
||||
}; |
||||
|
||||
int main(int argc, char** argv) { |
||||
QApplication app(argc, argv); app.setQuitOnLastWindowClosed(false); |
||||
debug_config_init(); debug_set_level(DEBUG_LEVEL_INFO); |
||||
if (app.arguments().size() == 3 && app.arguments()[1] == QStringLiteral("--peer")) |
||||
return peerMain(app, app.arguments()[2]); |
||||
InstanceTest test; |
||||
return QTest::qExec(&test, argc, argv); |
||||
} |
||||
|
||||
#include "test_single_instance.moc" |
||||
Loading…
Reference in new issue