Skip to content

fix: prevent tray startup stalls and follow the dock lifecycle - #523

Merged
deepin-bot[bot] merged 1 commit into
linuxdeepin:masterfrom
yixinshark:fix/bug-378589-tray-lifecycle
Oct 9, 2026
Merged

deepin-bot[bot] merged 1 commit into
linuxdeepin:masterfrom
yixinshark:fix/bug-378589-tray-lifecycle

Conversation

@yixinshark

@yixinshark yixinshark commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Slow initialization or synchronous power-property reads can hold a tray group's GUI thread and delay other plugin icons. Yield to the event loop between plugin initializations, restore the session environment for callbacks, and fetch session power properties asynchronously into a cache. Process GetAll replies separately from DTK's live notifications: keep properties updated while a snapshot is pending, fill the remaining properties from the snapshot, and coalesce in-flight invalidations into a follow-up request. Discard pending requests when the service owner changes. Retry failed snapshots after one second without blocking the GUI thread. Retain initialization timing logs to distinguish plugin work from group startup.

Start the existing loader directly with --group, resolving membership once. The tray target uses BindsTo=, PartOf= and After=dde-shell@DDE.service, waiting for the Dock's Type=dbus startup/restart job before launching groups. The Dock registers its bus name after its internal compositor is ready. There is no additional launcher or readiness service. Tray groups stop in parallel before the Dock; desktop/Dock ordering remains independent.

插件之间返回事件循环,电源属性改为异步缓存读取,避免慢插件或慢电源服务拖住同组图标。托盘直接绑定 Dock 的生命周期并等待其启动完成,删除 ready 服务;保留现有 loader,退出时各组并行停止后再停止 Dock,桌面与 Dock 保持独立。

Validation

  • Full Qt6 build with CMAKE_SKIP_BUILD_RPATH=ON passed.
  • Event-dispatch CTest 1/1 passed with HOME=/nonexistent and no inherited XDG_CACHE_HOME; the test supplies its own cache and build-tree interface library.
  • Systemd unit checks and clean staged installation passed; no ready service is installed.
  • Manual isolated systemd fixtures passed: startup, explicit restart/crash recovery using the replacement Dock, cancellation, failure recovery, slow Dock teardown and slow tray teardown. Python/systemd integration tests are not part of package builds.
  • Power proxy checks passed with delayed replies, late service appearance, property changes/invalidation, and restart with an in-flight request.
  • Added a manual D-Bus integration test for two injected NoReply failures while the service keeps its name and only percentage updates arrive. The prior code fails to restore the missing state; the patched code restores all cached properties, keeps dispatching events, and stops retries after success/destruction. The isolated bus was stopped after the test. The test executable is excluded from the default build and is not installed or run by package CTest.
  • Delayed-snapshot regression reproduced: the previous code overwrote a live 77%/charging update with the captured 55%/discharging state. The patched code preserves all live values without a follow-up request; a percentage-only update still allows the snapshot to fill OnBattery/BatteryState. In-flight invalidation triggers one fresh snapshot. All isolated D-Bus scenarios and the Qt6 power build passed.
  • Repeated deployed X11 login/logout checks and standalone Dock restart succeeded. Native outer Wayland remains unverified.

Initialization completion does not measure the first visible frame. One intermittent gap between plugins remains unrooted; this PR fixes the confirmed blocking paths rather than claiming every startup delay is resolved. A slow tray can still delay Dock shutdown through the required ordering.

Paired session change: linuxdeepin/dde-session#234

Pms: BUG-378589

@sourcery-ai

sourcery-ai Bot commented Sep 29, 2026

Copy link
Copy Markdown

Reviewer's Guide

The PR separates group resolution from GUI startup, asynchronously initializes tray plugins with controlled environment restoration, and introduces a dock-bound readiness gate so tray startup follows the correct dock generation while teardown remains parallel. It also adds build-tree/cache-isolated CTest coverage and a manual systemd lifecycle test suite for startup, restart, failure, recovery, and shutdown behavior.

Sequence diagram for asynchronous tray plugin initialization

