From ede2564e82f01c54b4109a61950bd61b49c28c1f Mon Sep 17 00:00:00 2001 From: zhaoyingzhen Date: Mon, 21 Sep 2026 16:33:22 +0800 Subject: [PATCH] feat: add systemd units and plugin groups for tray loader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add dde-tray-loader.target and a template instance service with lifetime coupled to dde-shell@DDE.service, group tray plugins via DConfig into per-group loader instances, add --group/--check-group support, and unset DDE_CURRENT_COMPOSITOR so loaders connect to the dock's internal dockplugin compositor instead of the outer session compositor. 新增 dde-tray-loader.target 及模板实例服务,生命周期与 dde-shell@DDE.service 绑定;通过 DConfig 将托盘插件分组为独立加载器实例, 新增 --group/--check-group 支持;并清除 DDE_CURRENT_COMPOSITOR,使加载器 连接 dock 内部的 dockplugin 合成器而非外层会话合成器。 Log: add systemd units and plugin groups for tray loader Pms: TASK-395865 --- CMakeLists.txt | 14 +++ debian/control | 2 + debian/dde-tray-loader.install | 3 + src/loader/CMakeLists.txt | 3 + .../org.deepin.dde.dock.plugin.groups.json | 64 +++++++++++ src/loader/main.cpp | 82 +++++++++++--- src/loader/trayplugingroups.cpp | 106 ++++++++++++++++++ src/loader/trayplugingroups.h | 33 ++++++ systemd/dde-tray-loader.target | 10 ++ ...-tray-loader@crashProneTrayPlugins.service | 1 + .../dde-tray-loader@otherTrayPlugins.service | 1 + ...-loader@selfMaintenanceTrayPlugins.service | 1 + ...-tray-loader@subprojectTrayPlugins.service | 1 + systemd/dde-tray-loader@.service.in | 34 ++++++ 14 files changed, 338 insertions(+), 17 deletions(-) create mode 100644 src/loader/configs/org.deepin.dde.dock.plugin.groups.json create mode 100644 src/loader/trayplugingroups.cpp create mode 100644 src/loader/trayplugingroups.h create mode 100644 systemd/dde-tray-loader.target create mode 120000 systemd/dde-tray-loader.target.wants/dde-tray-loader@crashProneTrayPlugins.service create mode 120000 systemd/dde-tray-loader.target.wants/dde-tray-loader@otherTrayPlugins.service create mode 120000 systemd/dde-tray-loader.target.wants/dde-tray-loader@selfMaintenanceTrayPlugins.service create mode 120000 systemd/dde-tray-loader.target.wants/dde-tray-loader@subprojectTrayPlugins.service create mode 100644 systemd/dde-tray-loader@.service.in diff --git a/CMakeLists.txt b/CMakeLists.txt index d5224b708..d8b6ffbca 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -46,6 +46,20 @@ pkg_get_variable(WAYLAND_PROTOCOLS_DATADIR wayland-protocols pkgdatadir) add_subdirectory(plugins) add_subdirectory(src) +## systemd user units +# The target (and its instances, pulled in via dde-tray-loader.target.wants) +# is started by dde-shell@DDE.service through Wants=dde-tray-loader.target. +# The instance unit is a configure_file template so the loader path follows +# CMAKE_INSTALL_FULL_LIBEXECDIR instead of being hard-coded. +configure_file(systemd/dde-tray-loader@.service.in + "${CMAKE_CURRENT_BINARY_DIR}/dde-tray-loader@.service" @ONLY) +install(FILES + systemd/dde-tray-loader.target + "${CMAKE_CURRENT_BINARY_DIR}/dde-tray-loader@.service" + DESTINATION lib/systemd/user) +install(DIRECTORY systemd/dde-tray-loader.target.wants + DESTINATION lib/systemd/user) + configure_file(dde-dock.pc.in dde-dock.pc @ONLY) install(FILES ${CMAKE_BINARY_DIR}/dde-dock.pc DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) diff --git a/debian/control b/debian/control index 5a2119eea..165ab13cb 100644 --- a/debian/control +++ b/debian/control @@ -46,6 +46,8 @@ Depends: ${shlibs:Depends}, ${misc:Depends}, libkf6windowsystem6, dde-daemon (>=6.1.26), lastore-daemon (>=5.2.9), +Breaks: + dde-shell (<< 2.0.55), Conflicts: dde-dock, dde-dock-onboard-plugin diff --git a/debian/dde-tray-loader.install b/debian/dde-tray-loader.install index 7754c4de3..a00299dc4 100644 --- a/debian/dde-tray-loader.install +++ b/debian/dde-tray-loader.install @@ -1,5 +1,8 @@ usr/libexec/trayplugin-loader usr/lib/*/libdde-trayplugin-interface.so.* +usr/lib/systemd/user/dde-tray-loader.target +usr/lib/systemd/user/dde-tray-loader@.service +usr/lib/systemd/user/dde-tray-loader.target.wants/ usr/share/dde-dock/translations usr/share/trayplugin-loader/translations usr/share/dsg/configs/*/*.json diff --git a/src/loader/CMakeLists.txt b/src/loader/CMakeLists.txt index 25dbe7439..2fe417322 100644 --- a/src/loader/CMakeLists.txt +++ b/src/loader/CMakeLists.txt @@ -37,6 +37,8 @@ add_executable(trayplugin-loader utils/setproctitle.cpp pluginmanager.h pluginmanager.cpp + trayplugingroups.h + trayplugingroups.cpp loader.qrc dqwaylandplatform.h dqwaylandplatform.cpp @@ -66,6 +68,7 @@ install(TARGETS trayplugin-loader DESTINATION ${CMAKE_INSTALL_LIBEXECDIR}) dtk_add_config_meta_files(APPID org.deepin.dde.dock FILES ./configs/org.deepin.dde.dock.plugin.common.json) # compat dtk_add_config_meta_files(APPID org.deepin.dde.tray-loader FILES ./configs/org.deepin.dde.dock.plugin.common.json) dtk_add_config_meta_files(APPID org.deepin.dde.tray-loader FILES "${PLUGIN_INTERACTION_CONFIG_META_FILE}") +dtk_add_config_meta_files(APPID org.deepin.dde.tray-loader FILES ./configs/org.deepin.dde.dock.plugin.groups.json) # 安装 .qm 文件 install(FILES ${QM_FILES} DESTINATION ${CMAKE_INSTALL_DATADIR}/trayplugin-loader/translations) diff --git a/src/loader/configs/org.deepin.dde.dock.plugin.groups.json b/src/loader/configs/org.deepin.dde.dock.plugin.groups.json new file mode 100644 index 000000000..1864e7aaf --- /dev/null +++ b/src/loader/configs/org.deepin.dde.dock.plugin.groups.json @@ -0,0 +1,64 @@ +{ + "magic": "dsg.config.meta", + "version": "1.0", + "contents": { + "selfMaintenanceTrayPlugins": { + "value": [ + "libapplication-tray.so", + "libbrightness.so", + "libdatetime.so", + "libdnd-mode.so", + "libeye-comfort-mode.so", + "libmedia.so", + "libnotification.so", + "libonboard.so", + "libshutdown.so", + "libairplane-mode.so", + "libbluetooth.so", + "libdock-network-plugin.so", + "libdock-wirelesscasting-plugin.so", + "libkeyboard-layout.so", + "libpower.so", + "libsound.so" + ], + "serial": 0, + "flags": [], + "name": "self maintenance tray plugins", + "name[zh_CN]": "自维护托盘插件", + "description": "Tray plugins maintained by their own lifecycle and not restarted on crash. Loaded together in one process group.", + "description[zh_CN]": "由自身维护生命周期、崩溃后不自动重启的托盘插件,集中在一个进程组中加载。", + "permissions": "readonly", + "visibility": "private" + }, + "subprojectTrayPlugins": { + "value": [ + "libdock-clipboard-plugin.so", + "libddegrandsearch_dockplugin.so", + "libdeepin-screen-recorder-plugin.so", + "libdeepin-system-monitor-plugin.so", + "libshot-start-plugin.so", + "libshot-start-record-plugin.so", + "libdde-disk-mount-plugin.so" + ], + "serial": 0, + "flags": [], + "name": "subproject tray plugins", + "name[zh_CN]": "子项目托盘插件", + "description": "Tray plugins maintained by sibling subprojects, isolated in their own process group.", + "description[zh_CN]": "由同级子项目维护的托盘插件,独立成组加载以便隔离。", + "permissions": "readwrite", + "visibility": "private" + }, + "crashProneTrayPlugins": { + "value": [], + "serial": 0, + "flags": [], + "name": "crash-prone tray plugins", + "name[zh_CN]": "易于崩溃的托盘插件", + "description": "Tray plugins known to be unstable, isolated in their own process group so a crash does not affect other plugins.", + "description[zh_CN]": "已知不稳定的托盘插件,独立成组加载,崩溃时不影响其他插件。", + "permissions": "readwrite", + "visibility": "private" + } + } +} diff --git a/src/loader/main.cpp b/src/loader/main.cpp index e29f83e6d..d6a7caf10 100644 --- a/src/loader/main.cpp +++ b/src/loader/main.cpp @@ -5,12 +5,14 @@ #include "setproctitle.h" #include "pluginsiteminterface_v2.h" #include "pluginmanager.h" +#include "trayplugingroups.h" #include #include #include #include +#include #include @@ -72,6 +74,21 @@ class LoaderApplication : public Dtk::Widget::DApplication int main(int argc, char *argv[], char *envp[]) { + // ExecCondition checks membership without initializing the GUI or connecting + // to the dock compositor. Exit 1 skips an empty group; 255 reports an error. + if (argc > 1 && qstrcmp(argv[1], "--check-group") == 0) { + QCoreApplication app(argc, argv); + if (argc != 3 || !loader::isValidGroup(QString::fromLocal8Bit(argv[2]))) { + qCritical() << "Expected --check-group ."; + return 255; + } + bool configOk = false; + const auto paths = loader::pluginPathsForGroup(QString::fromLocal8Bit(argv[2]), &configOk); + if (!configOk) + return 255; + return paths.isEmpty() ? 1 : 0; + } + #ifndef QT_DEBUG // 设置信号处理函数 struct sigaction sa; @@ -151,18 +168,50 @@ int main(int argc, char *argv[], char *envp[]) "Group name for the specified plugin path(s).", "group name" ); + QCommandLineOption pluginGroupLoadOption( + "group", + "Load all plugins belonging to " + "(selfMaintenanceTrayPlugins, subprojectTrayPlugins, " + "crashProneTrayPlugins, otherTrayPlugins).", + "group name" + ); parser.addOption(pluginPathsOption); parser.addOption(pluginGroupNameOption); + parser.addOption(pluginGroupLoadOption); parser.process(app); - if (!parser.isSet(pluginPathsOption)) { - qCritical() << "Error: -p is required."; - parser.showHelp(0); + if (parser.isSet(pluginPathsOption) == parser.isSet(pluginGroupLoadOption)) { + qCritical() << "Error: exactly one of -p or --group is required."; + parser.showHelp(1); } - auto paths = parser.value(pluginPathsOption); - auto pluginPaths = paths.split(';', Qt::SkipEmptyParts); + QStringList pluginPaths; + QString pluginGroupName; + if (parser.isSet(pluginGroupLoadOption)) { + pluginGroupName = parser.value(pluginGroupLoadOption); + if (!loader::isValidGroup(pluginGroupName)) { + qCritical() << "Error: unknown group" << pluginGroupName + << ". Valid groups:" << loader::SelfMaintenanceGroup + << loader::SubprojectGroup << loader::CrashProneGroup + << loader::OtherGroup; + return 1; + } + bool configOk = false; + pluginPaths = loader::pluginPathsForGroup(pluginGroupName, &configOk); + if (!configOk) { + // Do not mask a config load failure as a successful empty group. + qCritical() << "Failed to load group config for" << pluginGroupName; + return 2; + } + if (pluginPaths.isEmpty()) { + // Also handle a group becoming empty after ExecCondition ran. + qWarning() << "No plugins in group" << pluginGroupName << ", exiting."; + return 0; + } + } else { + pluginPaths = parser.value(pluginPathsOption).split(';', Qt::SkipEmptyParts); + } #ifdef QT_DEBUG const QDir shellDir(QString("%1/../../plugins/").arg(QCoreApplication::applicationDirPath())); @@ -183,18 +232,17 @@ int main(int argc, char *argv[], char *envp[]) return -1; } - QString pluginGroupName; - if (parser.isSet(pluginPathsOption)) { - pluginGroupName = parser.value(pluginGroupNameOption); - } - - if (pluginGroupName.isEmpty()) { - pluginGroupName = pluginManager.loadedPlugins()[0]->pluginName(); - } - - app.setApplicationName(pluginGroupName); - app.setApplicationDisplayName(pluginGroupName); - setproctitle((QStringLiteral("tray plugin: ") + pluginGroupName).toStdString().c_str()); + // Display name precedence: explicit -g override, else the --group name + // (empty in -p mode), else fall back to the first loaded plugin's name. + QString displayGroupName = parser.value(pluginGroupNameOption); + if (displayGroupName.isEmpty()) + displayGroupName = pluginGroupName; + if (displayGroupName.isEmpty()) + displayGroupName = pluginManager.loadedPlugins()[0]->pluginName(); + + app.setApplicationName(displayGroupName); + app.setApplicationDisplayName(displayGroupName); + setproctitle((QStringLiteral("tray plugin: ") + displayGroupName).toStdString().c_str()); qunsetenv("QT_SCALE_FACTOR"); for (auto iter = oldEnvs.begin(); iter != oldEnvs.end(); iter++) { if (iter.value().isEmpty()) { diff --git a/src/loader/trayplugingroups.cpp b/src/loader/trayplugingroups.cpp new file mode 100644 index 000000000..147d9d972 --- /dev/null +++ b/src/loader/trayplugingroups.cpp @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#include "trayplugingroups.h" + +#include + +#include +#include +#include + +Q_LOGGING_CATEGORY(loaderLog, "org.deepin.dde.tray-loader") + +namespace loader { + +namespace { + +const QStringList &pluginDirs() +{ + // Keep in sync with the directories previously scanned by dde-shell + // (panels/dock/loadtrayplugins.h). + static const QStringList dirs = { + "/usr/lib/dde-dock/plugins/", + "/usr/lib/dde-dock/plugins/quick-trays/", + "/usr/lib/dde-dock/plugins/system-trays/" + }; + return dirs; +} + +QStringList allPluginPaths() +{ + QStringList dirs; + const auto debugPaths = qEnvironmentVariable("TRAY_DEBUG_PLUGIN_PATH"); + if (!debugPaths.isEmpty()) + dirs << debugPaths.split(QDir::listSeparator()); + + if (dirs.isEmpty()) + dirs << pluginDirs(); + + QStringList pluginPaths; + for (const auto &pluginDir : std::as_const(dirs)) { + QDir dir(pluginDir); + if (!dir.exists()) { + qCWarning(loaderLog) << "The plugin directory does not exist:" << pluginDir; + continue; + } + + const auto pluginFileInfos = dir.entryInfoList({"*.so"}, QDir::Files); + for (const auto &pluginInfo : pluginFileInfos) { + pluginPaths.append(pluginInfo.absoluteFilePath()); + } + } + + return pluginPaths; +} + +} // namespace + +bool isValidGroup(const QString &groupName) +{ + return groupName == SelfMaintenanceGroup + || groupName == SubprojectGroup + || groupName == CrashProneGroup + || groupName == OtherGroup; +} + +QStringList pluginPathsForGroup(const QString &groupName, bool *ok) +{ + // The grouping schema is owned by dde-tray-loader itself (appid + // org.deepin.dde.tray-loader, config org.deepin.dde.dock.plugin.groups). + // It supersedes the grouping keys that previously lived in dde-shell's + // org.deepin.ds.dock.tray schema. + QScopedPointer dConfig(Dtk::Core::DConfig::create( + "org.deepin.dde.tray-loader", "org.deepin.dde.dock.plugin.groups", QString())); + if (!dConfig || !dConfig->isValid()) { + qCWarning(loaderLog) << "Failed to load group config for" << groupName; + if (ok) *ok = false; + return {}; + } + if (ok) *ok = true; + + const QStringList crashPronePlugins = dConfig->value(CrashProneGroup).toStringList(); + const QStringList selfMaintenancePlugins = dConfig->value(SelfMaintenanceGroup).toStringList(); + const QStringList subprojectPlugins = dConfig->value(SubprojectGroup).toStringList(); + + QStringList groupPluginPaths; + for (const auto &filePath : allPluginPaths()) { + const QString pluginName = filePath.section("/", -1); + // Each plugin belongs to exactly one group, with crash isolation taking priority. + const char *assignedGroup = OtherGroup; + if (crashPronePlugins.contains(pluginName)) + assignedGroup = CrashProneGroup; + else if (selfMaintenancePlugins.contains(pluginName)) + assignedGroup = SelfMaintenanceGroup; + else if (subprojectPlugins.contains(pluginName)) + assignedGroup = SubprojectGroup; + + if (groupName == assignedGroup) + groupPluginPaths.append(filePath); + } + + return groupPluginPaths; +} + +} diff --git a/src/loader/trayplugingroups.h b/src/loader/trayplugingroups.h new file mode 100644 index 000000000..53a108911 --- /dev/null +++ b/src/loader/trayplugingroups.h @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: 2024 - 2026 UnionTech Software Technology Co., Ltd. +// +// SPDX-License-Identifier: GPL-3.0-or-later + +#pragma once + +#include + +namespace loader { + +// Well-known plugin group names. They match the keys of the +// org.deepin.dde.dock.plugin.groups DConfig schema (owned by dde-tray-loader), +// which assigns each plugin .so to the process group it should be loaded in. +inline constexpr auto SelfMaintenanceGroup = "selfMaintenanceTrayPlugins"; +inline constexpr auto SubprojectGroup = "subprojectTrayPlugins"; +inline constexpr auto CrashProneGroup = "crashProneTrayPlugins"; +inline constexpr auto OtherGroup = "otherTrayPlugins"; + +// Whether is one of the well-known group names. Used to reject +// typos in manual invocations early instead of silently loading nothing. +bool isValidGroup(const QString &groupName); + +// Scan the standard dde-dock plugin directories and return the absolute +// paths of all plugins that belong to the given group according to the +// DConfig grouping configuration. Plugins not listed in any configured +// group fall into OtherGroup. +// +// When is non-null, it is set to false if the grouping config could +// not be loaded (so callers can distinguish a genuine empty group from a +// config failure), and to true otherwise. +QStringList pluginPathsForGroup(const QString &groupName, bool *ok = nullptr); + +} diff --git a/systemd/dde-tray-loader.target b/systemd/dde-tray-loader.target new file mode 100644 index 000000000..35bb7b81e --- /dev/null +++ b/systemd/dde-tray-loader.target @@ -0,0 +1,10 @@ +[Unit] +Description=DDE Tray Plugin Loader target +# Tray plugin processes render into the dock's internal dockplugin Wayland +# compositor exported by dde-shell. Their lifetime is therefore bound to the +# dock shell: when the shell stops or crashes, the loader instances are +# stopped as well and restarted together with the shell. +BindsTo=dde-shell@DDE.service +After=dde-shell@DDE.service +# Stop with the session core without delaying its startup for dock readiness. +PartOf=dde-session-core.target diff --git a/systemd/dde-tray-loader.target.wants/dde-tray-loader@crashProneTrayPlugins.service b/systemd/dde-tray-loader.target.wants/dde-tray-loader@crashProneTrayPlugins.service new file mode 120000 index 000000000..e44bdf056 --- /dev/null +++ b/systemd/dde-tray-loader.target.wants/dde-tray-loader@crashProneTrayPlugins.service @@ -0,0 +1 @@ +../dde-tray-loader@.service \ No newline at end of file diff --git a/systemd/dde-tray-loader.target.wants/dde-tray-loader@otherTrayPlugins.service b/systemd/dde-tray-loader.target.wants/dde-tray-loader@otherTrayPlugins.service new file mode 120000 index 000000000..e44bdf056 --- /dev/null +++ b/systemd/dde-tray-loader.target.wants/dde-tray-loader@otherTrayPlugins.service @@ -0,0 +1 @@ +../dde-tray-loader@.service \ No newline at end of file diff --git a/systemd/dde-tray-loader.target.wants/dde-tray-loader@selfMaintenanceTrayPlugins.service b/systemd/dde-tray-loader.target.wants/dde-tray-loader@selfMaintenanceTrayPlugins.service new file mode 120000 index 000000000..e44bdf056 --- /dev/null +++ b/systemd/dde-tray-loader.target.wants/dde-tray-loader@selfMaintenanceTrayPlugins.service @@ -0,0 +1 @@ +../dde-tray-loader@.service \ No newline at end of file diff --git a/systemd/dde-tray-loader.target.wants/dde-tray-loader@subprojectTrayPlugins.service b/systemd/dde-tray-loader.target.wants/dde-tray-loader@subprojectTrayPlugins.service new file mode 120000 index 000000000..e44bdf056 --- /dev/null +++ b/systemd/dde-tray-loader.target.wants/dde-tray-loader@subprojectTrayPlugins.service @@ -0,0 +1 @@ +../dde-tray-loader@.service \ No newline at end of file diff --git a/systemd/dde-tray-loader@.service.in b/systemd/dde-tray-loader@.service.in new file mode 100644 index 000000000..8a4e4f7fd --- /dev/null +++ b/systemd/dde-tray-loader@.service.in @@ -0,0 +1,34 @@ +[Unit] +Description=DDE Tray Plugin Loader (%i) +Documentation=https://github.com/linuxdeepin/dde-tray-loader +# Lifetime is bound to the dock shell transitively: the target's +# BindsTo=dde-shell@DDE.service stops the target (and via PartOf, this +# instance) whenever the shell stops or crashes, and the shell's +# Wants=dde-tray-loader.target re-pulls the target (and this instance) on +# shell restart. No direct BindsTo/After on the shell here — it would be +# redundant with the target-level coupling. +PartOf=dde-tray-loader.target +After=dde-tray-loader.target +# Match the former dock supervisor: one initial start and at most five crash +# retries per dock lifetime. dde-shell@DDE.service resets this counter on start. +StartLimitIntervalSec=infinity +StartLimitBurst=6 + +[Service] +Type=simple +# dde-shell@DDE.service is Type=dbus with BusName=org.deepin.dde.Dock1, +# which is only acquired after the dockplugin compositor is ready, so by +# the time this unit starts it is safe to connect to the dockplugin socket. +# Empty groups skip ExecStart before any GUI or Wayland initialization. +ExecCondition=@CMAKE_INSTALL_FULL_LIBEXECDIR@/trayplugin-loader --check-group %i +ExecStart=@CMAKE_INSTALL_FULL_LIBEXECDIR@/trayplugin-loader --group %i -platform wayland +# Normal exits (including nonzero codes) and explicit TERM/KILL do not restart. +Restart=on-abnormal +RestartPreventExitStatus=SIGTERM SIGKILL +# systemd treats them as crashes. +RestartForceExitStatus=SIGHUP SIGINT SIGPIPE +RestartSec=1s +Slice=session.slice +# The loader connects to the dock's internal dockplugin compositor; it must +# not pick up the outer session compositor configuration. +UnsetEnvironment=DDE_CURRENT_COMPOSITOR