File indexing completed on 2024-11-17 04:55:44

0001 /*
0002  *   SPDX-FileCopyrightText: 2021 Mariam Fahmy Sobhy <mariamfahmy66@gmail.com>
0003  *
0004  *   SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005  */
0006 
0007 #include "RpmOstreeTransaction.h"
0008 
0009 #include <KLocalizedString>
0010 
0011 #include <QDebug>
0012 #include <QJsonArray>
0013 #include <QJsonDocument>
0014 #include <QJsonObject>
0015 #include <QVersionNumber>
0016 
0017 static const QString TransactionConnection = QStringLiteral("discover_transaction");
0018 static const QString DBusServiceName = QStringLiteral("org.projectatomic.rpmostree1");
0019 
0020 RpmOstreeTransaction::RpmOstreeTransaction(QObject *parent,
0021                                            AbstractResource *resource,
0022                                            OrgProjectatomicRpmostree1SysrootInterface *interface,
0023                                            Operation operation,
0024                                            QString arg)
0025     : Transaction(parent, resource, Transaction::Role::InstallRole, {})
0026     , m_timer(nullptr)
0027     , m_operation(operation)
0028     , m_resource((RpmOstreeResource *)resource)
0029     , m_cancelled(false)
0030     , m_interface(interface)
0031 {
0032     setStatus(Status::SetupStatus);
0033 
0034     // This should never happen. We need a reference to the DBus interface to be
0035     // able to cancel a running transaction.
0036     if (interface == nullptr) {
0037         qWarning() << "rpm-ostree-backend: Error: No DBus interface provided. Please file a bug.";
0038         passiveMessage(i18n("rpm-ostree-backend: Error: No DBus interface provided. Please file a bug."));
0039         setStatus(Status::CancelledStatus);
0040         return;
0041     }
0042 
0043     // Make sure we are asking for a supported operation and set up arguments
0044     switch (m_operation) {
0045     case Operation::CheckForUpdate: {
0046         qInfo() << "rpm-ostree-backend: Starting transaction to check for updates";
0047         if (m_resource->isClassic()) {
0048             m_prog = QStringLiteral("rpm-ostree");
0049             m_args.append({QStringLiteral("update"), QStringLiteral("--check")});
0050         } else if (m_resource->isOCI()) {
0051             m_prog = QStringLiteral("skopeo");
0052             // This will fail on non-remote transports (oci, oci-archive, containers-storage) but
0053             // that's OK as we can not check for updates in those cases.
0054             m_args.append({QStringLiteral("inspect"), m_resource->OCIUrl()});
0055         } else {
0056             // Should never happen
0057             qWarning() << "rpm-ostree-backend: Error: Can not start a transaction for resource with an invalid format. Please file a bug.";
0058             passiveMessage(i18n("rpm-ostree-backend: Error: Can not start a transaction for resource with an invalid format. Please file a bug."));
0059             setStatus(Status::CancelledStatus);
0060             return;
0061         }
0062         break;
0063     }
0064     case Operation::DownloadOnly:
0065         qInfo() << "rpm-ostree-backend: Starting transaction to only download updates";
0066         m_prog = QStringLiteral("rpm-ostree");
0067         m_args.append({QStringLiteral("update"), QStringLiteral("--download-only ")});
0068         break;
0069     case Operation::Update:
0070         qInfo() << "rpm-ostree-backend: Starting transaction to update";
0071         m_prog = QStringLiteral("rpm-ostree");
0072         m_args.append({QStringLiteral("update")});
0073         break;
0074     case Operation::Rebase:
0075         // This should never happen
0076         if (arg.isEmpty()) {
0077             qWarning() << "rpm-ostree-backend: Error: Can not rebase to an empty ref. Please file a bug.";
0078             passiveMessage(i18n("rpm-ostree-backend: Error: Can not rebase to an empty ref. Please file a bug."));
0079             setStatus(Status::CancelledStatus);
0080             return;
0081         }
0082         qInfo() << "rpm-ostree-backend: Starting transaction to rebase to:" << arg;
0083         m_prog = QStringLiteral("rpm-ostree");
0084         m_args.append({QStringLiteral("rebase"), arg});
0085         break;
0086     case Operation::Unknown:
0087         // This is a transaction started externally to Discover. We'll just
0088         // display it as best as we can.
0089         qInfo() << "rpm-ostree-backend: Creating a transaction for an operation not started by Discover";
0090         setupExternalTransaction();
0091         return;
0092         break;
0093     default:
0094         // This should never happen
0095         qWarning() << "rpm-ostree-backend: Error: Unknown operation requested. Please file a bug.";
0096         passiveMessage(i18n("rpm-ostree-backend: Error: Unknown operation requested. Please file a bug."));
0097         setStatus(Status::CancelledStatus);
0098         return;
0099     }
0100 
0101     // Directly run the command via a QProcess
0102     m_process = new QProcess(this);
0103     m_process->setProgram(m_prog);
0104     m_process->setArguments(m_args);
0105 
0106     // Store stderr output for later
0107     connect(m_process, &QProcess::readyReadStandardError, [this]() {
0108         QByteArray message = m_process->readAllStandardError();
0109         qWarning() << m_prog << QLatin1String("(error):") << message;
0110         m_stderr += message;
0111     });
0112 
0113     // Store stdout output for later and process it to fake progress
0114     connect(m_process, &QProcess::readyReadStandardOutput, [this]() {
0115         QByteArray message = m_process->readAllStandardOutput();
0116         qInfo() << (m_prog + QStringLiteral(":")) << message;
0117         m_stdout += message;
0118         fakeProgress(message);
0119     });
0120 
0121     // Process the result of the transaction once rpm-ostree is done
0122     connect(m_process, &QProcess::finished, this, &RpmOstreeTransaction::processCommand);
0123 
0124     // Wait for the start command to effectively start the transaction so that
0125     // the caller has the time to setup signal/slots connections.
0126 }
0127 
0128 RpmOstreeTransaction::~RpmOstreeTransaction()
0129 {
0130     delete m_timer;
0131 }
0132 
0133 void RpmOstreeTransaction::start()
0134 {
0135     // Calling this function only makes sense if we have a QProcess for the
0136     // current transaction.
0137     if (m_process != nullptr) {
0138         m_process->start();
0139         setStatus(Status::DownloadingStatus);
0140         setProgress(5);
0141         setDownloadSpeed(0);
0142     }
0143 }
0144 
0145 void RpmOstreeTransaction::processCommand(int exitCode, QProcess::ExitStatus exitStatus)
0146 {
0147     m_process->deleteLater();
0148     m_process = nullptr;
0149     if (exitStatus != QProcess::NormalExit) {
0150         if (m_cancelled) {
0151             // If the user requested the transaction to be cancelled then we
0152             // don't need to show any error
0153             qWarning() << "rpm-ostree-backend: Transaction cancelled: rpm-ostree " << m_args;
0154         } else {
0155             // The transaction was cancelled unexpectedly so let's display the
0156             // error to the user
0157             qWarning() << "rpm-ostree-backend: Error while calling: rpm-ostree " << m_args;
0158             passiveMessage(i18n("rpm-ostree transaction failed with:\n%1", QString::fromUtf8(m_stderr)));
0159         }
0160         setStatus(Status::CancelledStatus);
0161         return;
0162     }
0163     if (exitCode != 0) {
0164         if ((m_operation == Operation::CheckForUpdate) && (exitCode == 77)) {
0165             // rpm-ostree will exit with status 77 when no updates are available
0166             qInfo() << "rpm-ostree-backend: No updates available";
0167             // Tell the backend to look for a new major version
0168             Q_EMIT lookForNextMajorVersion();
0169             setStatus(Status::DoneStatus);
0170             return;
0171         } else if (m_cancelled) {
0172             // If the user requested the transaction to be cancelled then we
0173             // don't need to show any error
0174             qInfo() << "rpm-ostree-backend: Transaction cancelled: rpm-ostree " << m_args;
0175             setStatus(Status::DoneWithErrorStatus);
0176             return;
0177         } else {
0178             // The transaction failed unexpectedly so let's display the error to
0179             // the user
0180             qWarning() << "rpm-ostree-backend: rpm-ostree" << m_args << "returned with an error code:" << exitCode;
0181             passiveMessage(i18n("rpm-ostree transaction failed with:\n%1", QString::fromUtf8(m_stderr)));
0182             setStatus(Status::DoneWithErrorStatus);
0183             return;
0184         }
0185     }
0186 
0187     // The transaction was successful. Let's process the result.
0188     switch (m_operation) {
0189     case Operation::CheckForUpdate: {
0190         if (m_resource->isClassic()) {
0191             // Look for new version in rpm-ostree stdout
0192             QString newVersion, line;
0193             QString output = QString::fromUtf8(m_stdout);
0194             QTextStream stream(&output);
0195             while (stream.readLineInto(&line)) {
0196                 if (line.contains(QLatin1String("Version: "))) {
0197                     newVersion = line;
0198                     break;
0199                 }
0200             }
0201             // If we found a new version then offer it as an update
0202             if (!newVersion.isEmpty()) {
0203                 newVersion = newVersion.trimmed();
0204                 newVersion.remove(0, QStringLiteral("Version: ").length());
0205                 newVersion.remove(newVersion.size() - QStringLiteral(" (XXXX-XX-XXTXX:XX:XXZ)").length(), newVersion.size() - 1);
0206                 qInfo() << "rpm-ostree-backend: Found new version:" << newVersion;
0207                 Q_EMIT newVersionFound(newVersion);
0208             }
0209         } else if (m_resource->isOCI()) {
0210             // Parse stdout as JSON and look at the container image labels for the version
0211             const QJsonDocument jsonDocument = QJsonDocument::fromJson(m_stdout);
0212             if (!jsonDocument.isObject()) {
0213                 qWarning() << "rpm-ostree-backend: Could not parse output as JSON:" << m_prog << m_args;
0214                 return;
0215             }
0216 
0217             // Get the version stored in .Labels.version
0218             const QString newVersion = jsonDocument.object().value(QLatin1String("Labels")).toObject().value(QLatin1String("version")).toString();
0219             if (newVersion.isEmpty()) {
0220                 qInfo() << "rpm-ostree-backend: Could not get the version from the container labels";
0221                 return;
0222             }
0223 
0224             QVersionNumber newVersionNumber = QVersionNumber::fromString(newVersion);
0225             QVersionNumber currentVersionNumber = QVersionNumber::fromString(m_resource->version());
0226             if (newVersionNumber <= currentVersionNumber) {
0227                 qInfo() << "rpm-ostree-backend: No new version found";
0228             } else {
0229                 qInfo() << "rpm-ostree-backend: Found new version:" << newVersion;
0230                 Q_EMIT newVersionFound(newVersion);
0231             }
0232         } else {
0233             // Should never happen
0234             qWarning() << "rpm-ostree-backend: Error: Unknown resource format. Please file a bug.";
0235             passiveMessage(i18n("rpm-ostree-backend: Error: Unknown resource format. Please file a bug."));
0236         }
0237 
0238         // Always tell the backend to look for a new major version
0239         Q_EMIT lookForNextMajorVersion();
0240 
0241         break;
0242     }
0243     case Operation::DownloadOnly:
0244         // Nothing to do here after downloading pending updates.
0245         break;
0246     case Operation::Update:
0247         // Refresh ressources (deployments) and update state
0248         Q_EMIT deploymentsUpdated();
0249         break;
0250     case Operation::Rebase:
0251         // Refresh ressources (deployments) and update state
0252         Q_EMIT deploymentsUpdated();
0253         // Tell the backend to refresh the new major version message now that
0254         // we've reabsed to the new version
0255         Q_EMIT lookForNextMajorVersion();
0256         break;
0257     case Operation::Unknown:
0258     default:
0259         // This should never happen
0260         qWarning() << "rpm-ostree-backend: Error: Unknown operation requested. Please file a bug.";
0261         passiveMessage(i18n("rpm-ostree-backend: Error: Unknown operation requested. Please file a bug."));
0262     }
0263     setStatus(Status::DoneStatus);
0264 }
0265 
0266 void RpmOstreeTransaction::setupExternalTransaction()
0267 {
0268     // Create a timer to periodically look for updates on the transaction
0269     m_timer = new QTimer(this);
0270     m_timer->setSingleShot(true);
0271     m_timer->setInterval(2000);
0272 
0273     // Update transaction status
0274     connect(m_timer, &QTimer::timeout, [this]() {
0275         // Is the transaction finished?
0276         qDebug() << "rpm-ostree-backend: External transaction update timer triggered";
0277         QString transaction = m_interface->activeTransactionPath();
0278         if (transaction.isEmpty()) {
0279             qInfo() << "rpm-ostree-backend: External transaction finished";
0280             Q_EMIT deploymentsUpdated();
0281             setStatus(Status::DoneStatus);
0282             return;
0283         }
0284 
0285         // Read status and fake progress
0286         QStringList transactionInfo = m_interface->activeTransaction();
0287         if (transactionInfo.length() != 3) {
0288             qInfo() << "rpm-ostree-backend: External transaction:" << transactionInfo;
0289         } else {
0290             qInfo() << "rpm-ostree-backend: External transaction '" << transactionInfo.at(0) << "' requested by '" << transactionInfo.at(1);
0291         }
0292         fakeProgress({});
0293 
0294         // Restart the timer
0295         m_timer->start();
0296     });
0297 
0298     // Setup status, fake progress and start the timer
0299     setStatus(Status::DownloadingStatus);
0300     setProgress(5);
0301     setDownloadSpeed(0);
0302     m_timer->start();
0303 }
0304 
0305 void RpmOstreeTransaction::fakeProgress(const QByteArray &msg)
0306 {
0307     QString message = QString::fromUtf8(msg);
0308     int progress = this->progress();
0309     if (message.contains(QLatin1String("Receiving metadata objects"))) {
0310         progress += 10;
0311     } else if (message.contains(QLatin1String("Checking out tree"))) {
0312         progress += 5;
0313     } else if (message.contains(QLatin1String("Enabled rpm-md repositories:"))) {
0314         progress += 1;
0315     } else if (message.contains(QLatin1String("Updating metadata for"))) {
0316         progress += 1;
0317     } else if (message.contains(QLatin1String("rpm-md repo"))) {
0318         progress += 1;
0319     } else if (message.contains(QLatin1String("Resolving dependencies"))) {
0320         progress += 5;
0321     } else if (message.contains(QLatin1String("Applying")) && (message.contains(QLatin1String("overrides")) || message.contains(QLatin1String("overlays")))) {
0322         progress += 5;
0323         setStatus(Status::CommittingStatus);
0324     } else if (message.contains(QLatin1String("Processing packages"))) {
0325         progress += 5;
0326     } else if (message.contains(QLatin1String("Running pre scripts"))) {
0327         progress += 5;
0328     } else if (message.contains(QLatin1String("Running post scripts"))) {
0329         progress += 5;
0330     } else if (message.contains(QLatin1String("Running posttrans scripts"))) {
0331         progress += 5;
0332     } else if (message.contains(QLatin1String("Writing rpmdb"))) {
0333         progress += 5;
0334     } else if (message.contains(QLatin1String("Generating initramfs"))) {
0335         progress += 10;
0336     } else if (message.contains(QLatin1String("Writing OSTree commit"))) {
0337         progress += 10;
0338         setCancellable(false);
0339     } else if (message.contains(QLatin1String("Staging deployment"))) {
0340         progress += 5;
0341     } else if (message.contains(QLatin1String("Freed"))) {
0342         progress += 1;
0343     } else if (message.contains(QLatin1String("Upgraded"))) {
0344         progress = 99;
0345     } else {
0346         progress += 1;
0347     }
0348     // As we're faking progress, let's make sure that it stays in expected bounds
0349     setProgress(qBound(1, progress, 99));
0350 }
0351 
0352 void RpmOstreeTransaction::cancel()
0353 {
0354     qInfo() << "rpm-ostree-backend: Cancelling current transaction";
0355     passiveMessage(i18n("Cancelling rpm-ostree transaction. This may take some time. Please wait."));
0356 
0357     // Cancel directly using the DBus interface to work in all cases whether we
0358     // started the transaction or if it's an externally started one.
0359     QString transaction = m_interface->activeTransactionPath();
0360     QDBusConnection peerConnection = QDBusConnection::connectToPeer(transaction, TransactionConnection);
0361     OrgProjectatomicRpmostree1TransactionInterface transactionInterface(DBusServiceName, QStringLiteral("/"), peerConnection, this);
0362     auto reply = transactionInterface.Cancel();
0363 
0364     // Cancelled marker that is used to avoid displaying an error message to the
0365     // user when they asked to cancel a transaction.
0366     m_cancelled = true;
0367 
0368     QDBusPendingCallWatcher *callWatcher = new QDBusPendingCallWatcher(reply, this);
0369     connect(callWatcher, &QDBusPendingCallWatcher::finished, [callWatcher]() {
0370         callWatcher->deleteLater();
0371         QDBusConnection::disconnectFromPeer(TransactionConnection);
0372     });
0373 }
0374 
0375 void RpmOstreeTransaction::proceed()
0376 {
0377     qInfo() << "rpm-ostree-backend: proceed";
0378 }
0379 
0380 QString RpmOstreeTransaction::name() const
0381 {
0382     switch (m_operation) {
0383     case Operation::CheckForUpdate:
0384         return i18n("Checking for a system update");
0385         break;
0386     case Operation::DownloadOnly:
0387         return i18n("Downloading system update");
0388         break;
0389     case Operation::Update:
0390         return i18n("Updating the system");
0391         break;
0392     case Operation::Rebase:
0393         return i18n("Updating to the next major version");
0394         break;
0395     case Operation::Unknown:
0396         return i18n("Operation in progress (started outside of Discover)");
0397         break;
0398     default:
0399         break;
0400     }
0401     // This should never happen
0402     return i18n("Unknown transaction type");
0403 }
0404 
0405 QVariant RpmOstreeTransaction::icon() const
0406 {
0407     return QStringLiteral("application-x-rpm");
0408 }