1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qloggingregistry_p.h"
5
6#include <QtCore/qfile.h>
7#include <QtCore/qlibraryinfo.h>
8#include <QtCore/private/qlocking_p.h>
9#include <QtCore/qstandardpaths.h>
10#include <QtCore/qstringtokenizer.h>
11#include <QtCore/qtextstream.h>
12#include <QtCore/qdir.h>
13#include <QtCore/qcoreapplication.h>
14
15#if QT_CONFIG(settings)
16#include <QtCore/qsettings.h>
17#include <QtCore/private/qsettings_p.h>
18#endif
19
20// We can't use the default macros because this would lead to recursion.
21// Instead let's define our own one that unconditionally logs...
22#define debugMsg QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, QT_MESSAGELOG_FUNC, "qt.core.logging").debug
23#define warnMsg QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, QT_MESSAGELOG_FUNC, "qt.core.logging").warning
24
25QT_BEGIN_NAMESPACE
26
27using namespace Qt::StringLiterals;
28
29Q_GLOBAL_STATIC(QLoggingRegistry, qtLoggingRegistry)
30
31/*!
32 \internal
33 Constructs a logging rule with default values.
34*/
35QLoggingRule::QLoggingRule()
36{
37}
38
39/*!
40 \internal
41 Constructs a logging rule.
42*/
43QLoggingRule::QLoggingRule(QStringView pattern, bool enabled) : enabled(enabled)
44{
45 parse(pattern);
46}
47
48/*!
49 \internal
50 Return value 1 means filter passed, 0 means filter doesn't influence this
51 category, -1 means category doesn't pass this filter.
52 */
53int QLoggingRule::pass(QLatin1StringView cat, QtMsgType msgType) const
54{
55 // check message type
56 if (messageType > -1 && messageType != msgType)
57 return 0;
58
59 if (flags == FullText) {
60 // full match
61 if (category == cat)
62 return (enabled ? 1 : -1);
63 else
64 return 0;
65 }
66
67 const qsizetype idx = cat.indexOf(s: category);
68 if (idx >= 0) {
69 if (flags == MidFilter) {
70 // matches somewhere
71 return (enabled ? 1 : -1);
72 } else if (flags == LeftFilter) {
73 // matches left
74 if (idx == 0)
75 return (enabled ? 1 : -1);
76 } else if (flags == RightFilter) {
77 // matches right
78 if (idx == (cat.size() - category.size()))
79 return (enabled ? 1 : -1);
80 }
81 }
82 return 0;
83}
84
85/*!
86 \internal
87 Parses \a pattern.
88 Allowed is f.ex.:
89 qt.core.io.debug FullText, QtDebugMsg
90 qt.core.* LeftFilter, all types
91 *.io.warning RightFilter, QtWarningMsg
92 *.core.* MidFilter
93 */
94void QLoggingRule::parse(QStringView pattern)
95{
96 QStringView p;
97
98 // strip trailing ".messagetype"
99 if (pattern.endsWith(s: ".debug"_L1)) {
100 p = pattern.chopped(n: 6); // strlen(".debug")
101 messageType = QtDebugMsg;
102 } else if (pattern.endsWith(s: ".info"_L1)) {
103 p = pattern.chopped(n: 5); // strlen(".info")
104 messageType = QtInfoMsg;
105 } else if (pattern.endsWith(s: ".warning"_L1)) {
106 p = pattern.chopped(n: 8); // strlen(".warning")
107 messageType = QtWarningMsg;
108 } else if (pattern.endsWith(s: ".critical"_L1)) {
109 p = pattern.chopped(n: 9); // strlen(".critical")
110 messageType = QtCriticalMsg;
111 } else {
112 p = pattern;
113 }
114
115 const QChar asterisk = u'*';
116 if (!p.contains(c: asterisk)) {
117 flags = FullText;
118 } else {
119 if (p.endsWith(c: asterisk)) {
120 flags |= LeftFilter;
121 p = p.chopped(n: 1);
122 }
123 if (p.startsWith(c: asterisk)) {
124 flags |= RightFilter;
125 p = p.mid(pos: 1);
126 }
127 if (p.contains(c: asterisk)) // '*' only supported at start/end
128 flags = PatternFlags();
129 }
130
131 category = p.toString();
132}
133
134/*!
135 \class QLoggingSettingsParser
136 \since 5.3
137 \internal
138
139 Parses a .ini file with the following format:
140
141 [rules]
142 rule1=[true|false]
143 rule2=[true|false]
144 ...
145
146 [rules] is the default section, and therefore optional.
147*/
148
149/*!
150 \internal
151 Parses configuration from \a content.
152*/
153void QLoggingSettingsParser::setContent(QStringView content)
154{
155 _rules.clear();
156 for (auto line : qTokenize(h&: content, n: u'\n'))
157 parseNextLine(line);
158}
159
160/*!
161 \internal
162 Parses configuration from \a stream.
163*/
164void QLoggingSettingsParser::setContent(QTextStream &stream)
165{
166 _rules.clear();
167 QString line;
168 while (stream.readLineInto(line: &line))
169 parseNextLine(line: qToStringViewIgnoringNull(s: line));
170}
171
172/*!
173 \internal
174 Parses one line of the configuration file
175*/
176
177void QLoggingSettingsParser::parseNextLine(QStringView line)
178{
179 // Remove whitespace at start and end of line:
180 line = line.trimmed();
181
182 // comment
183 if (line.startsWith(c: u';'))
184 return;
185
186 if (line.startsWith(c: u'[') && line.endsWith(c: u']')) {
187 // new section
188 auto sectionName = line.mid(pos: 1).chopped(n: 1).trimmed();
189 m_inRulesSection = sectionName.compare(s: "rules"_L1, cs: Qt::CaseInsensitive) == 0;
190 return;
191 }
192
193 if (m_inRulesSection) {
194 const qsizetype equalPos = line.indexOf(c: u'=');
195 if (equalPos != -1) {
196 if (line.lastIndexOf(c: u'=') == equalPos) {
197 const auto key = line.left(n: equalPos).trimmed();
198#if QT_CONFIG(settings)
199 QString tmp;
200 QSettingsPrivate::iniUnescapedKey(key: key.toUtf8(), result&: tmp);
201 QStringView pattern = qToStringViewIgnoringNull(s: tmp);
202#else
203 QStringView pattern = key;
204#endif
205 const auto valueStr = line.mid(pos: equalPos + 1).trimmed();
206 int value = -1;
207 if (valueStr == "true"_L1)
208 value = 1;
209 else if (valueStr == "false"_L1)
210 value = 0;
211 QLoggingRule rule(pattern, (value == 1));
212 if (rule.flags != 0 && (value != -1))
213 _rules.append(t: std::move(rule));
214 else
215 warnMsg(msg: "Ignoring malformed logging rule: '%s'", line.toUtf8().constData());
216 } else {
217 warnMsg(msg: "Ignoring malformed logging rule: '%s'", line.toUtf8().constData());
218 }
219 }
220 }
221}
222
223/*!
224 \internal
225 QLoggingRegistry constructor
226 */
227QLoggingRegistry::QLoggingRegistry()
228 : categoryFilter(defaultCategoryFilter)
229{
230#if defined(Q_OS_ANDROID)
231 // Unless QCoreApplication has been constructed we can't be sure that
232 // we are on Qt's main thread. If we did allow logging here, we would
233 // potentially set Qt's main thread to Android's thread 0, which would
234 // confuse Qt later when running main().
235 if (!qApp)
236 return;
237#endif
238
239 initializeRules(); // Init on first use
240}
241
242static bool qtLoggingDebug()
243{
244 static const bool debugEnv = [] {
245 bool debug = qEnvironmentVariableIsSet(varName: "QT_LOGGING_DEBUG");
246 if (debug)
247 debugMsg(msg: "QT_LOGGING_DEBUG environment variable is set.");
248 return debug;
249 }();
250 return debugEnv;
251}
252
253static QList<QLoggingRule> loadRulesFromFile(const QString &filePath)
254{
255 if (qtLoggingDebug()) {
256 debugMsg(msg: "Checking \"%s\" for rules",
257 QDir::toNativeSeparators(pathName: filePath).toUtf8().constData());
258 }
259
260 QFile file(filePath);
261 if (file.open(flags: QIODevice::ReadOnly | QIODevice::Text)) {
262 QTextStream stream(&file);
263 QLoggingSettingsParser parser;
264 parser.setContent(stream);
265 if (qtLoggingDebug())
266 debugMsg(msg: "Loaded %td rules", static_cast<ptrdiff_t>(parser.rules().size()));
267 return parser.rules();
268 }
269 return QList<QLoggingRule>();
270}
271
272/*!
273 \internal
274 Initializes the rules database by loading
275 $QT_LOGGING_CONF, $QT_LOGGING_RULES, and .config/QtProject/qtlogging.ini.
276 */
277void QLoggingRegistry::initializeRules()
278{
279 if (qtLoggingDebug()) {
280 debugMsg(msg: "Initializing the rules database ...");
281 debugMsg(msg: "Checking %s environment variable", "QT_LOGGING_CONF");
282 }
283 QList<QLoggingRule> er, qr, cr;
284 // get rules from environment
285 const QByteArray rulesFilePath = qgetenv(varName: "QT_LOGGING_CONF");
286 if (!rulesFilePath.isEmpty())
287 er = loadRulesFromFile(filePath: QFile::decodeName(localFileName: rulesFilePath));
288
289 if (qtLoggingDebug())
290 debugMsg(msg: "Checking %s environment variable", "QT_LOGGING_RULES");
291
292 const QByteArray rulesSrc = qgetenv(varName: "QT_LOGGING_RULES").replace(before: ';', after: '\n');
293 if (!rulesSrc.isEmpty()) {
294 QTextStream stream(rulesSrc);
295 QLoggingSettingsParser parser;
296 parser.setImplicitRulesSection(true);
297 parser.setContent(stream);
298
299 if (qtLoggingDebug())
300 debugMsg(msg: "Loaded %td rules", static_cast<ptrdiff_t>(parser.rules().size()));
301
302 er += parser.rules();
303 }
304
305 const QString configFileName = QStringLiteral("qtlogging.ini");
306
307 // get rules from Qt data configuration path
308 const QString qtConfigPath
309 = QDir(QLibraryInfo::path(p: QLibraryInfo::DataPath)).absoluteFilePath(fileName: configFileName);
310 qr = loadRulesFromFile(filePath: qtConfigPath);
311
312 // get rules from user's/system configuration
313 const QString envPath = QStandardPaths::locate(type: QStandardPaths::GenericConfigLocation,
314 fileName: QString::fromLatin1(ba: "QtProject/") + configFileName);
315 if (!envPath.isEmpty())
316 cr = loadRulesFromFile(filePath: envPath);
317
318 const QMutexLocker locker(&registryMutex);
319
320 ruleSets[EnvironmentRules] = std::move(er);
321 ruleSets[QtConfigRules] = std::move(qr);
322 ruleSets[ConfigRules] = std::move(cr);
323
324 if (!ruleSets[EnvironmentRules].isEmpty() || !ruleSets[QtConfigRules].isEmpty() || !ruleSets[ConfigRules].isEmpty())
325 updateRules();
326}
327
328/*!
329 \internal
330 Registers a category object.
331
332 This method might be called concurrently for the same category object.
333*/
334void QLoggingRegistry::registerCategory(QLoggingCategory *cat, QtMsgType enableForLevel)
335{
336 const auto locker = qt_scoped_lock(mutex&: registryMutex);
337
338 const auto oldSize = categories.size();
339 auto &e = categories[cat];
340 if (categories.size() != oldSize) {
341 // new entry
342 e = enableForLevel;
343 (*categoryFilter)(cat);
344 }
345}
346
347/*!
348 \internal
349 Unregisters a category object.
350*/
351void QLoggingRegistry::unregisterCategory(QLoggingCategory *cat)
352{
353 const auto locker = qt_scoped_lock(mutex&: registryMutex);
354 categories.remove(key: cat);
355}
356
357/*!
358 \since 6.3
359 \internal
360
361 Registers the environment variable \a environment as the control variable
362 for enabling debugging by default for category \a categoryName. The
363 category name must start with "qt."
364*/
365void QLoggingRegistry::registerEnvironmentOverrideForCategory(const char *categoryName,
366 const char *environment)
367{
368 qtCategoryEnvironmentOverrides.insert_or_assign(k: categoryName, obj&: environment);
369}
370
371/*!
372 \internal
373 Installs logging rules as specified in \a content.
374 */
375void QLoggingRegistry::setApiRules(const QString &content)
376{
377 QLoggingSettingsParser parser;
378 parser.setImplicitRulesSection(true);
379 parser.setContent(content);
380
381 if (qtLoggingDebug())
382 debugMsg(msg: "Loading logging rules set by QLoggingCategory::setFilterRules ...");
383
384 const QMutexLocker locker(&registryMutex);
385
386 ruleSets[ApiRules] = parser.rules();
387
388 updateRules();
389}
390
391/*!
392 \internal
393 Activates a new set of logging rules for the default filter.
394
395 (The caller must lock registryMutex to make sure the API is thread safe.)
396*/
397void QLoggingRegistry::updateRules()
398{
399 for (auto it = categories.keyBegin(), end = categories.keyEnd(); it != end; ++it)
400 (*categoryFilter)(*it);
401}
402
403/*!
404 \internal
405 Installs a custom filter rule.
406*/
407QLoggingCategory::CategoryFilter
408QLoggingRegistry::installFilter(QLoggingCategory::CategoryFilter filter)
409{
410 const auto locker = qt_scoped_lock(mutex&: registryMutex);
411
412 if (!filter)
413 filter = defaultCategoryFilter;
414
415 QLoggingCategory::CategoryFilter old = categoryFilter;
416 categoryFilter = filter;
417
418 updateRules();
419
420 return old;
421}
422
423QLoggingRegistry *QLoggingRegistry::instance()
424{
425 return qtLoggingRegistry();
426}
427
428/*!
429 \internal
430 Updates category settings according to rules.
431
432 As a category filter, it is run with registryMutex held.
433*/
434void QLoggingRegistry::defaultCategoryFilter(QLoggingCategory *cat)
435{
436 const QLoggingRegistry *reg = QLoggingRegistry::instance();
437 Q_ASSERT(reg->categories.contains(cat));
438 QtMsgType enableForLevel = reg->categories.value(key: cat);
439
440 // NB: note that the numeric values of the Qt*Msg constants are
441 // not in severity order.
442 bool debug = (enableForLevel == QtDebugMsg);
443 bool info = debug || (enableForLevel == QtInfoMsg);
444 bool warning = info || (enableForLevel == QtWarningMsg);
445 bool critical = warning || (enableForLevel == QtCriticalMsg);
446
447 // hard-wired implementation of
448 // qt.*.debug=false
449 // qt.debug=false
450 if (const char *categoryName = cat->categoryName()) {
451 // == "qt" or startsWith("qt.")
452 if (strcmp(s1: categoryName, s2: "qt") == 0) {
453 debug = false;
454 } else if (strncmp(s1: categoryName, s2: "qt.", n: 3) == 0) {
455 // may be overridden
456 auto it = reg->qtCategoryEnvironmentOverrides.find(x: categoryName);
457 if (it == reg->qtCategoryEnvironmentOverrides.end())
458 debug = false;
459 else
460 debug = qEnvironmentVariableIntValue(varName: it->second);
461 }
462 }
463
464 const auto categoryName = QLatin1StringView(cat->categoryName());
465
466 for (const auto &ruleSet : reg->ruleSets) {
467 for (const auto &rule : ruleSet) {
468 int filterpass = rule.pass(cat: categoryName, msgType: QtDebugMsg);
469 if (filterpass != 0)
470 debug = (filterpass > 0);
471 filterpass = rule.pass(cat: categoryName, msgType: QtInfoMsg);
472 if (filterpass != 0)
473 info = (filterpass > 0);
474 filterpass = rule.pass(cat: categoryName, msgType: QtWarningMsg);
475 if (filterpass != 0)
476 warning = (filterpass > 0);
477 filterpass = rule.pass(cat: categoryName, msgType: QtCriticalMsg);
478 if (filterpass != 0)
479 critical = (filterpass > 0);
480 }
481 }
482
483 cat->setEnabled(type: QtDebugMsg, enable: debug);
484 cat->setEnabled(type: QtInfoMsg, enable: info);
485 cat->setEnabled(type: QtWarningMsg, enable: warning);
486 cat->setEnabled(type: QtCriticalMsg, enable: critical);
487}
488
489
490QT_END_NAMESPACE
491

source code of qtbase/src/corelib/io/qloggingregistry.cpp