From dd0843bf983d2f230c0a6d53965689441f5b64df Mon Sep 17 00:00:00 2001 From: yeshanshan Date: Tue, 25 Aug 2026 19:49:40 +0800 Subject: [PATCH] feat: release single instance lock on aboutToQuit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. Add SingleInstanceLock wrapper class to manage QLockFile lifecycle 2. Replace static QScopedPointer with global SingleInstanceLock instance 3. Connect to QCoreApplication::aboutToQuit to release single instance resources early 4. Close QLocalServer and unlock lock file before process fully exits 5. Add single-instance-demo example to demonstrate the fix scenario 6. Handle repeated setSingleInstance calls with different keys correctly The problem: when a process exits its event loop but remains alive due to stuck threads, the single instance lock file and local server remain held, preventing new instances from starting during that window. The fix releases these resources in aboutToQuit signal, allowing a new instance to become primary immediately. Log: Add single instance demo example and fix lock release timing on application exit Influence: 1. Test launching two instances of the demo simultaneously - second should exit 2. Test launching second instance while first is in aboutToQuit state with stuck thread - second should start successfully 3. Test repeated setSingleInstance calls with different keys within same process 4. Verify single instance protection still works for normal exit scenarios 5. Test the demo with --lifetime parameter to control exit timing feat: 在 aboutToQuit 时释放单实例锁 1. 添加 SingleInstanceLock 包装类来管理 QLockFile 生命周期 2. 用全局 SingleInstanceLock 实例替换静态 QScopedPointer 3. 连接 QCoreApplication::aboutToQuit 信号以提前释放单实例资源 4. 在进程完全退出前关闭 QLocalServer 并解锁锁文件 5. 添加 single-instance-demo 示例来演示修复场景 6. 正确处理同一进程内多次使用不同 key 调用 setSingleInstance 的情况 问题背景:当进程退出事件循环但线程卡住导致进程未完全退出时,单实例锁文件 和本地服务器仍然被持有,阻止新实例在该窗口期内启动。此修复在 aboutToQuit 信号中释放这些资源,使新实例能立即成为主实例。 Log: 新增单实例示例并修复应用退出时的锁释放时机 Influence: 1. 同时启动两个演示实例,第二个应正常退出 2. 在第一个实例处于 aboutToQuit 状态(线程卡住)时启动第二个实例,第二个 应能成功启动 3. 测试同一进程内使用不同 key 多次调用 setSingleInstance 4. 验证正常退出场景下单实例保护功能仍然有效 5. 使用 --lifetime 参数控制演示退出时机进行测试 PMS: TASK-372823 --- examples/CMakeLists.txt | 1 + examples/single-instance-demo/CMakeLists.txt | 17 ++++++ examples/single-instance-demo/main.cpp | 55 ++++++++++++++++++ src/kernel/dguiapplicationhelper.cpp | 59 +++++++++++++++++++- 4 files changed, 129 insertions(+), 3 deletions(-) create mode 100644 examples/single-instance-demo/CMakeLists.txt create mode 100644 examples/single-instance-demo/main.cpp 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, [] {