sequenceDiagram
    participant Loader as trayplugin-loader
    participant Manager as PluginManager
    participant Plugin as TrayPlugin
    participant EventLoop as QtEventLoop

    Loader->>Manager: loadPlugins()
    Manager->>EventLoop: QTimer.singleShot(0, loadNextPlugin)
    EventLoop->>Manager: loadNextPlugin()
    Manager->>Loader: pluginLoadStarted()
    Loader->>Manager: applyEnvs(pluginEnvs)
    Manager->>Plugin: loadPlugin(pluginPath)
    Plugin-->>Manager: plugin initialized
    Manager->>Loader: pluginLoadFinished()
    Loader->>Manager: applyEnvs(oldEnvs)
    Manager->>EventLoop: QTimer.singleShot(0, loadNextPlugin)
    Manager-->>Loader: loadingFinished(success)
Loading

Sequence diagram for group launcher and dock-bound tray readiness

sequenceDiagram
    participant Systemd as systemd
    participant GroupLoader as trayplugin-group-loader
    participant Loader as trayplugin-loader
    participant Ready as dde-tray-loader-ready.service
    participant Dock as Dock

    Systemd->>GroupLoader: start group target
    GroupLoader->>GroupLoader: pluginPathsForGroup(group)
    alt group is empty
        GroupLoader-->>Systemd: exit 0 without GUI initialization
    else group has plugins
        GroupLoader->>Loader: execl(trayplugin-loader, paths, group)
        Systemd->>Ready: start after dock startup/restart
        Ready->>Dock: readiness check
        alt dock is ready
            Ready-->>Systemd: readiness succeeded
            Systemd->>Loader: activate tray target
        else readiness fails
            Ready-->>Systemd: readiness failed
            Systemd-->>Loader: block tray startup
        end
    end
Loading

File-Level Changes

Change Details Files
Introduces a headless group resolver that delegates nonempty groups to the existing loader via exec.
  • Validates group names and resolves plugin membership once.
  • Exits cleanly for empty groups without GUI initialization.
  • Execs the loader with resolved paths, preserving MainPID and service semantics.
src/loader/groupmain.cpp
src/loader/CMakeLists.txt
CMakeLists.txt
Makes plugin initialization event-loop friendly while restoring the session environment around each plugin.
  • Queues plugin loading one plugin per event-loop turn.
  • Adds environment restoration before and after plugin initialization.
  • Reports per-plugin and total startup durations and handles asynchronous load failure.
src/loader/main.cpp
src/loader/pluginmanager.cpp
src/loader/pluginmanager.h
Coordinates dock and tray lifecycle through a one-shot readiness service instead of bus-name-only readiness.
  • Installs and wires a readiness unit ordered after dock startup/restart.
  • Gates tray target activation on successful dock readiness while retaining lifetime binding.
  • Preserves parallel teardown and prevents tray startup on dock failure or cancellation.
systemd/dde-tray-loader-ready.service
systemd/dde-tray-loader.target
systemd/dde-tray-loader@.service.in
debian/dde-tray-loader.install
debian/control
Adds isolated automated coverage for launcher behavior, event dispatch, and systemd lifecycle coordination.
  • Tests empty/invalid groups, exec failure, configuration failure, and MainPID preservation.
  • Tests queued event dispatch and environment restoration across plugin initialization.
  • Adds real-user-systemd scenarios for replacement dock ownership, startup failure, recovery, and parallel shutdown.
  • Configures CTest to use the build-tree interface library with disabled build RPATH and an isolated cache.
tests/CMakeLists.txt
tests/startupplugin.cpp
tests/test_group_launcher.py
tests/test_systemd_lifecycle.py

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

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

@yixinshark
yixinshark force-pushed the fix/bug-378589-tray-lifecycle branch 4 times, most recently from e4dac92 to 98565ab Compare September 30, 2026 06:15
@yixinshark yixinshark changed the title fix: coordinate tray startup and shutdown with dock lifecycle fix: prevent tray startup stalls and follow the dock lifecycle Sep 30, 2026
@yixinshark
yixinshark force-pushed the fix/bug-378589-tray-lifecycle branch 2 times, most recently from 19b075b to b6bb666 Compare September 30, 2026 07:11
@deepin-ci-robot

Copy link
Copy Markdown

deepin pr auto review

AI 代码审查报告

项目: linuxdeepin/dde-tray-loader
PR: #523 - fix: prevent tray startup stalls and follow the dock lifecycle
作者: yixinshark
分支: fix/bug-378589-tray-lifecycle → master
审查时间: 2026-09-30 15:15:00
分析模式: 全量分析(GitHub PR)


