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