本次审查覆盖 dfm-base(文件信息 / 目录迭代 / 缓存 / 监视器)、dfm-framework(插件与事件框架)、plugins/common/dfmplugin-fileoperations(拷贝剪切删除引擎)、plugins/filemanager 与 plugins/desktop(视图与模型)、services/(索引、访问控制、加解密、USB 修复)。
整体判断:架构分层清晰,缓存与异步化设计意图明确,代码注释与日志规范度较高;但存在若干确定性缺陷——其中"文件操作失败被上报为成功"和"拷贝写循环可能死循环"两项直接影响数据正确性,属于必须优先修复的问题。此外全仓库有 54 处无超时阻塞等待,是界面卡死类问题的系统性来源。
| 指标 | 实测值 | 说明 |
|---|---|---|
waitForFinished() 无超时调用 | 54 处 / 80 处 | 26 处已带超时参数;无超时者均可无限阻塞 |
QThread::msleep 忙等点 | 17 处 | 其中 4 处在拷贝热路径,用于替代条件变量 |
processEvents() 调用 | 5 处 | 局部嵌套事件循环,有重入风险 |
| 文件级静态可变共享状态 | 86 处 | 多为单例,需关注析构顺序 |
拷贝热路径 QStorageInfo 构造 | 每个文件 1 次 | 解析挂载表,万级文件额外数秒 |
| 拷贝热路径正则编译 | 每个文件 1~2 次 | QRegularExpression 在循环内构造 |
三个函数各自创建了一份互不相干的局部 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
removeUrlsInClipboard(..., 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);
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 不推进
}
write() 返回 0 是合法返回(写入零字节),此时 bytesWritten 不前进,循环永久自旋。isStopped() 能退出)。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 回退逻辑 ... */
}
// 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 });
LifeCycle::shutdownPlugin()(单个插件卸载)的公开能力,但该路径不设置 g_shuttingDown 标志,因此 EventDispatcher::dispatch 的关停短路保护(eventdispatcher.cpp:25)不生效。publish 都会经 EventHelper 调用已析构对象,且函数体所在 .so 已被 unload() 解除映射 → 确定性崩溃。doStopPlugin 内向三个 manager 遍历该插件实例的所有注册并逐一 unsubscribe / removeFilter / disconnect / unfollow;② EventHandler 内改存 QPointer<QObject>,调用前判空,作为兜底。
// EventDispatcher:调用前判活
for (const auto &h : handlersCopy)
if (h.objectIndex && h.handler) // QPointer 语义
h.handler(params);
// 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(); // 无参数 = 无限等待
read 永久阻塞 → 整个 accesscontrol 服务不再响应任何 D-Bus 请求;同时 buffer 无上限,存在内存耗尽风险。accesscontroldbus:先 fcntl(fd, F_SETFL, O_NONBLOCK),改用 poll() 带 3s 超时,并设置累计长度上限(密码凭据实际仅数百字节)。reply.waitForFinished(kDBusTimeoutMs),超时返回降级值;UI 线程路径优先改为 asyncCall + QDBusPendingCallWatcher。
// 统一超时常量与降级
static constexpr int kDBusTimeoutMs = 5000;
if (!reply.isFinished() && !reply.waitForFinished(kDBusTimeoutMs)) {
fmWarning() << "DBus timeout:" << iface << method;
return {}; // 返回空值,由调用方走 fallback
}
// 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();
}
cacheAsyncAttributes() 返回 0 表示"属性尚未就绪"。在 SMB / NFS 挂载不可达、或 gvfs 后端挂起时,该值可长期保持 0 → 池任务永不返回。max(CPU, 10),累计 10 个此类任务即可令 pool 彻底饥饿:后续所有文件的缩略图、MIME 探测、属性刷新全部排队,界面表现为"文件图标永远不出现 / 属性面板一直转圈"。stoped,即使进入退出流程也无法中止。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;
}
// 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 中预取比较数据,避免在比较器内构造索引。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, "_");
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> 成员,过滤时仅复用。// 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(...))。// 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 + 末尾统一排序去重。// :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。// :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 时选择有误。// :798 columnToRole 内,每个单元格调用一次
WorkspaceEventSequence::instance()->doFetchCustomColumnRoles(...); // 事件总线分发
getFileViewStateValue(dirRootUrl, "headerList"); // 读设置 + toMap() 拷贝
滚动与重绘时,每个可见单元格触发一次事件总线分发 + 设置读取/字典拷贝。列表视图下成为绘制主开销。另 :1415 在 UI 线程对 smb/sftp 等远程 scheme 执行同步 updateAttributes()(网络 IO),切目录时阻塞界面。
dirRootUrl 缓存列角色列表,仅在 headerList 或自定义列变更时失效;远程 scheme 的属性刷新改异步并 dataChanged 回传。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 往返,其余查询全部排队。
isServiceRegistered 结果用 QDBusServiceWatcher 的 registered/unregistered 信号维护缓存;:499 先在锁外取 info,再短暂持锁更新容器。// 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 内先拷贝到局部再释放锁),使拷贝成为原子快照。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() 成为死代码。后果:
kCacheFileWatcherCount = 5000 的上限被动淘汰,长期驻留大量 watch 句柄,并持续消耗 inotify 配额。take:auto info = d->mainCache.take(url);
if (info)
infos.insert(url, info); // 使用已取出的 info
{
QReadLocker lk(&d->mainLock); // ← 读锁
for (const auto &info : d->mainCache) {
if (info) info->setExtendedAttributes(type, QVariant()); // ← 写共享对象
}
}
读锁允许多个读者并发进入,而此处遍历并修改每个 FileInfo 的扩展属性。与其它持读锁读取同一批 FileInfo 的线程(如 getCacheInfo 的调用方)构成数据竞争,可读到半更新的属性值,极端情况下触碰 Qt 容器内部指针导致崩溃。
QWriteLocker;若担心持写锁期间遍历 20000 条造成阻塞,则先持读锁快照指针列表,再在锁外逐项更新,并在每项更新时对其内部锁加保护。// 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,并用去重机制避免任务叠加。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²) 次分配。
true 才拦截并返回;publish 中对 globalFilterMap.isEmpty() 的读取也需纳入读锁(当前 eventdispatcher.h:191 为无锁读取)。| 位置 | 问题 | 建议 |
|---|---|---|
| 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 配额。 |
与索引服务统一监视策略,或按需启停(仅监视当前可见目录)。 |
write()==0 分支,并为拷贝路径加"零写连续 N 次即报错"的日志埋点。doStopPlugin 中解绑事件注册;EventHandler 改存 QPointer 兜底。验收:连续执行"大批量拷贝→中途失败→撤销/重做"、"反复切换目录 200 次"、"加载 5000 文件目录并滚动"三组场景,无崩溃、无残留 inotify 句柄增长。
kDBusTimeoutMs 统一常量,将 54 处无超时 waitForFinished() 全部替换为带超时版本,超时返回降级值。asyncCall + watcher(优先 tagproxyhandle、operationsstackproxy、deviceproxymanager)。accesscontroldbus 的管道 read 改非阻塞 + poll 超时 + 长度上限。msleep 忙等,改用 waitForDone(timeout) 与条件变量。验收:以 SIGSTOP 冻结 udisks/tag daemon 后操作文件管理器,界面应保持可交互并在 5 秒内给出降级结果。
QSet(P1-5);目录扫描延迟构造 QUrl、进度信号改共享指针(P1-7)。验收:建立基准——10000 文件目录首次加载耗时、桌面 2000 图标滚动帧率、5 万小文件拷贝总耗时、回收站清空耗时。目标为各项提升 30% 以上。
EventDispatcher 补锁(P1-10),修正全局过滤器短路语义(P1-14);EventChannel::send 加锁(P1-13)。ThreadSanitizer 全量单测、禁止裸 waitForFinished() 的静态检查规则、禁止库代码内 abort()/assert 的 lint 规则。linuxdeepin/dde-file-manager 6.6.5 单一提交(8503f5e)的干净检出,src/ 无本地改动,因此所列问题均为上游真实存在,不是本地魔改引入。文件:行号 与关键代码片段,并经过二次回读确认;对已有锁保护(DThreadHash/DThreadList)、已有超时参数、已有边界检查的实现未列为问题,例如 FileUtils::getMemoryPageSize/getCpuProcessCount 已用静态变量缓存、abstractfilewatcher 的 cacheInfoConnectSize 已是 QAtomicInt。