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>&nbsp;</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;\">&gt;&gt;&gt;&nbsp;</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;\">&lt;&lt;&lt;&nbsp;</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 }