dde-file-manager 核心代码审查报告

聚焦性能与稳定性 · 基于 upstream 6.6.5 干净检出(commit 8503f5e)
审查范围:src/ 全部 8 个模块 代码规模:2341 个 C++ 源文件 发现问题:P0 5 项 · P1 14 项 · P2 11 项 所有结论均含 file:line 证据

一、执行摘要

本次审查覆盖 dfm-base(文件信息 / 目录迭代 / 缓存 / 监视器)、dfm-framework(插件与事件框架)、plugins/common/dfmplugin-fileoperations(拷贝剪切删除引擎)、plugins/filemanager 与 plugins/desktop(视图与模型)、services/(索引、访问控制、加解密、USB 修复)。

整体判断:架构分层清晰,缓存与异步化设计意图明确,代码注释与日志规范度较高;但存在若干确定性缺陷——其中"文件操作失败被上报为成功"和"拷贝写循环可能死循环"两项直接影响数据正确性,属于必须优先修复的问题。此外全仓库有 54 处无超时阻塞等待,是界面卡死类问题的系统性来源。

5
P0 严重
数据正确性 / 崩溃 / 永久卡死
14
P1 中等
明显卡顿 / 数据竞争 / 资源泄漏
11
P2 轻微
潜在缺陷 / 可维护性
54
无超时阻塞等待
(共 80 处 waitForFinished)

量化基线(可用于回归对比)

指标实测值说明
waitForFinished() 无超时调用54 处 / 80 处26 处已带超时参数;无超时者均可无限阻塞
QThread::msleep 忙等点17 处其中 4 处在拷贝热路径,用于替代条件变量
processEvents() 调用5 处局部嵌套事件循环,有重入风险
文件级静态可变共享状态86 处多为单例,需关注析构顺序
拷贝热路径 QStorageInfo 构造每个文件 1 次解析挂载表,万级文件额外数秒
拷贝热路径正则编译每个文件 1~2 次QRegularExpression 在循环内构造

二、P0 严重问题(必须优先修复)

P0 P0-1 文件操作失败被上报为"成功",导致撤销链与剪贴板状态失真
src/plugins/common/dfmplugin-fileoperations/fileoperationsevent/fileoperationseventhandler.cpp:108-112 / 121-129 / 143-156
证据

三个函数各自创建了一份互不相干的局部 ok / errMsg:

// handleJobResult:108 —— 创建的 ok 从未被使用
QSharedPointer<bool> ok { new bool { true } };
QSharedPointer<QString> errMsg { new QString };
connect(ptr.get(), &AbstractJobHandler::errorNotify,  this, &FileOperationsEventHandler::handleErrorNotify);
connect(ptr.get(), &AbstractJobHandler::finishedNotify, this, &FileOperationsEventHandler::handleFinishedNotify);

// handleErrorNotify:121 —— 改写的是另一个局部对象,函数结束即丢弃
QSharedPointer<bool> ok { new bool { true } };
if (errType != AbstractJobHandler::JobErrorType::kNoError) {
    *ok = false;                          // ← 写入后立即销毁
    *errMsg = jobInfo->value(...).toString();
}

