Skip to content

fix(notification): fix memory leak in NotifyServerApplet destructor#1598

Open
heysion wants to merge 1 commit into
linuxdeepin:masterfrom
heysion:heysion-dev
Open

fix(notification): fix memory leak in NotifyServerApplet destructor#1598
heysion wants to merge 1 commit into
linuxdeepin:masterfrom
heysion:heysion-dev

Conversation

@heysion
Copy link
Copy Markdown
Member

@heysion heysion commented May 15, 2026

Fixed the issue where NotificationManager was not properly destroyed when the worker thread exited. The deleteLater() call was scheduled but never executed because the thread's event loop had already stopped.

Added comprehensive unit tests to verify the fix covers all destruction scenarios including worker-only, manager-only, and both present cases.

Log: Fix memory leak in NotifyServerApplet destructor

fix(notification): 修复 NotifyServerApplet 析构函数中的内存泄漏

修复了当工作线程退出时 NotificationManager 未被正确销毁的问题。
deleteLater() 被调度但从未执行,因为线程的事件循环已经停止。

添加了全面的单元测试来验证修复涵盖了所有销毁场景,
包括仅 worker、仅 manager 和两者都存在的情况。

Log: 修复 NotifyServerApplet 析构函数中的内存泄漏

Summary by Sourcery

Ensure NotifyServerApplet correctly cleans up NotificationManager and its worker thread to prevent memory leaks on destruction.

Bug Fixes:

  • Fix memory leak where NotificationManager was not destroyed if its worker thread event loop had already stopped when the applet was destructed.

Enhancements:

  • Improve NotifyServerApplet destructor logic to explicitly shut down and delete the worker thread and manager in all ownership scenarios.

Tests:

  • Add unit tests covering NotifyServerApplet destruction when only the manager exists, only the worker exists, and when both are present, including verification that NotificationManager is actually destroyed.

@deepin-ci-robot
Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: heysion

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 15, 2026

Reviewer's Guide

Refactors NotifyServerApplet’s destructor to ensure NotificationManager is destroyed correctly in all worker-thread and main-thread cases, and adds targeted unit tests that validate the new destruction behavior and prevent regressions around the previous memory leak.

Sequence diagram for updated NotifyServerApplet destruction logic

sequenceDiagram
    participant NotifyServerApplet
    participant QThread_worker as m_worker
    participant NotificationManager_manager as m_manager

    NotifyServerApplet->>NotifyServerApplet: ~NotifyServerApplet()
    alt m_worker != nullptr
        opt m_manager != nullptr
            NotifyServerApplet->>QThread_worker: connect(m_worker.finished, m_manager.deleteLater)
        end
        NotifyServerApplet->>QThread_worker: exit()
        NotifyServerApplet->>QThread_worker: wait()
        NotifyServerApplet->>NotifyServerApplet: delete m_worker
        NotifyServerApplet->>NotifyServerApplet: m_worker = nullptr
        NotifyServerApplet->>NotifyServerApplet: m_manager = nullptr
    else m_worker == nullptr and m_manager != nullptr
        NotifyServerApplet->>NotifyServerApplet: delete m_manager
        NotifyServerApplet->>NotifyServerApplet: m_manager = nullptr
    end
Loading

File-Level Changes

Change Details Files
Make NotifyServerApplet destructor correctly handle lifetime of NotificationManager and worker thread depending on which objects are present.
  • Remove unconditional use of deleteLater() on m_manager at the beginning of the destructor.
  • When a worker thread exists, connect QThread::finished to the manager’s deleteLater() with Qt::UniqueConnection so the deletion happens while the worker event loop is still active.
  • Exit and wait for the worker thread, then delete the worker directly, nulling out m_worker and m_manager and returning early.
  • When no worker exists but a manager does, delete the manager directly and null out m_manager.
panels/notification/server/notifyserverapplet.cpp
Add tests to cover all destructor scenarios and verify that NotificationManager is destroyed exactly once in each case, preventing the prior leak.
  • Introduce TrackableNotificationManager subclass that counts destructor invocations via a static std::atomic_int.
  • Use QPointer to track manager object lifetime in tests.
  • Add tests for: manager-only case, worker-only case, and worker+manager case with the manager moved to the worker thread, verifying correct destruction and no crashes.
  • Include QTest utilities and temporarily expose NotifyServerApplet private members via #define private public to set up internal state for tests.
