Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,8 @@ install(FILES ${CMAKE_CURRENT_BINARY_DIR}/DdeDockConfig.cmake
DESTINATION ${CMAKE_INSTALL_LIBDIR}/cmake/DdeDock)

configure_file(environments.h.in environments.h @ONLY)

include(CTest)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
45 changes: 42 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,9 @@ $ cmake -Bbuild
$ cmake --build build
```

After building, a `dde-tray-loader` binary can be found inside the build folder, you can run it
by `dde-tray-loader -p xxx.so` to start an example. And you can optionally install it by:
After building, the executable is `build/src/loader/trayplugin-loader`.
With a running DDE Dock compositor, use `trayplugin-loader -p /path/to/plugin.so`
to load a plugin. You can optionally install it by:

```shell
$ cmake --install build # only do this if you know what you are doing
Expand All @@ -29,9 +30,47 @@ $ sudo apt build-dep . # install build dependencies
$ dpkg-buildpackage -uc -us -nc -b # build binary package(s)
```

## Tray lifecycle and tests

`dde-shell@DDE.service` starts `dde-tray-loader.target`. The target waits for
the Dock's `Type=dbus` startup to complete, then starts the configured
`dde-tray-loader@<group>.service` instances in parallel. Each instance runs
`trayplugin-loader --group <group>` and resolves its plugin list once.
Plugins within a group initialize on the GUI thread, yielding to the event
loop between plugins so initialized plugins can handle display events.

The target uses `BindsTo=` and `PartOf=` to follow the Dock's lifetime and
restarts. On stop, the tray groups stop in parallel before the Dock. The
desktop and Dock have no mutual startup or shutdown ordering. Readiness is
provided by the Dock's bus name, registered after its compositor is ready;
there is no separate readiness service.

Run the event-dispatch test with `ctest --test-dir build --output-on-failure`.
It uses an offscreen platform, the build-tree interface library, and a writable
cache under the build directory. No running desktop is needed.

For systemd integration, run
`python3 tests/test_systemd_lifecycle.py ../dde-session` with a running user
systemd manager and `python3-gi`. This manual test creates temporary fixture
units to check startup, restart, failure recovery, and shutdown ordering; it
does not restart desktop services and is not part of package-build tests.

For the power cache integration test, build and run it on an isolated bus:

```shell
cmake --build build --target power-dbus-cache-test
dbus-run-session --config-file=tests/power-dbus-session.conf -- build/tests/power-dbus-cache-test
```

It checks failed `GetAll` recovery, delayed snapshots racing with live updates,
partial updates, invalidation, retry cancellation, and service restart.
Snapshots preserve properties updated in flight, and invalidation requests a
fresh snapshot. Failures retry asynchronously after one second; service owner
changes discard pending replies. This manual test is not part of CTest.

## Getting Involved

- [Code contribution via GitHub](https://github.com/linuxdeepin/dde-shell/)
- [Code contribution via GitHub](https://github.com/linuxdeepin/dde-tray-loader/)
- [Submit bug or suggestions to GitHub Issues or GitHub Discussions](https://github.com/linuxdeepin/developer-center/issues/new/choose)

## License
Expand Down
38 changes: 36 additions & 2 deletions README.zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ $ cmake -Bbuild
$ cmake --build build
```

构建完毕后,将可在构建目录内获得名为 `dde-tray-loader` 的二进制文件,可以执行`dde-tray-loader -p xxx.so`来运行托盘插件。此时你也可以通过如下方式进行安装:
构建完毕后,可执行文件位于 `build/src/loader/trayplugin-loader`。
在 DDE Dock 合成器运行时,可以执行 `trayplugin-loader -p /path/to/plugin.so` 加载插件。
也可以通过如下方式进行安装:

```shell
$ cmake --install build # 当你知道这条命令的作用时再执行它
Expand All @@ -28,9 +30,41 @@ $ sudo apt build-dep . # 安装构建依赖
$ dpkg-buildpackage -uc -us -nc -b # 构建二进制软件包
```

## 托盘生命周期与测试

`dde-shell@DDE.service` 拉起 `dde-tray-loader.target`。target 等待 Dock 的
`Type=dbus` 启动完成后,并行启动配置的 `dde-tray-loader@<group>.service` 实例。
每个实例执行 `trayplugin-loader --group <group>`,只解析一次插件列表。
同组插件在 GUI 线程上逐个初始化,插件之间返回事件循环,使已初始化的插件
能够处理显示事件。