总体评价

评分:99/100
等级:优秀
风险等级:None

本次 PR 修复了托盘启动卡顿问题,将插件加载从同步改为异步(通过 QTimer::singleShot 在插件之间返回事件循环),将电源属性读取从同步 DBus 调用改为异步缓存模式(使用 QDBusPendingCallWatcher 和 m_liveProperties 集合管理属性一致性),并通过 systemd 单元文件将托盘生命周期绑定到 Dock。代码结构清晰,注释充分,测试覆盖全面,无安全漏洞。


漏洞统计

指标 数量
当前漏洞总数 0
新增漏洞 0
修复漏洞 0
持平漏洞 0

漏洞对比统计:新增漏洞 0 个,减少漏洞 0 个,持平 0 个


四维度评分

维度1:语法逻辑 ✓ — 25/25

语法正确,逻辑清晰

  • 异步插件加载模式正确:loadPlugins() 收集路径后通过 QTimer::singleShot(0, ...) 调度 loadNextPlugin(),在每次插件加载之间返回事件循环
  • 异步 DBus 属性缓存逻辑正确:getAllProperties() 使用 QDBusPendingCallWatcher 异步获取快照,m_liveProperties 集合正确防止延迟快照覆盖实时更新的属性
  • 环境变量切换逻辑正确:pluginLoadStarted 信号应用合成器环境,pluginLoadFinished 信号恢复会话环境,loadingFinished 处理加载失败情况
  • 服务拥有者变更处理正确:停止定时器、断开并删除待处理快照、清除状态后重新获取
  • updateProperties() 正确处理部分更新:缺失属性回退到缓存值,先更新全部缓存再发射变更信号
  • systemd 单元文件配置正确:DefaultDependencies=no + Conflicts=shutdown.target + Before=shutdown.target 适用于会话级单元
  • 资源管理正确:QDBusPendingCallWatcher 以 this 为父对象自动管理生命周期,服务变更时正确 deleteLater()

维度2:代码质量 ✓ — 24/25

代码结构清晰,注释完整

  • 注释质量高:注释解释了"为什么"而非仅"是什么",例如 "DTK handles live notifications only. Process GetAll replies separately so a delayed snapshot cannot overwrite properties updated in flight."
  • 代码结构合理:函数大小适中,getAllProperties() 约25行,updateProperties() 约20行
  • 无重复代码:applyEnvs lambda 定义一次多处使用
  • 信息日志有明确目的:PR 描述说明"保留初始化计时日志以区分插件工作与组启动",qInfo() 日志记录插件和组初始化耗时
  • 测试覆盖全面:事件分发测试、电源缓存集成测试、systemd 生命周期测试
  • 轻微问题:测试代码 powerdbuscache.cpp(306行)和 test_systemd_lifecycle.py(263行)较为密集,但对于多场景集成测试属于合理范围

维度3:代码性能 ✓ — 20/20

性能良好,资源使用合理

  • 本 PR 本身即为性能优化:异步插件加载防止 GUI 线程阻塞,异步电源属性读取避免等待慢速 DBus 响应
  • QTimer::singleShot(0, ...) 是 Qt 中协作式调度的标准模式,让已初始化插件有机会处理显示事件
  • 属性缓存避免重复同步 DBus 调用
  • 重试定时器(1秒)对瞬时故障合理,0ms 定时器用于失效后立即刷新
  • m_liveProperties.clear() 在每次请求开始时调用,集合通常很小,O(n) 开销可忽略
  • 测试代码中的 QThread::msleep(5) 忙等待仅用于测试,不影响生产性能

维度4:代码安全 ✓ — 30/30

存在0个安全漏洞

  • 无命令注入风险:systemd 单元文件使用 %i 模板变量,非 shell 注入
  • 无路径遍历风险:插件路径来自配置/组解析,无用户输入
  • 无敏感信息泄露:代码中无凭证、API 密钥或密码
  • DBus 测试配置文件(power-dbus-session.conf)使用宽松策略,但仅用于隔离测试总线,注释说明"fixtures must not activate desktop services"
  • 环境变量处理安全:变量名来自已保存的会话环境,无用户输入
  • 无缓冲区溢出风险:使用 Qt 字符串/容器类,自带边界检查
  • 无竞态条件:异步 DBus 状态管理通过 m_pendingSnapshot、m_refreshPending 和 m_liveProperties 仔细管理
  • 无资源泄漏:QDBusPendingCallWatcher 以 this 为父对象或显式 deleteLater(),QDBusServiceWatcher 以 this 为父对象
  • 错误处理完善:reply.isError() 检查并重试,服务拥有者变更时正确清理

