QGroundControl
Ground Control Station for MAVLink Drones
Loading...
Searching...
No Matches
QGCLogging.cc
Go to the documentation of this file.
1#include "QGCLogging.h"
2#include "AppSettings.h"
3#include "QGCApplication.h"
5#include "SettingsManager.h"
6
7#include <QtConcurrent/QtConcurrentRun>
8#include <QtCore/QGlobalStatic>
9#include <QtCore/QMutex>
10#include <QtCore/QMutexLocker>
11#include <QtCore/QStringListModel>
12#include <QtCore/QTextStream>
13
14#include <atomic>
15
16QGC_LOGGING_CATEGORY(QGCLoggingLog, "Utilities.QGCLogging")
17
18Q_GLOBAL_STATIC(QGCLogging, _qgcLogging)
19
20static QtMessageHandler defaultHandler = nullptr;
21
22// ---------------------------------------------------------------------------
23// Test‐log‐capture storage (thread‐safe, lightweight when disabled)
24// ---------------------------------------------------------------------------
25static std::atomic<bool> s_captureEnabled{false};
26static QMutex s_captureMutex;
27static QList<CapturedLogMessage> s_capturedMessages;
28
29static void msgHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
30{
31 // Call the previous handler FIRST to ensure QTest::ignoreMessage works correctly.
32 // QTest's message filtering happens in the default handler, so we must call it
33 // before any processing that might interfere with message matching.
34 if (defaultHandler) {
35 defaultHandler(type, context, msg);
36 }
37
38 // Format the message using Qt's pattern
39 const QString message = qFormatLogMessage(type, context, msg);
40
41 // Filter out Qt Quick internals
42 if (QGCLogging::instance() && !QString(context.category).startsWith("qt.quick")) {
43 // Capture for unit-test introspection
44 QGCLogging::captureIfEnabled(type, context, msg);
45
46 QGCLogging::instance()->log(message);
47 }
48}
49
51{
52 return _qgcLogging();
53}
54
55QGCLogging::QGCLogging(QObject *parent)
56 : QStringListModel(parent)
57{
58 qCDebug(QGCLoggingLog) << this;
59
60 _flushTimer.setInterval(kFlushIntervalMSecs);
61 _flushTimer.setSingleShot(false);
62 (void) connect(&_flushTimer, &QTimer::timeout, this, &QGCLogging::_flushToDisk);
63 _flushTimer.start();
64
65 // Connect the emitLog signal to threadsafeLog slot
66#if defined(Q_OS_ANDROID) || defined(Q_OS_IOS)
67 const Qt::ConnectionType conntype = Qt::QueuedConnection;
68#else
69 const Qt::ConnectionType conntype = Qt::AutoConnection;
70#endif
71 (void) connect(this, &QGCLogging::emitLog, this, &QGCLogging::_threadsafeLog, conntype);
72}
73
75{
76 qCDebug(QGCLoggingLog) << this;
77}
78
80{
81 // Define the format for qDebug/qWarning/etc output
82 qSetMessagePattern(QStringLiteral("%{time process}%{if-warning} Warning:%{endif}%{if-critical} Critical:%{endif} %{message} - %{category} - (%{function}:%{line})"));
83
84 // Install our custom handler
85 defaultHandler = qInstallMessageHandler(msgHandler);
86}
87
88// ---------------------------------------------------------------------------
89// Test‑log‑capture API
90// ---------------------------------------------------------------------------
91
93{
94 s_captureEnabled.store(enabled, std::memory_order_relaxed);
95}
96
98{
99 const QMutexLocker locker(&s_captureMutex);
100 s_capturedMessages.clear();
101}
102
103QList<CapturedLogMessage> QGCLogging::capturedMessages(const QString &category)
104{
105 const QMutexLocker locker(&s_captureMutex);
106
107 if (category.isEmpty()) {
108 return s_capturedMessages;
109 }
110
111 QList<CapturedLogMessage> filtered;
112 for (const auto &msg : std::as_const(s_capturedMessages)) {
113 if (msg.category == category) {
114 filtered.append(msg);
115 }
116 }
117 return filtered;
118}
119
120bool QGCLogging::hasCapturedMessage(const QString &category, QtMsgType type)
121{
122 const QMutexLocker locker(&s_captureMutex);
123
124 for (const auto &msg : std::as_const(s_capturedMessages)) {
125 if (msg.category == category && msg.type == type) {
126 return true;
127 }
128 }
129 return false;
130}
131
132bool QGCLogging::hasCapturedWarning(const QString &category)
133{
134 return hasCapturedMessage(category, QtWarningMsg);
135}
136
137bool QGCLogging::hasCapturedCritical(const QString &category)
138{
139 return hasCapturedMessage(category, QtCriticalMsg);
140}
141
143{
144 const QMutexLocker locker(&s_captureMutex);
145
146 for (const auto &msg : std::as_const(s_capturedMessages)) {
147 if (msg.category.isEmpty() || msg.category == QStringLiteral("default")) {
148 return true;
149 }
150 }
151 return false;
152}
153
154void QGCLogging::captureIfEnabled(QtMsgType type, const QMessageLogContext &context, const QString &msg)
155{
156 if (!s_captureEnabled.load(std::memory_order_relaxed)) {
157 return;
158 }
159
160 const QMutexLocker locker(&s_captureMutex);
161 s_capturedMessages.append({type,
162 context.category ? QString::fromLatin1(context.category) : QString(),
163 msg});
164}
165
166void QGCLogging::log(const QString &message)
167{
168 // Emit the signal so threadsafeLog runs in the correct thread
169 if (!_ioError) {
170 emit emitLog(message);
171 }
172}
173
174void QGCLogging::_threadsafeLog(const QString &message)
175{
176 // Notify view of new row
177 const int line = rowCount();
178 (void) QStringListModel::insertRows(line, 1);
179 (void) setData(index(line, 0), message, Qt::DisplayRole);
180
181 // Trim old entries to cap memory usage
182 static constexpr const int kMaxLogRows = kMaxLogFileSize / 100;
183 if (rowCount() > kMaxLogRows) {
184 const int removeCount = rowCount() - kMaxLogRows;
185 beginRemoveRows(QModelIndex(), 0, removeCount - 1);
186 (void) removeRows(0, removeCount);
187 endRemoveRows();
188 }
189
190 // Queue for disk flush
191 _pendingDiskWrites.append(message);
192}
193
194void QGCLogging::_rotateLogs()
195{
196 // Close the current log
197 _logFile.close();
198
199 // Full path without extension
200 const QString basePath = _logFile.fileName(); // e.g. "/path/QGCConsole.log"
201 const QFileInfo fileInfo(basePath);
202 const QString dir = fileInfo.absolutePath();
203 const QString name = fileInfo.baseName(); // "QGCConsole"
204 const QString ext = fileInfo.completeSuffix(); // "log"
205
206 // Rotate existing backups: QGCConsole.4.log → QGCConsole.5.log, …
207 for (int i = kMaxBackupFiles - 1; i >= 1; --i) {
208 const QString from = QStringLiteral("%1/%2.%3.%4").arg(dir, name).arg(i).arg(ext);
209 const QString to = QStringLiteral("%1/%2.%3.%4").arg(dir, name).arg(i+1).arg(ext);
210 if (QFile::exists(to)) {
211 (void) QFile::remove(to);
212 }
213 if (QFile::exists(from)) {
214 (void) QFile::rename(from, to);
215 }
216 }
217
218 // Move the just‐closed log to “.1”
219 const QString firstBackup = QStringLiteral("%1/%2.1.%3").arg(dir, name, ext);
220 if (QFile::exists(firstBackup)) {
221 (void) QFile::remove(firstBackup);
222 }
223 (void) QFile::rename(basePath, firstBackup);
224
225 // Re‑open a fresh log file
226 _logFile.setFileName(basePath);
227 if (!_logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
228 _ioError = true;
229 qgcApp()->showAppMessage(tr("Unable to reopen log file %1: %2").arg(_logFile.fileName(), _logFile.errorString()));
230 }
231}
232
233void QGCLogging::_flushToDisk()
234{
235 if (_pendingDiskWrites.isEmpty() || _ioError) {
236 return;
237 }
238
239 // Ensure log output enabled and file open
240 if (!_logFile.isOpen()) {
241 if (!qgcApp()->logOutput()) {
242 _pendingDiskWrites.clear();
243 return;
244 }
245
246 const QString saveDirPath = SettingsManager::instance()->appSettings()->crashSavePath();
247 const QDir saveDir(saveDirPath);
248 const QString saveFilePath = saveDir.absoluteFilePath("QGCConsole.log");
249
250 _logFile.setFileName(saveFilePath);
251 if (!_logFile.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text)) {
252 _ioError = true;
253 qgcApp()->showAppMessage(tr("Open console log output file failed %1 : %2").arg(_logFile.fileName(), _logFile.errorString()));
254 return;
255 }
256 }
257
258 // Check size before writing
259 if (_logFile.size() >= kMaxLogFileSize) {
260 _rotateLogs();
261 }
262
263 // Write all pending lines
264 QTextStream out(&_logFile);
265 for (const QString &line : std::as_const(_pendingDiskWrites)) {
266 out << line << '\n';
267 if (out.status() != QTextStream::Ok) {
268 _ioError = true;
269 qCWarning(QGCLoggingLog) << "Error writing to log file:" << _logFile.errorString();
270 break;
271 }
272 }
273 (void) _logFile.flush();
274 _pendingDiskWrites.clear();
275}
276
277void QGCLogging::writeMessages(const QString &destFile)
278{
279 // Snapshot current logs on GUI thread
280 const QStringList logs = stringList();
281
282 // Run the file write in a separate thread
283 (void) QtConcurrent::run([this, destFile, logs]() {
284 emit writeStarted();
285 bool success = false;
286 QSaveFile file(destFile);
287 if (file.open(QIODevice::WriteOnly | QIODevice::Text)) {
288 QTextStream out(&file);
289 for (const QString &line : logs) {
290 out << line << '\n';
291 }
292 success = ((out.status() == QTextStream::Ok) && file.commit());
293 } else {
294 qCWarning(QGCLoggingLog) << "write failed:" << file.errorString();
295 }
296 emit writeFinished(success);
297 });
298}
Q_GLOBAL_STATIC(FirmwarePluginFactoryRegister, _firmwarePluginFactoryRegisterInstance)
#define qgcApp()
#define QGC_LOGGING_CATEGORY(name, categoryStr)
static std::atomic< bool > s_captureEnabled
Definition QGCLogging.cc:25
static void msgHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg)
Definition QGCLogging.cc:29
static QList< CapturedLogMessage > s_capturedMessages
Definition QGCLogging.cc:27
static QMutex s_captureMutex
Definition QGCLogging.cc:26
QString crashSavePath()
static void setCaptureEnabled(bool enabled)
Definition QGCLogging.cc:92
static bool hasCapturedMessage(const QString &category, QtMsgType type)
void log(const QString &message)
Enqueue a log message (thread-safe)
void writeMessages(const QString &destFile)
Write current log messages to a file asynchronously.
static bool hasCapturedCritical(const QString &category)
Convenience: captured critical in category?
static QList< CapturedLogMessage > capturedMessages(const QString &category={})
QGCLogging(QObject *parent=nullptr)
Definition QGCLogging.cc:55
static QGCLogging * instance()
Get the singleton instance.
Definition QGCLogging.cc:50
static void installHandler()
Install Qt message handler to route logs through this class.
Definition QGCLogging.cc:79
static void clearCapturedMessages()
Discard all previously captured messages.
Definition QGCLogging.cc:97
void writeFinished(bool success)
Emitted when file write finishes (success flag)
static void captureIfEnabled(QtMsgType type, const QMessageLogContext &context, const QString &msg)
void emitLog(const QString &message)
Emitted when a log message is enqueued.
static bool hasCapturedWarning(const QString &category)
Convenience: captured warning in category?
void writeStarted()
Emitted when file write starts.
static bool hasCapturedUncategorizedMessage()
Return true if any uncategorized message was captured (e.g. raw qDebug/qWarning).