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 | |
25 | QT_BEGIN_NAMESPACE |
26 | |
27 | using namespace Qt::StringLiterals; |
28 | |
29 | Q_GLOBAL_STATIC(QLoggingRegistry, qtLoggingRegistry) |
30 | |
31 | /*! |
32 | \internal |
33 | Constructs a logging rule with default values. |
34 | */ |
35 | QLoggingRule::QLoggingRule() |
36 | { |
37 | } |
38 | |
39 | /*! |
40 | \internal |
41 | Constructs a logging rule. |
42 | */ |
43 | QLoggingRule::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 | */ |
53 | int 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 | */ |
94 | void 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 | */ |
153 | void 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 | */ |
164 | void 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 | |
177 | void 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 | */ |
227 | QLoggingRegistry::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 | |
242 | static 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 | |
253 | static 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 | */ |
277 | void 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 | #if !defined(QT_BOOTSTRAPPED) |
308 | // get rules from Qt data configuration path |
309 | const QString qtConfigPath |
310 | = QDir(QLibraryInfo::path(p: QLibraryInfo::DataPath)).absoluteFilePath(fileName: configFileName); |
311 | qr = loadRulesFromFile(filePath: qtConfigPath); |
312 | #endif |
313 | |
314 | // get rules from user's/system configuration |
315 | const QString envPath = QStandardPaths::locate(type: QStandardPaths::GenericConfigLocation, |
316 | fileName: QString::fromLatin1(ba: "QtProject/" ) + configFileName); |
317 | if (!envPath.isEmpty()) |
318 | cr = loadRulesFromFile(filePath: envPath); |
319 | |
320 | const QMutexLocker locker(®istryMutex); |
321 | |
322 | ruleSets[EnvironmentRules] = std::move(er); |
323 | ruleSets[QtConfigRules] = std::move(qr); |
324 | ruleSets[ConfigRules] = std::move(cr); |
325 | |
326 | if (!ruleSets[EnvironmentRules].isEmpty() || !ruleSets[QtConfigRules].isEmpty() || !ruleSets[ConfigRules].isEmpty()) |
327 | updateRules(); |
328 | } |
329 | |
330 | /*! |
331 | \internal |
332 | Registers a category object. |
333 | |
334 | This method might be called concurrently for the same category object. |
335 | */ |
336 | void QLoggingRegistry::registerCategory(QLoggingCategory *cat, QtMsgType enableForLevel) |
337 | { |
338 | const auto locker = qt_scoped_lock(mutex&: registryMutex); |
339 | |
340 | const auto oldSize = categories.size(); |
341 | auto &e = categories[cat]; |
342 | if (categories.size() != oldSize) { |
343 | // new entry |
344 | e = enableForLevel; |
345 | (*categoryFilter)(cat); |
346 | } |
347 | } |
348 | |
349 | /*! |
350 | \internal |
351 | Unregisters a category object. |
352 | */ |
353 | void QLoggingRegistry::unregisterCategory(QLoggingCategory *cat) |
354 | { |
355 | const auto locker = qt_scoped_lock(mutex&: registryMutex); |
356 | categories.remove(key: cat); |
357 | } |
358 | |
359 | /*! |
360 | \since 6.3 |
361 | \internal |
362 | |
363 | Registers the environment variable \a environment as the control variable |
364 | for enabling debugging by default for category \a categoryName. The |
365 | category name must start with "qt." |
366 | */ |
367 | void QLoggingRegistry::registerEnvironmentOverrideForCategory(const char *categoryName, |
368 | const char *environment) |
369 | { |
370 | qtCategoryEnvironmentOverrides.insert_or_assign(k: categoryName, obj&: environment); |
371 | } |
372 | |
373 | /*! |
374 | \internal |
375 | Installs logging rules as specified in \a content. |
376 | */ |
377 | void QLoggingRegistry::setApiRules(const QString &content) |
378 | { |
379 | QLoggingSettingsParser parser; |
380 | parser.setImplicitRulesSection(true); |
381 | parser.setContent(content); |
382 | |
383 | if (qtLoggingDebug()) |
384 | debugMsg(msg: "Loading logging rules set by QLoggingCategory::setFilterRules ..." ); |
385 | |
386 | const QMutexLocker locker(®istryMutex); |
387 | |
388 | ruleSets[ApiRules] = parser.rules(); |
389 | |
390 | updateRules(); |
391 | } |
392 | |
393 | /*! |
394 | \internal |
395 | Activates a new set of logging rules for the default filter. |
396 | |
397 | (The caller must lock registryMutex to make sure the API is thread safe.) |
398 | */ |
399 | void QLoggingRegistry::updateRules() |
400 | { |
401 | for (auto it = categories.keyBegin(), end = categories.keyEnd(); it != end; ++it) |
402 | (*categoryFilter)(*it); |
403 | } |
404 | |
405 | /*! |
406 | \internal |
407 | Installs a custom filter rule. |
408 | */ |
409 | QLoggingCategory::CategoryFilter |
410 | QLoggingRegistry::installFilter(QLoggingCategory::CategoryFilter filter) |
411 | { |
412 | const auto locker = qt_scoped_lock(mutex&: registryMutex); |
413 | |
414 | if (!filter) |
415 | filter = defaultCategoryFilter; |
416 | |
417 | QLoggingCategory::CategoryFilter old = categoryFilter; |
418 | categoryFilter = filter; |
419 | |
420 | updateRules(); |
421 | |
422 | return old; |
423 | } |
424 | |
425 | QLoggingRegistry *QLoggingRegistry::instance() |
426 | { |
427 | return qtLoggingRegistry(); |
428 | } |
429 | |
430 | /*! |
431 | \internal |
432 | Updates category settings according to rules. |
433 | |
434 | As a category filter, it is run with registryMutex held. |
435 | */ |
436 | void QLoggingRegistry::defaultCategoryFilter(QLoggingCategory *cat) |
437 | { |
438 | const QLoggingRegistry *reg = QLoggingRegistry::instance(); |
439 | Q_ASSERT(reg->categories.contains(cat)); |
440 | QtMsgType enableForLevel = reg->categories.value(key: cat); |
441 | |
442 | // NB: note that the numeric values of the Qt*Msg constants are |
443 | // not in severity order. |
444 | bool debug = (enableForLevel == QtDebugMsg); |
445 | bool info = debug || (enableForLevel == QtInfoMsg); |
446 | bool warning = info || (enableForLevel == QtWarningMsg); |
447 | bool critical = warning || (enableForLevel == QtCriticalMsg); |
448 | |
449 | // hard-wired implementation of |
450 | // qt.*.debug=false |
451 | // qt.debug=false |
452 | if (const char *categoryName = cat->categoryName()) { |
453 | // == "qt" or startsWith("qt.") |
454 | if (strcmp(s1: categoryName, s2: "qt" ) == 0) { |
455 | debug = false; |
456 | } else if (strncmp(s1: categoryName, s2: "qt." , n: 3) == 0) { |
457 | // may be overridden |
458 | auto it = reg->qtCategoryEnvironmentOverrides.find(x: categoryName); |
459 | if (it == reg->qtCategoryEnvironmentOverrides.end()) |
460 | debug = false; |
461 | else |
462 | debug = qEnvironmentVariableIntValue(varName: it->second); |
463 | } |
464 | } |
465 | |
466 | const auto categoryName = QLatin1StringView(cat->categoryName()); |
467 | |
468 | for (const auto &ruleSet : reg->ruleSets) { |
469 | for (const auto &rule : ruleSet) { |
470 | int filterpass = rule.pass(cat: categoryName, msgType: QtDebugMsg); |
471 | if (filterpass != 0) |
472 | debug = (filterpass > 0); |
473 | filterpass = rule.pass(cat: categoryName, msgType: QtInfoMsg); |
474 | if (filterpass != 0) |
475 | info = (filterpass > 0); |
476 | filterpass = rule.pass(cat: categoryName, msgType: QtWarningMsg); |
477 | if (filterpass != 0) |
478 | warning = (filterpass > 0); |
479 | filterpass = rule.pass(cat: categoryName, msgType: QtCriticalMsg); |
480 | if (filterpass != 0) |
481 | critical = (filterpass > 0); |
482 | } |
483 | } |
484 | |
485 | cat->setEnabled(type: QtDebugMsg, enable: debug); |
486 | cat->setEnabled(type: QtInfoMsg, enable: info); |
487 | cat->setEnabled(type: QtWarningMsg, enable: warning); |
488 | cat->setEnabled(type: QtCriticalMsg, enable: critical); |
489 | } |
490 | |
491 | |
492 | QT_END_NAMESPACE |
493 | |