Skip to content

Commit 1837008

Browse files
committed
fix(startup): lazy-start search/imagehost worker threads to avoid pre-QApplication QEventLoop warning
SearchService and ImageHostService started their worker QThread in their constructors, which run before the QApplication is created in main(). The default QThread::run() calls exec(), constructing a QEventLoop while QCoreApplication::instance() is still null, emitting "QEventLoop: Cannot be used without QCoreApplication". Defer moveToThread + start() to a lazy ensureWorkerThreadStarted() invoked on first async use (always after the app is running). Parent the worker to the service so a session that never searches/uploads still reclaims it via QObject child deletion; unparent before moveToThread on first start so the existing finished->deleteLater cleanup owns it thereafter.
1 parent b9ff3d8 commit 1837008

4 files changed

Lines changed: 70 additions & 6 deletions

File tree

src/core/services/imagehostservice.cpp

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,13 @@ ImageHostService::ImageHostService(HookManager *p_hookMgr, QObject *p_parent)
2525
qRegisterMetaType<ImageHostAsyncResult>();
2626

2727
m_thread = new QThread(this);
28-
m_worker = new ImageHostWorker();
29-
m_worker->moveToThread(m_thread);
28+
// Parent the worker to this service. If no async op runs this session the
29+
// worker QThread never starts, so the finished->deleteLater below never
30+
// fires; QObject child deletion then reclaims the worker. On the first async
31+
// op, ensureWorkerThreadStarted() unparents it and moves it onto m_thread
32+
// (deferred out of this constructor because it runs before QApplication
33+
// exists), after which finished->deleteLater owns it.
34+
m_worker = new ImageHostWorker(nullptr, this);
3035

3136
connect(m_thread, &QThread::finished, m_worker, &QObject::deleteLater);
3237

@@ -45,6 +50,22 @@ ImageHostService::ImageHostService(HookManager *p_hookMgr, QObject *p_parent)
4550
emit uploadFinished(p_token, result);
4651
});
4752

53+
// NOTE: m_thread is started lazily on the first async op (see
54+
// ensureWorkerThreadStarted()), NOT here. This constructor runs in main()
55+
// BEFORE the QApplication is created, so starting the QThread now would run
56+
// QThread::exec() -> QEventLoop while QCoreApplication::instance() is still
57+
// null, emitting "QEventLoop: Cannot be used without QCoreApplication".
58+
}
59+
60+
void ImageHostService::ensureWorkerThreadStarted() {
61+
// Idempotent and GUI-thread-only, so no lock is needed. Unparent before
62+
// moveToThread (Qt forbids moving a parented object across threads); after
63+
// this the finished->deleteLater connection owns the worker.
64+
if (m_thread->isRunning()) {
65+
return;
66+
}
67+
m_worker->setParent(nullptr);
68+
m_worker->moveToThread(m_thread);
4869
m_thread->start();
4970
}
5071