target 通过 `BindsTo=` 和 `PartOf=` 跟随 Dock 的生命周期及重启。
停止时各托盘组并行退出,然后 Dock 退出;桌面与 Dock 之间没有启动或退出顺序约束。
Dock 在合成器就绪后注册总线名称,以此提供就绪保证,无需单独的 ready 服务。

执行 `ctest --test-dir build --output-on-failure` 运行事件分发测试。
测试使用 offscreen 平台、构建目录中的接口库和可写缓存,无需运行中的桌面。

如需验证 systemd 集成,在具备用户 systemd manager 和 `python3-gi` 的环境运行
`python3 tests/test_systemd_lifecycle.py ../dde-session`。
此手动测试通过临时模拟单元验证启动、重启、失败恢复及退出顺序,不重启实际桌面服务,
也不参与打包阶段的自动测试。

电源缓存集成测试需在隔离总线上执行:

```shell
cmake --build build --target power-dbus-cache-test
dbus-run-session --config-file=tests/power-dbus-session.conf -- build/tests/power-dbus-cache-test
```

用例验证 `GetAll` 失败恢复、延迟快照与实时通知交错、部分属性更新、属性失效、
重试取消及服务重启。快照保留请求期间已更新的属性,属性失效后重新获取快照;
失败后间隔一秒异步重试,服务拥有者变化时丢弃未完成的旧请求。
此手动测试不参与 CTest。

## 参与贡献

