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 | // 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(®istryMutex); |
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 | */ |
334 | void 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 | */ |
351 | void 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 | */ |
365 | void 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 | */ |
375 | void 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(®istryMutex); |
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 | */ |
397 | void 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 | */ |
407 | QLoggingCategory::CategoryFilter |
408 | QLoggingRegistry::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 | |
423 | QLoggingRegistry *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 | */ |
434 | void 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 | |
490 | QT_END_NAMESPACE |
491 | |