Skip to content

feat(dock): add app launch event reporting for taskbar icons#1606

Open
Ivy233 wants to merge 1 commit into
linuxdeepin:masterfrom
Ivy233:fix/dde-am-eventlog
Open

feat(dock): add app launch event reporting for taskbar icons#1606
Ivy233 wants to merge 1 commit into
linuxdeepin:masterfrom
Ivy233:fix/dde-am-eventlog

Conversation

@Ivy233
Copy link
Copy Markdown
Contributor

@Ivy233 Ivy233 commented May 23, 2026

  1. Implement LaunchDurationReporter to report event 1000610003 when taskbar icons appear
  2. Report app metadata: name, launch type, version, unique ID, timestamp, and package type
  3. Query Application Manager via DBus for instance information and launch type
  4. Detect package type using ll-cli for linglong apps and dpkg-query for deb packages
  5. Cache linglong app versions with 10-minute TTL to handle version updates
  6. Use single-threaded worker pool for async DBus and process operations
  7. Thread-safe cache implementation with mutex protection

Log: Report app launch events when taskbar icons appear for analytics

Influence:

  1. Verify event 1000610003 triggers when new taskbar icons appear
  2. Verify correct version reporting for linglong and deb packages
  3. Verify cache refreshes after 10 minutes for updated app versions
  4. Verify no performance impact on icon appearance timing

feat(dock): 添加任务栏图标出现时的应用启动事件上报

  1. 实现 LaunchDurationReporter 在任务栏图标出现时上报 1000610003 事件
  2. 上报应用元数据:名称、启动类型、版本、唯一 ID、时间戳和包类型
  3. 通过 DBus 查询应用管理器获取实例信息和启动类型
  4. 使用 ll-cli 检测玲珑应用,使用 dpkg-query 检测 deb 包
  5. 缓存玲珑应用版本,10 分钟 TTL 处理版本更新
  6. 使用单线程工作池异步处理 DBus 和进程操作
  7. 使用互斥锁实现线程安全的缓存

Log: 任务栏图标出现时上报应用启动事件用于分析

Influence:

  1. 验证新图标出现时触发 1000610003 事件
  2. 验证玲珑和 deb 包版本正确上报
  3. 验证缓存在 10 分钟后刷新以获取更新的应用版本
  4. 验证对图标出现时机无性能影响

PMS: TASK-389405

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.

Sorry @Ivy233, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@deepin-ci-robot
Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: Ivy233

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

@Ivy233 Ivy233 requested a review from BLumia May 23, 2026 06:07
@Ivy233 Ivy233 force-pushed the fix/dde-am-eventlog branch 3 times, most recently from 35405ee to 0b79013 Compare May 23, 2026 07:19
@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

1. Implement LaunchDurationReporter to report event 1000610003 when taskbar
icons appear
2. Report app metadata: name, launch type, version, unique ID, timestamp, and
package type
3. Query Application Manager via DBus for instance information
4. Detect package type using ll-cli for linglong apps and dpkg-query for deb
packages
5. Implement cache with 30-minute TTL for linglong and deb packages
6. Async D-Bus query execution for better performance

Log: Report app launch events when taskbar icons appear for analytics

Influence:
1. Verify event 1000610003 triggers when new taskbar icons appear
2. Verify correct version reporting for linglong and deb packages

feat(dock): 添加任务栏图标出现时的应用启动事件上报

1. 实现 LaunchDurationReporter 在任务栏图标出现时上报 1000610003 事件
2. 上报应用元数据:名称、启动类型、版本、唯一 ID、时间戳和包类型
3. 通过 DBus 查询应用管理器获取实例信息
4. 使用 ll-cli 检测玲珑应用,使用 dpkg-query 检测 deb 包
5. 实现 30 分钟 TTL 的玲珑和 deb 包缓存
6. D-Bus 查询异步执行,提升性能

Log: 任务栏图标出现时上报应用启动事件用于分析

Influence:
1. 验证新图标出现时触发 1000610003 事件
2. 验证玲珑和 deb 包版本正确上报

