diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index 6b37d36c..ff74e845 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -2,3 +2,4 @@ add_subdirectory(dnd-example) #add_subdirectory(test-taskbar) # Don't build #add_subdirectory(frameless-window) # Don't build add_subdirectory(animation-dci) +add_subdirectory(single-instance-demo) diff --git a/examples/single-instance-demo/CMakeLists.txt b/examples/single-instance-demo/CMakeLists.txt new file mode 100644 index 00000000..824c901a --- /dev/null +++ b/examples/single-instance-demo/CMakeLists.txt @@ -0,0 +1,17 @@ +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) + +set(BIN single-instance-demo) +set(TARGET ${BIN}${DTK_VERSION_MAJOR}) + +add_executable(${TARGET} + main.cpp +) + +target_link_libraries(${TARGET} PRIVATE + Qt${QT_VERSION_MAJOR}::Widgets + ${LIB_NAME} +) + +set_target_properties(${TARGET} PROPERTIES OUTPUT_NAME ${BIN}) + +install(TARGETS ${TARGET} DESTINATION "${TOOL_INSTALL_DIR}") diff --git a/examples/single-instance-demo/main.cpp b/examples/single-instance-demo/main.cpp new file mode 100644 index 00000000..18c6876f --- /dev/null +++ b/examples/single-instance-demo/main.cpp @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include +#include +#include +#include +#include + +DGUI_USE_NAMESPACE + +// 演示场景: +// 1. 实例 A 启动,成为单实例 primary +// 2. 3 秒后 A 触发退出:aboutToQuit 发出(dtkgui 释放单实例锁和 server), +// 但 main() 中模拟"有线程卡住",进程继续存活 10 秒才真正退出 +// 3. 在这 10 秒内启动实例 B: +// - 若在 aboutToQuit 之前启动 → B 检测到 A 存活,正常退出(单实例保护) +// - 若在 aboutToQuit 之后启动 → B 应能成功成为新的 primary(本次修复的目标) + +int main(int argc, char *argv[]) +{ + QApplication app(argc, argv); + + if (!DGuiApplicationHelper::setSingleInstance("single-instance-demo")) { + qInfo() << "[demo] another instance is running, exit now. pid =" << app.applicationPid(); + return 0; + } + + qInfo() << "[demo] primary started. pid =" << app.applicationPid(); + + // 可通过参数控制退出前的存活时间,默认 3 秒后自动退出 + int lifetime = 3000; + if (app.arguments().contains("--lifetime")) { + int pos = app.arguments().indexOf("--lifetime"); + if (pos + 1 < app.arguments().size()) + lifetime = app.arguments().at(pos + 1).toInt(); + } + + // 模拟退出阶段有线程卡住:事件循环退出后进程并不会立即结束 + const int stuckSeconds = 10; + QObject::connect(&app, &QCoreApplication::aboutToQuit, &app, [stuckSeconds] { + qInfo() << "[demo] aboutToQuit: single instance released," + << "process will stay alive for" << stuckSeconds << "s (simulating stuck thread)"; + }); + + QTimer::singleShot(lifetime, &app, &QCoreApplication::quit); + const int ret = app.exec(); + + // 模拟卡住的清理逻辑阻塞进程完全退出 + QThread::sleep(stuckSeconds); + qInfo() << "[demo] stuck cleanup finished, real exit now."; + + return ret; +} diff --git a/src/kernel/dguiapplicationhelper.cpp b/src/kernel/dguiapplicationhelper.cpp index 61bc4041..38afda7b 100644 --- a/src/kernel/dguiapplicationhelper.cpp +++ b/src/kernel/dguiapplicationhelper.cpp @@ -105,6 +105,39 @@ Q_LOGGING_CATEGORY(dgAppHelper, "dtk.dguihelper", QtInfoMsg) Q_GLOBAL_STATIC(QLocalServer, _d_singleServer) +// 单实例锁文件,进程退出(aboutToQuit)时需提前释放, +// 避免线程卡住导致进程未完全退出时新实例无法获取锁。 +// 文件名在 setSingleInstance 中设置(多次调用使用最后一次的 key) +class SingleInstanceLock +{ +public: + SingleInstanceLock() = default; + ~SingleInstanceLock() + { + unlock(); + } + + void reset(const QString &fileName) + { + unlock(); + lock.reset(new QLockFile(fileName)); + } + + bool isNull() const { return lock.isNull(); } + QLockFile *data() const { return lock.data(); } + + void unlock() + { + if (lock) + lock->unlock(); + } + +private: + QScopedPointer lock; +}; + +Q_GLOBAL_STATIC(SingleInstanceLock, _d_singleInstanceLock) + // Protocol changelog: // v1: version + pid + arguments // v2: version + pid + arguments + envs (KEY=VALUE pairs, filtered by whitelist) @@ -1550,15 +1583,18 @@ bool DGuiApplicationHelper::setSingleInstance(const QString &key, DGuiApplicatio } lockfile += QStringLiteral(".lock"); - static QScopedPointer lock(new QLockFile(lockfile)); // 同一个进程多次调用本接口使用最后一次设置的 key // FIX dcc 使用不同的 key 两次调用 setSingleInstance 后无法启动的问题 + if (_d_singleInstanceLock->isNull()) + _d_singleInstanceLock->reset(lockfile); + + QLockFile *lock = _d_singleInstanceLock->data(); qint64 pid = -1; QString hostname, appname; if (lock->isLocked() && lock->getLockInfo(&pid, &hostname, &appname) && pid == getpid()) { qCWarning(dgAppHelper) << "call setSingleInstance again within the same process"; - lock->unlock(); - lock.reset(new QLockFile(lockfile)); + _d_singleInstanceLock->reset(lockfile); + lock = _d_singleInstanceLock->data(); } if (!lock->tryLock()) { @@ -1604,6 +1640,23 @@ bool DGuiApplicationHelper::setSingleInstance(const QString &key, DGuiApplicatio qCDebug(dgAppHelper) << "===> listen <===" << _d_singleServer->serverName() << getpid(); } + // 应用开始退出时提前释放单实例资源(关闭 server 并解锁锁文件), + // 避免有线程卡住导致进程未退出期间新实例无法启动 + if (qApp) { + // Qt::UniqueConnection 对 lambda 无效,用静态标志防止重复连接 + static bool connected = false; + if (!connected) { + connected = true; + QObject::connect(qApp, &QCoreApplication::aboutToQuit, qApp, [] { + if (_d_singleServer.exists() && _d_singleServer->isListening()) + _d_singleServer->close(); + if (_d_singleInstanceLock.exists()) + _d_singleInstanceLock->unlock(); + qCDebug(dgAppHelper) << "single instance released on quit."; + }); + } + } + if (new_server) { qCDebug(dgAppHelper) << "===> new server <===" << _d_singleServer->serverName() << getpid(); QObject::connect(_d_singleServer, &QLocalServer::newConnection, qApp, [] {