// handleFinishedNotify:143 —— 又新建一个 ok=true,永远为真
QSharedPointer<bool> ok { new bool { true } };
publishJobResultEvent(jobType, srcUrls, destUrls, customInfos, *ok, *errMsg);   // 恒为 true / ""
removeUrlsInClipboard(jobType, srcUrls, destUrls, *ok);                        // 恒为 true
影响
修复方案
把 ok / errMsg 提升为按 job handle 索引的成员状态(QHash<JobHandlePointer, JobResultState> + 互斥锁),三个函数读写同一份状态;或在 handleJobResult 中把共享指针通过 lambda 捕获进信号连接。
// 推荐做法:状态挂在 handle 上,随 handle 生命周期回收
struct JobResultState { bool ok { true }; QString errMsg; };
QHash<void *, JobResultState> m_jobStates;     // 配 QMutex 保护
// handleErrorNotify:   state.ok = false; state.errMsg = ...;
// handleFinishedNotify: publishJobResultEvent(..., state.ok, state.errMsg);
P0 P0-2 拷贝写循环在 write() 返回 0 时死循环,任务永不结束
src/plugins/common/dfmplugin-fileoperations/fileoperations/fileoperationutils/docopyfileworker.cpp:346-351
证据
ssize_t bytesWritten = 0;
while (bytesWritten < actualBytesToWrite && !isStopped()) {
    ssize_t written = write(writer.fd, buffer + bytesWritten,
                            actualBytesToWrite - bytesWritten);
    if (written < 0) { /* EINTR / O_DIRECT 回退 / 报错 */ }
    // ← 缺少 written == 0 的处理,bytesWritten 不推进
}
影响
修复方案
显式处理 written == 0:记录连续零写次数,超过阈值(如 3 次)即按写错误上报,并让调用方走统一的错误对话框路径。
int zeroWrites = 0;
while (bytesWritten < actualBytesToWrite && !isStopped()) {
    ssize_t written = write(writer.fd, buffer + bytesWritten,
                            actualBytesToWrite - bytesWritten);
    if (written > 0) { bytesWritten += written; zeroWrites = 0; continue; }
    if (written == 0) {
        if (++zeroWrites >= 3) { savedErrno = ENOSPC; goto handle_error; }
        continue;
    }
    if (errno == EINTR) continue;
    /* ... 原有错误与 O_DIRECT 回退逻辑 ... */
}
P0 P0-3 插件卸载后事件订阅仍指向已卸载代码,发布事件即 use-after-free
src/dfm-framework/lifecycle/private/pluginmanager_p.cpp:677-686
src/dfm-framework/event/eventdispatcher.h:52-57
证据
// pluginmanager_p.cpp:677 —— 只调用 stop(),未清理任何事件注册
pointer->d->plugin->stop();
pointer->d->plugin = nullptr;
pointer->d->state  = PluginMetaObject::State::kStoped;
if (!pointer->d->isVirtual && !pointer->d->loader->unload())   // ← .so 被卸载
    return false;
// eventdispatcher.h:52 —— handler 以裸指针 obj 捕获
auto func = [obj, method](const QVariantList &args) -> QVariant {
    EventHelper<decltype(method)> helper = (EventHelper<decltype(method)>(obj, method));
    return helper.invoke(args);                 // obj 已析构 → UAF
};
handlerList.push_back(EventHandler<Listener> { obj, memberFunctionVoidCast(method), func });
影响
修复方案
两条互补措施:① doStopPlugin 内向三个 manager 遍历该插件实例的所有注册并逐一 unsubscribe / removeFilter / disconnect / unfollow;② EventHandler 内改存 QPointer<QObject>,调用前判空,作为兜底。
// EventDispatcher:调用前判活
for (const auto &h : handlersCopy)
    if (h.objectIndex && h.handler)          // QPointer 语义
        h.handler(params);
P0 P0-4 54 处无超时阻塞等待,其中 1 处在 D-Bus 服务主线程做无限 read
src/services/accesscontrol/accesscontroldbus.cpp:103-105
src/dfm-base/base/device/deviceproxymanager.cpp:35,58,69,80
src/plugins/common/dfmplugin-tag/data/tagproxyhandle.cpp(20+ 处)
证据
// accesscontroldbus.cpp:103 —— D-Bus 服务主线程,阻塞 read,无 O_NONBLOCK、无 poll 超时
QByteArray buffer;  char readBuffer[1024];  ssize_t bytesRead;
while ((bytesRead = read(fd, readBuffer, sizeof(readBuffer))) > 0) {
    buffer.append(readBuffer, bytesRead);      // 亦无长度上限
}
// deviceproxymanager.cpp:35 —— 从文件操作 UI 线程调用
auto &&reply = d->devMngDBus->QueryBlockDeviceInfo(id, reload);
reply.waitForFinished();                        // 无参数 = 无限等待
影响
修复方案
① accesscontroldbus:先 fcntl(fd, F_SETFL, O_NONBLOCK),改用 poll() 带 3s 超时,并设置累计长度上限(密码凭据实际仅数百字节)。
② 所有 D-Bus 代理统一改为 reply.waitForFinished(kDBusTimeoutMs),超时返回降级值;UI 线程路径优先改为 asyncCall + QDBusPendingCallWatcher。
// 统一超时常量与降级
static constexpr int kDBusTimeoutMs = 5000;
if (!reply.isFinished() && !reply.waitForFinished(kDBusTimeoutMs)) {
    fmWarning() << "DBus timeout:" << iface << method;
    return {};                       // 返回空值,由调用方走 fallback
}
P0 P0-5 FileInfo 异步查询自旋无终止条件,可耗尽线程池导致界面假死
src/dfm-base/utils/fileinfohelper.cpp:54-59(配合 :42 线程池配置)
证据
// fileinfohelper.cpp:42
pool.setMaxThreadCount(std::max(FileUtils::getCpuProcessCount(), 10));

