swift
loghandler.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2014 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
5 
6 #include "misc/loghandler.h"
7 
8 #include "config/buildconfig.h"
9 #include "misc/algorithm.h"
11 #include "misc/threadutils.h"
12 
13 #ifdef SWIFT_USE_CRASHPAD
14 # include "crashpad/client/simulate_crash.h"
15 #endif
16 
17 #include <algorithm>
18 
19 #include <QAbstractNativeEventFilter>
20 #include <QCoreApplication>
21 #include <QGlobalStatic>
22 #include <QMessageLogContext>
23 #include <QMetaMethod>
24 #include <QString>
25 #include <Qt>
26 #include <QtDebug>
27 
28 #ifdef Q_OS_WIN
29 # include <windows.h>
30 #endif
31 
32 namespace swift::misc
33 {
34  Q_GLOBAL_STATIC(CLogHandler, g_handler)
35 
36  CLogHandler *CLogHandler::instance()
37  {
38  Q_ASSERT(!g_handler.isDestroyed());
39  return g_handler;
40  }
41 
43  void messageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message)
44  {
45  if (type == QtFatalMsg && CLogHandler::instance()->thread() != QThread::currentThread())
46  {
47  // Fatal message means this thread is about to crash the application. A queued connection would be useless.
48  // Blocking queued connection means we pause this thread just long enough to let the main thread handle the
49  // message.
50  QMetaObject::invokeMethod(
51  CLogHandler::instance(), [&] { messageHandler(type, context, message); }, Qt::BlockingQueuedConnection);
52  return;
53  }
54 #if defined(Q_CC_MSVC) && defined(QT_NO_DEBUG)
55  if (type == QtFatalMsg)
56  {
57  struct EventFilter : public QAbstractNativeEventFilter
58  {
59  // Prevent Qt from handling Windows Messages while the messagebox is open
60  virtual bool nativeEventFilter(const QByteArray &, void *message, qintptr *result) override
61  {
62  auto msg = static_cast<MSG *>(message);
63  *result = DefWindowProc(msg->hwnd, msg->message, msg->wParam, msg->lParam);
64  return true;
65  }
66  };
67  EventFilter ef;
68  qApp->installNativeEventFilter(&ef);
69  MessageBoxW(nullptr, message.toStdWString().c_str(), nullptr,
70  MB_OK); // display assert dialog in release build
71  qApp->removeNativeEventFilter(&ef);
72 # if defined(SWIFT_USE_CRASHPAD)
73  CRASHPAD_SIMULATE_CRASH(); // workaround inability to catch __fastfail
74 # endif
75  }
76 #endif
77  QMetaObject::invokeMethod(CLogHandler::instance(), [statusMessage = CStatusMessage(type, context, message)] {
78  CLogHandler::instance()->logLocalMessage(statusMessage);
79  });
80  }
81 
82  void CLogHandler::install(bool skipIfAlreadyInstalled)
83  {
84  if (skipIfAlreadyInstalled && m_oldHandler) { return; }
85  Q_ASSERT_X(!m_oldHandler, Q_FUNC_INFO, "Re-installing the log handler should be avoided");
86  m_oldHandler = qInstallMessageHandler(messageHandler);
87  }
88 
89  CLogHandler::CLogHandler()
90  {
91  // in case the first call to instance() is in a different thread
92  moveToThread(QCoreApplication::instance()->thread());
93  }
94 
95  CLogHandler::~CLogHandler() { qInstallMessageHandler(m_oldHandler); }
96 
97  CLogPatternHandler *CLogHandler::handlerForPattern(const CLogPattern &pattern)
98  {
99  Q_ASSERT(thread() == QThread::currentThread());
100 
101  auto finder = [&](const PatternPair &pair) { return pair.first == pattern; };
102  auto comparator = [](const PatternPair &a, const PatternPair &b) { return a.first.isProperSubsetOf(b.first); };
103 
104  auto it = std::find_if(m_patternHandlers.cbegin(), m_patternHandlers.cend(), finder);
105  if (it == m_patternHandlers.cend())
106  {
107  auto *handler = new CLogPatternHandler(this, pattern);
108  topologicallySortedInsert(m_patternHandlers, PatternPair(pattern, handler), comparator);
109  return handler;
110  }
111  else { return (*it).second; }
112  }
113 
114  CLogPatternHandler *CLogHandler::handlerForCategory(const CLogCategory &category)
115  {
116  return handlerForPattern(CLogPattern::exactMatch(category));
117  }
118 
119  QList<CLogPatternHandler *> CLogHandler::handlersForMessage(const CStatusMessage &message) const
120  {
121  QList<CLogPatternHandler *> m_handlers;
122  for (const auto &pair : m_patternHandlers)
123  {
124  if (pair.first.match(message)) { m_handlers.push_back(pair.second); }
125  }
126  return m_handlers;
127  }
128 
129  bool CLogHandler::isFallThroughEnabled(const QList<CLogPatternHandler *> &handlers) const
130  {
131  for (const auto *handler : handlers)
132  {
133  if (!handler->m_inheritFallThrough) { return handler->m_enableFallThrough; }
134  }
135  return m_enableFallThrough;
136  }
137 
138  void CLogHandler::logLocalMessage(const CStatusMessage &i_statusMessage)
139  {
140  using namespace swift::config;
141  CStatusMessage statusMessage = i_statusMessage;
142  if (!CBuildConfig::isLocalDeveloperDebugBuild() &&
143  CLogPattern::empty().withSeverity(CStatusMessage::SeverityError).match(statusMessage))
144  {
145  // 99% this is a complex Qt implementation warning generated by qErrnoWarning, so downgrade its severity
146  statusMessage.setSeverity(CStatusMessage::SeverityDebug);
147  }
148 
149  if (!CBuildConfig::isLocalDeveloperDebugBuild() &&
150  CLogPattern::exactMatch("default").withSeverity(CStatusMessage::SeverityWarning).match(statusMessage))
151  {
152  // All Qt warnings
153 
154  // demoted because caused by airline icons, we would need to re-create literally dozens of these images
155  if (statusMessage.getMessage().startsWith(QStringLiteral("libpng warning")))
156  {
157  statusMessage.setSeverity(CStatusMessage::SeverityDebug);
158  }
159 
160  // demoted, because in some swift APPs some options can be ignored
161  else if (statusMessage.getMessage().startsWith(QStringLiteral("QCommandLineParser: option not defined")))
162  {
163  statusMessage.setSeverity(CStatusMessage::SeverityDebug);
164  }
165  }
166 
167  auto bucket = m_tokenBuckets.find(statusMessage);
168  if (bucket == m_tokenBuckets.end()) { bucket = m_tokenBuckets.insert(statusMessage, { { 5, 1000, 1 }, 0 }); }
169  if (!bucket->first.tryConsume())
170  {
171  bucket->second++;
172  return;
173  }
174  if (bucket->second > 0)
175  {
176  auto copy = statusMessage;
177  copy.appendMessage(QStringLiteral(" (+%1 identical messages)").arg(bucket->second));
178  bucket->second = 0;
179 
180  logMessage(copy);
181  emit localMessageLogged(copy);
182  return;
183  }
184 
185  logMessage(statusMessage);
186  emit localMessageLogged(statusMessage);
187  }
188 
189  void CLogHandler::logRemoteMessage(const CStatusMessage &statusMessage)
190  {
191  logMessage(statusMessage);
192  emit remoteMessageLogged(statusMessage);
193  }
194 
196  {
197  Q_ASSERT_X(m_oldHandler, Q_FUNC_INFO, "Install the log handler before using it");
198  Q_ASSERT_X(thread() == QThread::currentThread(), Q_FUNC_INFO, "Wrong thread");
199  m_enableFallThrough = enable;
200  }
201 
202  void CLogHandler::logMessage(const CStatusMessage &statusMessage)
203  {
204  const auto handlers = handlersForMessage(statusMessage);
205 
206  if (isFallThroughEnabled(handlers))
207  {
208  Q_ASSERT_X(m_oldHandler, Q_FUNC_INFO, "Handler must be installed");
209  QtMsgType type;
210  QString category;
211  QString message;
212  statusMessage.toQtLogTriple(&type, &category, &message);
213  m_oldHandler(type, QMessageLogContext(nullptr, 0, nullptr, qPrintable(category)), message);
214  }
215 
216  for (auto *handler : handlers) { emit handler->messageLogged(statusMessage); }
217  }
218 
219  void CLogHandler::removePatternHandler(CLogPatternHandler *handler)
220  {
221  auto it = std::find_if(m_patternHandlers.begin(), m_patternHandlers.end(),
222  [handler](const PatternPair &pair) { return pair.second == handler; });
223  if (it != m_patternHandlers.end())
224  {
225  it->second->deleteLater();
226  m_patternHandlers.erase(it);
227  }
228  }
229 
230  QList<CLogPattern> CLogHandler::getAllSubscriptions() const
231  {
232  QList<CLogPattern> result;
233  for (const auto &pair : m_patternHandlers)
234  {
235  if (pair.second->isSignalConnected(QMetaMethod::fromSignal(&CLogPatternHandler::messageLogged)))
236  {
237  result.push_back(pair.first);
238  }
239  }
240  return result;
241  }
242 
243  CLogPatternHandler::CLogPatternHandler(CLogHandler *parent, const CLogPattern &pattern)
244  : QObject(parent), m_parent(parent), m_pattern(pattern)
245  {
246  connect(&m_subscriptionUpdateTimer, &QTimer::timeout, this, &CLogPatternHandler::updateSubscription);
247  m_subscriptionUpdateTimer.start(1);
248  }
249 
250  void CLogPatternHandler::updateSubscription()
251  {
252  if (m_subscriptionNeedsUpdate)
253  {
254  m_subscriptionNeedsUpdate = false;
255  bool isSubscribed = isSignalConnected(QMetaMethod::fromSignal(&CLogPatternHandler::messageLogged));
256 
257  if (isSubscribed != m_isSubscribed)
258  {
259  m_isSubscribed = isSubscribed;
260  if (m_isSubscribed) { emit m_parent->subscriptionAdded(m_pattern); }
261  else { emit m_parent->subscriptionRemoved(m_pattern); }
262  }
263 
264  if (m_inheritFallThrough && !m_isSubscribed) { m_parent->removePatternHandler(this); }
265  }
266  }
267 
268  void CLogSubscriber::changeSubscription(const CLogPattern &pattern)
269  {
270  if (CLogHandler::instance()->thread() != QThread::currentThread())
271  {
272  Q_ASSERT(thread() == QThread::currentThread());
273  singleShot(0, CLogHandler::instance(), [pattern, self = QPointer<CLogSubscriber>(this)]() {
274  if (self) { self->changeSubscription(pattern); }
275  });
276  return;
277  }
278 
279  unsubscribe();
280  m_handler = CLogHandler::instance()->handlerForPattern(pattern);
281 
282  if (!m_inheritFallThrough) { m_handler->enableConsoleOutput(m_enableFallThrough); }
283  connect(m_handler.data(), &CLogPatternHandler::messageLogged, this, &CLogSubscriber::ps_logMessage,
284  Qt::DirectConnection);
285  }
286 
287  void CLogSubscriber::unsubscribe()
288  {
289  if (CLogHandler::instance()->thread() != QThread::currentThread())
290  {
291  Q_ASSERT(thread() == QThread::currentThread());
292  singleShot(0, CLogHandler::instance(), [self = QPointer<CLogSubscriber>(this)]() {
293  if (self) { self->unsubscribe(); }
294  });
295  return;
296  }
297 
298  if (m_handler)
299  {
300  if (!m_inheritFallThrough) { m_handler->inheritConsoleOutput(); }
301  m_handler->disconnect(this);
302  }
303  }
304 
305  void CLogSubscriber::inheritConsoleOutput()
306  {
307  if (CLogHandler::instance()->thread() != QThread::currentThread())
308  {
309  Q_ASSERT(thread() == QThread::currentThread());
310  singleShot(0, CLogHandler::instance(), [self = QPointer<CLogSubscriber>(this)]() {
311  if (self) { self->inheritConsoleOutput(); }
312  });
313  return;
314  }
315 
316  m_inheritFallThrough = true;
317  if (m_handler) { m_handler->inheritConsoleOutput(); }
318  }
319 
320  void CLogSubscriber::enableConsoleOutput(bool enable)
321  {
322  if (CLogHandler::instance()->thread() != QThread::currentThread())
323  {
324  Q_ASSERT(thread() == QThread::currentThread());
325  singleShot(0, CLogHandler::instance(), [enable, self = QPointer<CLogSubscriber>(this)]() {
326  if (self) { self->enableConsoleOutput(enable); }
327  });
328  return;
329  }
330 
331  m_inheritFallThrough = false;
332  m_enableFallThrough = enable;
333  if (m_handler) { m_handler->enableConsoleOutput(enable); }
334  }
335 } // namespace swift::misc
336 
void localMessageLogged(const swift::misc::CStatusMessage &message)
Emitted when a message is logged in this process.
void enableConsoleOutput(bool enable)
Enable or disable the default Qt handler.
QList< CLogPattern > getAllSubscriptions() const
Returns all log patterns for which there are currently subscribed log pattern handlers.
static CLogHandler * instance()
Return pointer to the CLogHandler singleton.
CLogPatternHandler * handlerForCategory(const CLogCategory &category)
Return a pattern handler for subscribing to all messages which contain the given category.
CLogPatternHandler * handlerForPattern(const CLogPattern &pattern)
Return a pattern handler for subscribing to all messages which match the given pattern.
void logLocalMessage(const swift::misc::CStatusMessage &message)
Called by our QtMessageHandler to log a message.
void logRemoteMessage(const swift::misc::CStatusMessage &message)
Called by the context to relay a message.
void remoteMessageLogged(const swift::misc::CStatusMessage &message)
Emitted when a log message is relayed from a different process.
void install(bool skipIfAlreadyInstalled=false)
Tell the CLogHandler to install itself with qInstallMessageHandler.
void messageLogged(const swift::misc::CStatusMessage &message)
Emitted when a message is logged which matches the relevant pattern.
static CLogPattern exactMatch(const CLogCategory &category)
Returns a CLogPattern which will match any message with the given category.
Definition: logpattern.cpp:101
static CLogPattern empty()
Returns a CLogPattern which will match any message without a category.
Definition: logpattern.cpp:128
constexpr static auto SeverityDebug
Status severities.
constexpr static auto SeverityError
Status severities.
constexpr static auto SeverityWarning
Status severities.
Free functions in swift::misc.
void topologicallySortedInsert(C &container, T &&value, F comparator)
Insert an element into a sequential container while preserving the topological ordering of the contai...
Definition: algorithm.h:142
auto singleShot(int msec, QObject *target, F &&task)
Starts a single-shot timer which will call a task in the thread of the given object when it times out...
Definition: threadutils.h:30