PMS: TASK-389405
@Ivy233 Ivy233 force-pushed the fix/dde-am-eventlog branch from 0b79013 to 2e0b988 Compare May 29, 2026 08:28
@deepin-ci-robot
Copy link
Copy Markdown

deepin pr auto review

这份代码实现了一个应用启动时长上报器,通过 D-Bus 查询应用实例信息,结合 ll-clidpkg-query 获取包版本,最后通过事件日志系统上报。整体架构清晰,使用了线程池避免阻塞主线程,并引入了缓存机制减少子进程调用。

但在语法逻辑、代码质量、性能和安全性方面存在一些需要改进的地方。以下是详细的审查意见:

1. 语法与逻辑问题

  • ll-cli 输出解析逻辑脆弱loadAllLinglongVersions 函数按空格分割列(split(QLatin1Char(' '))),如果应用名称或版本字符串中本身包含空格,会导致列错位,解析出错误的 name 和 version。
    • 建议ll-cli list 通常支持 JSON 格式输出(如 ll-cli list --json--format json)。强烈建议使用 JSON 格式输出并使用 QJsonDocument 解析,这比按空格切割字符串健壮得多。如果必须解析纯文本,请使用 QRegularExpression 匹配连续空格进行分割。
  • QDBusReply 使用不当:在 queryInstances 中,QDBusReply<QVariant> reply = appIface.call(...) 是正确的;但在查询 LaunchType 时,直接使用了 auto launchTypeReply = instIface.call(...),返回的是 QDBusMessage。后续通过 .arguments().constFirst() 提取数据非常繁琐且容易因类型不匹配崩溃。
    • 建议:统一使用 QDBusReply<QVariant> 来获取 D-Bus 属性,代码会更简洁安全:
      QDBusReply<QVariant> launchTypeReply = instIface.call("Get", QString::fromUtf8(kInstanceIface), QStringLiteral("LaunchType"));
      if (launchTypeReply.isValid()) {
          info.launchType = launchTypeReply.value().toString();
      }
  • 版权年份错误:SPDX 头部写了 2026,通常应为当前年份或项目创建年份 2023/2024

2. 代码性能

  • 子进程调用阻塞线程池loadAllLinglongVersionsdpkg-query 都是通过 QProcess 同步调用的外部进程。QProcess::waitForFinished() 会阻塞当前线程。
    • loadAllLinglongVersions 执行 ll-cli list,如果系统中 linglong 应用很多,该命令可能耗时较长。虽然它有 3 秒超时且被放在线程池中执行,但 m_workerPool.setMaxThreadCount(1) 意味着这是一个串行队列。如果短时间内启动多个应用,前一个应用的 ll-cli list 阻塞会导致后续应用的启动上报全部排队等待。
    • 建议
      1. 考虑将 ll-cli 缓存的刷新移到独立的线程或使用异步 QProcess,不阻塞上报线程池。
      2. 可以在应用启动时(或定时器)预热 m_linglongCache,而不是在 reportWindowAppeared 时才懒加载并阻塞。
  • D-Bus 循环查询queryInstances 中,先获取了所有 Instance 的 Path,然后在循环中对每个 Path 发起一次 D-Bus 同步调用查询 LaunchType。如果应用有多个实例,这会导致多次 D-Bus 往返。
    • 建议:如果 AM 服务支持,尝试一次性获取所有需要的信息。如果必须循环查询,确保线程池不被长时间占用。

3. 代码安全

  • 命令注入风险dpkg-query 的参数 desktopId 直接来源于外部输入(窗口系统)。虽然 QProcess::start() 传递的是 QStringList 参数,不像 system() 那样容易引发 shell 注入,但恶意的 desktopId(如包含路径遍历字符或特殊构造的包名)可能导致 dpkg-query 执行非预期行为或读取非预期文件。
    • 建议:在传递给 dpkg-query 之前,对 desktopId 进行合法性校验(例如:正则匹配 ^[a-z0-9][a-z0-9+.-]+$,确保它符合 Debian 包名规范)。
  • 版权与许可证合规:使用了 GPL-3.0-or-later,请确保项目整体兼容该许可证,且 HAVE_DDE_API_EVENTLOGGER 引用的闭源或第三方库的许可证不冲突。

