44
55#include < qml/models/debuglogmodel.h>
66
7+ #include < util/threadnames.h>
8+
79#include < algorithm>
10+ #include < utility>
811
912#include < QDateTime>
1013#include < QDesktopServices>
1114#include < QFile>
12- #include < QFutureWatcher>
15+ #include < QMetaObject>
16+ #include < QObject>
1317#include < QRegularExpression>
1418#include < QTextStream>
19+ #include < QThread>
20+ #include < QTimer>
1521#include < QUrl>
16- #include < QtConcurrent/QtConcurrentRun>
1722
1823static const QRegularExpression TIMESTAMP_RX (
1924 QStringLiteral (R"( ^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z)\s*(.*)$)" ));
@@ -24,13 +29,30 @@ DebugLogModel::DebugLogModel(const fs::path& log_path, QObject* parent)
2429 : QAbstractListModel(parent)
2530 , m_log_path(log_path)
2631{
32+ m_reader = new QObject;
33+ m_reader_thread = new QThread (this );
34+ m_reader->moveToThread (m_reader_thread);
35+ connect (m_reader_thread, &QThread::finished, m_reader, &QObject::deleteLater);
36+ m_reader_thread->start ();
37+ QTimer::singleShot (0 , m_reader, [] {
38+ util::ThreadRename (" qml-debuglog" );
39+ });
40+
2741 m_debounce.setSingleShot (true );
2842 m_debounce.setInterval (500 );
2943 connect (&m_debounce, &QTimer::timeout, this , [this ]() { refresh (); });
3044
3145 connectFileWatcher ();
3246}
3347
48+ DebugLogModel::~DebugLogModel ()
49+ {
50+ stop ();
51+ if (m_reader_thread) {
52+ m_reader_thread->wait ();
53+ }
54+ }
55+
3456int DebugLogModel::rowCount (const QModelIndex& parent) const
3557{
3658 if (parent.isValid ()) return 0 ;
@@ -85,9 +107,11 @@ void DebugLogModel::setFilter(const QString& filter)
85107
86108void DebugLogModel::refresh (bool full_load)
87109{
110+ if (m_stopping) return ;
111+
88112 // Single-read-in-flight guard. If a read is already running, fold this
89113 // request into a trailing re-run rather than piling another job onto the
90- // thread pool . A burst of watcher events on a noisy node therefore
114+ // worker thread . A burst of watcher events on a noisy node therefore
91115 // collapses to at most two reads: the one in flight, plus one trailer
92116 // that sees the final file state.
93117 if (m_read_in_flight) {
@@ -106,14 +130,32 @@ void DebugLogModel::refresh(bool full_load)
106130 const fs::path path = m_log_path;
107131 const int load_limit = m_load_limit;
108132
109- auto * watcher = new QFutureWatcher<ReadResult>(this );
110- connect (watcher, &QFutureWatcher<ReadResult>::finished, this ,
111- [this , watcher, prev_top_identity, full_load]() {
112- onReadCompleted (watcher->result (), prev_top_identity, full_load);
113- watcher->deleteLater ();
114- });
115- watcher->setFuture (QtConcurrent::run (&DebugLogModel::ReadAndFilter,
116- path, load_limit, full_load));
133+ if (!m_reader || !m_reader_thread || !m_reader_thread->isRunning ()) {
134+ m_read_in_flight = false ;
135+ return ;
136+ }
137+
138+ const bool queued = QMetaObject::invokeMethod (m_reader,
139+ [this , path, load_limit, full_load, prev_top_identity]() mutable {
140+ if (m_read_cancelled.load (std::memory_order_relaxed)) return ;
141+
142+ ReadResult result = ReadAndFilter (path, load_limit, full_load, m_read_cancelled);
143+ if (m_read_cancelled.load (std::memory_order_relaxed)) return ;
144+
145+ QMetaObject::invokeMethod (this ,
146+ [this ,
147+ result = std::move (result),
148+ prev_top_identity,
149+ full_load]() mutable {
150+ if (m_stopping || m_read_cancelled.load (std::memory_order_relaxed)) return ;
151+ onReadCompleted (result, prev_top_identity, full_load);
152+ },
153+ Qt::QueuedConnection);
154+ },
155+ Qt::QueuedConnection);
156+ if (!queued) {
157+ m_read_in_flight = false ;
158+ }
117159}
118160
119161void DebugLogModel::loadMore ()
@@ -180,13 +222,41 @@ void DebugLogModel::updateRelativeTimes()
180222 }
181223}
182224
225+ void DebugLogModel::stop ()
226+ {
227+ if (m_stopping) return ;
228+
229+ m_stopping = true ;
230+ m_read_cancelled.store (true , std::memory_order_relaxed);
231+ m_debounce.stop ();
232+
233+ const auto watched_files = m_watcher.files ();
234+ if (!watched_files.isEmpty ()) {
235+ m_watcher.removePaths (watched_files);
236+ }
237+
238+ m_refresh_pending = false ;
239+ m_pending_full_load = false ;
240+ m_read_in_flight = false ;
241+
242+ if (m_reader_thread) {
243+ m_reader_thread->quit ();
244+ if (QThread::currentThread () != m_reader_thread) {
245+ m_reader_thread->wait ();
246+ }
247+ }
248+ }
249+
183250// ── Private ──────────────────────────────────────────────────────────────────
184251
185252DebugLogModel::ReadResult DebugLogModel::ReadAndFilter (const fs::path& log_path,
186253 int load_limit,
187- bool full_load)
254+ bool full_load,
255+ const std::atomic_bool& cancelled)
188256{
189257 ReadResult result;
258+ if (cancelled.load (std::memory_order_relaxed)) return result;
259+
190260 const QString path_str = QString::fromStdString (log_path.utf8string ());
191261 QFile probe (path_str);
192262 if (!probe.open (QIODevice::ReadOnly | QIODevice::Text)) {
@@ -204,9 +274,14 @@ DebugLogModel::ReadResult DebugLogModel::ReadAndFilter(const fs::path& log_path,
204274 QList<LogLine> filtered;
205275 int fetch_size = load_limit;
206276 while (true ) {
207- const QList<LogLine> raw = ReadRawLines (log_path, fetch_size);
277+ if (cancelled.load (std::memory_order_relaxed)) return {};
278+
279+ const QList<LogLine> raw = ReadRawLines (log_path, fetch_size, cancelled);
280+ if (cancelled.load (std::memory_order_relaxed)) return {};
281+
208282 filtered.clear ();
209283 for (const LogLine& l : raw) {
284+ if (cancelled.load (std::memory_order_relaxed)) return {};
210285 if (!l.content .trimmed ().isEmpty () || l.timestamp_ms >= 0 )
211286 filtered.append (l);
212287 }
@@ -219,8 +294,11 @@ DebugLogModel::ReadResult DebugLogModel::ReadAndFilter(const fs::path& log_path,
219294}
220295
221296QList<DebugLogModel::LogLine> DebugLogModel::ReadRawLines (const fs::path& log_path,
222- int max_lines)
297+ int max_lines,
298+ const std::atomic_bool& cancelled)
223299{
300+ if (cancelled.load (std::memory_order_relaxed)) return {};
301+
224302 const QString path_str = QString::fromStdString (log_path.utf8string ());
225303 QFile file (path_str);
226304 if (!file.open (QIODevice::ReadOnly | QIODevice::Text)) return {};
@@ -236,14 +314,19 @@ QList<DebugLogModel::LogLine> DebugLogModel::ReadRawLines(const fs::path& log_pa
236314
237315 QStringList raw;
238316 raw.reserve (max_lines);
239- while (!in.atEnd ()) raw.append (in.readLine ());
317+ while (!in.atEnd ()) {
318+ if (cancelled.load (std::memory_order_relaxed)) return {};
319+ raw.append (in.readLine ());
320+ }
240321 if (raw.size () > max_lines)
241322 raw = raw.mid (raw.size () - max_lines);
242323
243324 const qint64 now_ms = QDateTime::currentMSecsSinceEpoch ();
244325 QList<LogLine> result;
245326 result.reserve (raw.size ());
246327 for (const QString& line : raw) {
328+ if (cancelled.load (std::memory_order_relaxed)) return {};
329+
247330 LogLine entry;
248331 QString raw_message;
249332 const QRegularExpressionMatch m = TIMESTAMP_RX .match (line);
@@ -268,6 +351,8 @@ void DebugLogModel::onReadCompleted(const ReadResult& result,
268351 const QString& prev_top_identity,
269352 bool full_load)
270353{
354+ if (m_stopping) return ;
355+
271356 // Propagate open-error state from the background read.
272357 if (!result.file_opened ) {
273358 if (m_open_error != result.error_message ) {
@@ -362,7 +447,7 @@ void DebugLogModel::onReadCompleted(const ReadResult& result,
362447
363448 m_read_in_flight = false ;
364449 // If changes arrived while we were reading, run one trailing refresh.
365- if (m_refresh_pending) {
450+ if (!m_stopping && m_refresh_pending) {
366451 m_refresh_pending = false ;
367452 const bool do_full = m_pending_full_load;
368453 m_pending_full_load = false ;
@@ -377,6 +462,7 @@ void DebugLogModel::connectFileWatcher()
377462 m_watcher.addPath (path_str);
378463 connect (&m_watcher, &QFileSystemWatcher::fileChanged,
379464 this , [this ](const QString& path) {
465+ if (m_stopping) return ;
380466 m_watcher.addPath (path); // re-add in case of log rotation
381467 m_debounce.start ();
382468 });
0 commit comments