改进建议

  1. 环境变量 unset 模式说明:main.cpp 中使用空 QByteArray 表示变量应被 unset 的模式虽是既有写法,建议添加简短注释说明此约定
  2. 测试代码密度:powerdbuscache.cpp 包含多个测试场景,可考虑拆分为独立测试函数以提高可读性

代码目的匹配度

本次 PR 的目的是修复托盘启动卡顿并绑定 Dock 生命周期,代码实现与目的完全一致:

  • ✅ 插件之间返回事件循环 — QTimer::singleShot(0, ...) 实现
  • ✅ 电源属性改为异步缓存读取 — QDBusPendingCallWatcher + m_liveProperties 实现
  • ✅ 恢复回调的会话环境 — pluginLoadStarted/pluginLoadFinished 信号切换环境
  • ✅ GetAll 回复与 DTK 实时通知分开处理 — 独立的 getAllProperties() 和 propertyChanged 连接
  • ✅ 服务拥有者变更时丢弃待处理请求 — serviceOwnerChanged 处理器
  • ✅ 失败快照异步重试 — m_refreshTimer.start(1000)
  • ✅ 托盘绑定 Dock 生命周期 — BindsTo= + PartOf= + After=
  • ✅ 删除 ready 服务 — 移除 ExecCondition,使用 Dock 总线名提供就绪保证
  • ✅ 保留现有 loader — --group 参数直接启动
  • ✅ 退出时各组并行停止 — systemd target 配置实现

审查结论

本次代码变更质量优秀,异步加载和异步属性缓存的实现逻辑正确,注释充分,测试覆盖全面,无安全漏洞。建议合并。

@deepin-ci-robot

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: robertkill, yixinshark

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

Resolve each plugin group once in the existing loader and yield to the
GUI event loop between plugin initializations, restoring the session
environment for callbacks. Fetch session power properties asynchronously
and serve cached values so a slow power service cannot block the group.
Handle GetAll replies separately from live notifications, preserving
properties updated in flight and refreshing invalidated properties after
the pending request. Discard old requests when the service owner changes.
Retry failed snapshots asynchronously after one second. Cover cache
ordering and recovery with a manual isolated D-Bus integration test.

Bind the tray target to the Dock with BindsTo, PartOf and After. Wait for
the replacement Type=dbus Dock on restart; stop tray groups in parallel
before the Dock. No extra launcher or readiness service is needed.
Add isolated event-dispatch and manual systemd lifecycle tests, startup
timing logs, and documentation of the final lifecycle.

在现有 loader 中仅解析一次插件分组,在插件初始化之间返回 GUI 事件循环,
并为回调恢复会话环境。异步获取会话电源属性并提供缓存读取,避免电源服务
响应缓慢时阻塞整个分组。单独处理 GetAll 回复,保留请求期间的实时属性更新,
并在当前请求结束后重新获取失效属性。服务拥有者变化时丢弃旧请求,快照失败后
间隔一秒异步重试,通过手动隔离 D-Bus 集成测试验证缓存时序与失败恢复。

托盘 target 通过 BindsTo、PartOf 和 After 绑定 Dock,重启时等待替换后的
Type=dbus Dock;退出时各托盘组并行停止,再停止 Dock。无需额外启动器或
就绪服务。增加隔离的事件分发测试、手动 systemd 生命周期测试、初始化耗时
日志,并记录最终生命周期。

Log: prevent tray startup stalls and follow the dock lifecycle
Pms: BUG-378589
@yixinshark
yixinshark force-pushed the fix/bug-378589-tray-lifecycle branch from b6bb666 to 6aa0479 Compare October 9, 2026 07:36
@yixinshark

Copy link
Copy Markdown
Contributor Author

/forcemerge

@deepin-bot

deepin-bot Bot commented Oct 9, 2026

Copy link
Copy Markdown

This pr force merged! (status: blocked)

@deepin-bot
deepin-bot Bot merged commit c273718 into linuxdeepin:master Oct 9, 2026
5 of 7 checks passed
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.

3 participants