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 }