- [通过 GitHub 发起代码贡献](https://github.com/linuxdeepin/dde-shell/)
- [通过 GitHub 发起代码贡献](https://github.com/linuxdeepin/dde-tray-loader/)
- [通过 GitHub Issues 与 GitHub Discussions 汇报缺陷与反馈建议](https://github.com/linuxdeepin/developer-center/issues/new/choose)

## 许可协议
Expand Down
113 changes: 91 additions & 22 deletions plugins/dde-dock/power/dbus/dbuspower.cpp
Original file line number Diff line number Diff line change
@@ -1,39 +1,108 @@
// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later

/*
* This file was generated by qdbusxml2cpp version 0.8
* Command line was: qdbusxml2cpp -c DBusPower -p dbuspower org.deepin.dde.Power1.xml
*
* qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies).
*
* This is an auto-generated file.
* This file may have been hand-edited. Look for HAND-EDIT comments
* before re-generating it.
*/

#include "dbuspower.h"

/*
* Implementation of interface class DBusPower
*/

DBusPower::DBusPower(QObject *parent)
: QDBusAbstractInterface("org.deepin.dde.Power1", "/org/deepin/dde/Power1", staticInterfaceName(), QDBusConnection::sessionBus(), parent)
: DDBusExtendedAbstractInterface("org.deepin.dde.Power1", "/org/deepin/dde/Power1", staticInterfaceName(), QDBusConnection::sessionBus(), parent)
, m_refreshTimer(this)
{
qRegisterMetaType<BatteryStateMap>("BatteryStateMap");
qDBusRegisterMetaType<BatteryStateMap>();
qRegisterMetaType<BatteryPercentageMap>("BatteryPercentageMap");
qDBusRegisterMetaType<BatteryPercentageMap>();

QDBusConnection::sessionBus().connect(this->service(), this->path(), "org.freedesktop.DBus.Properties", "PropertiesChanged","sa{sv}as", this, SLOT(__propertyChanged__(QDBusMessage)));
QDBusConnection::systemBus().connect("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(__propertyChanged__(QDBusMessage)));
// DTK handles live notifications only. Process GetAll replies separately
// so a delayed snapshot cannot overwrite properties updated in flight.
connect(this, &DBusPower::propertyChanged, this, [this](const QString &name, const QVariant &value) {
if (m_pendingSnapshot)
m_liveProperties.insert(name);
updateProperties({{name, value}});
});
connect(this, &DBusPower::propertyInvalidated, this, [this](const QString &name) {
if (m_pendingSnapshot)
m_liveProperties.insert(name);
getAllProperties();
});

m_refreshTimer.setSingleShot(true);
connect(&m_refreshTimer, &QTimer::timeout, this, &DBusPower::getAllProperties);
auto *ownerWatcher = new QDBusServiceWatcher(service(), connection(), QDBusServiceWatcher::WatchForOwnerChange, this);
connect(ownerWatcher, &QDBusServiceWatcher::serviceOwnerChanged, this,
[this](const QString &, const QString &, const QString &newOwner) {
m_refreshTimer.stop();
if (m_pendingSnapshot) {
disconnect(m_pendingSnapshot, nullptr, this, nullptr);
m_pendingSnapshot->deleteLater();
m_pendingSnapshot = nullptr;
}
m_refreshPending = false;
m_liveProperties.clear();
// Let QDBusAbstractInterface update its owner before fetching again.
if (!newOwner.isEmpty())
m_refreshTimer.start(0);
});

setSync(false);
getAllProperties();
}

DBusPower::~DBusPower()
void DBusPower::getAllProperties()
{
QDBusConnection::sessionBus().disconnect(service(), path(), "org.freedesktop.DBus.Properties", "PropertiesChanged", "sa{sv}as", this, SLOT(propertyChanged(QDBusMessage)));
QDBusConnection::systemBus().disconnect("org.freedesktop.UPower", "/org/freedesktop/UPower", "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(propertyChanged(QDBusMessage)));
if (m_pendingSnapshot) {
m_refreshPending = true;
return;
}
if (!isValid())
return;

m_refreshTimer.stop();
m_liveProperties.clear();
auto request = QDBusMessage::createMethodCall(service(), path(),
QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("GetAll"));
request << interface();
m_pendingSnapshot = new QDBusPendingCallWatcher(connection().asyncCall(request), this);
connect(m_pendingSnapshot, &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) {
QDBusPendingReply<QVariantMap> reply = *watcher;
watcher->deleteLater();
m_pendingSnapshot = nullptr;
const bool refresh = m_refreshPending;
m_refreshPending = false;
if (reply.isError()) {
if (isValid())
m_refreshTimer.start(1000);
return;
}

auto snapshot = reply.value();
for (const auto &name : m_liveProperties)
snapshot.remove(name);
// An invalidation/request during GetAll needs a newer snapshot.
if (refresh)
m_refreshTimer.start(0);
updateProperties(snapshot);
});
}

void DBusPower::updateProperties(const QVariantMap &properties)
{
const bool onBattery = properties.contains("OnBattery") ? properties.value("OnBattery").toBool() : m_onBattery;
const auto percentage = properties.contains("BatteryPercentage")
? qdbus_cast<BatteryPercentageMap>(properties.value("BatteryPercentage")) : m_batteryPercentage;
const auto state = properties.contains("BatteryState")
? qdbus_cast<BatteryStateMap>(properties.value("BatteryState")) : m_batteryState;
const bool onBatteryChanged = m_onBattery != onBattery;
const bool percentageChanged = m_batteryPercentage != percentage;
const bool stateChanged = m_batteryState != state;
// Publish the whole snapshot before notifying readers of any property.
m_onBattery = onBattery;
m_batteryPercentage = percentage;
m_batteryState = state;
if (onBatteryChanged)
Q_EMIT OnBatteryChanged();
if (percentageChanged)
Q_EMIT BatteryPercentageChanged();
if (stateChanged)
Q_EMIT BatteryStateChanged();
}
60 changes: 25 additions & 35 deletions plugins/dde-dock/power/dbus/dbuspower.h
Original file line number Diff line number Diff line change
@@ -1,27 +1,21 @@
// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd.
// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd.
//
// SPDX-License-Identifier: LGPL-3.0-or-later

/*
* This file was generated by qdbusxml2cpp version 0.8
* Command line was: qdbusxml2cpp -c DBusPower -p dbuspower org.deepin.dde.Power1.xml
*
* qdbusxml2cpp is Copyright (C) 2015 Digia Plc and/or its subsidiary(-ies).
*
* This is an auto-generated file.
* Do not edit! All changes made to it will be lost.
*/

#ifndef DBUSPOWER_H_1467941176
#define DBUSPOWER_H_1467941176

#include <DDBusExtendedAbstractInterface>

Check warning on line 8 in plugins/dde-dock/power/dbus/dbuspower.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <DDBusExtendedAbstractInterface> not found. Please note: Cppcheck does not need standard library headers to get proper results.

#include <QtCore/QObject>

Check warning on line 10 in plugins/dde-dock/power/dbus/dbuspower.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QtCore/QObject> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QtCore/QByteArray>

Check warning on line 11 in plugins/dde-dock/power/dbus/dbuspower.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QtCore/QByteArray> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QtCore/QList>

Check warning on line 12 in plugins/dde-dock/power/dbus/dbuspower.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QtCore/QList> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QtCore/QMap>

Check warning on line 13 in plugins/dde-dock/power/dbus/dbuspower.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QtCore/QMap> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QtCore/QSet>

Check warning on line 14 in plugins/dde-dock/power/dbus/dbuspower.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QtCore/QSet> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QtCore/QString>

Check warning on line 15 in plugins/dde-dock/power/dbus/dbuspower.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QtCore/QString> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QtCore/QStringList>

Check warning on line 16 in plugins/dde-dock/power/dbus/dbuspower.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QtCore/QStringList> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QtCore/QTimer>

Check warning on line 17 in plugins/dde-dock/power/dbus/dbuspower.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QtCore/QTimer> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QtCore/QVariant>

Check warning on line 18 in plugins/dde-dock/power/dbus/dbuspower.h

View workflow job for this annotation

GitHub Actions / cppcheck

Include file: <QtCore/QVariant> not found. Please note: Cppcheck does not need standard library headers to get proper results.
#include <QtDBus/QtDBus>

typedef QMap<QString, quint32> BatteryStateMap;
Expand All @@ -32,56 +26,52 @@
/*
* Proxy class for interface org.deepin.dde.Power1
*/
class DBusPower: public QDBusAbstractInterface
class DBusPower: public Dtk::Core::DDBusExtendedAbstractInterface
{
Q_OBJECT

Q_SLOT void __propertyChanged__(const QDBusMessage& msg)
{
QList<QVariant> arguments = msg.arguments();
if (3 != arguments.count())
return;
QString interfaceName = msg.arguments().at(0).toString();
if (interfaceName !="org.deepin.dde.Power1")
return;
QVariantMap changedProps = qdbus_cast<QVariantMap>(arguments.at(1).value<QDBusArgument>());
foreach(const QString &prop, changedProps.keys()) {
const QMetaObject* self = metaObject();
for (int i=self->propertyOffset(); i < self->propertyCount(); ++i) {
QMetaProperty p = self->property(i);
if (p.name() == prop) {
Q_EMIT p.notifySignal().invoke(this);
}
}
}
}
public:
static inline const char *staticInterfaceName()
{ return "org.deepin.dde.Power1"; }

public:
explicit DBusPower(QObject *parent = 0);

~DBusPower();
~DBusPower() override = default;

// Keep snapshots separate from DTK's live propertyChanged notifications.
void getAllProperties();

// Icon refreshes must not wait for the session power service to reply.
Q_PROPERTY(bool OnBattery READ onBattery NOTIFY OnBatteryChanged)
inline bool onBattery() const
{ return qvariant_cast< bool >(property("OnBattery")); }
{ return m_onBattery; }

Q_PROPERTY(BatteryPercentageMap BatteryPercentage READ batteryPercentage NOTIFY BatteryPercentageChanged)
inline BatteryPercentageMap batteryPercentage() const
{ return qvariant_cast< BatteryPercentageMap >(property("BatteryPercentage")); }
{ return m_batteryPercentage; }

Q_PROPERTY(BatteryStateMap BatteryState READ batteryState NOTIFY BatteryStateChanged)
inline BatteryStateMap batteryState() const
{ return qvariant_cast< BatteryStateMap >(property("BatteryState")); }
{ return m_batteryState; }

public Q_SLOTS: // METHODS
Q_SIGNALS: // SIGNALS
// begin property changed signals
void OnBatteryChanged();
void BatteryPercentageChanged();
void BatteryStateChanged();

private:
void updateProperties(const QVariantMap &properties);

QTimer m_refreshTimer;
QDBusPendingCallWatcher *m_pendingSnapshot = nullptr;
QSet<QString> m_liveProperties;
bool m_refreshPending = false;
bool m_onBattery = false;
BatteryPercentageMap m_batteryPercentage;
BatteryStateMap m_batteryState;
};

namespace org {
Expand Down
Loading
Loading