tests/panels/notification/server/notifyserverapplet_test.cpp

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The destructor logic assumes the worker thread is still running when the connection to finished is made; if the thread has already finished (but m_worker is non-null), the finished signal will never fire and m_manager will leak, so consider checking m_worker->isRunning() and deleting m_manager directly in the non-running case.
  • The tests use #define private public to access NotifyServerApplet internals; it would be more robust to expose the needed state via test-only helpers or friend declarations rather than redefining access specifiers.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The destructor logic assumes the worker thread is still running when the connection to `finished` is made; if the thread has already finished (but `m_worker` is non-null), the `finished` signal will never fire and `m_manager` will leak, so consider checking `m_worker->isRunning()` and deleting `m_manager` directly in the non-running case.
- The tests use `#define private public` to access `NotifyServerApplet` internals; it would be more robust to expose the needed state via test-only helpers or friend declarations rather than redefining access specifiers.

## Individual Comments

### Comment 1
<location path="panels/notification/server/notifyserverapplet.cpp" line_range="28-31" />
<code_context>
 NotifyServerApplet::~NotifyServerApplet()
 {
     qDebug(notifyLog) << "Exit notification server.";
-    if (m_manager) {
-        m_manager->deleteLater();
-    }
     if (m_worker) {
+        if (m_manager) {
+            // Schedule manager deletion while the worker event loop is still able to process it.
+            connect(m_worker, &QThread::finished, m_manager, &QObject::deleteLater, Qt::UniqueConnection);
+        }
         m_worker->exit();
</code_context>
<issue_to_address>
**issue (bug_risk):** QThread::finished → deleteLater on an object in the worker thread is likely never processed after calling exit(), leading to a leak.

Because `QThread::finished` is emitted in the owning (likely main/UI) thread, the `deleteLater` call on `m_manager` becomes a queued event to the worker’s event loop. But `exit()` stops that event loop and `finished` is emitted only after it has already exited, so the queued `deleteLater` may never run, leaking `m_manager`.

Instead, delete `m_manager` before shutting down the worker, for example by:
- Calling `QMetaObject::invokeMethod(m_manager, "deleteLater", Qt::BlockingQueuedConnection)` before `m_worker->exit()`/`wait()`, or
- Deleting it directly if it already lives in the main thread.

The key point is to clean up `m_manager` synchronously in the correct thread, rather than relying on `finished` after the event loop is gone.
</issue_to_address>

### Comment 2
<location path="tests/panels/notification/server/notifyserverapplet_test.cpp" line_range="127-110" />
<code_context>
+    EXPECT_NO_THROW(delete testApplet);
+}
+
+TEST_F(NotifyServerAppletTest, DestructorDeletesManagerAfterWorkerThreadExit) {
+    TrackableNotificationManager::destroyedCount = 0;
+
+    auto *testApplet = new NotifyServerApplet();
+    auto *worker = new QThread();
+    auto *manager = new TrackableNotificationManager();
+    QPointer<TrackableNotificationManager> managerGuard(manager);
+
+    testApplet->m_manager = manager;
+    testApplet->m_worker = worker;
+
+    manager->moveToThread(worker);
+    worker->start();
+
+    QTRY_VERIFY_WITH_TIMEOUT(worker->isRunning(), 1000);
+
+    delete testApplet;
+
+    EXPECT_TRUE(managerGuard.isNull());
+    EXPECT_EQ(TrackableNotificationManager::destroyedCount.load(), 1);
+}
+
</code_context>
<issue_to_address>
**suggestion (testing):** The null QPointer and destroyedCount checks may race with an asynchronous deleteLater, making this test potentially flaky.

Because the manager is deleted via a signal/`deleteLater`, its actual destruction depends on event loop processing. Immediately checking `managerGuard.isNull()` and `destroyedCount == 1` after `delete testApplet;` can therefore be timing‑dependent. To avoid flakiness, either wait for the event loop until the guard becomes null (e.g. `QTRY_VERIFY_WITH_TIMEOUT(managerGuard.isNull(), 1000);`) before asserting the count, or make the manager deletion synchronous in this path so the assertions can be deterministic.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +28 to +31
if (m_worker) {
if (m_manager) {
// Schedule manager deletion while the worker event loop is still able to process it.
connect(m_worker, &QThread::finished, m_manager, &QObject::deleteLater, Qt::UniqueConnection);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): QThread::finished → deleteLater on an object in the worker thread is likely never processed after calling exit(), leading to a leak.

Because QThread::finished is emitted in the owning (likely main/UI) thread, the deleteLater call on m_manager becomes a queued event to the worker’s event loop. But exit() stops that event loop and finished is emitted only after it has already exited, so the queued deleteLater may never run, leaking m_manager.

Instead, delete m_manager before shutting down the worker, for example by:

  • Calling QMetaObject::invokeMethod(m_manager, "deleteLater", Qt::BlockingQueuedConnection) before m_worker->exit()/wait(), or
  • Deleting it directly if it already lives in the main thread.

The key point is to clean up m_manager synchronously in the correct thread, rather than relying on finished after the event loop is gone.

Comment thread tests/panels/notification/server/notifyserverapplet_test.cpp
@deepin-bot
Copy link
Copy Markdown

deepin-bot Bot commented May 20, 2026

TAG Bot

TAG: 2.0.42
EXISTED: yes
DISTRIBUTION: unstable

m_worker->deleteLater();
delete m_worker;
m_worker = nullptr;
m_manager = nullptr;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这里有问题,此时无法确保 m_manager 的确被销毁了。

@deepin-bot
Copy link
Copy Markdown

deepin-bot Bot commented May 28, 2026

TAG Bot

New tag: 2.0.43
DISTRIBUTION: unstable
Suggest: synchronizing this PR through rebase #1611

@heysion heysion force-pushed the heysion-dev branch 8 times, most recently from 825bbf7 to e606777 Compare May 29, 2026 02:52
m_worker = nullptr;
if (m_manager) {
m_manager->deleteLater();
QCoreApplication::sendPostedEvents(m_manager, QEvent::DeferredDelete);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

释放需要这么复杂么?这里的场景很简单,将m_manager移动到worker线程中执行,释放NotifyServerApplet后需要停止这个worker线程,再释放m_manager,和worker,

m_worker = nullptr;
if (m_manager) {
m_manager->deleteLater();
QCoreApplication::sendPostedEvents(m_manager, QEvent::DeferredDelete);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

啊,不要搞这种骚操作呀,这个 manager 对象是所属哪个线程的?如果是在 worker 线程,那要确保在退出worker线程之前把manager对象销毁掉,然后再退出worker线程

@heysion heysion force-pushed the heysion-dev branch 4 times, most recently from b1a79a6 to bd77fe9 Compare May 29, 2026 05:40
@heysion heysion requested review from 18202781743 and zccrs May 29, 2026 05:40
bool NotifyServerApplet::init()
{
// Reentrancy protection
if (m_manager || m_worker) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

init不会执行多次,不用判断,另外应该是你的那个test存在问题,应该不用测直接赋值m_manager,m_worker这种场景,

@heysion heysion force-pushed the heysion-dev branch 3 times, most recently from e2533ae to 67fa2f8 Compare May 29, 2026 06:21
Fixed the issue where NotificationManager was not properly destroyed
when the worker thread exited. The deleteLater() call was scheduled
but never executed because the thread's event loop had already stopped.

Added comprehensive unit tests to verify the fix covers all destruction
scenarios including worker-only, manager-only, and both present cases.

Log: Fix memory leak in NotifyServerApplet destructor

fix(notification): 修复 NotifyServerApplet 析构函数中的内存泄漏

修复了当工作线程退出时 NotificationManager 未被正确销毁的问题。
deleteLater() 被调度但从未执行,因为线程的事件循环已经停止。

添加了全面的单元测试来验证修复涵盖了所有销毁场景,
包括仅 worker、仅 manager 和两者都存在的情况。

Log: 修复 NotifyServerApplet 析构函数中的内存泄漏
@deepin-ci-robot
Copy link
Copy Markdown

deepin pr auto review

这份 Git Diff 对通知服务小程序(NotifyServerApplet)进行了重构,主要涉及线程生命周期管理优化空指针防护以及单元测试补全。整体改进方向非常好,显著增强了代码的健壮性。但在语法逻辑、代码质量和安全性方面,仍有一些值得注意和改进的地方。

以下是详细的审查意见:

一、 语法与逻辑

  1. QPointerdelete 的逻辑冲突(严重逻辑问题)

    • 问题:析构函数中使用了 delete m_manager;delete m_worker;,但头文件中 m_managerm_worker 已被声明为 QPointerQPointer 的核心特性是“弱引用”,当对象被销毁时它会自动置为 nullptr。如果在 delete m_manager; 之后紧接着写 m_manager = nullptr;,这不仅是冗余的,更掩盖了一个潜在的逻辑漏洞:如果 m_managerdelete 之前就已经被外部或事件循环销毁了,QPointer 会变为 nullptr,此时 delete m_manager; 等价于 delete nullptr;,这是安全的(C++标准规定),但会导致原本期望执行的清理逻辑被跳过,且没有任何警告。
    • 建议:既然使用了 QPointer,就应该利用它的特性,或者改回裸指针。如果坚持使用 QPointer,建议在析构时先检查是否为空,如果不为空则 delete,并利用 QPointer 自动置空的特性,省略手动赋值 nullptr
      if (m_manager) {
          delete m_manager.data(); // 或者 delete m_manager; 
          // m_manager 会被 QPointer 自动置为 nullptr,无需手动赋值
      }
  2. 跨线程 delete 的逻辑隐患(严重逻辑问题)

    • 问题:在析构函数中,m_manager 被移动到了 m_worker 线程(m_manager->moveToThread(m_worker);)。在 Qt 中,绝对不能在对象所属线程之外直接 delete 该对象(除非你能保证它没有事件正在处理)。原代码使用了 m_manager->deleteLater();,这是 Qt 推荐的安全清理跨线程对象的方式。新代码改为直接 delete m_manager;,这在 m_worker 线程刚退出后立即调用,如果 m_manager 还有待处理的事件(如 DeferredDelete 事件),极易引发崩溃。
    • 建议:恢复使用 deleteLater(),并配合 QThread::finished 信号,或者确保在 delete 之前将 m_manager 移回当前线程(m_manager->moveToThread(QThread::currentThread());)。更优雅的做法是使用信号槽连接:
      // 在 init() 中:
      connect(m_worker, &QThread::finished, m_manager, &QObject::deleteLater);
      // 析构时只需确保线程退出,无需手动 delete m_manager
  3. CHECK_MANAGER 宏的 do-while(0) 格式问题

    • 问题CHECK_MANAGER_RET 宏的 while(0) 后面缺少分号,且缩进不一致。虽然在调用处可能加分号,但宏定义本身不规范。
    • 建议
      #define CHECK_MANAGER_RET(val) \
          do { \
              if (!m_manager) { \
                  qWarning(notifyLog) << "NotificationManager is null."; \
                  return val; \
              } \
          } while(0) // 保持风格统一

二、 代码质量

  1. CHECK_MANAGER 宏的现代化替代

    • 问题:使用宏来进行空指针检查和返回是 C 风格的做法,缺乏类型安全,且在调试时宏展开不易定位。
    • 建议:C++11/14 提供了更优雅的内联函数或模板方式。对于 void 返回和带返回值的函数,可以分别提供辅助函数,或者使用 C++17 的 [[nodiscard]] 配合异常/错误码。如果必须用宏,建议合并为一个:
      #define CHECK_MANAGER_RET(val) \
          do { \
              if (!m_manager) { \
                  qWarning(notifyLog) << "NotificationManager is null."; \
                  return val; \
              } \
          } while(0)
      #define CHECK_MANAGER() CHECK_MANAGER_RET(void)
  2. 析构函数中的硬编码超时时间

    • 问题constexpr int kWaitTimeoutMs = 3000;kTerminateWaitMs = 1000; 定义在析构函数内部,且是局部变量。
    • 建议:将这些常量提取为类的静态常量成员,或者匿名命名空间内的常量,提高可读性和可维护性:
      static constexpr int kWaitTimeoutMs = 3000;
      static constexpr int kTerminateWaitMs = 1000;
  3. init() 函数的重入保护逻辑不够完善

    • 问题if (m_manager || m_worker) { return true; } 这个逻辑有问题。如果 m_manager 存在但 m_worker 为空,或者反过来,说明对象处于一种异常的半初始化状态,直接 return true 会掩盖错误。
    • 建议:如果是重入保护,应该要求两者不为空才算已初始化;否则应视为错误状态:
      if (m_manager && m_worker) {
          qWarning(notifyLog) << "NotifyServerApplet is already initialized.";
          return true;
      }
      if (m_manager || m_worker) {
          qCritical(notifyLog) << "NotifyServerApplet is in an inconsistent state.";
          return false;
      }

三、 代码性能

  1. QMetaObject::invokeMethod 使用了 Qt::DirectConnection
    • 问题:在 actionInvoked 等函数中,QMetaObject::invokeMethod(m_manager, ..., Qt::DirectConnection, ...) 这种调用方式绕过了线程安全机制DirectConnection 意味着函数将在调用者的线程中直接执行,而不是在 m_manager 所在的 m_worker 线程中执行。这完全违背了将 m_manager 移动到独立线程的初衷,且可能导致严重的并发问题(如果 m_manager 内部有非线程安全的状态)。
    • 建议:如果 m_manager 生活在另一个线程,跨线程调用必须使用 Qt::QueuedConnection(这是 invokeMethod 跨线程的默认行为),除非你明确知道该方法是无状态的且线程安全的。建议去掉 Qt::DirectConnection
      QMetaObject::invokeMethod(m_manager, "actionInvoked", Q_ARG(qint64, id), ...);

四、 代码安全

  1. 测试代码中的 #define private public(严重安全隐患)

    • 问题:在测试文件中使用了 #define private public 来访问私有成员。这不仅破坏了封装,在 C++ 中还会导致未定义行为,因为 C++ 标准不保证包含 #define private public 的翻译单元和正常编译的类具有相同的内存布局(ODR 违背)。
    • 建议
      • 方案 A:在 NotifyServerApplet 类中声明测试友元类:FRIEND_TEST(NotifyServerAppletTest, QPointerNullByDefaultTest);(GTest 提供的宏)。
      • 方案 B:为需要测试的私有成员提供 protectedinternal 的访问器函数。
      • 方案 C:如果只是测试公共接口的副作用,尽量避免直接访问内部成员。
  2. terminate() 的使用

    • 问题:析构函数中使用了 m_worker->terminate()terminate() 是极其危险的,它会强行中断线程,可能导致死锁(不释放锁)、内存泄漏(不释放堆内存)、文件描述符泄漏等。
    • 建议:虽然这里加了超时保护作为最后手段,但必须确保 m_worker 线程中的代码(即 NotificationManager 的事件循环)不会持有任何锁或系统资源。如果可能,应设计更优雅的退出机制(如设置退出标志),而不是依赖 terminate()。至少应在文档或注释中明确说明:被 terminate 的线程必须能够承受强行终止的后果。

总结与修改建议代码片段

针对最严重的跨线程 delete 问题QPointer 逻辑问题,建议对析构函数和 init 函数进行如下调整:

notifyserverapplet.cpp:

NotifyServerApplet::~NotifyServerApplet()
{
    qDebug(notifyLog) << "Exit notification server.";

    static constexpr int kWaitTimeoutMs = 3000;
    static constexpr int kTerminateWaitMs = 1000;

    if (m_worker) {
        if (m_worker->isRunning()) {
            m_worker->quit();

            if (!m_worker->wait(kWaitTimeoutMs)) {
                qWarning(notifyLog) << "Worker thread did not exit in time, terminating.";
                m_worker->terminate();
                if (!m_worker->wait(kTerminateWaitMs)) {
                    qCritical(notifyLog) << "Worker thread terminate timeout.";
                }
            }
        }

        // 由于 m_manager 已经通过 connect 绑定了 finished -> deleteLater,
        // 这里不需要也不应该直接 delete m_manager,QPointer 会自动置空。
        // 如果没有绑定信号,则必须先移回当前线程再 delete:
        // if (m_manager) {
        //     m_manager->moveToThread(QThread::currentThread());
        //     delete m_manager.data(); 
        // }

        delete m_worker.data(); // QPointer 自动置空
    } else if (m_manager) {
        // 如果没有 worker,直接在当前线程 delete
        delete m_manager.data(); // QPointer 自动置空
    }
}

bool NotifyServerApplet::init()
{
    if (m_manager && m_worker) {
        qWarning(notifyLog) << "NotifyServerApplet is already initialized.";
        return true;
    }

    DApplet::init();

    m_manager = new NotificationManager();

    if (!m_manager->registerDbusService()) {
        qWarning(notifyLog) << QString("Can't register Notifications to the D-Bus object.");
        delete m_manager.data(); // QPointer 自动置空
        return false;
    }

    m_worker = new QThread();
    m_manager->moveToThread(m_worker);
    
    // 安全的异步清理机制
    connect(m_worker, &QThread::finished, m_manager, &QObject::deleteLater);

    m_worker->start();
    return true;
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants