1 /*
2  * \copyright Copyright (c) 2014-2021 Governikus GmbH & Co. KG, Germany
3  */
4 
5 #include "LogHandler.h"
6 
7 #include "SingletonHelper.h"
8 
9 #include <QCoreApplication>
10 #include <QDir>
11 #include <QScopeGuard>
12 #include <QStringBuilder>
13 
14 using namespace governikus;
15 
16 defineSingleton(LogHandler)
17 
Q_DECLARE_LOGGING_CATEGORY(fileprovider)18 Q_DECLARE_LOGGING_CATEGORY(fileprovider)
19 Q_DECLARE_LOGGING_CATEGORY(securestorage)
20 Q_DECLARE_LOGGING_CATEGORY(configuration)
21 
22 #define LOGCAT(name) QString::fromLatin1(name().categoryName())
23 
24 
25 #if !defined(Q_OS_ANDROID) && !defined(QT_USE_JOURNALD)
26 #define ENABLE_MESSAGE_PATTERN
27 #endif
28 
29 
30 LogHandler::LogHandler()
31 	: mEventHandler()
32 	, mEnvPattern(!qEnvironmentVariableIsEmpty("QT_MESSAGE_PATTERN"))
33 	, mFunctionFilenameSize(74)
34 	, mBacklogPosition(0)
35 	, mCriticalLog(false)
36 	, mCriticalLogWindow(10)
37 	, mCriticalLogIgnore({LOGCAT(fileprovider), LOGCAT(securestorage), LOGCAT(configuration)})
38 	, mMessagePattern(QStringLiteral("%{category} %{time yyyy.MM.dd hh:mm:ss.zzz} %{threadid} %{if-debug} %{endif}%{if-info}I%{endif}%{if-warning}W%{endif}%{if-critical}C%{endif}%{if-fatal}F%{endif} %{function}(%{file}:%{line}) %{message}"))
39 	, mDefaultMessagePattern(QStringLiteral("%{if-category}%{category}: %{endif}%{message}")) // as defined in qlogging.cpp
40 	, mLogFile()
41 	, mHandler(nullptr)
42 	, mUseHandler(true)
43 	, mFilePrefix("/src/")
44 	, mMutex()
45 {
46 }
47 
48 
~LogHandler()49 LogHandler::~LogHandler()
50 {
51 	reset();
52 }
53 
54 
getLogFileTemplate()55 QString LogHandler::getLogFileTemplate()
56 {
57 	// if you change value you need to adjust getOtherLogfiles()
58 	return QDir::tempPath() % QLatin1Char('/') % QCoreApplication::applicationName() % QStringLiteral(".XXXXXX.log");
59 }
60 
61 
reset()62 void LogHandler::reset()
63 {
64 	const QMutexLocker mutexLocker(&mMutex);
65 	if (isInstalled())
66 	{
67 		qInstallMessageHandler(nullptr);
68 		mHandler = nullptr;
69 	}
70 }
71 
72 
init()73 void LogHandler::init()
74 {
75 	const QMutexLocker mutexLocker(&mMutex);
76 
77 	QStringList rules;
78 
79 	// Enable this warning again when our minimum Qt version is 5.14
80 	rules << QStringLiteral("qt.qml.connections.warning=false");
81 
82 #ifndef QT_NO_DEBUG
83 	rules << QStringLiteral("qt.qml.binding.removal.info=true");
84 #endif
85 
86 	QLoggingCategory::setFilterRules(rules.join(QLatin1Char('\n')));
87 
88 	if (mEventHandler.isNull())
89 	{
90 		mEventHandler = new LogEventHandler();
91 		QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, mEventHandler.data(), &QObject::deleteLater);
92 		QObject::connect(QCoreApplication::instance(), &QCoreApplication::aboutToQuit, mEventHandler.data(), [this] {
93 					mEventHandler.clear(); // clear immediately, otherwise logging in &QObject::destroyed is dangerous!
94 				});
95 	}
96 
97 	if (mLogFile.isNull())
98 	{
99 		mLogFile = new QTemporaryFile(getLogFileTemplate());
100 		QObject::connect(QCoreApplication::instance(), &QCoreApplication::destroyed, mLogFile.data(), [this] {
101 					delete this->mLogFile.data();
102 				});
103 
104 		if (useLogfile())
105 		{
106 			mLogFile->open();
107 		}
108 
109 		// Avoid deadlock with subsequent logging of this call.
110 		QMetaObject::invokeMethod(mLogFile.data(), [this] {removeOldLogfiles();}, Qt::QueuedConnection);
111 	}
112 
113 	if (!isInstalled())
114 	{
115 		mHandler = qInstallMessageHandler(&LogHandler::messageHandler);
116 	}
117 }
118 
119 
getEventHandler() const120 const LogEventHandler* LogHandler::getEventHandler() const
121 {
122 	return mEventHandler;
123 }
124 
125 
isInstalled() const126 bool LogHandler::isInstalled() const
127 {
128 	return mHandler;
129 }
130 
131 
setAutoRemove(bool pRemove)132 bool LogHandler::setAutoRemove(bool pRemove)
133 {
134 	if (mLogFile)
135 	{
136 		mLogFile->setAutoRemove(pRemove);
137 		return true;
138 	}
139 
140 	return false;
141 }
142 
143 
logToFile(const QString & pOutput)144 void LogHandler::logToFile(const QString& pOutput)
145 {
146 	if (mLogFile && mLogFile->isOpen() && mLogFile->isWritable())
147 	{
148 		mLogFile->write(pOutput.toUtf8());
149 		mLogFile->flush();
150 	}
151 }
152 
153 
readLogFile(qint64 pStart,qint64 pLength)154 QByteArray LogHandler::readLogFile(qint64 pStart, qint64 pLength)
155 {
156 	if (mLogFile && mLogFile->isOpen() && mLogFile->isReadable())
157 	{
158 		const auto currentPos = mLogFile->pos();
159 		const auto resetPosition = qScopeGuard([this, currentPos] {
160 					mLogFile->seek(currentPos);
161 				});
162 
163 		mLogFile->seek(pStart);
164 		return pLength > 0 ? mLogFile->read(pLength) : mLogFile->readAll();
165 	}
166 
167 	if (useLogfile())
168 	{
169 		//: LABEL ALL_PLATFORMS
170 		return QObject::tr("An error occurred in log handling: %1").arg(mLogFile->errorString()).toUtf8();
171 	}
172 
173 	return QByteArray();
174 }
175 
176 
getBacklog(bool pAll)177 QByteArray LogHandler::getBacklog(bool pAll)
178 {
179 	const QMutexLocker mutexLocker(&mMutex);
180 	return readLogFile(pAll ? 0 : mBacklogPosition);
181 }
182 
183 
getCriticalLogWindow()184 QByteArray LogHandler::getCriticalLogWindow()
185 {
186 	const QMutexLocker mutexLocker(&mMutex);
187 
188 	if (mCriticalLog)
189 	{
190 		const auto first = qAsConst(mCriticalLogWindow).first();
191 		const auto last = qAsConst(mCriticalLogWindow).last();
192 		return readLogFile(first.mPosition, last.mPosition - first.mPosition + last.mLength);
193 	}
194 
195 	return QByteArray();
196 }
197 
198 
hasCriticalLog() const199 bool LogHandler::hasCriticalLog() const
200 {
201 	return mCriticalLog;
202 }
203 
204 
getCriticalLogCapacity() const205 int LogHandler::getCriticalLogCapacity() const
206 {
207 	return mCriticalLogWindow.capacity();
208 }
209 
210 
setCriticalLogCapacity(int pSize)211 void LogHandler::setCriticalLogCapacity(int pSize)
212 {
213 	const QMutexLocker mutexLocker(&mMutex);
214 	mCriticalLogWindow.setCapacity(pSize);
215 }
216 
217 
getFileDate(const QFileInfo & pInfo)218 QDateTime LogHandler::getFileDate(const QFileInfo& pInfo)
219 {
220 	if (const auto& dateTime = pInfo.birthTime(); dateTime.isValid())
221 	{
222 		return dateTime;
223 	}
224 
225 	return pInfo.metadataChangeTime();
226 }
227 
228 
getCurrentLogfileDate() const229 QDateTime LogHandler::getCurrentLogfileDate() const
230 {
231 	return useLogfile() ? getFileDate(QFileInfo(*mLogFile)) : QDateTime();
232 }
233 
234 
resetBacklog()235 void LogHandler::resetBacklog()
236 {
237 	const QMutexLocker mutexLocker(&mMutex);
238 
239 	if (useLogfile())
240 	{
241 		mBacklogPosition = mLogFile->pos();
242 		mCriticalLog = false;
243 		mCriticalLogWindow.clear();
244 	}
245 }
246 
247 
copyMessageLogContext(const QMessageLogContext & pSource,QMessageLogContext & pDestination,const QByteArray & pFilename,const QByteArray & pFunction,const QByteArray & pCategory) const248 void LogHandler::copyMessageLogContext(const QMessageLogContext& pSource,
249 		QMessageLogContext& pDestination,
250 		const QByteArray& pFilename,
251 		const QByteArray& pFunction,
252 		const QByteArray& pCategory) const
253 {
254 	pDestination.file = pFilename.isNull() ? pSource.file : pFilename.constData();
255 	pDestination.function = pFunction.isNull() ? pSource.function : pFunction.constData();
256 	pDestination.category = pCategory.isNull() ? pSource.category : pCategory.constData();
257 
258 	pDestination.line = pSource.line;
259 	pDestination.version = pSource.version;
260 }
261 
262 
formatFilename(const char * const pFilename) const263 QByteArray LogHandler::formatFilename(const char* const pFilename) const
264 {
265 	QByteArray filename(pFilename);
266 
267 	// Normalize the file name
268 	filename.replace(QByteArrayLiteral("\\"), "/");
269 
270 	// Remove useless path
271 	return filename.mid(filename.lastIndexOf(mFilePrefix) + mFilePrefix.size());
272 }
273 
274 
formatFunction(const char * const pFunction,const QByteArray & pFilename,int pLine) const275 QByteArray LogHandler::formatFunction(const char* const pFunction, const QByteArray& pFilename, int pLine) const
276 {
277 	QByteArray function(pFunction);
278 
279 	// Remove namespace governikus::
280 	function.replace(QByteArrayLiteral("governikus::"), "");
281 
282 	// Remove the parameter list
283 	const auto start = function.indexOf('(');
284 	const auto end = function.indexOf(')');
285 	function = function.left(start) + function.mid(end + 1);
286 
287 	if (function.endsWith(" const"))
288 	{
289 		function = function.left(function.size() - 6);
290 	}
291 
292 	// Remove the return type (if any)
293 	if (function.indexOf(' ') != -1)
294 	{
295 		function = function.mid(function.lastIndexOf(' ') + 1);
296 	}
297 
298 	// Trim function name
299 	const auto size = mFunctionFilenameSize - 3 - pFilename.size() - QString::number(pLine).size();
300 
301 	if (size >= function.size())
302 	{
303 		return function;
304 	}
305 
306 	if (size < 3)
307 	{
308 		return QByteArray();
309 	}
310 
311 	if (size < 10)
312 	{
313 		return QByteArrayLiteral("...");
314 	}
315 
316 	return QByteArrayLiteral("...") + function.right(size - 3);
317 }
318 
319 
formatCategory(const QByteArray & pCategory) const320 QByteArray LogHandler::formatCategory(const QByteArray& pCategory) const
321 {
322 	const int MAX_CATEGORY_LENGTH = 10;
323 	if (pCategory.length() > MAX_CATEGORY_LENGTH)
324 	{
325 		return pCategory.left(MAX_CATEGORY_LENGTH - 3) + QByteArrayLiteral("...");
326 	}
327 
328 	return pCategory + QByteArray(MAX_CATEGORY_LENGTH - pCategory.size(), ' ');
329 }
330 
331 
getPaddedLogMsg(const QMessageLogContext & pContext,const QString & pMsg) const332 QString LogHandler::getPaddedLogMsg(const QMessageLogContext& pContext, const QString& pMsg) const
333 {
334 	const auto paddingSize = (pContext.function == nullptr && pContext.file == nullptr && pContext.line == 0) ?
335 			mFunctionFilenameSize - 18 : // padding for nullptr == "unknown(unknown:0)"
336 			mFunctionFilenameSize - 3 - static_cast<int>(qstrlen(pContext.function)) - static_cast<int>(qstrlen(pContext.file)) - QString::number(pContext.line).size();
337 
338 	QString padding;
339 	padding.reserve(paddingSize + pMsg.size() + 3);
340 	padding.fill(QLatin1Char(' '), paddingSize);
341 	padding += QStringLiteral(": ");
342 	padding += pMsg;
343 	return padding;
344 }
345 
346 
handleMessage(QtMsgType pType,const QMessageLogContext & pContext,const QString & pMsg)347 void LogHandler::handleMessage(QtMsgType pType, const QMessageLogContext& pContext, const QString& pMsg)
348 {
349 	const QMutexLocker mutexLocker(&mMutex);
350 
351 	const QByteArray& filename = formatFilename(pContext.file);
352 	const QByteArray& function = formatFunction(pContext.function, filename, pContext.line);
353 	const QByteArray& category = formatCategory(pContext.category);
354 
355 	QMessageLogContext ctx;
356 	copyMessageLogContext(pContext, ctx, filename, function, category);
357 
358 	const QString& message = mEnvPattern ? pMsg : getPaddedLogMsg(ctx, pMsg);
359 
360 	qSetMessagePattern(mMessagePattern);
361 
362 #ifdef Q_OS_WIN
363 	const QLatin1String lineBreak("\r\n");
364 #else
365 	const QLatin1Char lineBreak('\n');
366 #endif
367 
368 	const QString logMsg = qFormatLogMessage(pType, ctx, message) + lineBreak;
369 	handleLogWindow(pType, pContext.category, logMsg);
370 	logToFile(logMsg);
371 
372 	if (Q_LIKELY(mUseHandler))
373 	{
374 #ifdef ENABLE_MESSAGE_PATTERN
375 		mHandler(pType, ctx, message);
376 #else
377 		qSetMessagePattern(mDefaultMessagePattern);
378 		mHandler(pType, ctx, pMsg);
379 #endif
380 	}
381 
382 	if (mEventHandler)
383 	{
384 		Q_EMIT mEventHandler->fireRawLog(pMsg, QString::fromLatin1(pContext.category));
385 		Q_EMIT mEventHandler->fireLog(logMsg);
386 	}
387 }
388 
389 
handleLogWindow(QtMsgType pType,const char * pCategory,const QString & pMsg)390 void LogHandler::handleLogWindow(QtMsgType pType, const char* pCategory, const QString& pMsg)
391 {
392 	if (!useLogfile())
393 	{
394 		return;
395 	}
396 
397 	if (mCriticalLog && mCriticalLogWindow.isFull())
398 	{
399 		return;
400 	}
401 	else if (pType == QtCriticalMsg && !mCriticalLogIgnore.contains(QLatin1String(pCategory)))
402 	{
403 		mCriticalLog = true;
404 	}
405 
406 	mCriticalLogWindow.append({mLogFile->pos(), pMsg.size()});
407 }
408 
409 
copy(const QString & pDest)410 bool LogHandler::copy(const QString& pDest)
411 {
412 	const QMutexLocker mutexLocker(&mMutex);
413 
414 	if (useLogfile())
415 	{
416 		return copyOther(mLogFile->fileName(), pDest);
417 	}
418 
419 	return false;
420 }
421 
422 
copyOther(const QString & pSource,const QString & pDest) const423 bool LogHandler::copyOther(const QString& pSource, const QString& pDest) const
424 {
425 	if (pDest.trimmed().isEmpty())
426 	{
427 		return false;
428 	}
429 
430 	Q_ASSERT(mLogFile);
431 	if (pSource != mLogFile->fileName() && !getOtherLogfiles().contains(QFileInfo(pSource)))
432 	{
433 		return false;
434 	}
435 
436 	if (QFile::exists(pDest) && !QFile::remove(pDest))
437 	{
438 		return false;
439 	}
440 
441 	return QFile::copy(pSource, pDest);
442 }
443 
444 
getOtherLogfiles() const445 QFileInfoList LogHandler::getOtherLogfiles() const
446 {
447 	QDir tmpPath = QDir::temp();
448 	tmpPath.setSorting(QDir::Time);
449 	tmpPath.setFilter(QDir::Files);
450 	tmpPath.setNameFilters(QStringList({QCoreApplication::applicationName() + QStringLiteral(".*.log")}));
451 
452 	QFileInfoList list = tmpPath.entryInfoList();
453 
454 	if (useLogfile())
455 	{
456 		list.removeAll(QFileInfo(*mLogFile));
457 	}
458 
459 	return list;
460 }
461 
462 
removeOldLogfiles()463 void LogHandler::removeOldLogfiles()
464 {
465 	const auto& threshold = QDateTime::currentDateTime().addDays(-14);
466 	const QFileInfoList& logfileInfos = getOtherLogfiles();
467 	for (const QFileInfo& entry : logfileInfos)
468 	{
469 		if (entry.fileTime(QFileDevice::FileModificationTime) < threshold)
470 		{
471 			const auto result = QFile::remove(entry.absoluteFilePath());
472 			qDebug() << "Auto-remove old logfile:" << entry.absoluteFilePath() << '|' << result;
473 		}
474 	}
475 }
476 
477 
removeOtherLogfiles()478 bool LogHandler::removeOtherLogfiles()
479 {
480 	const auto otherLogFiles = getOtherLogfiles();
481 	for (const auto& entry : otherLogFiles)
482 	{
483 		const auto result = QFile::remove(entry.absoluteFilePath());
484 		qDebug() << "Remove old logfile:" << entry.absoluteFilePath() << '|' << result;
485 	}
486 
487 	return !otherLogFiles.isEmpty();
488 }
489 
490 
setLogfile(bool pEnable)491 void LogHandler::setLogfile(bool pEnable)
492 {
493 	const QMutexLocker mutexLocker(&mMutex);
494 	Q_ASSERT(mLogFile);
495 
496 	if (pEnable)
497 	{
498 		if (!mLogFile->isOpen())
499 		{
500 			mLogFile->setFileTemplate(getLogFileTemplate());
501 			mLogFile->open();
502 		}
503 	}
504 	else
505 	{
506 		if (mLogFile->isOpen())
507 		{
508 			mLogFile->close();
509 			mLogFile->remove();
510 			mBacklogPosition = 0;
511 			mCriticalLog = false;
512 			mCriticalLogWindow.clear();
513 		}
514 		mLogFile->setFileTemplate(QString());
515 	}
516 }
517 
518 
useLogfile() const519 bool LogHandler::useLogfile() const
520 {
521 	return mLogFile && !mLogFile->fileTemplate().isNull();
522 }
523 
524 
setUseHandler(bool pEnable)525 void LogHandler::setUseHandler(bool pEnable)
526 {
527 	mUseHandler = pEnable;
528 }
529 
530 
useHandler() const531 bool LogHandler::useHandler() const
532 {
533 	return mUseHandler;
534 }
535 
536 
messageHandler(QtMsgType pType,const QMessageLogContext & pContext,const QString & pMsg)537 void LogHandler::messageHandler(QtMsgType pType, const QMessageLogContext& pContext, const QString& pMsg)
538 {
539 	getInstance().handleMessage(pType, pContext, pMsg);
540 }
541 
542 
543 static_assert(!std::is_base_of<QObject, LogHandler>::value,
544 		"LogHandler cannot be a QObject because it is a Singleton that should not be destroyed at all!");
545