From 4e090e6ec2a52720262d6ae4f6e0930e6ac91a19 Mon Sep 17 00:00:00 2001 From: Beat Hangartner Date: Tue, 6 Oct 2026 16:31:21 +0200 Subject: [PATCH 1/2] Write the messages of a headless run to stderr on Windows too The exports, --info, --resave and --run return before QetLogger's handler is installed, so they keep Qt's default handler. On Linux and macOS it writes to stderr. On Windows it writes to the debugger when the program has no console, and a GUI program started by another program never has one: everything a headless run reported -- a wire that could not be reconnected, which way the project database was filled, an element that could not be read -- was lost to the caller, a test or a tool like qet-mcp. Install a handler for these runs that writes to stderr in the form the default handler uses on the other systems (the default message pattern, UTF-8). On macOS the stderr of --info, --resave, --run and --export-svg is byte-identical before and after (memory addresses aside). On Windows, tst_databasefromdocument and tst_terminaluuids, which read these messages, fail before and pass after. Co-Authored-By: Claude Fable 5.1 --- sources/main.cpp | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/sources/main.cpp b/sources/main.cpp index 9058b7cc8..827c38329 100644 --- a/sources/main.cpp +++ b/sources/main.cpp @@ -42,6 +42,13 @@ #include #include +#include +#include +#ifdef Q_OS_WIN +#include +#include +#endif + #ifdef Q_OS_MACOS #include @@ -120,6 +127,41 @@ static QStringList headlessArguments(const QStringList &args) return kept; } +/** + The headless runs below (the exports, --info, --resave and --run) return + before the log-file handler is installed further down, so they keep + Qt's default handler. On Linux and macOS that one writes to stderr. On + Windows it writes to the debugger when the program has no console, and + a GUI program started by another program never has one: what the run + reports -- a wire that could not be reconnected, which way the project + database was filled -- never reached the caller. Write it to stderr + ourselves, in the form the default handler uses on the other systems, + so a caller reads the same lines on every system. +*/ +static void headlessMessageHandler(QtMsgType type, + const QMessageLogContext &context, + const QString &msg) +{ + const QByteArray line = qFormatLogMessage(type, context, msg).toUtf8(); + fprintf(stderr, "%s\n", line.constData()); + fflush(stderr); + if (type == QtFatalMsg) { + abort(); + } +} + +static void installHeadlessMessageHandler() +{ +#ifdef Q_OS_WIN + // stderr is in text mode on Windows and turns "\n" into "\r\n": a + // caller would read lines ending in '\r' where the other systems give + // none, and what the script logs with qet.log() already ends in "\n" + // alone. Binary mode gives one line ending for both. + _setmode(_fileno(stderr), _O_BINARY); +#endif + qInstallMessageHandler(headlessMessageHandler); +} + int main(int argc, char **argv) { // before creating Application: @@ -190,6 +232,7 @@ int main(int argc, char **argv) for (int i = 0; i < argc; ++i) raw_args << QString::fromLocal8Bit(argv[i]); if (CLIExport::isExportRequest(raw_args)) { + installHeadlessMessageHandler(); QApplication export_app(argc, argv); // No crash-recovery backups in one-shot CLI mode: the backup write // runs on a background thread referencing the project and races the @@ -207,6 +250,7 @@ int main(int argc, char **argv) // #162). Same reasoning as the export branch above for running // before SingleApplication and answering message boxes headlessly. if (QetScripting::isRunRequest(raw_args)) { + installHeadlessMessageHandler(); QApplication script_app(argc, argv); QETProject::setBackupEnabled(false); QET::QetMessageBox::setNonInteractive(true); From e50e6a492e0a9fbd04336e2e79241f958128e1e8 Mon Sep 17 00:00:00 2001 From: Beat Hangartner Date: Wed, 7 Oct 2026 01:51:00 +0200 Subject: [PATCH 2/2] Put stdout of a headless run into binary mode on Windows too stderr got it in the previous commit; stdout (the JSON of --info, the messages of the exports) still ended its lines in "\r\n" on Windows while stderr ended them in "\n". Both streams now give the same bytes on every system. Co-Authored-By: Claude Fable 5.1 --- sources/main.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/sources/main.cpp b/sources/main.cpp index 827c38329..09aab1f21 100644 --- a/sources/main.cpp +++ b/sources/main.cpp @@ -153,10 +153,12 @@ static void headlessMessageHandler(QtMsgType type, static void installHeadlessMessageHandler() { #ifdef Q_OS_WIN - // stderr is in text mode on Windows and turns "\n" into "\r\n": a - // caller would read lines ending in '\r' where the other systems give - // none, and what the script logs with qet.log() already ends in "\n" - // alone. Binary mode gives one line ending for both. + // stdout and stderr are in text mode on Windows and turn "\n" into + // "\r\n": a caller would read lines ending in '\r' where the other + // systems give none. Binary mode gives both streams the same line + // ending on every system, for the JSON of --info, the messages of the + // exports and what a script logs. + _setmode(_fileno(stdout), _O_BINARY); _setmode(_fileno(stderr), _O_BINARY); #endif qInstallMessageHandler(headlessMessageHandler);