@@ -263,6 +284,7 @@ int ImageHostService::uploadAsync(IImageHostProvider *p_provider, const QByteArr
263284
item.path = p_path;
264285
item.providerName = p_provider->getName();
265286

287+
ensureWorkerThreadStarted();
266288
QMetaObject::invokeMethod(m_worker, "doUpload", Qt::QueuedConnection,
267289
Q_ARG(ImageHostWorkItem, item));
268290
return item.token;
@@ -292,6 +314,7 @@ int ImageHostService::removeAsync(IImageHostProvider *p_provider, const QString
292314
item.path = p_url;
293315
item.providerName = p_provider->getName();
294316

317+
ensureWorkerThreadStarted();
295318
QMetaObject::invokeMethod(m_worker, "doRemove", Qt::QueuedConnection,
296319
Q_ARG(ImageHostWorkItem, item));
297320
return item.token;
@@ -304,6 +327,7 @@ int ImageHostService::testConfigAsync(const QString &p_typeId, const QJsonObject
304327
item.typeId = p_typeId;
305328
item.config = p_config;
306329

330+
ensureWorkerThreadStarted();
307331
QMetaObject::invokeMethod(m_worker, "doTestConfig", Qt::QueuedConnection,
308332
Q_ARG(ImageHostWorkItem, item));
309333
return item.token;

src/core/services/imagehostservice.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,14 @@ private slots:
119119
void onRemoveCompleted(int p_token, const ImageHostAsyncResult &p_result);
120120

121121
private:
122+
// Lazily start the worker QThread on the first async operation. Deferred out
123+
// of the constructor because ImageHostService is constructed in main() BEFORE
124+
// the QApplication exists; starting the QThread there would run
125+
// QThread::exec() -> QEventLoop with no QCoreApplication, emitting
126+
// "QEventLoop: Cannot be used without QCoreApplication". Called on the GUI
127+
// thread from the *Async entry points only, so it needs no locking.
128+
void ensureWorkerThreadStarted();
129+
122130
HookManager *m_hookMgr = nullptr;
123131

124132
QVector<IImageHostProvider *> m_providers;

src/core/services/searchservice.cpp

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -287,8 +287,13 @@ SearchService::SearchService(SearchCoreService *p_coreService, QObject *p_parent
287287
qRegisterMetaType<Error>();
288288
qRegisterMetaType<std::atomic<int> *>("std::atomic<int>*");
289289

290-
m_worker = new SearchWorker(m_coreService, m_mutex);
291-
m_worker->moveToThread(m_thread);
290+
// Parent the worker to this service. If no search runs this session the
291+
// worker QThread never starts, so the finished->deleteLater below never
292+
// fires; QObject child deletion then reclaims the worker. On the first
293+
// search, ensureWorkerThreadStarted() unparents it and moves it onto
294+
// m_thread (deferred out of this constructor because it runs before
295+
// QApplication exists), after which finished->deleteLater owns it.
296+
m_worker = new SearchWorker(m_coreService, m_mutex, this);
292297

293298
connect(m_thread, &QThread::finished, m_worker, &QObject::deleteLater);
294299

@@ -317,8 +322,12 @@ SearchService::SearchService(SearchCoreService *p_coreService, QObject *p_parent
317322
emit searchCancelled(p_token);
318323
});
319324

320-
m_thread->start();
321-
325+
// NOTE: m_thread is started lazily on the first search (see
326+
// ensureWorkerThreadStarted()), NOT here. This constructor runs in main()
327+
// BEFORE the QApplication is created, so starting the QThread now would run
328+
// QThread::exec() -> QEventLoop while QCoreApplication::instance() is still
329+
// null, emitting "QEventLoop: Cannot be used without QCoreApplication". The
330+
// std::thread drain pool below runs no event loop, so it stays here.
322331
unsigned n = std::thread::hardware_concurrency();
323332
if (n == 0) {
324333
n = 2;
@@ -353,6 +362,18 @@ SearchService::~SearchService() {
353362
m_mutex = nullptr;
354363
}
355364

365+
void SearchService::ensureWorkerThreadStarted() {
366+
// Idempotent and GUI-thread-only, so no lock is needed. Unparent before
367+
// moveToThread (Qt forbids moving a parented object across threads); after
368+
// this the finished->deleteLater connection owns the worker.
369+
if (m_thread->isRunning()) {
370+
return;
371+
}
372+
m_worker->setParent(nullptr);
373+
m_worker->moveToThread(m_thread);
374+
m_thread->start();
375+
}
376+
356377
int SearchService::searchFiles(const QString &p_notebookId, const QString &p_queryJson,
357378
const QString &p_inputFilesJson) {
358379
qDebug() << "SearchService::searchFiles: notebookId:" << p_notebookId;
@@ -363,6 +384,7 @@ int SearchService::searchFiles(const QString &p_notebookId, const QString &p_que
363384
m_activeTokens.insert(token);
364385
emit searchStarted(token);
365386

387+
ensureWorkerThreadStarted();
366388
const bool invoked = QMetaObject::invokeMethod(
367389
m_worker, "doSearchFiles", Qt::QueuedConnection, Q_ARG(int, token),
368390
Q_ARG(QString, p_notebookId), Q_ARG(QString, p_queryJson), Q_ARG(QString, p_inputFilesJson),
@@ -386,6 +408,7 @@ int SearchService::searchContent(const QString &p_notebookId, const QString &p_q
386408
m_activeTokens.insert(token);
387409
emit searchStarted(token);
388410

411+
ensureWorkerThreadStarted();
389412
const bool invoked = QMetaObject::invokeMethod(
390413
m_worker, "doSearchContent", Qt::QueuedConnection, Q_ARG(int, token),
391414
Q_ARG(QString, p_notebookId), Q_ARG(QString, p_queryJson), Q_ARG(QString, p_inputFilesJson),
@@ -409,6 +432,7 @@ int SearchService::searchByTags(const QString &p_notebookId, const QString &p_qu
409432
m_activeTokens.insert(token);
410433
emit searchStarted(token);
411434

435+
ensureWorkerThreadStarted();
412436
const bool invoked = QMetaObject::invokeMethod(
413437
m_worker, "doSearchByTags", Qt::QueuedConnection, Q_ARG(int, token),
414438
Q_ARG(QString, p_notebookId), Q_ARG(QString, p_queryJson), Q_ARG(QString, p_inputFilesJson),

src/core/services/searchservice.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,14 @@ class SearchService : public QObject {
6565
void searchBatch(int p_token, const SearchResult &p_result);
6666

6767
private:
68+
// Lazily start the worker QThread on first search. Deferred out of the
69+
// constructor because SearchService is constructed in main() BEFORE the
70+
// QApplication exists; starting the QThread there would run QThread::exec()
71+
// -> QEventLoop with no QCoreApplication, emitting
72+
// "QEventLoop: Cannot be used without QCoreApplication". Called on the GUI
73+
// thread from the search entry points only, so it needs no locking.
74+
void ensureWorkerThreadStarted();
75+
6876
SearchCoreService *m_coreService = nullptr;
6977
SearchWorker *m_worker = nullptr;
7078
QThread *m_thread = nullptr;

0 commit comments

Comments
 (0)