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 "OpenFilesTab.h"
0010 
0011 #include <QDir>
0012 #include <QGraphicsOpacityEffect>
0013 #include <QHeaderView>
0014 #include <QLabel>
0015 #include <QLayout>
0016 #include <QLineEdit>
0017 #include <QPushButton>
0018 #include <QSortFilterProxyModel>
0019 #include <QTimer>
0020 #include <QTreeView>
0021 
0022 #include <KLocalizedString>
0023 #include <KMessageWidget>
0024 
0025 #include <unistd.h>
0026 #include <sys/stat.h>
0027 
0028 QString fileTypeFromPath(const QString &path)
0029 {
0030     struct stat statbuf;
0031     if (stat(qPrintable(path), &statbuf) == 0) {
0032         if (S_ISREG(statbuf.st_mode) || S_ISLNK(statbuf.st_mode)) {
0033             return i18nc("Device type", "File");
0034         }
0035         if (S_ISCHR(statbuf.st_mode)) {
0036             return i18nc("Device type", "Character device");
0037         }
0038         if (S_ISBLK(statbuf.st_mode)) {
0039             return i18nc("Device type", "Block device");
0040         }
0041         if (S_ISFIFO(statbuf.st_mode)) {
0042             return i18nc("Device type", "Pipe");
0043         }
0044         if (S_ISSOCK(statbuf.st_mode)) {
0045             return i18nc("Device type", "Socket");
0046         }
0047     }
0048 
0049     return QString();
0050 }
0051 
0052 QString symLinkTargetFromPath(const QString &path)
0053 {
0054     struct stat statbuf;
0055     if (lstat(qPrintable(path), &statbuf) == 0) {
0056         QVarLengthArray<char, 256> symLinkTarget(statbuf.st_size + 1);
0057         ssize_t count;
0058         if ((count = readlink(qPrintable(path), symLinkTarget.data(), symLinkTarget.size() - 1)) > 0) {
0059             symLinkTarget[count] = '\0';
0060 
0061             return symLinkTarget.constData();
0062         }
0063     }
0064 
0065     return QString();
0066 }
0067 
0068 class OpenFilesModel : public QAbstractTableModel
0069 {
0070 public:
0071     using QAbstractTableModel::QAbstractTableModel;
0072     using QAbstractTableModel::setData;
0073 
0074     enum Column
0075     {
0076         Column_Id,
0077         Column_Type,
0078         Column_Filename,
0079         ColumnCount
0080     };
0081 
0082     using DataItem = struct
0083     {
0084         uint id;
0085         QString type;
0086         QString filename;
0087     };
0088     using Data = QVector<DataItem>;
0089 
0090     void setData(Data &&data)
0091     {
0092         beginResetModel();
0093         m_data = std::forward<Data>(data);
0094         endResetModel();
0095     }
0096 
0097     QVariant data(const QModelIndex &index, int role) const override
0098     {
0099         if (index.isValid() && (role == Qt::DisplayRole || role == Qt::EditRole)) {
0100             const int row = index.row();
0101             if (row >= 0 && row < m_data.count()) {
0102                 const DataItem &dataItem = m_data.at(row);
0103                 switch (index.column()) {
0104                     case Column_Id:
0105                         return dataItem.id;
0106                     case Column_Type:
0107                         return dataItem.type;
0108                     case Column_Filename:
0109                         return dataItem.filename;
0110                     default:
0111                         Q_UNREACHABLE();
0112                 }
0113             }
0114         }
0115 
0116         return QVariant();
0117     }
0118 
0119     QVariant headerData(int section, Qt::Orientation orientation, int role) const override
0120     {
0121         if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
0122             switch (section) {
0123                 case Column_Id:
0124                     return i18nc("@title:column File ID", "Id");
0125                 case Column_Type:
0126                     return i18nc("@title:column File type", "Type");
0127                 case Column_Filename:
0128                     return i18nc("@title:column", "Filename");
0129                 default:
0130                     Q_UNREACHABLE();
0131             }
0132         }
0133 
0134         return QVariant();
0135     }
0136 
0137     int columnCount(const QModelIndex &parent) const override
0138     {
0139         Q_UNUSED(parent);
0140 
0141         return ColumnCount;
0142     }
0143 
0144     int rowCount(const QModelIndex &parent) const override
0145     {
0146         Q_UNUSED(parent);
0147 
0148         return m_data.count();
0149     }
0150 
0151 private:
0152     Data m_data;
0153 };
0154 
0155 OpenFilesTab::OpenFilesTab(QWidget *parent)
0156     : QWidget(parent)
0157 {
0158     m_errorWidget = new KMessageWidget;
0159     m_errorWidget->setCloseButtonVisible(false);
0160     m_errorWidget->setMessageType(KMessageWidget::Error);
0161     m_errorWidget->setWordWrap(true);
0162     m_errorWidget->hide();
0163 
0164     QPushButton *refreshButton = new QPushButton(i18nc("@action:button", "Refresh"));
0165 
0166     m_searchEdit = new QLineEdit;
0167     m_searchEdit->setPlaceholderText(i18n("Quick search"));
0168 
0169     m_dataModel = new OpenFilesModel(this);
0170 
0171     m_proxyModel = new QSortFilterProxyModel(this);
0172     m_proxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
0173     m_proxyModel->setFilterKeyColumn(-1);
0174     m_proxyModel->setSortRole(Qt::EditRole);
0175     m_proxyModel->setSourceModel(m_dataModel);
0176 
0177     QTreeView *dataTreeView = new QTreeView;
0178     dataTreeView->setAlternatingRowColors(true);
0179     dataTreeView->setRootIsDecorated(false);
0180     dataTreeView->setSortingEnabled(true);
0181     dataTreeView->sortByColumn(OpenFilesModel::Column_Id, Qt::AscendingOrder);
0182     dataTreeView->setModel(m_proxyModel);
0183     dataTreeView->header()->setStretchLastSection(true);
0184 
0185     QGridLayout *rootLayout = new QGridLayout;
0186     rootLayout->addWidget(m_errorWidget, 0, 0, 1, 2);
0187     rootLayout->addWidget(refreshButton, 1, 0);
0188     rootLayout->addWidget(m_searchEdit, 1, 1);
0189     rootLayout->addWidget(dataTreeView, 2, 0, 1, 2);
0190     setLayout(rootLayout);
0191 
0192     m_placeholderLabel = new QLabel;
0193     m_placeholderLabel->setAlignment(Qt::AlignCenter);
0194     m_placeholderLabel->setMargin(20);
0195     m_placeholderLabel->setTextInteractionFlags(Qt::NoTextInteraction);
0196     m_placeholderLabel->setWordWrap(true);
0197     // To match the size of a level 2 Heading/KTitleWidget
0198     QFont placeholderFont = m_placeholderLabel->font();
0199     placeholderFont.setPointSize(qRound(placeholderFont.pointSize() * 1.3));
0200     m_placeholderLabel->setFont(placeholderFont);
0201     // Match opacity of QML placeholder label component
0202     QGraphicsOpacityEffect *opacityEffect = new QGraphicsOpacityEffect(m_placeholderLabel);
0203     opacityEffect->setOpacity(0.5);
0204     m_placeholderLabel->setGraphicsEffect(opacityEffect);
0205 
0206     QVBoxLayout *placeholderLayout = new QVBoxLayout;
0207     placeholderLayout->addWidget(m_placeholderLabel);
0208     dataTreeView->setLayout(placeholderLayout);
0209 
0210     // use some delay while searching as you type, because an immediate
0211     // search in large data can slow down the UI
0212     QTimer *applySearchTimer = new QTimer(this);
0213     applySearchTimer->setInterval(350);
0214     applySearchTimer->setSingleShot(true);
0215 
0216     connect(refreshButton, &QPushButton::clicked, this, &OpenFilesTab::refresh);
0217 
0218     connect(m_searchEdit, &QLineEdit::textChanged, applySearchTimer, qOverload<>(&QTimer::start));
0219     connect(m_searchEdit, &QLineEdit::editingFinished, applySearchTimer, &QTimer::stop);
0220     connect(m_searchEdit, &QLineEdit::editingFinished, this, &OpenFilesTab::onSearchEditEditingFinished);
0221 
0222     connect(m_proxyModel, &QSortFilterProxyModel::modelReset, this, &OpenFilesTab::onProxyModelChanged);
0223     connect(m_proxyModel, &QSortFilterProxyModel::rowsInserted, this, &OpenFilesTab::onProxyModelChanged);
0224     connect(m_proxyModel, &QSortFilterProxyModel::rowsRemoved, this, &OpenFilesTab::onProxyModelChanged);
0225 
0226     connect(applySearchTimer, &QTimer::timeout, this, &OpenFilesTab::onSearchEditEditingFinished);
0227 }
0228 
0229 void OpenFilesTab::setProcessId(long processId)
0230 {
0231     if (m_processId != processId) {
0232         m_processId = processId;
0233         refresh();
0234     }
0235 }
0236 
0237 void OpenFilesTab::refresh()
0238 {
0239     OpenFilesModel::Data data;
0240 
0241     if (m_processId <= 0) {
0242         m_errorWidget->animatedHide();
0243     } else {
0244         const QString dirPath = QStringLiteral("/proc/%1/fd").arg(m_processId);
0245         const QFileInfo dirFileInfo(dirPath);
0246         if (!dirFileInfo.exists() || !dirFileInfo.isReadable() || !dirFileInfo.isDir()) {
0247             m_errorWidget->setText(i18nc("@info:status", "%1: Failed to list directory contents", dirPath));
0248             m_errorWidget->animatedShow();
0249         } else {
0250             m_errorWidget->animatedHide();
0251 
0252             const QFileInfoList filesInfo = QDir(dirPath).entryInfoList(QDir::Files);
0253             for (const QFileInfo &fileInfo : filesInfo) {
0254                 OpenFilesModel::DataItem dataItem;
0255                 dataItem.id = fileInfo.fileName().toUInt();
0256                 dataItem.type = fileTypeFromPath(fileInfo.absoluteFilePath());
0257                 if (fileInfo.isFile()) {
0258                     dataItem.filename = fileInfo.symLinkTarget();
0259                 } else {
0260                     dataItem.filename = symLinkTargetFromPath(fileInfo.absoluteFilePath());
0261                 }
0262                 data << std::move(dataItem);
0263             }
0264         }
0265     }
0266 
0267     m_dataModel->setData(std::move(data));
0268 }
0269 
0270 void OpenFilesTab::onProxyModelChanged()
0271 {
0272     if (m_proxyModel->rowCount() > 0) {
0273         m_placeholderLabel->hide();
0274     } else {
0275         if (m_proxyModel->sourceModel()->rowCount() == 0) {
0276             m_placeholderLabel->setText(i18nc("@info:status", "No data to display"));
0277         } else {
0278             m_placeholderLabel->setText(i18nc("@info:status", "No data matching the filter"));
0279         }
0280         m_placeholderLabel->show();
0281     }
0282 }
0283 
0284 void OpenFilesTab::onSearchEditEditingFinished()
0285 {
0286     m_proxyModel->setFilterFixedString(m_searchEdit->text());
0287 }