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 <noreply@anthropic.com>
This commit is contained in:
Beat Hangartner
2026-10-06 16:31:21 +02:00
parent 0708338bce
commit 4e090e6ec2
+44
View File
@@ -42,6 +42,13 @@
#include <QStyleFactory>
#include <QtConcurrentRun>
#include <cstdio>
#include <cstdlib>
#ifdef Q_OS_WIN
#include <fcntl.h>
#include <io.h>
#endif
#ifdef Q_OS_MACOS
#include <QFileOpenEvent>
@@ -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);