|
| 1 | +// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd. |
| 2 | +// |
| 3 | +// SPDX-License-Identifier: GPL-3.0-or-later |
| 4 | + |
| 5 | +#include "globals.h" |
| 6 | +#include "launchdurationreporter.h" |
| 7 | +#include "applicationmanager1interface.h" |
| 8 | + |
| 9 | +#ifdef HAVE_DDE_API_EVENTLOGGER |
| 10 | +#include <dde-api/eventlogger.hpp> |
| 11 | +#endif |
| 12 | + |
| 13 | +#include <QDBusConnection> |
| 14 | +#include <QDBusObjectPath> |
| 15 | +#include <QDBusReply> |
| 16 | +#include <QDBusUnixFileDescriptor> |
| 17 | +#include <QDateTime> |
| 18 | +#include <QFileInfo> |
| 19 | +#include <QJsonDocument> |
| 20 | +#include <QJsonObject> |
| 21 | +#include <QLoggingCategory> |
| 22 | +#include <QProcess> |
| 23 | +#include <QSettings> |
| 24 | +#include <QStandardPaths> |
| 25 | +#include <QtConcurrent> |
| 26 | + |
| 27 | +#include <sys/syscall.h> |
| 28 | +#include <unistd.h> |
| 29 | + |
| 30 | +Q_LOGGING_CATEGORY(launchDurationReporter, "org.deepin.dde.shell.dock.launchDurationReporter") |
| 31 | + |
| 32 | +namespace { |
| 33 | + |
| 34 | +constexpr auto kAmService = "org.desktopspec.ApplicationManager1"; |
| 35 | +constexpr auto kAmPath = "/org/desktopspec/ApplicationManager1"; |
| 36 | +constexpr auto kInstanceIface = "org.desktopspec.ApplicationManager1.Instance"; |
| 37 | +constexpr auto kPackageCacheTTLSeconds = 1800; |
| 38 | + |
| 39 | +// pidfd_open is available since Linux 5.3; glibc may not wrap it, so call the syscall directly. |
| 40 | +int pidfd_open(pid_t pid, unsigned int flags) |
| 41 | +{ |
| 42 | + return static_cast<int>(syscall(SYS_pidfd_open, pid, flags)); |
| 43 | +} |
| 44 | + |
| 45 | +struct InstanceIdentity { |
| 46 | + QString instanceId; |
| 47 | + QString launchType; |
| 48 | +}; |
| 49 | + |
| 50 | +// Map the just-appeared window (by pid) to its exact ApplicationManager instance via Identify(pidfd), |
| 51 | +// reading that instance's LaunchType from the same reply. This is reliable per-window, unlike |
| 52 | +// enumerating Application.Instances and guessing the latest one. |
| 53 | +InstanceIdentity identifyInstance(pid_t pid) |
| 54 | +{ |
| 55 | + InstanceIdentity identity; |
| 56 | + if (pid <= 0) { |
| 57 | + return identity; |
| 58 | + } |
| 59 | + |
| 60 | + const int pidfd = pidfd_open(pid, 0); |
| 61 | + if (pidfd < 0) { |
| 62 | + qCWarning(launchDurationReporter) << "[DockIconTiming] pidfd_open failed for pid:" << pid; |
| 63 | + return identity; |
| 64 | + } |
| 65 | + |
| 66 | + ApplicationManager am(QString::fromUtf8(kAmService), |
| 67 | + QString::fromUtf8(kAmPath), |
| 68 | + QDBusConnection::sessionBus()); |
| 69 | + am.setTimeout(1000); |
| 70 | + |
| 71 | + QDBusObjectPath instancePath; |
| 72 | + ObjectInterfaceMap instanceInfo; |
| 73 | + const QDBusReply<QString> reply = am.Identify(QDBusUnixFileDescriptor(pidfd), instancePath, instanceInfo); |
| 74 | + close(pidfd); |
| 75 | + |
| 76 | + if (!reply.isValid()) { |
| 77 | + return identity; |
| 78 | + } |
| 79 | + |
| 80 | + identity.instanceId = instancePath.path().section(QLatin1Char('/'), -1); |
| 81 | + identity.launchType = instanceInfo.value(QString::fromUtf8(kInstanceIface)) |
| 82 | + .value(QStringLiteral("LaunchType")).toString().trimmed(); |
| 83 | + if (identity.launchType.isEmpty()) { |
| 84 | + identity.launchType = QStringLiteral("unknown"); |
| 85 | + } |
| 86 | + |
| 87 | + return identity; |
| 88 | +} |
| 89 | + |
| 90 | +QString resolveDesktopFilePath(const QString &desktopId, const QString &desktopSourcePath) |
| 91 | +{ |
| 92 | + QString desktopFilePath = desktopSourcePath; |
| 93 | + if (desktopFilePath.isEmpty() || !QFileInfo::exists(desktopFilePath)) { |
| 94 | + const auto desktopFileName = desktopId.endsWith(QStringLiteral(".desktop")) ? desktopId : desktopId + QStringLiteral(".desktop"); |
| 95 | + desktopFilePath = QStandardPaths::locate(QStandardPaths::ApplicationsLocation, desktopFileName); |
| 96 | + } |
| 97 | + |
| 98 | + return desktopFilePath; |
| 99 | +} |
| 100 | + |
| 101 | +QString linglongIdFromDesktopFile(const QString &desktopFilePath) |
| 102 | +{ |
| 103 | + if (desktopFilePath.isEmpty()) { |
| 104 | + return QString(); |
| 105 | + } |
| 106 | + |
| 107 | + QSettings settings(desktopFilePath, QSettings::IniFormat); |
| 108 | + return settings.value(QStringLiteral("Desktop Entry/X-linglong")).toString().trimmed(); |
| 109 | +} |
| 110 | + |
| 111 | +QString queryLinglongVersion(const QString &linglongId) |
| 112 | +{ |
| 113 | + QProcess proc; |
| 114 | + proc.start(QStringLiteral("ll-cli"), {QStringLiteral("--json"), QStringLiteral("info"), linglongId}); |
| 115 | + if (!proc.waitForFinished(1000)) { |
| 116 | + qCWarning(launchDurationReporter) << "[DockIconTiming] ll-cli info timeout for" << linglongId; |
| 117 | + return QString(); |
| 118 | + } |
| 119 | + |
| 120 | + if (proc.exitCode() != 0) { |
| 121 | + return QString(); |
| 122 | + } |
| 123 | + |
| 124 | + const auto document = QJsonDocument::fromJson(proc.readAllStandardOutput()); |
| 125 | + if (!document.isObject()) { |
| 126 | + return QString(); |
| 127 | + } |
| 128 | + |
| 129 | + return document.object().value(QStringLiteral("version")).toString().trimmed(); |
| 130 | +} |
| 131 | + |
| 132 | +QString queryDebVersion(const QString &desktopFilePath) |
| 133 | +{ |
| 134 | + if (desktopFilePath.isEmpty()) { |
| 135 | + return QString(); |
| 136 | + } |
| 137 | + |
| 138 | + // The desktopId is often a reverse-DNS id (e.g. org.deepin.dde.control-center) that is NOT the |
| 139 | + // deb package name, so reverse-lookup the owning package from the .desktop file path. |
| 140 | + // |
| 141 | + // dpkg's data dir defaults to /var/lib/dpkg (overridable via DPKG_ADMINDIR); per-package file |
| 142 | + // lists live under <admindir>/info/*.list. grepping those directly is several times faster than |
| 143 | + // `dpkg -S`, which parses its whole database. When that dir is missing (non-standard layout) we |
| 144 | + // fall back to `dpkg -S` so correctness never depends on the directory guess. |
| 145 | + const auto infoDir = qEnvironmentVariable("DPKG_ADMINDIR", QStringLiteral("/var/lib/dpkg")) + QStringLiteral("/info"); |
| 146 | + |
| 147 | + QString packageName; |
| 148 | + if (QFileInfo::exists(infoDir)) { |
| 149 | + QProcess search; |
| 150 | + search.start(QStringLiteral("grep"), |
| 151 | + {QStringLiteral("-rlFx"), QStringLiteral("--include=*.list"), desktopFilePath, infoDir}); |
| 152 | + if (!search.waitForFinished(1000)) { |
| 153 | + qCWarning(launchDurationReporter) << "[DockIconTiming] grep dpkg file list timeout for" << desktopFilePath; |
| 154 | + return QString(); |
| 155 | + } |
| 156 | + // grep exit code: 0 = matched, 1 = no match, >1 = error; empty output means no owning package. |
| 157 | + const auto listPath = QString::fromUtf8(search.readAllStandardOutput()).section(QLatin1Char('\n'), 0, 0).trimmed(); |
| 158 | + if (!listPath.isEmpty()) { |
| 159 | + // <admindir>/info/<package>[:arch].list -> <package> |
| 160 | + packageName = QFileInfo(listPath).completeBaseName().section(QLatin1Char(':'), 0, 0); |
| 161 | + } |
| 162 | + } else { |
| 163 | + QProcess search; |
| 164 | + search.start(QStringLiteral("dpkg"), {QStringLiteral("-S"), desktopFilePath}); |
| 165 | + if (!search.waitForFinished(2000)) { |
| 166 | + qCWarning(launchDurationReporter) << "[DockIconTiming] dpkg -S timeout for" << desktopFilePath; |
| 167 | + return QString(); |
| 168 | + } |
| 169 | + if (search.exitCode() == 0) { |
| 170 | + // Output format: "package[:arch][, package2 ...]: /path/to/file". |
| 171 | + packageName = QString::fromUtf8(search.readAllStandardOutput()) |
| 172 | + .section(QLatin1Char(':'), 0, 0).section(QLatin1Char(','), 0, 0).trimmed(); |
| 173 | + } |
| 174 | + } |
| 175 | + |
| 176 | + if (packageName.isEmpty()) { |
| 177 | + return QString(); |
| 178 | + } |
| 179 | + |
| 180 | + QProcess query; |
| 181 | + query.start(QStringLiteral("dpkg-query"), {QStringLiteral("-W"), QStringLiteral("-f=${Version}"), packageName}); |
| 182 | + if (!query.waitForFinished(1000)) { |
| 183 | + qCWarning(launchDurationReporter) << "[DockIconTiming] dpkg-query timeout for" << packageName; |
| 184 | + return QString(); |
| 185 | + } |
| 186 | + if (query.exitCode() != 0) { |
| 187 | + return QString(); |
| 188 | + } |
| 189 | + |
| 190 | + return QString::fromUtf8(query.readAllStandardOutput()).trimmed(); |
| 191 | +} |
| 192 | + |
| 193 | +} |
| 194 | + |
| 195 | +namespace dock { |
| 196 | + |
| 197 | +LaunchDurationReporter::LaunchDurationReporter(QObject *parent) |
| 198 | + : QObject(parent) |
| 199 | +{ |
| 200 | +} |
| 201 | + |
| 202 | +LaunchDurationReporter::~LaunchDurationReporter() |
| 203 | +{ |
| 204 | + m_workerPool.waitForDone(); |
| 205 | +} |
| 206 | + |
| 207 | +void LaunchDurationReporter::reportWindowAppeared(const QString &desktopId, const QString &desktopSourcePath, pid_t pid) |
| 208 | +{ |
| 209 | + if (desktopId.isEmpty()) { |
| 210 | + return; |
| 211 | + } |
| 212 | + |
| 213 | + auto future = QtConcurrent::run(&m_workerPool, [this, desktopId, desktopSourcePath, pid]() { |
| 214 | + const auto identity = identifyInstance(pid); |
| 215 | + const QString uniqueId = identity.instanceId; |
| 216 | + const QString launchType = identity.launchType; |
| 217 | + |
| 218 | + if (uniqueId.isEmpty()) { |
| 219 | + return; |
| 220 | + } |
| 221 | + |
| 222 | + const auto desktopFilePath = resolveDesktopFilePath(desktopId, desktopSourcePath); |
| 223 | + const auto linglongId = linglongIdFromDesktopFile(desktopFilePath); |
| 224 | + const auto packageName = linglongId.isEmpty() ? desktopId : linglongId; |
| 225 | + QString version; |
| 226 | + QString pakType; |
| 227 | + |
| 228 | + { |
| 229 | + QMutexLocker locker(&m_cacheMutex); |
| 230 | + const auto entry = m_packageCache.value(packageName); |
| 231 | + if ((QDateTime::currentSecsSinceEpoch() - entry.timestamp) <= kPackageCacheTTLSeconds) { |
| 232 | + version = entry.version; |
| 233 | + pakType = entry.pakType; |
| 234 | + } |
| 235 | + } |
| 236 | + |
| 237 | + if (pakType.isEmpty()) { |
| 238 | + if (!linglongId.isEmpty()) { |
| 239 | + version = queryLinglongVersion(linglongId); |
| 240 | + pakType = QStringLiteral("linglong"); |
| 241 | + } else { |
| 242 | + version = queryDebVersion(desktopFilePath); |
| 243 | + pakType = version.isEmpty() ? QStringLiteral("unknown") : QStringLiteral("deb"); |
| 244 | + } |
| 245 | + |
| 246 | + QMutexLocker locker(&m_cacheMutex); |
| 247 | + m_packageCache.insert(packageName, {version, pakType, QDateTime::currentSecsSinceEpoch()}); |
| 248 | + } |
| 249 | + |
| 250 | + QMetaObject::invokeMethod(this, [this, desktopId, uniqueId, launchType, version, pakType]() { |
| 251 | + doReport(desktopId, uniqueId, launchType, version, pakType); |
| 252 | + }, Qt::QueuedConnection); |
| 253 | + }); |
| 254 | + Q_UNUSED(future) |
| 255 | +} |
| 256 | + |
| 257 | +void LaunchDurationReporter::doReport(const QString &desktopId, |
| 258 | + const QString &uniqueId, |
| 259 | + const QString &launchType, |
| 260 | + const QString &version, |
| 261 | + const QString &pakType) |
| 262 | +{ |
| 263 | +#ifdef HAVE_DDE_API_EVENTLOGGER |
| 264 | + DDE_EventLogger::EventLogger::instance().writeEventLog({ |
| 265 | + 1000610003, |
| 266 | + desktopId, |
| 267 | + QJsonObject{ |
| 268 | + {"app_name", desktopId}, |
| 269 | + {"launch_type", launchType}, |
| 270 | + {"app_version", version}, |
| 271 | + {"unique_id", uniqueId}, |
| 272 | + {"time", QDateTime::currentMSecsSinceEpoch()}, |
| 273 | + {"app_package_type", pakType}, |
| 274 | + }, |
| 275 | + }); |
| 276 | +#else |
| 277 | + Q_UNUSED(desktopId) |
| 278 | + Q_UNUSED(uniqueId) |
| 279 | + Q_UNUSED(launchType) |
| 280 | + Q_UNUSED(version) |
| 281 | + Q_UNUSED(pakType) |
| 282 | +#endif |
| 283 | +} |
| 284 | + |
| 285 | +} |
0 commit comments