// fileinfohelper.cpp:54 —— 在池任务内无限轮询,无超时、无 stoped 检查
auto resluts = asyncInfo->cacheAsyncAttributes();
while (resluts == 0) {
    QThread::msleep(50);
    resluts = asyncInfo->cacheAsyncAttributes();
}
影响
修复方案
引入带截止时间的等待条件,并检查停止标志;超时后走 checkInfoRefresh 的降级分支而非继续自旋。
QElapsedTimer t; t.start();
auto results = asyncInfo->cacheAsyncAttributes();
while (results == 0 && !stoped && t.elapsed() < kAttrWaitTimeoutMs /*5000*/) {
    QThread::msleep(20);
    results = asyncInfo->cacheAsyncAttributes();
}
if (results == 0) {                       // 超时:不再占用池线程
    checkInfoRefresh(asyncInfo);
    return;
}

三、P1 中等问题

3.1 性能 — 热路径 O(n²) 与重复计算

P1P1-1 桌面画布每次取数据都做 O(n) 线性查找,整体退化为 O(n²)
src/plugins/desktop/ddplugin-canvas/model/canvasproxymodel.cpp:486-489, 638-644, 847
src/plugins/desktop/ddplugin-canvas/model/fileinfomodel.cpp:433-435
// canvasproxymodel.cpp:486 —— fileMap 是哈希,却用 fileList.indexOf 反查行号
if (d->fileMap.contains(url)) {
    int row = d->fileList.indexOf(url);      // O(n)
    return createIndex(row, column);
}
// :693 data() → :695 mapToSource(index) → :644 d->srcModel->index(url) → fileinfomodel.cpp:434 又一次 O(n)
影响

每个可见项的每次 data() 调用耗时随文件数线性增长,绘制 N 项即 O(N²)。桌面 2000 个图标时,单次重绘的索引定位可达数百万次 QUrl 比较,滚动与拖拽明显掉帧;sort() 中 lessThan 亦内联查索引,排序再叠加一层 O(n·log n)·O(n)。

修复方案
维护 QHash<QUrl,int> m_rowIndex,在 fileList 增删改处同步更新,把 index(QUrl) 降为 O(1);lessThan 中预取比较数据,避免在比较器内构造索引。
P1P1-2 拷贝热路径每个文件构造 QStorageInfo、编译正则
src/plugins/common/dfmplugin-fileoperations/fileoperations/fileoperationutils/abstractworker.cpp:664, 669
调用链:fileoperatebaseworker.cpp:1069 createNewTargetUrl → formatFileName(每文件)
const QString &fs_type = QStorageInfo(targetUrl.path()).fileSystemType();   // 解析挂载表,每文件一次
if (fs_type == "vfat") {
    QString new_name = fileName;
    return new_name.replace(QRegularExpression("[\"*:<>?\\|]"), "_");        // 每文件编译一次正则
}
影响

targetUrl 在一次任务内恒定,但结果被逐文件重算。QStorageInfo 需读取并解析挂载表,万级文件时额外引入数秒纯开销;正则编译同样是可完全消除的重复工作。

修复方案
initArgs 阶段计算一次并缓存 m_targetFsType;正则改为函数内 static const QRegularExpression(C++11 起线程安全初始化)。
static const QRegularExpression kVfatForbidden(R"(["*:<>?\\|])");
if (m_targetFsType == QLatin1String("vfat"))
    return QString(fileName).replace(kVfatForbidden, "_");