4. 代码质量与可维护性

  • 硬编码的中文字符loadAllLinglongVersions 中的注释 // 名称 column// 版本 column 包含中文。虽然只是注释,但在跨平台或国际化团队中,建议统一使用英文注释。
  • 魔法数字appIface.setTimeout(1000)proc.waitForFinished(3000) 等散落在代码中。
    • 建议:将其提取为与 kLinglongCacheTTLSeconds 类似的具名常量,如 kDbusTimeoutMskSubprocessTimeoutMs
  • Q_UNUSED(future) 的替代方案:在 reportWindowAppeared 中,QtConcurrent::run 的返回值被标记为 Q_UNUSED。如果不关心返回值,直接调用即可,无需接收变量再 UNUSED。
  • 缓存失效策略m_linglongCache 是基于全局 TTL (30分钟) 刷新的,如果用户在这 30 分钟内安装或卸载了 linglong 应用,上报的版本号将是过期的。
    • 建议:考虑监听系统 D-Bus 信号(如 linglong 的安装/卸载信号)来主动更新缓存,而不是单纯依赖 TTL。

改进后的代码示例

针对以上核心问题,以下是修改后的 launchdurationreporter.cpp 关键部分:

// ... 头部保持不变,建议修改年份 ...
// SPDX-FileCopyrightText: 2024 UnionTech Software Technology Co., Ltd.

namespace {

// ... 常量定义增加 ...
constexpr auto kAmService = "org.desktopspec.ApplicationManager1";
constexpr auto kApplicationIface = "org.desktopspec.ApplicationManager1.Application";
constexpr auto kInstanceIface = "org.desktopspec.ApplicationManager1.Instance";
constexpr int kLinglongCacheTTLSeconds = 1800;
constexpr int kDebCacheTTLSeconds = 1800;
constexpr int kDbusTimeoutMs = 1000;
constexpr int kSubprocessTimeoutMs = 3000;

QList<InstanceInfo> queryInstances(const QString &desktopId)
{
    QList<InstanceInfo> result;
    auto appPath = QStringLiteral("/org/desktopspec/ApplicationManager1/%1").arg(escapeToObjectPath(desktopId));

    QDBusInterface appIface(QString::fromUtf8(kAmService),
                            appPath,
                            QStringLiteral("org.freedesktop.DBus.Properties"),
                            QDBusConnection::sessionBus());
    appIface.setTimeout(kDbusTimeoutMs);
    
    QDBusReply<QVariant> reply = appIface.call(QStringLiteral("Get"),
                                                QString::fromUtf8(kApplicationIface),
                                                QStringLiteral("Instances"));
    if (!reply.isValid()) {
        qCDebug(launchDurationReporter) << "[DockIconTiming] queryInstances failed for" << desktopId << ":" << reply.error().message();
        return result;
    }

    const auto paths = qdbus_cast<QList<QDBusObjectPath>>(reply.value());
    for (const auto &path : paths) {
        QDBusInterface instIface(QString::fromUtf8(kAmService),
                                 path.path(),
                                 QStringLiteral("org.freedesktop.DBus.Properties"),
                                 QDBusConnection::sessionBus());
        instIface.setTimeout(kDbusTimeoutMs);

        InstanceInfo info;
        info.instanceId = path.path().section(QLatin1Char('/'), -1);

        // 优化:使用 QDBusReply 简化提取逻辑
        QDBusReply<QVariant> launchTypeReply = instIface.call(QStringLiteral("Get"),
                                               QString::fromUtf8(kInstanceIface),
                                               QStringLiteral("LaunchType"));
        if (launchTypeReply.isValid()) {
            info.launchType = launchTypeReply.value().toString();
        }
        
        if (info.launchType.isEmpty()) {
            info.launchType = QStringLiteral("unknown");
        }

        result.append(info);
    }
    return result;
}

QHash<QString, QString> loadAllLinglongVersions()
{
    QHash<QString, QString> result;

    QProcess proc;
    // 建议:如果 ll-cli 支持 --json,请使用 JSON 解析代替字符串分割
    proc.start(QStringLiteral("ll-cli"), {QStringLiteral("list"), QStringLiteral("--type"), QStringLiteral("app")});
    
    if (!proc.waitForFinished(kSubprocessTimeoutMs)) {
        qCWarning(launchDurationReporter) << "ll-cli list timeout";
        proc.kill();
        proc.waitForFinished();
        return result;
    }

    if (proc.exitCode() != 0) {
        qCWarning(launchDurationReporter) << "ll-cli list failed, exitCode:" << proc.exitCode();
        return result;
    }

    QString output = QString::fromUtf8(proc.readAllStandardOutput());
    QStringList lines = output.split(QLatin1Char('\n'), Qt::SkipEmptyParts);

    // Skip header line
    for (int i = 1; i < lines.size(); ++i) {
        // 优化:使用正则匹配连续空格,防止单元格内包含空格导致错位
        QStringList columns = lines[i].simplified().split(QRegularExpression("\\s+"));
        if (columns.size() >= 3) {
            QString name = columns[1];
            QString version = columns[2];
            if (!name.isEmpty()) {
                result.insert(name, version);
            }
        }
    }

    return result;
}

// 正则表达式验证包名合法性,防止注入或异常字符
static bool isValidDesktopId(const QString &desktopId) {
    static QRegularExpression validNameRegex("^[a-z0-9][a-z0-9+.-]+$");
    return validNameRegex.match(desktopId).hasMatch();
}

} // namespace

