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 }