From e3d11a499223251afd75ca7600d26e771364d244 Mon Sep 17 00:00:00 2001 From: ispyisail Date: Wed, 5 Aug 2026 05:26:57 +1200 Subject: [PATCH] Fix command-line tools hanging forever on a modal message box `qelectrotech --resave examples/schema_indus.qet out.qet` never returns. It is not slow -- ten minutes of wall clock consumed 0.16s of CPU, so it is blocked, not working. The GUI opens the same project without complaint, so the file is fine and the fault is in the headless path. A backtrace of the stuck process: main CLIExport::run QETProject::QETProject(QString const&, QObject*) QETProject::openFile(QFile*) QETProject::readProjectXml(QDomDocument&) QET::QetMessageBox::warning(...) QDialog::exec() <- waits forever That project records version="0.3", so loading it raises the "partially compatible with your version" warning. Interactively somebody presses Open; with no display nobody can, and exec() spins its event loop indefinitely. Any modal box reachable while loading does this -- the version warning is just the one an example file happens to trigger. Fixed at the wrapper all 52 call sites already go through rather than at the one warning, so the whole class is closed: QetMessageBox gains a non-interactive mode which writes the message to stderr and returns an answer instead of constructing a dialog. main.cpp turns it on in the CLI branch, beside the existing setBackupEnabled(false). The answer is the caller's defaultButton when it gave one, otherwise the first "carry on" button offered (Ok, Open, Yes, Save...), otherwise the first button set. Both warnings in readProjectXml offer Open|Cancel and abort on Cancel, so they resolve to Open and the project loads, which is what a batch invocation wants. The text still reaches the user on stderr, where previously it was lost inside an invisible dialog. GUI behaviour is unchanged: the flag defaults to false and is set in exactly one place, the command-line branch of main(). Verified: schema_indus.qet goes from hanging to resaving in 0.3s; all 23 example projects now complete a double-resave with element, conductor, terminal and uuid sets intact; unit tests pass. --- sources/main.cpp | 6 +++ sources/qetmessagebox.cpp | 90 +++++++++++++++++++++++++++++++++++++++ sources/qetmessagebox.h | 22 ++++++++++ 3 files changed, 118 insertions(+) diff --git a/sources/main.cpp b/sources/main.cpp index eede1bfb8..e723d0d43 100644 --- a/sources/main.cpp +++ b/sources/main.cpp @@ -19,6 +19,7 @@ #include "machine_info.h" #include "qet.h" #include "qetapp.h" +#include "qetmessagebox.h" #include "qetproject.h" #include "singleapplication.h" #include "utils/qetsettings.h" @@ -249,6 +250,11 @@ QGuiApplication::setHighDpiScaleFactorRoundingPolicy(QetSettings::hdpiScaleFacto // runs on a background thread referencing the project and races the // process exit (intermittent segfault in QET::writeToFile). QETProject::setBackupEnabled(false); + // Answer message boxes instead of showing them: opening a project + // saved by an older QElectroTech raises a warning from + // QETProject::readProjectXml(), and with nobody able to dismiss it + // QDialog::exec() would spin its event loop forever. + QET::QetMessageBox::setNonInteractive(true); return CLIExport::run(export_app.arguments()); } } diff --git a/sources/qetmessagebox.cpp b/sources/qetmessagebox.cpp index 31d6e41a8..2731baf00 100644 --- a/sources/qetmessagebox.cpp +++ b/sources/qetmessagebox.cpp @@ -17,6 +17,84 @@ */ #include "qetmessagebox.h" +#include + +namespace { + bool g_non_interactive = false; + + /** + @brief autoAnswer + Report a message box on stderr and pick an answer, for use when there + is no user to click anything. @see QET::QetMessageBox::setNonInteractive + @param severity : short word naming the kind of box, for the log line + @param title + @param text + @param buttons : the buttons the caller offered + @param defaultButton : the caller's preferred answer, may be NoButton + @return the button to report as pressed + */ + QMessageBox::StandardButton autoAnswer( + const char *severity, + const QString &title, + const QString &text, + QMessageBox::StandardButtons buttons, + QMessageBox::StandardButton defaultButton) + { + //Honour the caller's own default when it named one. + if (defaultButton != QMessageBox::NoButton + && (buttons & defaultButton)) { + QTextStream(stderr) << severity << ": " << title << " -- " << text + << "\n(no display: answered with the caller's default button)\n"; + return defaultButton; + } + + //Otherwise prefer a "carry on" answer over one that cancels, so a + //batch run completes rather than silently doing nothing. + static const QMessageBox::StandardButton preference[] = { + QMessageBox::Ok, QMessageBox::Open, QMessageBox::Yes, + QMessageBox::Save, QMessageBox::Apply, QMessageBox::YesToAll, + QMessageBox::Retry, QMessageBox::Ignore, QMessageBox::Close + }; + for (auto candidate : preference) { + if (buttons & candidate) { + QTextStream(stderr) << severity << ": " << title << " -- " << text + << "\n(no display: continuing)\n"; + return candidate; + } + } + + //Nothing affirmative on offer -- fall back to whatever is set. + for (int bit = QMessageBox::Ok; bit <= QMessageBox::RestoreDefaults; bit <<= 1) { + auto candidate = static_cast(bit); + if (buttons & candidate) { + QTextStream(stderr) << severity << ": " << title << " -- " << text + << "\n(no display: answered automatically)\n"; + return candidate; + } + } + + QTextStream(stderr) << severity << ": " << title << " -- " << text + << "\n(no display: no button offered)\n"; + return QMessageBox::NoButton; + } +} + +/** + @brief QET::QetMessageBox::setNonInteractive + @param non_interactive +*/ +void QET::QetMessageBox::setNonInteractive(bool non_interactive) { + g_non_interactive = non_interactive; +} + +/** + @brief QET::QetMessageBox::isNonInteractive + @return true when message boxes are answered without a user +*/ +bool QET::QetMessageBox::isNonInteractive() { + return g_non_interactive; +} + /** @see Documentation Qt pour QMessageBox::critical */ @@ -27,6 +105,9 @@ QMessageBox::StandardButton QET::QetMessageBox::critical ( QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { + if (g_non_interactive) { + return autoAnswer("Critical", title, text, buttons, defaultButton); + } #ifdef Q_OS_MACOS QMessageBox message_box( QMessageBox::Critical, @@ -59,6 +140,9 @@ QMessageBox::StandardButton QET::QetMessageBox::information( QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { + if (g_non_interactive) { + return autoAnswer("Information", title, text, buttons, defaultButton); + } #ifdef Q_OS_MACOS QMessageBox message_box( QMessageBox::Information, @@ -91,6 +175,9 @@ QMessageBox::StandardButton QET::QetMessageBox::question ( QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { + if (g_non_interactive) { + return autoAnswer("Question", title, text, buttons, defaultButton); + } #ifdef Q_OS_MACOS QMessageBox message_box( QMessageBox::Question, @@ -123,6 +210,9 @@ QMessageBox::StandardButton QET::QetMessageBox::warning ( QMessageBox::StandardButtons buttons, QMessageBox::StandardButton defaultButton) { + if (g_non_interactive) { + return autoAnswer("Warning", title, text, buttons, defaultButton); + } #ifdef Q_OS_MACOS QMessageBox message_box( QMessageBox::Warning, diff --git a/sources/qetmessagebox.h b/sources/qetmessagebox.h index 60238595a..6e838f4e4 100644 --- a/sources/qetmessagebox.h +++ b/sources/qetmessagebox.h @@ -27,6 +27,28 @@ namespace QET { Qt:Sheet flag, thus enabling a better MacOS integration. */ namespace QetMessageBox { + /** + Enable non-interactive mode. + + In non-interactive mode the functions below never construct a + dialog. They write the message to stderr and return an answer + immediately, so a headless run cannot block on a modal box that + nobody is there to dismiss. + + This is needed because these are reachable from the command-line + tools: opening a project written by an older QElectroTech raises + a warning from QETProject::readProjectXml(), and with no display + to click it, QDialog::exec() spins its event loop forever. + + The answer is chosen as: the caller's defaultButton when it gave + one, otherwise the first "carry on" button among those offered + (Ok, Open, Yes, Save, Apply...), otherwise the first button set. + So the two warnings above resolve to Open and the project loads, + which is what a batch invocation wants. + */ + void setNonInteractive(bool non_interactive); + bool isNonInteractive(); + QMessageBox::StandardButton critical ( QWidget *, const QString &,