File indexing completed on 2024-05-05 17:39:54

0001 /*
0002  *  KSysGuard, the KDE System Guard
0003  *
0004  *  SPDX-FileCopyrightText: 2022 Eugene Popov <popov895@ukr.net>
0005  *
0006  *  SPDX-License-Identifier: LGPL-2.0-or-later
0007  */
0008 
0009 #include "MemoryMapsTab.h"
0010 
0011 #include <QFile>
0012 #include <QGraphicsOpacityEffect>
0013 #include <QHeaderView>
0014 #include <QLabel>
0015 #include <QLayout>
0016 #include <QLineEdit>
0017 #include <QPushButton>
0018 #include <QSortFilterProxyModel>
0019 #include <QTextStream>
0020 #include <QTimer>
0021 #include <QTreeView>
0022 
0023 #include <KLocalizedString>
0024 #include <KMessageWidget>
0025 
0026 class MemoryMapsModel : public QAbstractTableModel
0027 {
0028 public:
0029     using QAbstractTableModel::QAbstractTableModel;
0030     using QAbstractTableModel::setData;
0031 
0032     // we know for sure that the next columns are in these positions
0033     enum KnownColumn
0034     {
0035         Column_Filename,
0036         Column_Start,
0037         Column_End,
0038         Column_Permissions,
0039         Column_Offset,
0040         Column_Inode,
0041         KnownColumnCount
0042     };
0043 
0044     using DataItem = QVector<QVariant>;
0045     using Data = struct
0046     {
0047         QVector<QString> columns;
0048         QVector<DataItem> rows;
0049     };
0050 
0051     void setData(Data &&data)
0052     {
0053         beginResetModel();
0054         m_data = std::forward<Data>(data);
0055         endResetModel();
0056     }
0057 
0058     QVariant data(const QModelIndex &index, int role) const override
0059     {
0060         if (index.isValid() && (role == Qt::DisplayRole || role == Qt::EditRole)) {
0061             const int row = index.row();
0062             if (row >= 0 && row < m_data.rows.count()) {
0063                 const DataItem &dataItem = m_data.rows.at(row);
0064                 const int column = index.column();
0065                 if (column >= 0 && column < dataItem.count()) {
0066                     const QVariant data = dataItem.at(column);
0067                     Q_ASSERT(data.isValid());
0068                     if (role == Qt::DisplayRole && data.type() != QVariant::String) {
0069                         if (column == Column_Start || column == Column_End || column == Column_Offset) {
0070                             return QString::number(data.toULongLong(), 16); // show in hex
0071                         }
0072                         if (column > Column_Inode) {
0073                             return i18nc("kilobytes", "%1 kB", data.toString());
0074                         }
0075                     }
0076 
0077                     return data;
0078                 }
0079             }
0080         }
0081 
0082         return QVariant();
0083     }
0084 
0085     QVariant headerData(int section, Qt::Orientation orientation, int role) const override
0086     {
0087         if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
0088             if (section >= 0 && section < m_data.columns.count()) {
0089                 return m_data.columns.at(section);
0090             }
0091         }
0092 
0093         return QVariant();
0094     }
0095 
0096     int columnCount(const QModelIndex &parent) const override
0097     {
0098         Q_UNUSED(parent);
0099 
0100         return m_data.columns.count();
0101     }
0102 
0103     int rowCount(const QModelIndex &parent) const override
0104     {
0105         Q_UNUSED(parent);
0106 
0107         return m_data.rows.count();
0108     }
0109 
0110 private:
0111     Data m_data;
0112 };
0113 
0114 MemoryMapsTab::MemoryMapsTab(QWidget *parent)
0115     : QWidget(parent)
0116 {
0117     m_errorWidget = new KMessageWidget;
0118     m_errorWidget->setCloseButtonVisible(false);
0119     m_errorWidget->setMessageType(KMessageWidget::Error);
0120     m_errorWidget->setWordWrap(true);
0121     m_errorWidget->hide();
0122 
0123     QPushButton *refreshButton = new QPushButton(i18nc("@action:button", "Refresh"));
0124 
0125     m_searchEdit = new QLineEdit;
0126     m_searchEdit->setPlaceholderText(i18n("Quick search"));
0127 
0128     m_dataModel = new MemoryMapsModel(this);
0129 
0130     m_proxyModel = new QSortFilterProxyModel(this);
0131     m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
0132     m_proxyModel->setFilterKeyColumn(-1);
0133     m_proxyModel->setSortRole(Qt::EditRole);
0134     m_proxyModel->setSourceModel(m_dataModel);
0135 
0136     QTreeView *dataTreeView = new QTreeView;
0137     dataTreeView->setAlternatingRowColors(true);
0138     dataTreeView->setRootIsDecorated(false);
0139     dataTreeView->setSortingEnabled(true);
0140     dataTreeView->sortByColumn(MemoryMapsModel::Column_Start, Qt::AscendingOrder);
0141     dataTreeView->setModel(m_proxyModel);
0142     dataTreeView->header()->setStretchLastSection(true);
0143 
0144     QGridLayout *rootLayout = new QGridLayout;
0145     rootLayout->addWidget(m_errorWidget, 0, 0, 1, 2);
0146     rootLayout->addWidget(refreshButton, 1, 0);
0147     rootLayout->addWidget(m_searchEdit, 1, 1);
0148     rootLayout->addWidget(dataTreeView, 2, 0, 1, 2);
0149     setLayout(rootLayout);
0150 
0151     m_placeholderLabel = new QLabel;
0152     m_placeholderLabel->setAlignment(Qt::AlignCenter);
0153     m_placeholderLabel->setMargin(20);
0154     m_placeholderLabel->setTextInteractionFlags(Qt::NoTextInteraction);
0155     m_placeholderLabel->setWordWrap(true);
0156     // To match the size of a level 2 Heading/KTitleWidget
0157     QFont placeholderFont = m_placeholderLabel->font();
0158     placeholderFont.setPointSize(qRound(placeholderFont.pointSize() * 1.3));
0159     m_placeholderLabel->setFont(placeholderFont);
0160     // Match opacity of QML placeholder label component
0161     QGraphicsOpacityEffect *opacityEffect = new QGraphicsOpacityEffect(m_placeholderLabel);
0162     opacityEffect->setOpacity(0.5);
0163     m_placeholderLabel->setGraphicsEffect(opacityEffect);
0164 
0165     QVBoxLayout *placeholderLayout = new QVBoxLayout;
0166     placeholderLayout->addWidget(m_placeholderLabel);
0167     dataTreeView->setLayout(placeholderLayout);
0168 
0169     // use some delay while searching as you type, because an immediate
0170     // search in large data can slow down the UI
0171     QTimer *applySearchTimer = new QTimer(this);
0172     applySearchTimer->setInterval(350);
0173     applySearchTimer->setSingleShot(true);
0174 
0175     connect(refreshButton, &QPushButton::clicked, this, &MemoryMapsTab::refresh);
0176 
0177     connect(m_searchEdit, &QLineEdit::textChanged, applySearchTimer, qOverload<>(&QTimer::start));
0178     connect(m_searchEdit, &QLineEdit::editingFinished, applySearchTimer, &QTimer::stop);
0179     connect(m_searchEdit, &QLineEdit::editingFinished, this, &MemoryMapsTab::onSearchEditEditingFinished);
0180 
0181     connect(m_proxyModel, &QSortFilterProxyModel::modelReset, this, &MemoryMapsTab::onProxyModelChanged);
0182     connect(m_proxyModel, &QSortFilterProxyModel::rowsInserted, this, &MemoryMapsTab::onProxyModelChanged);
0183     connect(m_proxyModel, &QSortFilterProxyModel::rowsRemoved, this, &MemoryMapsTab::onProxyModelChanged);
0184 
0185     connect(applySearchTimer, &QTimer::timeout, this, &MemoryMapsTab::onSearchEditEditingFinished);
0186 }
0187 
0188 void MemoryMapsTab::setProcessId(long processId)
0189 {
0190     if (m_processId != processId) {
0191         m_processId = processId;
0192         refresh();
0193     }
0194 }
0195 
0196 void MemoryMapsTab::refresh()
0197 {
0198     MemoryMapsModel::Data data;
0199 
0200     if (m_processId <= 0) {
0201         m_errorWidget->animatedHide();
0202     } else {
0203         const QString filePath = QStringLiteral("/proc/%1/smaps").arg(m_processId);
0204         QFile file(filePath);
0205         if (!file.open(QFile::ReadOnly)) {
0206             m_errorWidget->setText(QStringLiteral("%1: %2").arg(filePath).arg(file.errorString()));
0207             m_errorWidget->animatedShow();
0208         } else {
0209             m_errorWidget->animatedHide();
0210 
0211             MemoryMapsModel::DataItem dataItem;
0212             QTextStream smapsTextStream(file.readAll());
0213             QString line;
0214             QRegularExpressionMatch match;
0215             while (smapsTextStream.readLineInto(&line)) {
0216                 // e.g. "Size: 80 kB"
0217                 static QRegularExpression regexLineKb(QStringLiteral("^([^ ]+): +(\\d+) kB$"));
0218                 match = regexLineKb.match(line);
0219                 if (match.hasMatch()) {
0220                     if (data.rows.isEmpty()) {
0221                         // add the parsed column while we are parsing the first item
0222                         data.columns << match.captured(1);
0223                     }
0224                     dataItem << match.captured(2).toUInt();
0225                     continue;
0226                 }
0227 
0228                 // e.g. "VmFlags: rd ex mr mw me sd"
0229                 static QRegularExpression regexLine(QStringLiteral("^([^ ]+): +(.+)$"));
0230                 match = regexLine.match(line);
0231                 if (match.hasMatch()) {
0232                     if (data.rows.isEmpty()) {
0233                         // add the parsed column while we are parsing the first item
0234                         data.columns << match.captured(1);
0235                     }
0236                     dataItem << match.captured(2);
0237                     continue;
0238                 }
0239 
0240                 // e.g. "7f935d6a5000-7f935d6a6000 rw-p 00040000 08:02 7457 /usr/lib64/libxcb.so.1.1.0"
0241                 static QRegularExpression regexHeader(QStringLiteral("^([0-9A-Fa-f]+)-([0-9A-Fa-f]+) +([^ ]*) +([0-9A-Fa-f]+) "
0242                                                                      "+([0-9A-Fa-f]+:[0-9A-Fa-f]+) +(\\d+) +(.*)$"));
0243                 match = regexHeader.match(line);
0244                 if (match.hasMatch()) {
0245                     // we have reached the next header so we need to store the parsed smapsDataItem
0246                     if (!dataItem.isEmpty()) {
0247                         data.rows << std::move(dataItem);
0248                     }
0249                     if (data.rows.isEmpty()) {
0250                         // add known columns while we are parsing the first item
0251                         data.columns << i18nc("@title:column", "Filename");
0252                         data.columns << i18nc("@title:column Start of the address space", "Start");
0253                         data.columns << i18nc("@title:column End of the address space", "End");
0254                         data.columns << i18nc("@title:column", "Permissions");
0255                         data.columns << i18nc("@title:column Offset into the file", "Offset");
0256                         data.columns << i18nc("@title:column", "Inode");
0257                     }
0258                     dataItem << match.captured(7);                          // filename
0259                     dataItem << match.captured(1).toULongLong(nullptr, 16); // start
0260                     dataItem << match.captured(2).toULongLong(nullptr, 16); // end
0261                     dataItem << match.captured(3);                          // permissions
0262                     dataItem << match.captured(4).toULongLong(nullptr, 16); // offset
0263                     dataItem << match.captured(6).toUInt();                 // inode
0264                 }
0265             }
0266             if (!dataItem.isEmpty()) {
0267                 data.rows << std::move(dataItem);
0268             }
0269         }
0270     }
0271 
0272     m_dataModel->setData(std::move(data));
0273 }
0274 
0275 void MemoryMapsTab::onProxyModelChanged()
0276 {
0277     if (m_proxyModel->rowCount() > 0) {
0278         m_placeholderLabel->hide();
0279     } else {
0280         if (m_proxyModel->sourceModel()->rowCount() == 0) {
0281             m_placeholderLabel->setText(i18nc("@info:status", "No data to display"));
0282         } else {
0283             m_placeholderLabel->setText(i18nc("@info:status", "No data matching the filter"));
0284         }
0285         m_placeholderLabel->show();
0286     }
0287 }
0288 
0289 void MemoryMapsTab::onSearchEditEditingFinished()
0290 {
0291     m_proxyModel->setFilterFixedString(m_searchEdit->text());
0292 }