File indexing completed on 2024-11-24 04:53:03
0001 /* Copyright (C) 2006 - 2014 Jan Kundrát <jkt@flaska.net> 0002 0003 This file is part of the Trojita Qt IMAP e-mail client, 0004 http://trojita.flaska.net/ 0005 0006 This program is free software; you can redistribute it and/or 0007 modify it under the terms of the GNU General Public License as 0008 published by the Free Software Foundation; either version 2 of 0009 the License or (at your option) version 3 or any later version 0010 accepted by the membership of KDE e.V. (or its successor approved 0011 by the membership of KDE e.V.), which shall act as a proxy 0012 defined in Section 14 of version 3 of the license. 0013 0014 This program is distributed in the hope that it will be useful, 0015 but WITHOUT ANY WARRANTY; without even the implied warranty of 0016 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 0017 GNU General Public License for more details. 0018 0019 You should have received a copy of the GNU General Public License 0020 along with this program. If not, see <http://www.gnu.org/licenses/>. 0021 */ 0022 0023 #include <QDateTime> 0024 #include <QFile> 0025 #include <QPlainTextEdit> 0026 #include <QPushButton> 0027 #include <QTabWidget> 0028 #include <QTextStream> 0029 #include <QTimer> 0030 #include <QVBoxLayout> 0031 #include "ProtocolLoggerWidget.h" 0032 #include "Common/FileLogger.h" 0033 #include "Imap/Model/Utils.h" 0034 0035 namespace Gui { 0036 0037 ConnectionLog::ConnectionLog(): widget(0), buffer(Common::RingBuffer<Common::LogMessage>(900)), closedTime(0) 0038 { 0039 } 0040 0041 ProtocolLoggerWidget::ProtocolLoggerWidget(QWidget *parent) : 0042 QWidget(parent), loggingActive(false), m_fileLogger(0) 0043 { 0044 QVBoxLayout *layout = new QVBoxLayout(this); 0045 tabs = new QTabWidget(this); 0046 tabs->setTabsClosable(true); 0047 tabs->setTabPosition(QTabWidget::South); 0048 layout->addWidget(tabs); 0049 connect(tabs, &QTabWidget::tabCloseRequested, this, &ProtocolLoggerWidget::closeTab); 0050 0051 clearAll = new QPushButton(tr("Clear all"), this); 0052 connect(clearAll, &QAbstractButton::clicked, this, &ProtocolLoggerWidget::clearLogDisplay); 0053 tabs->setCornerWidget(clearAll, Qt::BottomRightCorner); 0054 0055 delayedDisplay = new QTimer(this); 0056 delayedDisplay->setSingleShot(true); 0057 delayedDisplay->setInterval(300); 0058 connect(delayedDisplay, &QTimer::timeout, this, &ProtocolLoggerWidget::slotShowLogs); 0059 } 0060 0061 void ProtocolLoggerWidget::slotSetPersistentLogging(const bool enabled) 0062 { 0063 if (enabled == !!m_fileLogger) 0064 return; 0065 0066 if (enabled) { 0067 Q_ASSERT(!m_fileLogger); 0068 m_fileLogger = new Common::FileLogger(this); 0069 m_fileLogger->setFileLogging(true, Imap::Mailbox::persistentLogFileName()); 0070 m_fileLogger->setAutoFlush(true); 0071 } else { 0072 delete m_fileLogger; 0073 m_fileLogger = 0; 0074 } 0075 emit persistentLoggingChanged(!!m_fileLogger); 0076 } 0077 0078 ProtocolLoggerWidget::~ProtocolLoggerWidget() 0079 { 0080 } 0081 0082 QPlainTextEdit *ProtocolLoggerWidget::getLogger(const uint connectionId) 0083 { 0084 QPlainTextEdit *res = logs[connectionId].widget; 0085 if (!res) { 0086 res = new QPlainTextEdit(); 0087 res->setLineWrapMode(QPlainTextEdit::NoWrap); 0088 res->setCenterOnScroll(true); 0089 res->setMaximumBlockCount(1000); 0090 res->setReadOnly(true); 0091 res->setUndoRedoEnabled(false); 0092 res->setWordWrapMode(QTextOption::NoWrap); 0093 // Got to output something here using the default background, 0094 // otherwise the QPlainTextEdit would default its background 0095 // to the very first value we throw at it, which might be a 0096 // grey one. 0097 res->appendHtml(QStringLiteral("<p> </p>")); 0098 tabs->addTab(res, tr("Connection %1").arg(connectionId)); 0099 logs[connectionId].widget = res; 0100 } 0101 return res; 0102 } 0103 0104 void ProtocolLoggerWidget::closeTab(int index) 0105 { 0106 QPlainTextEdit *w = qobject_cast<QPlainTextEdit *>(tabs->widget(index)); 0107 Q_ASSERT(w); 0108 for (auto it = logs.begin(); it != logs.end(); ++it) { 0109 if (it->widget != w) 0110 continue; 0111 logs.erase(it); 0112 tabs->removeTab(index); 0113 w->deleteLater(); 0114 return; 0115 } 0116 } 0117 0118 void ProtocolLoggerWidget::clearLogDisplay() 0119 { 0120 // We use very different indexing internally, to an extent where QTabWidget's ints are not easily obtainable from that, 0121 // so it's much better to clean up the GUI at first and only after that purge the underlying data 0122 while (tabs->count()) { 0123 QWidget *w = tabs->widget(0); 0124 Q_ASSERT(w); 0125 tabs->removeTab(0); 0126 w->deleteLater(); 0127 } 0128 0129 logs.clear(); 0130 } 0131 0132 void ProtocolLoggerWidget::showEvent(QShowEvent *e) 0133 { 0134 loggingActive = true; 0135 QWidget::showEvent(e); 0136 slotShowLogs(); 0137 } 0138 0139 void ProtocolLoggerWidget::hideEvent(QHideEvent *e) 0140 { 0141 loggingActive = false; 0142 QWidget::hideEvent(e); 0143 } 0144 0145 void ProtocolLoggerWidget::log(uint connectionId, Common::LogMessage message) 0146 { 0147 using namespace Common; 0148 0149 if (m_fileLogger) { 0150 m_fileLogger->log(connectionId, message); 0151 } 0152 enum {CUTOFF=200}; 0153 if (message.message.size() > CUTOFF) { 0154 message.truncatedBytes = message.message.size() - CUTOFF; 0155 message.message = message.message.left(CUTOFF); 0156 } 0157 // we rely on the default constructor and QMap's behavior of operator[] to call it here 0158 logs[connectionId].buffer.append(message); 0159 if (loggingActive && !delayedDisplay->isActive()) 0160 delayedDisplay->start(); 0161 } 0162 0163 void ProtocolLoggerWidget::flushToWidget(const uint connectionId, Common::RingBuffer<Common::LogMessage> &buf) 0164 { 0165 using namespace Common; 0166 0167 QPlainTextEdit *w = getLogger(connectionId); 0168 0169 if (buf.skippedCount()) { 0170 w->appendHtml(tr("<p style=\"color: #bb0000\"><i><b>%n message(s)</b> were skipped because this widget was hidden.</i></p>", 0171 "", buf.skippedCount())); 0172 } 0173 0174 for (RingBuffer<LogMessage>::const_iterator it = buf.begin(); it != buf.end(); ++it) { 0175 QString message = QStringLiteral("<pre><span style=\"color: #808080\">%1</span> %2<span style=\"color: %3;%4\">%5</span>%6</pre>"); 0176 QString direction; 0177 QString textColor; 0178 QString bgColor; 0179 QString trimmedInfo; 0180 0181 switch (it->kind) { 0182 case LOG_IO_WRITTEN: 0183 if (it->message.startsWith(QLatin1String("***"))) { 0184 textColor = QStringLiteral("#800080"); 0185 bgColor = QStringLiteral("#d0d0d0"); 0186 } else { 0187 textColor = QStringLiteral("#800000"); 0188 direction = QStringLiteral("<span style=\"color: #c0c0c0;\">>>> </span>"); 0189 } 0190 break; 0191 case LOG_IO_READ: 0192 if (it->message.startsWith(QLatin1String("***"))) { 0193 textColor = QStringLiteral("#808000"); 0194 bgColor = QStringLiteral("#d0d0d0"); 0195 } else { 0196 textColor = QStringLiteral("#008000"); 0197 direction = QStringLiteral("<span style=\"color: #c0c0c0;\"><<< </span>"); 0198 } 0199 break; 0200 case LOG_MAILBOX_SYNC: 0201 case LOG_MESSAGES: 0202 case LOG_OTHER: 0203 case LOG_PARSE_ERROR: 0204 case LOG_TASKS: 0205 case LOG_SUBMISSION: 0206 direction = QLatin1String("<span style=\"color: #c0c0c0;\">") + it->source + QLatin1String("</span> "); 0207 break; 0208 } 0209 0210 if (it->truncatedBytes) { 0211 trimmedInfo = tr("<br/><span style=\"color: #808080; font-style: italic;\">(+ %n more bytes)</span>", "", it->truncatedBytes); 0212 } 0213 0214 QString niceLine = it->message.toHtmlEscaped(); 0215 niceLine.replace(QLatin1Char('\r'), 0x240d /* SYMBOL FOR CARRIAGE RETURN */) 0216 .replace(QLatin1Char('\n'), 0x240a /* SYMBOL FOR LINE FEED */); 0217 0218 w->appendHtml(message.arg(it->timestamp.toString(QStringLiteral("hh:mm:ss.zzz")), 0219 direction, textColor, 0220 bgColor.isEmpty() ? QString() : QStringLiteral("background-color: %1").arg(bgColor), 0221 niceLine, trimmedInfo)); 0222 } 0223 buf.clear(); 0224 } 0225 0226 void ProtocolLoggerWidget::slotShowLogs() 0227 { 0228 // Please note that we can't return to the event loop from this context, as the log buffer has to be read atomically 0229 for (auto it = logs.begin(); it != logs.end(); ++it ) { 0230 flushToWidget(it.key(), it->buffer); 0231 } 0232 } 0233 0234 /** @short Check whether some of the logs need cleaning */ 0235 void ProtocolLoggerWidget::onConnectionClosed(uint connectionId, Imap::ConnectionState state) 0236 { 0237 if (state == Imap::CONN_STATE_LOGOUT) { 0238 auto now = QDateTime::currentMSecsSinceEpoch(); 0239 auto cutoff = now - 3 * 60 * 1000; // upon each disconnect, trash logs older than three minutes 0240 auto it = logs.find(connectionId); 0241 if (it != logs.end()) { 0242 it->closedTime = now; 0243 } 0244 0245 it = logs.begin() + 1; // do not ever delete log#0, that's a special one 0246 while (it != logs.end()) { 0247 if (it->closedTime != 0 && it->closedTime < cutoff) { 0248 if (it->widget) { 0249 it->widget->deleteLater(); 0250 } 0251 it = logs.erase(it); 0252 } else { 0253 ++it; 0254 } 0255 } 0256 } 0257 } 0258 0259 }