From afb089bee6f6002dfb3fa14a4ebd67166156f225 Mon Sep 17 00:00:00 2001 From: mr-forust Date: Sat, 25 Jul 2026 22:22:03 +0200 Subject: [PATCH] fix(search): make stop interrupt all phases --- src/main.cpp | 76 ++++++++++++++++++++++++++++++++------ src/search_core.cpp | 31 ++++++++++++---- src/search_core.h | 9 +++-- tests/search_core_test.cpp | 18 +++++++++ 4 files changed, 111 insertions(+), 23 deletions(-) diff --git a/src/main.cpp b/src/main.cpp index 84b53ba..bf51329 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -255,6 +255,12 @@ public slots: void search(const SearchOptions &o) { cancelled_.store(false, std::memory_order_relaxed); + const auto finishIfCancelled = [this] { + if (!cancelled_.load(std::memory_order_relaxed)) + return false; + emit finished({}, tr("Search stopped")); + return true; + }; cancelledByLimit_ = false; cache_.setEnabled(o.useCache); cacheHits_.store(0, std::memory_order_relaxed); @@ -284,7 +290,8 @@ public slots: dirOptions |= fs::directory_options::follow_directory_symlink; if (!o.recursive) { for (fs::directory_iterator it(root, dirOptions, ec), end; - it != end && !ec; it.increment(ec)) { + it != end && !ec && !cancelled_.load(std::memory_order_relaxed); + it.increment(ec)) { ++totalEntries; if (totalEntries % 2048 == 0) emit phaseProgress(tr("Counting files"), totalEntries, 0); @@ -397,7 +404,9 @@ public slots: if (!o.recursive) { for (fs::directory_iterator it(root, dirOptions, ec), end; - it != end && !ec && !cancelledByLimit_; it.increment(ec)) { + it != end && !ec && !cancelled_.load(std::memory_order_relaxed) + && !cancelledByLimit_; + it.increment(ec)) { process(*it, root); if (++scanned % 512 == 0) { if (totalEntries) @@ -438,10 +447,8 @@ public slots: if (totalEntries) emit phaseProgress(tr("Scanning files"), std::min(scanned, totalEntries), totalEntries); - if (cancelled_.load(std::memory_order_relaxed)) { - emit finished({}, tr("Search stopped")); + if (finishIfCancelled()) return; - } if (!o.contains.isEmpty() && !o.findFolders) { emit phaseProgress(tr("Searching file contents"), 0, candidates.size()); @@ -449,6 +456,8 @@ public slots: const qsizetype total = candidates.size(); QFuture future = QtConcurrent::filtered( candidates, [this, o, &checked, total](const FileRecord &r) { + if (cancelled_.load(std::memory_order_relaxed)) + return false; const bool matched = cachedFileContains(r, o); const qsizetype done = checked.fetch_add(1, std::memory_order_relaxed) + 1; if (done == total || done % 32 == 0) @@ -456,6 +465,8 @@ public slots: return matched; }); future.waitForFinished(); + if (finishIfCancelled()) + return; candidates = future.results(); if (o.maxResults > 0 && candidates.size() > o.maxResults) candidates.resize(o.maxResults); @@ -480,6 +491,8 @@ public slots: int group = 1; QSet duplicatedPaths; for (auto &[size, sameSize] : bySize) { + if (cancelled_.load(std::memory_order_relaxed)) + break; Q_UNUSED(size); if (sameSize.size() < 2) continue; @@ -490,18 +503,24 @@ public slots: emit phaseProgress(tr("Checking duplicates"), done, hashTotal); return result; }); + if (cancelled_.load(std::memory_order_relaxed)) + break; QHash> sameHash; for (auto &[hash, record] : hashes) if (!hash.isEmpty()) sameHash[hash].push_back(record); for (const auto &hashCandidates : sameHash) { + if (cancelled_.load(std::memory_order_relaxed)) + break; if (hashCandidates.size() < 2) continue; QVector> exactGroups; for (const auto &record : hashCandidates) { + if (cancelled_.load(std::memory_order_relaxed)) + break; bool placed = false; for (auto &exact : exactGroups) { - if (filesEqual(exact.front().path, record.path)) { + if (filesEqual(exact.front().path, record.path, &cancelled_)) { exact.push_back(record); placed = true; break; @@ -511,6 +530,8 @@ public slots: exactGroups.push_back({record}); } for (auto &exact : exactGroups) { + if (cancelled_.load(std::memory_order_relaxed)) + break; if (exact.size() < 2) continue; int duplicateCopy = 1; @@ -528,6 +549,8 @@ public slots: } } } + if (finishIfCancelled()) + return; if (o.mode == QStringLiteral("Non-Duplicates search")) { for (const auto &record : candidates) if (record.type == QStringLiteral("File") && !duplicatedPaths.contains(record.path)) @@ -545,6 +568,8 @@ public slots: quint64 namesDone = 0; emit phaseProgress(tr("Comparing duplicate names"), 0, byName.size()); for (auto sameName : byName) { + if (cancelled_.load(std::memory_order_relaxed)) + break; ++namesDone; if (namesDone % 32 == 0 || namesDone == quint64(byName.size())) emit phaseProgress(tr("Comparing duplicate names"), namesDone, byName.size()); @@ -558,7 +583,8 @@ public slots: bool identical = !firstHash.isEmpty(); for (qsizetype i = 1; identical && i < sameName.size(); ++i) { identical = cachedSha256(sameName[i]) == firstHash - && filesEqual(sameName.front().path, sameName[i].path); + && filesEqual( + sameName.front().path, sameName[i].path, &cancelled_); } const bool wantsNonIdentical = o.duplicateNameMode.startsWith(QStringLiteral("Only non")); if (identical == wantsNonIdentical) @@ -572,9 +598,13 @@ public slots: } ++group; } + if (finishIfCancelled()) + return; } else if (o.mode == QStringLiteral("Summary")) { QHash> byFolder; for (const auto &record : candidates) { + if (cancelled_.load(std::memory_order_relaxed)) + break; byFolder[record.folder].push_back(record); if (o.includeSubfoldersInSummary) { QDir parent(record.folder); @@ -594,6 +624,8 @@ public slots: } } for (auto it = byFolder.cbegin(); it != byFolder.cend(); ++it) { + if (cancelled_.load(std::memory_order_relaxed)) + break; qint64 total = 0; qint64 totalOnDisk = 0; qint64 newest = 0; @@ -607,10 +639,14 @@ public slots: newest, 0, 0, 0, tr("%1 files").arg(it.value().size()), {}, {}, 0, 0}); } + if (finishIfCancelled()) + return; } else { results = std::move(candidates); } + if (finishIfCancelled()) + return; const double seconds = std::chrono::duration( std::chrono::steady_clock::now() - started).count(); cache_.save(); @@ -634,8 +670,9 @@ private: return digest; } cacheMisses_.fetch_add(1, std::memory_order_relaxed); - digest = sha256(record.path); - cache_.storeHash(record, digest); + digest = sha256(record.path, &cancelled_); + if (!cancelled_.load(std::memory_order_relaxed)) + cache_.storeHash(record, digest); return digest; } @@ -650,8 +687,9 @@ private: return *cached; } cacheMisses_.fetch_add(1, std::memory_order_relaxed); - const bool matched = fileContains(record.path, o); - cache_.storeContent(record, queryKey, matched); + const bool matched = fileContains(record.path, o, &cancelled_); + if (!cancelled_.load(std::memory_order_relaxed)) + cache_.storeContent(record, queryKey, matched); return matched; } @@ -1315,12 +1353,16 @@ public: statusBar()->showMessage(tr("Search cache cleared"), 5000); }); connect(engine_, &SearchEngine::progress, this, [this](const QString &path, quint64 count) { + if (isStopping_) + return; progress_->setRange(0, 0); progress_->setFormat(tr("Scanning… %1 entries").arg(count)); statusBar()->showMessage(tr("Scanning %1 (%2 entries)").arg(path).arg(count)); }); connect(engine_, &SearchEngine::phaseProgress, this, [this](const QString &phase, quint64 completed, quint64 total) { + if (isStopping_) + return; if (total == 0) { progress_->setRange(0, 0); progress_->setFormat(completed @@ -1342,7 +1384,14 @@ public: workerThread_.start(); connect(searchAction_, &QAction::triggered, this, &MainWindow::showOptions); - connect(stopAction_, &QAction::triggered, this, [this] { emit stopRequested(); }); + connect(stopAction_, &QAction::triggered, this, [this] { + isStopping_ = true; + stopAction_->setEnabled(false); + progress_->setRange(0, 0); + progress_->setFormat(tr("Stopping…")); + statusBar()->showMessage(tr("Stopping search…")); + emit stopRequested(); + }); connect(refreshAction_, &QAction::triggered, this, &MainWindow::refreshSearch); connect(exportAction, &QAction::triggered, this, &MainWindow::exportResults); connect(openAction, &QAction::triggered, this, &MainWindow::openSelected); @@ -1524,6 +1573,7 @@ private slots: if (isSearching_ || options_.roots.trimmed().isEmpty()) return; isSearching_ = true; + isStopping_ = false; lastSearchOptions_ = options_; updateStaleSearchWarning(); applyDisplayOptions(); @@ -1544,6 +1594,7 @@ private slots: void searchFinished(const QVector &results, const QString &message) { isSearching_ = false; + isStopping_ = false; model_->setRows(results); searchAction_->setEnabled(true); stopAction_->setEnabled(false); @@ -2099,6 +2150,7 @@ private: bool showGrid_ = true; bool showTooltips_ = true; bool isSearching_ = false; + bool isStopping_ = false; }; int main(int argc, char **argv) diff --git a/src/search_core.cpp b/src/search_core.cpp index e04b759..1f32747 100644 --- a/src/search_core.cpp +++ b/src/search_core.cpp @@ -94,8 +94,18 @@ std::optional hexNeedle(QString text) return QByteArray::fromHex(text.toLatin1()); } -bool fileContains(const QString &path, const SearchOptions &options) +namespace { +bool isCancelled(const std::atomic_bool *cancelled) { + return cancelled && cancelled->load(std::memory_order_relaxed); +} +} + +bool fileContains(const QString &path, const SearchOptions &options, + const std::atomic_bool *cancelled) +{ + if (isCancelled(cancelled)) + return false; if (options.contains.isEmpty()) return true; @@ -124,7 +134,7 @@ bool fileContains(const QString &path, const SearchOptions &options) for (const QByteArray &needle : needles) overlap = std::max(overlap, needle.size()); QByteArray tail; - while (!file.atEnd()) { + while (!file.atEnd() && !isCancelled(cancelled)) { QByteArray data = tail + file.read(1024 * 1024); if (!options.caseSensitive && !options.binary) data = data.toLower(); @@ -145,31 +155,36 @@ bool fileContains(const QString &path, const SearchOptions &options) return false; } -bool filesEqual(const QString &leftPath, const QString &rightPath) +bool filesEqual(const QString &leftPath, const QString &rightPath, + const std::atomic_bool *cancelled) { + if (isCancelled(cancelled)) + return false; QFile left(leftPath); QFile right(rightPath); if (!left.open(QIODevice::ReadOnly) || !right.open(QIODevice::ReadOnly) || left.size() != right.size()) return false; - while (!left.atEnd()) { + while (!left.atEnd() && !isCancelled(cancelled)) { const QByteArray leftData = left.read(1024 * 1024); const QByteArray rightData = right.read(leftData.size()); if (leftData != rightData) return false; } - return right.atEnd(); + return !isCancelled(cancelled) && right.atEnd(); } -QByteArray sha256(const QString &path) +QByteArray sha256(const QString &path, const std::atomic_bool *cancelled) { + if (isCancelled(cancelled)) + return {}; QFile file(path); if (!file.open(QIODevice::ReadOnly)) return {}; QCryptographicHash hash(QCryptographicHash::Sha256); - while (!file.atEnd()) + while (!file.atEnd() && !isCancelled(cancelled)) hash.addData(file.read(1024 * 1024)); - return hash.result(); + return isCancelled(cancelled) ? QByteArray{} : hash.result(); } bool parseOptionalIsoDate(const QString &text, qint64 &seconds) diff --git a/src/search_core.h b/src/search_core.h index 581f5e3..e737379 100644 --- a/src/search_core.h +++ b/src/search_core.h @@ -7,6 +7,7 @@ #include #include +#include #include #include @@ -76,8 +77,10 @@ bool wildcardMatch(const QString &value, const QStringList &items, Qt::CaseSensi QString humanSize(qint64 bytes); QString ownerName(uid_t uid); std::optional hexNeedle(QString text); -bool fileContains(const QString &path, const SearchOptions &options); -bool filesEqual(const QString &leftPath, const QString &rightPath); -QByteArray sha256(const QString &path); +bool fileContains(const QString &path, const SearchOptions &options, + const std::atomic_bool *cancelled = nullptr); +bool filesEqual(const QString &leftPath, const QString &rightPath, + const std::atomic_bool *cancelled = nullptr); +QByteArray sha256(const QString &path, const std::atomic_bool *cancelled = nullptr); bool parseOptionalIsoDate(const QString &text, qint64 &seconds); bool shouldShowDuplicateResult(bool copiesOnly, int keeperPriority); diff --git a/tests/search_core_test.cpp b/tests/search_core_test.cpp index cc1d0a1..6f97bba 100644 --- a/tests/search_core_test.cpp +++ b/tests/search_core_test.cpp @@ -90,6 +90,24 @@ private slots: QVERIFY(!filesEqual(firstPath, differentPath)); QCOMPARE(sha256(firstPath), QCryptographicHash::hash(content, QCryptographicHash::Sha256)); } + + void cancelsFileOperations() + { + QTemporaryDir directory; + QVERIFY(directory.isValid()); + const QString path = directory.filePath(QStringLiteral("file.bin")); + QFile file(path); + QVERIFY(file.open(QIODevice::WriteOnly)); + QCOMPARE(file.write("needle"), qint64(6)); + file.close(); + + std::atomic_bool cancelled = true; + SearchOptions options; + options.contains = QStringLiteral("needle"); + QVERIFY(!fileContains(path, options, &cancelled)); + QVERIFY(!filesEqual(path, path, &cancelled)); + QVERIFY(sha256(path, &cancelled).isEmpty()); + } }; QTEST_GUILESS_MAIN(SearchCoreTest)