File indexing completed on 2024-05-19 05:38:00

0001 /*
0002     SPDX-FileCopyrightText: 2023 Thenujan Sandramohan <sthenujan2002@gmail.com>
0003 
0004     SPDX-License-Identifier: GPL-2.0-or-later
0005 */
0006 
0007 #include "unit.h"
0008 
0009 #include <KConfig>
0010 #include <KConfigGroup>
0011 #include <KLocalizedString>
0012 
0013 #include <QDBusPendingCall>
0014 #include <QDBusPendingCallWatcher>
0015 #include <QDBusPendingReply>
0016 #include <QDateTime>
0017 #include <QFile>
0018 
0019 #if HAVE_SYSTEMD
0020 #include <systemd/sd-journal.h>
0021 #endif
0022 
0023 using namespace Qt::Literals::StringLiterals;
0024 
0025 static const QMap<QString, QString> STATE_MAP = {
0026     {u"active"_s, i18nc("@label Entry is running right now", "Running")},
0027     {u"inactive"_s, i18nc("@label Entry is not running right now (exited without error)", "Not running")},
0028     {u"activating"_s, i18nc("@label Entry is being started", "Starting")},
0029     {u"deactivating"_s, i18nc("@label Entry is being stopped", "Stopping")},
0030     {u"failed"_s, i18nc("@label Entry has failed (exited with an error)", "Failed")},
0031 };
0032 
0033 Unit::Unit(QObject *parent, bool invalid)
0034     : QObject(parent)
0035 {
0036     m_invalid = invalid;
0037 }
0038 
0039 Unit::~Unit()
0040 {
0041 }
0042 
0043 void Unit::setId(const QString &id)
0044 {
0045     m_id = id;
0046     loadAllProperties();
0047 }
0048 
0049 void Unit::loadAllProperties()
0050 {
0051     // Get unit path using Manager interface
0052     auto message = QDBusMessage::createMethodCall(m_connSystemd, m_pathSysdMgr, m_ifaceMgr, "GetUnit");
0053     message.setArguments(QList<QVariant>{m_id});
0054     QDBusPendingCall async = m_sessionBus.asyncCall(message);
0055     QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(async, this);
0056     connect(watcher, &QDBusPendingCallWatcher::finished, this, &Unit::callFinishedSlot);
0057 }
0058 
0059 void Unit::callFinishedSlot(QDBusPendingCallWatcher *call)
0060 {
0061     QDBusPendingReply<QDBusObjectPath> reply = *call;
0062     if (reply.isError()) {
0063         m_invalid = true;
0064         Q_EMIT dataChanged();
0065         return;
0066     } else {
0067         m_dbusObjectPath = reply.argumentAt(0).value<QDBusObjectPath>();
0068     }
0069     call->deleteLater();
0070     QDBusMessage message =
0071         QDBusMessage::createMethodCall(m_connSystemd, m_dbusObjectPath.path(), QStringLiteral("org.freedesktop.DBus.Properties"), QStringLiteral("GetAll"));
0072 
0073     message << m_ifaceUnit;
0074     QDBusPendingCall newCall = m_sessionBus.asyncCall(message);
0075     QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(newCall, this);
0076     connect(watcher, &QDBusPendingCallWatcher::finished, this, &Unit::getAllCallback);
0077 }
0078 
0079 void Unit::getAllCallback(QDBusPendingCallWatcher *call)
0080 {
0081     QDBusPendingReply<QVariantMap> reply = *call;
0082     if (reply.isError()) {
0083         Q_EMIT error(i18n("Error occurred when receiving reply of GetAll call %1", reply.error().message()));
0084         call->deleteLater();
0085         return;
0086     }
0087 
0088     QVariantMap properties = reply.argumentAt<0>();
0089     call->deleteLater();
0090 
0091     m_activeState = properties[QStringLiteral("ActiveState")].toString();
0092     m_activeStateValue = STATE_MAP[m_activeState];
0093     m_description = properties[QStringLiteral("Description")].toString();
0094     qulonglong ActiveEnterTimestamp = properties[QStringLiteral("ActiveEnterTimestamp")].toULongLong();
0095 
0096     setActiveEnterTimestamp(ActiveEnterTimestamp);
0097 
0098     reloadLogs();
0099     QDBusConnection userbus = QDBusConnection::connectToBus(QDBusConnection::SessionBus, m_connSystemd);
0100     userbus.connect(m_connSystemd,
0101                     m_dbusObjectPath.path(),
0102                     "org.freedesktop.DBus.Properties",
0103                     "PropertiesChanged",
0104                     this,
0105                     SLOT(dbusPropertiesChanged(QString, QVariantMap, QStringList)));
0106 }
0107 
0108 void Unit::setActiveEnterTimestamp(qulonglong ActiveEnterTimestamp)
0109 {
0110     if (ActiveEnterTimestamp == 0) {
0111         m_timeActivated = QStringLiteral("N/A");
0112     } else {
0113         QDateTime dateTimeActivated;
0114         dateTimeActivated.setMSecsSinceEpoch(ActiveEnterTimestamp / 1000);
0115         m_timeActivated = dateTimeActivated.toString();
0116     }
0117 }
0118 
0119 void Unit::dbusPropertiesChanged(QString name, QVariantMap map, QStringList list)
0120 {
0121     Q_UNUSED(name);
0122     Q_UNUSED(list);
0123     if (map.contains("ActiveEnterTimestamp")) {
0124         setActiveEnterTimestamp(map["ActiveEnterTimestamp"].toULongLong());
0125     }
0126     if (map.contains("ActiveState")) {
0127         m_activeState = map["ActiveState"].toString();
0128         m_activeStateValue = STATE_MAP[m_activeState];
0129     }
0130 
0131     Q_EMIT dataChanged();
0132 }
0133 
0134 void Unit::start()
0135 {
0136     auto message = QDBusMessage::createMethodCall(m_connSystemd, m_dbusObjectPath.path(), m_ifaceUnit, "Start");
0137     message << QStringLiteral("replace");
0138     m_sessionBus.send(message);
0139 }
0140 
0141 void Unit::stop()
0142 {
0143     auto message = QDBusMessage::createMethodCall(m_connSystemd, m_dbusObjectPath.path(), m_ifaceUnit, "Stop");
0144     message << QStringLiteral("replace");
0145     m_sessionBus.send(message);
0146 }
0147 
0148 QStringList Unit::getLastJournalEntries(const QString &unit)
0149 {
0150 #if HAVE_SYSTEMD
0151     sd_journal *journal;
0152 
0153     int returnValue = sd_journal_open(&journal, (SD_JOURNAL_LOCAL_ONLY | SD_JOURNAL_CURRENT_USER));
0154     if (returnValue != 0) {
0155         Q_EMIT journalError(i18n("Failed to open journal"));
0156         sd_journal_close(journal);
0157         return {};
0158     }
0159 
0160     sd_journal_flush_matches(journal);
0161 
0162     const QString match1 = QStringLiteral("USER_UNIT=%1").arg(unit);
0163     returnValue = sd_journal_add_match(journal, match1.toUtf8(), 0);
0164     if (returnValue != 0) {
0165         sd_journal_close(journal);
0166         return {};
0167     }
0168 
0169     sd_journal_add_disjunction(journal);
0170 
0171     const QString match2 = QStringLiteral("_SYSTEMD_USER_UNIT=%1").arg(unit);
0172     returnValue = sd_journal_add_match(journal, match2.toUtf8(), 0);
0173     if (returnValue != 0) {
0174         sd_journal_close(journal);
0175         return {};
0176     }
0177 
0178     returnValue = sd_journal_seek_tail(journal);
0179     if (returnValue != 0) {
0180         sd_journal_close(journal);
0181         return {};
0182     }
0183 
0184     QStringList reply;
0185     QString lastDateTime;
0186     // Fetch the last 50 entries
0187     for (int i = 0; i < 50; ++i) {
0188         returnValue = sd_journal_previous(journal);
0189         if (returnValue != 1) {
0190             // previous failed, no more entries
0191             sd_journal_close(journal);
0192             return reply;
0193         }
0194         QString line;
0195 
0196         // Get the date and time
0197         uint64_t time;
0198         returnValue = sd_journal_get_realtime_usec(journal, &time);
0199 
0200         if (returnValue == 0) {
0201             QDateTime date;
0202             date.setMSecsSinceEpoch(time / 1000);
0203             if (lastDateTime != date.toString()) {
0204                 line.append(date.toString("yyyy.MM.dd hh:mm: "));
0205                 lastDateTime = date.toString();
0206             } else {
0207                 line.append(QString("&#8203;&#32;").repeated(33));
0208             }
0209         }
0210 
0211         // Color messages according to priority
0212         size_t length;
0213         const void *data;
0214         returnValue = sd_journal_get_data(journal, "PRIORITY", &data, &length);
0215         if (returnValue == 0) {
0216             int prio = QString::fromUtf8((const char *)data, length).section('=', 1).toInt();
0217             // Adding color to the logs is done in c++ so we can't add Kirigami color here so we add a string here
0218             //  and then replace it all with actual colors in qml code
0219             if (prio <= 3)
0220                 line.append("<font color='Kirigami.Theme.negativeTextColor'>");
0221             else if (prio == 4)
0222                 line.append("<font color='Kirigami.Theme.neutralTextColor'>");
0223             else
0224                 line.append("<font>");
0225         }
0226 
0227         // Get the message itself
0228         returnValue = sd_journal_get_data(journal, "MESSAGE", &data, &length);
0229         if (returnValue == 0) {
0230             line.append(QString::fromUtf8((const char *)data, length).section('=', 1) + "</font>");
0231             reply << line;
0232         }
0233     }
0234 
0235     sd_journal_close(journal);
0236 
0237     return reply;
0238 #else
0239     return QStringList();
0240 #endif
0241 }
0242 
0243 void Unit::reloadLogs()
0244 {
0245     QStringList logsList = getLastJournalEntries(m_id);
0246 
0247     logsList.removeAll({});
0248     m_logs = logsList.join(QStringLiteral("<br>"));
0249 
0250     Q_EMIT dataChanged();
0251 }