P1P1-3 名称过滤逐文件逐模式重复编译通配符正则
src/plugins/filemanager/dfmplugin-workspace/utils/filesortworker.cpp:1862(同类逻辑见 :1276)
QRegularExpression re("", QRegularExpression::NoPatternOption);
for (int i = 0; i < nameFilters.size(); ++i) {
    QString pattern = QRegularExpression::wildcardToRegularExpression(nameFilters.at(i));
    re.setPattern(pattern);                 // 每个文件 × 每个模式 = 重新编译
    ...
}
影响

一次目录过滤的开销为 O(文件数 × 模式数) 次正则编译。含多个过滤模式的目录(如 *.txt;*.md;*.log)在数万文件下 CPU 占用显著。

修复方案
在 nameFilters 变更时一次性预编译为 QList<QRegularExpression> 成员,过滤时仅复用。
P1P1-4 QHash::value() 按值拷贝整个子哈希,单文件操作退化为 O(n·m)
src/plugins/filemanager/dfmplugin-workspace/utils/filesortworker.cpp:497(同模式见 866、1438)
// children 类型:QHash<QUrl, QHash<QUrl, SortInfoPointer>>
if (this->children.value(makeParentUrl(sortInfo->fileUrl())).contains(...)) {
    //     ^^^^^^^^^^^^^^^ 按值返回整个子哈希 → 深拷贝该目录全部条目
影响

监控到 n 个文件变化即触发 n 次"整目录条目拷贝",单次批量操作复杂度 O(n·目录大小)。大目录(万级条目)下复制粘贴、批量重命名会明显变慢。

修复方案
改用 constFind() 取迭代器,或直接持有引用:auto it = children.constFind(parentUrl); if (it != children.constEnd() && it->contains(...))。
P1P1-5 回收站枚举与未完成文件登记使用 QList::contains 去重(O(n²))
src/plugins/common/dfmplugin-fileoperations/fileoperationsevent/trashfileeventreceiver.cpp:246-249
src/plugins/common/dfmplugin-fileoperations/fileoperations/fileoperationutils/filecleanupmanager.cpp:17
// trashfileeventreceiver.cpp:246 —— 枚举回收站,逐项 O(n) 查重
auto url = FileUtils::bindUrlTransform(enumerator.next());
if (!allFilesList.contains(url)) { allFilesList.append(url); processedCount++; }
影响

5 万项约 12 亿次 QUrl 比较:清空回收站、退出程序时明显卡顿。同文件 :53 处 future.waitForFinished() 无超时会进一步放大等待。

修复方案
改 QSet<QUrl> 去重(O(1)),或使用 QList + 末尾统一排序去重。
P1P1-6 线程池等待用 msleep 忙等替代条件变量,且存在无进展风险
src/plugins/common/dfmplugin-fileoperations/fileoperations/fileoperationutils/fileoperatebaseworker.cpp:704-706, 909-918
// :909 —— 固定 10ms 无条件延时 + 轮询替代 waitForDone
void FileOperateBaseWorker::waitThreadPoolOver() {
    if (!isStopped() && threadPool) QThread::msleep(10);
    while (threadPool && threadPool->activeThreadCount() > 0) QThread::msleep(10);
    applyAllPendingReplacements();
}
// :704 —— 跨任务共享的 bigFileCopy 标志,其余任务在此空转
while (bigFileCopy && !isStopped()) { QThread::msleep(10); }
影响

① 轮询而非 waitForDone(),每次等待固定引入 10ms 延迟并持续唤醒 CPU;② 任一池线程阻塞(如 P0-5)则本函数永久死循环,任务无法结束;③ bigFileCopy 为静态共享,大文件拷贝期间其他任务每 10ms 空转。

修复方案
改用 threadPool->waitForDone(timeout) 并在循环内检查 isStopped();大文件串行化改用 QWaitCondition / QSemaphore 通知,去掉固定 msleep。
P1P1-7 目录扫描:热循环无谓构造 QUrl;进度信号跨线程整体拷贝 ScanResult
src/dfm-base/utils/filescanner.cpp:462, 867-882, 932-938
// :462 —— 未启用 CollectFiles 时仍为每个条目构造 QString + QUrl
const QUrl entryUrl = QUrl::fromLocalFile(QString::fromUtf8(entryPath));
// :867 —— 每次进度回调把整个 ScanResult(含 allFiles 列表)按值投递到主线程
state.progressCallback(state.result);
// ScannerWorker::start 中 emit resultReady(result, false) → 跨线程队列 → 全量深拷贝
影响

① 纯计数场景(统计目录大小)下每条目仍产生两次堆分配,百万文件量级浪费可观;② 进度信号每 500ms 触发一次,若开启 CollectFiles,每 500ms 深拷贝一次随文件数增长的 QList<QUrl>,累计复杂度 O(n²)。

修复方案
① 将 entryUrl 构造移入 collectFileIfEnabled 内部(先判选项);② 进度信号只传数值字段(结构体去掉 allFiles,或改为 QSharedPointer<const ScanResult> / QExplicitlySharedDataPointer),文件列表仅在最终 finished 时传递一次。另注:urls.first().scheme() 决定扫描分支,混合本地+远程 URL 时选择有误。
P1P1-8 视图/模型 data() 每单元格走事件总线并读取设置
src/plugins/filemanager/dfmplugin-workspace/models/fileviewmodel.cpp:798-803(被 :448-450 的 data() 调用)
同文件 :1415 切目录时对远程 scheme 同步 updateAttributes()
// :798 columnToRole 内,每个单元格调用一次
WorkspaceEventSequence::instance()->doFetchCustomColumnRoles(...);   // 事件总线分发
getFileViewStateValue(dirRootUrl, "headerList");                     // 读设置 + toMap() 拷贝
影响

滚动与重绘时,每个可见单元格触发一次事件总线分发 + 设置读取/字典拷贝。列表视图下成为绘制主开销。另 :1415 在 UI 线程对 smb/sftp 等远程 scheme 执行同步 updateAttributes()(网络 IO),切目录时阻塞界面。

修复方案
按 dirRootUrl 缓存列角色列表,仅在 headerList 或自定义列变更时失效;远程 scheme 的属性刷新改异步并 dataChanged 回传。
P1P1-9 设备轮询每 3 秒在主线程做同步 D-Bus;服务注册状态每次重复查询
src/dfm-base/base/device/private/discdevicescanner.cpp:81, 113
src/dfm-base/base/device/deviceproxymanager.cpp:231-234, 499-506
discScanTimer->start(kTimerInterval);                        // 3000ms
const auto &info { DevProxyMng->queryBlockInfo(id) };        // UI 线程阻塞往返
// :231 —— 每次查询都做一次同步总线探测
return QDBusConnection::sessionBus().interface()->isServiceRegistered(kDeviceService);
// :499 —— 持写锁期间做阻塞 D-Bus(注释自承可能死锁)
QWriteLocker lk(&lock);  auto &&info = q->queryBlockInfo(id);
影响

① 光驱扫描每 3s 阻塞 UI 线程一次;② initMounts 对 N 台设备逐个查询,每次叠加一次 isServiceRegistered 同步往返,设备枚举 O(N) 次同步总线调用;③ :499 写锁覆盖 D-Bus 往返,其余查询全部排队。

修复方案
扫描移入线程池或改由 daemon 属性变更信号驱动;isServiceRegistered 结果用 QDBusServiceWatcher 的 registered/unregistered 信号维护缓存;:499 先在锁外取 info,再短暂持锁更新容器。

3.2 稳定性 — 数据竞争与资源生命周期

P1P1-10 事件分发无锁读取 handler 列表,与 subscribe 并发修改构成数据竞争
src/dfm-framework/event/eventdispatcher.cpp:30-31
src/dfm-framework/event/eventdispatcher.h:119-121, 148-157, 198-203
// eventdispatcher.h:198 —— publish 在读锁内取到 dispatcher 后立即解锁
QReadLocker lk(&rwLock);
auto dispatcher = dispatcherMap.value(type);
lk.unlock();                             // ← 锁已释放
return dispatcher->dispatch(...);        // dispatch 内部无任何锁

// eventdispatcher.cpp:30 —— 无锁拷贝正在被并发修改的 QList
auto filtersCopy  = filterList;
auto handlersCopy = handlerList;
// 而 subscribe → append → handlerList.push_back() 只受 rwLock 保护
影响

EventDispatcher 自身没有互斥量(:119-121 仅有裸 QList)。一个线程 subscribe(持 rwLock 写 handlerList)与另一线程 publish → dispatch(无锁读同一 QList)并发时,QList 内部指针可被撕裂 → 崩溃或随机跳过 handler。异步 asyncPublish 走 QtConcurrent,该竞争窗口在正常使用中即可命中。

修复方案
为 EventDispatcher 增加 QReadWriteLock,append/appendFilter/remove/removeFilter/dispatch 全部加锁(dispatch 内先拷贝到局部再释放锁),使拷贝成为原子快照。
P1P1-11 InfoCache::removeCaches 重复 take 同一 key,导致监视器引用计数释放路径永久失效
src/dfm-base/utils/infocache.cpp:234-246
QWriteLocker wlk(&d->mainLock);
for (const auto &url : urls) {
    auto info = d->mainCache.take(url);            // 第一次取出,已从容器删除
    if (info)
        infos.insert(url, d->mainCache.take(url));  // ← 第二次 take 恒为 nullptr
}
if (infos.size() > 0)
    emit cacheDisconnectWatcher(infos);            // 投递的 map 全为空值
影响

下游 disconnectWatcher()(infocache.cpp:59-62)以 if (!info) continue; 跳过空值,因此没有任何一次 reduceCacheInfoConnectSize() 会被执行,其后的 getCacheInfoConnectSize() <= 0 分支与 removeCacheWatcher() 成为死代码。后果:

修复方案
去掉第二次 take:
auto info = d->mainCache.take(url);
if (info)
    infos.insert(url, info);          // 使用已取出的 info
P1P1-12 clearCachedExtendedAttribute 持读锁修改共享对象状态
src/dfm-base/utils/infocache.cpp:285-302
{
    QReadLocker lk(&d->mainLock);                     // ← 读锁
    for (const auto &info : d->mainCache) {
        if (info) info->setExtendedAttributes(type, QVariant());   // ← 写共享对象
    }
}
影响

读锁允许多个读者并发进入,而此处遍历并修改每个 FileInfo 的扩展属性。与其它持读锁读取同一批 FileInfo 的线程(如 getCacheInfo 的调用方)构成数据竞争,可读到半更新的属性值,极端情况下触碰 Qt 容器内部指针导致崩溃。

修复方案
改为 QWriteLocker;若担心持写锁期间遍历 20000 条造成阻塞,则先持读锁快照指针列表,再在锁外逐项更新,并在每项更新时对其内部锁加保护。
P1P1-13 事件通道与设备监视器:无锁读取可被并发重写的 std::function / 共享容器
src/dfm-framework/event/eventchannel.cpp:74-79(对照 eventchannel.h:67-71)
src/dfm-base/base/device/private/devicewatcher.cpp:83-90
// eventchannel.cpp:74 —— send 完全不持锁
if (!conn) { ... }
return conn(params);
// 而 setReceiver 在 receiverMutex 保护下重写 conn(eventchannel.h:67-71)

// devicewatcher.cpp:83 —— 池任务捕获 this 并拷贝共享容器,主线程同时增删
QThreadPool::globalInstance()->start([this] {
    auto blocks    = allBlockInfos;      // 主线程并发 insert/remove,无锁
    auto protocols = allProtocolInfos;
    ...
});
影响

① 并发重连时 conn(std::function)读写竞争,可能调用已释放的可调用体;② devicewatcher 每周期无条件新建池任务,慢盘下任务堆积,多份任务并发读取正被主线程修改的 QHash → 崩溃;对象析构后任务仍持有 this → use-after-free。

修复方案
EventChannel::send 内在 receiverMutex 保护下取本地副本再调用;devicewatcher 在主线程加锁生成不可变快照(或原子交换 QSharedPointer<const Snapshot>)后传给任务,任务开始处校验 isWatching,并用去重机制避免任务叠加。
P1P1-14 全局事件过滤器只生效第一个,且每次发布重建 key 列表
src/dfm-framework/event/eventdispatcher.cpp:76-91
QReadLocker lk(&rwLock);
qsizetype size { globalFilterMap.size() };
for (qsizetype i = 0; i != size; ++i) {
    auto key { globalFilterMap.keys()[i] };      // 每次迭代重建整个 key 列表
    if (key) {
        auto func { globalFilterMap.value(key) };
        lk.unlock();
        return func(type, params);               // ← 首个非空 key 直接 return
    }
}
return false;
影响

① 语义错误:注册多个全局过滤器时,后续过滤器永远不会执行,安全/权限类拦截可能被静默跳过;② 循环内 keys()[i] 使 map 非空时每次发布产生 O(n²) 次分配。

修复方案
先取出 key 列表快照,遍历全部过滤器,任一返回 true 才拦截并返回;publish 中对 globalFilterMap.isEmpty() 的读取也需纳入读锁(当前 eventdispatcher.h:191 为无锁读取)。

四、P2 轻微问题与潜在缺陷

位置问题建议
src/dfm-base/file/local/localdiriterator.cpp:332-337 条件取反:if (d->dfmioDirIterator) return {};。构造时该指针必然非空,故 fileInfos() 恒返回空列表。它是 AbstractDirIterator 的 public 虚函数,目前仓库内无调用者,属潜在缺陷。 改为 if (!d->dfmioDirIterator) return {};,并补充单元测试覆盖该 API。
src/plugins/common/dfmplugin-fileoperations/fileoperations/fileoperationutils/docopyfileworker.cpp:88, 150-153, 1080 库代码内使用 assert(!fromInfo.isNull());:1080 处 InfoFactory::create<FileInfo> 结果未判空即解引用。 Debug 下空指针直接 abort、Release 下 assert 被移除后仍解引用。改为运行时判空并返回错误码。
src/plugins/common/dfmplugin-fileoperations/fileoperations/deletefiles/dodeletefilesworker.cpp:253, 277 反向迭代 for (it = --end(); it != --begin(); --it) 在空列表时为 UB;:277 的 deleteFilesCount++ 位于 skip/fail 判断之前,计数虚高。 改用 std::reverse_iterator 或反向 range-for;仅在成功分支自增计数。
src/dfm-base/file/local/localfilewatcher.cpp:94-97 watcher.reset(new DWatcher(url)); if (!watcher) { abort(); } —— new 失败会抛异常,reset 后判空恒为假(死代码);且库代码中调用 abort() 会直接终止整个应用。 移除死代码,改为返回 false/记录错误并降级(不监视),绝不在库中 abort。
src/dfm-base/utils/watchercache.cpp:106-121 removeCacheWatcherByParent 用 url.path().startsWith(parent.path()) 做前缀匹配,/home/user 会误伤 /home/user2/… 的 watcher。调用方(rootinfo.cpp:49、filedatamanager.cpp:198)传入的均为无尾斜杠目录 URL。 规范化比较:先补尾分隔符再匹配,或按路径分量边界判断。
src/dfm-base/utils/watchercache.cpp:73-84 cacheWatcher 的"查缓存→不存在则创建→插入"三步非原子,多线程并发同 URL 时可能创建重复 watcher 并相互覆盖(泄漏一个)。 把判断与插入合并为一次原子操作(如 DThreadHash 增加 getOrInsert)。
src/services/usbrepair/usbrepairmonitor.cpp:280, 325, 355, 417 D-Bus 槽 onInterfacesAdded 内同步执行 e2fsck 等 QProcess,最长阻塞主线程 30 秒;期间服务不响应任何插拔/修复请求。 移入独立线程 / QtConcurrent,或改异步 QProcess::finished 回调。
src/services/diskencrypt/workers/resumeencryptworker.cpp:115-135 while (1) { emit requestAuthInfo(...); QThread::sleep(3); } 无总超时,仅靠 m_ignoreFlag 退出。 增加最大等待时长/次数上限,超时置失败退出。
src/dfm-base/utils/processprioritymanager.cpp:39-91, 146-191 优先降级使用 setpriority(PRIO_PROCESS, 0, …) / sched_setscheduler(0, …) / ioprio_set(WHO_PROCESS, 0, …)。在 Linux 上 who=0 仅作用于调用线程,而真正做 IO 的是后续创建的工作线程,因此对 textindex 索引、extractor 抽取的降级意图基本无效。 在各工作线程内部对自身 TID 设置 ioprio / nice,或使用 cgroup io.weight 统一限流。
src/dfm-framework/event/event.cpp:114-115, 132 读锁下使用 eventsMap[stratege](非 const operator[] 可隐式插入元素);pluginTopics() 读取 eventsMap 时完全无锁。 改用 constFind() / value(),pluginTopics 补读锁。
src/dfm-base/utils/infocache.cpp:28-29 主缓存与副缓存各持有至多 20000 个 FileInfoPointer,同一对象被两份哈希同时引用,缓存内存占用翻倍,并放大 P1-12 的竞争面。 若"双缓存"仅为规避读写切换竞态,建议评估以读写锁 + 短临界区替代,或至少下调副缓存容量。
src/dfm-base/file/local/localfilewatcher.cpp:105-108 每个目录各建一个 DWatcher(inotify backend),无跨目录复用;配合 P1-11 的释放路径失效,长时间运行易触碰 max_user_watches 配额。 与索引服务统一监视策略,或按需启停(仅监视当前可见目录)。

五、改进路线图

阶段一 · 数据正确性与崩溃(建议立即修复,改动面小)

  1. P0-1 修复作业结果上报链路,恢复撤销与剪贴板语义;补一条"拷贝失败后剪贴板仍可粘贴"的回归用例。
  2. P0-2 补齐 write()==0 分支,并为拷贝路径加"零写连续 N 次即报错"的日志埋点。
  3. P0-3 在 doStopPlugin 中解绑事件注册;EventHandler 改存 QPointer 兜底。
  4. P0-5 / P1-11 / P1-12 三处缓存与异步查询的确定性缺陷(自旋加超时、双重 take、读锁写数据)。

验收:连续执行"大批量拷贝→中途失败→撤销/重做"、"反复切换目录 200 次"、"加载 5000 文件目录并滚动"三组场景,无崩溃、无残留 inotify 句柄增长。

阶段二 · 消除界面卡死(系统性收敛阻塞调用)

  1. 建立 kDBusTimeoutMs 统一常量,将 54 处无超时 waitForFinished() 全部替换为带超时版本,超时返回降级值。
  2. UI 线程上的 D-Bus 调用改 asyncCall + watcher(优先 tagproxyhandle、operationsstackproxy、deviceproxymanager)。
  3. accesscontroldbus 的管道 read 改非阻塞 + poll 超时 + 长度上限。
  4. 消除 4 处拷贝热路径的 msleep 忙等,改用 waitForDone(timeout) 与条件变量。

验收:以 SIGSTOP 冻结 udisks/tag daemon 后操作文件管理器,界面应保持可交互并在 5 秒内给出降级结果。

阶段三 · 性能热点优化(可量化收益)

  1. 桌面画布与文件模型引入行号哈希索引(P1-1),绘制复杂度由 O(N²) 降至 O(N)。
  2. 拷贝热路径缓存文件系统类型、预编译正则(P1-2/P1-3/P1-4)。
  3. 去重容器统一改 QSet(P1-5);目录扫描延迟构造 QUrl、进度信号改共享指针(P1-7)。
  4. 视图模型缓存列角色映射,远程 scheme 属性刷新异步化(P1-8)。

验收:建立基准——10000 文件目录首次加载耗时、桌面 2000 图标滚动帧率、5 万小文件拷贝总耗时、回收站清空耗时。目标为各项提升 30% 以上。

阶段四 · 机制性加固(防回归)

  1. 为 EventDispatcher 补锁(P1-10),修正全局过滤器短路语义(P1-14);EventChannel::send 加锁(P1-13)。
  2. 设备监视器共享容器改"主线程快照 + 不可变传递"(P1-13)。
  3. 在 CI 中加入:ThreadSanitizer 全量单测、禁止裸 waitForFinished() 的静态检查规则、禁止库代码内 abort()/assert 的 lint 规则。
  4. 把本报告"量化基线"表接入性能回归看板,按版本跟踪。
关于修复优先级的说明:P0-1 与 P0-4 虽然分属"正确性"与"可用性"两类,但在真实用户场景中会相互放大——例如网络盘不可达时,同步 D-Bus 阻塞会让界面假死,而 P0-1 又把失败后台判定为成功,用户既看不到卡顿原因也得不到失败提示。建议二者在同一批次内修复并联合验证。

六、审查方法说明