namespace dock {

// ... 构造和析构保持不变 ...

void LaunchDurationReporter::reportWindowAppeared(const QString &desktopId)
{
    if (desktopId.isEmpty() || !isValidDesktopId(desktopId)) {
        return;
    }

    // 不需要接收 future 则不接收,避免 Q_UNUSED
    QtConcurrent::run(&m_workerPool, [this, desktopId]() {
        auto instances = queryInstances(desktopId);

        QString uniqueId;
        QString launchType = QStringLiteral("unknown");
        if (!instances.isEmpty()) {
            const auto &latest = instances.constLast();
            uniqueId = latest.instanceId;
            launchType = latest.launchType;
        }

        if (uniqueId.isEmpty()) {
            return;
        }

        QString version;
        QString pakType;

        {
            QMutexLocker locker(&m_cacheMutex);
            qint64 currentTime = QDateTime::currentSecsSinceEpoch();

            if ((currentTime - m_linglongCacheTime) > kLinglongCacheTTLSeconds) {
                m_linglongCache = loadAllLinglongVersions();
                m_linglongCacheTime = currentTime;
            }

            if (m_linglongCache.contains(desktopId)) {
                version = m_linglongCache.value(desktopId);
                pakType = QStringLiteral("linglong");
            } else if (m_debCache.contains(desktopId)) {
                const auto &entry = m_debCache.value(desktopId);
                if ((currentTime - entry.timestamp) <= kDebCacheTTLSeconds) {
                    version = entry.version;
                    pakType = entry.pakType;
                }
            }
        }

        if (pakType.isEmpty()) {
            QProcess proc;
            proc.start(QStringLiteral("dpkg-query"), {QStringLiteral("-W"), QStringLiteral("-f=${Version}"), desktopId});
            proc.waitForFinished(kDbusTimeoutMs); // 复用超时常量
            
            if (proc.exitCode() == 0) {
                version = QString::fromUtf8(proc.readAllStandardOutput()).trimmed();
                pakType = QStringLiteral("deb");
            } else {
                qCDebug(launchDurationReporter) << "dpkg-query failed for" << desktopId << "exitCode:" << proc.exitCode();
                pakType = QStringLiteral("unknown");
            }

            QMutexLocker locker(&m_cacheMutex);
            m_debCache.insert(desktopId, {version, pakType, QDateTime::currentSecsSinceEpoch()});
        }

        QMetaObject::invokeMethod(this, [this, desktopId, uniqueId, launchType, version, pakType]() {
            doReport(desktopId, uniqueId, launchType, version, pakType);
        }, Qt::QueuedConnection);
    });
}

// ... doReport 保持不变 ...

} // namespace dock

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.

2 participants