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 "RpmOstreeNotifier.h" 0008 0009 #include <QDirIterator> 0010 #include <QFile> 0011 #include <QJsonArray> 0012 #include <QJsonDocument> 0013 #include <QJsonObject> 0014 #include <QProcess> 0015 #include <QTimer> 0016 #include <QVersionNumber> 0017 0018 RpmOstreeNotifier::RpmOstreeNotifier(QObject *parent) 0019 : BackendNotifierModule(parent) 0020 , m_version(QString()) 0021 , m_hasUpdates(false) 0022 , m_needsReboot(false) 0023 { 0024 // Refuse to run on systems not managed by rpm-ostree 0025 if (!isValid()) { 0026 qWarning() << "rpm-ostree-notifier: Not starting on a system not managed by rpm-ostree"; 0027 return; 0028 } 0029 0030 // Setup a watcher to trigger a check for reboot when the deployments are changed 0031 // and there is thus likely an new deployment installed following an update. 0032 m_watcher = new QFileSystemWatcher(this); 0033 0034 // We also setup a timer to avoid triggering a check immediately when a new 0035 // deployment is made available and instead wait a bit to let things settle down. 0036 m_timer = new QTimer(this); 0037 m_timer->setSingleShot(true); 0038 // Wait 10 seconds for all rpm-ostree operations to complete 0039 m_timer->setInterval(10000); 0040 connect(m_timer, &QTimer::timeout, this, &RpmOstreeNotifier::checkForPendingDeployment); 0041 0042 // Find all ostree managed system installations available. There is usualy only one but 0043 // doing that dynamically here avoids hardcoding a specific value or doing a DBus call. 0044 QDirIterator it(QStringLiteral("/ostree/deploy/"), QDir::AllDirs | QDir::NoDotAndDotDot); 0045 while (it.hasNext()) { 0046 QString path = QStringLiteral("%1/deploy/").arg(it.next()); 0047 m_watcher->addPath(path); 0048 qInfo() << "rpm-ostree-notifier: Looking for new deployments in" << path; 0049 } 0050 connect(m_watcher, &QFileSystemWatcher::directoryChanged, [this]() { 0051 m_timer->start(); 0052 }); 0053 0054 qInfo() << "rpm-ostree-notifier: Looking for ostree format"; 0055 m_process = new QProcess(this); 0056 m_stdout = QByteArray(); 0057 0058 // Display stderr 0059 connect(m_process, &QProcess::readyReadStandardError, [this]() { 0060 qWarning() << "rpm-ostree (error):" << m_process->readAllStandardError(); 0061 }); 0062 0063 // Store stdout to process as JSON 0064 connect(m_process, &QProcess::readyReadStandardOutput, [this]() { 0065 m_stdout += m_process->readAllStandardOutput(); 0066 }); 0067 0068 // Process command result 0069 connect(m_process, &QProcess::finished, [this](int exitCode, QProcess::ExitStatus exitStatus) { 0070 m_process->deleteLater(); 0071 m_process = nullptr; 0072 if (exitStatus != QProcess::NormalExit) { 0073 qWarning() << "rpm-ostree-notifier: Failed to check for existing deployments"; 0074 return; 0075 } 0076 if (exitCode != 0) { 0077 // Unexpected error 0078 qWarning() << "rpm-ostree-notifier: Failed to check for existing deployments. Exit code:" << exitCode; 0079 return; 0080 } 0081 0082 // Parse stdout as JSON and look at the currently booted deployments to figure out 0083 // the format used by ostree 0084 const QJsonDocument jsonDocument = QJsonDocument::fromJson(m_stdout); 0085 if (!jsonDocument.isObject()) { 0086 qWarning() << "rpm-ostree-notifier: Could not parse 'rpm-ostree status' output as JSON"; 0087 return; 0088 } 0089 const QJsonArray deployments = jsonDocument.object().value(QLatin1String("deployments")).toArray(); 0090 if (deployments.isEmpty()) { 0091 qWarning() << "rpm-ostree-notifier: Could not find the deployments in 'rpm-ostree status' JSON output"; 0092 return; 0093 } 0094 bool booted; 0095 for (const QJsonValue &deployment : deployments) { 0096 booted = deployment.toObject()[QLatin1String("booted")].toBool(); 0097 if (!booted) { 0098 continue; 0099 } 0100 // Look for "classic" ostree origin format first 0101 QString origin = deployment.toObject()[QLatin1String("origin")].toString(); 0102 if (!origin.isEmpty()) { 0103 m_ostreeFormat.reset(new ::OstreeFormat(::OstreeFormat::Format::Classic, origin)); 0104 if (!m_ostreeFormat->isValid()) { 0105 // This should never happen 0106 qWarning() << "rpm-ostree-notifier: Invalid origin for classic ostree format:" << origin; 0107 } 0108 } else { 0109 // Then look for OCI container format 0110 origin = deployment.toObject()[QLatin1String("container-image-reference")].toString(); 0111 if (!origin.isEmpty()) { 0112 m_ostreeFormat.reset(new ::OstreeFormat(::OstreeFormat::Format::OCI, origin)); 0113 if (!m_ostreeFormat->isValid()) { 0114 // This should never happen 0115 qWarning() << "rpm-ostree-notifier: Invalid reference for OCI container ostree format:" << origin; 0116 } 0117 } else { 0118 // This should never happen 0119 m_ostreeFormat.reset(new ::OstreeFormat(::OstreeFormat::Format::Unknown, {})); 0120 qWarning() << "rpm-ostree-notifier: Could not find a valid remote ostree format for the booted deployment"; 0121 } 0122 } 0123 // Look for the base-version first. This is the case where we have changes layered 0124 m_version = deployment.toObject()[QLatin1String("base-version")].toString(); 0125 if (m_version.isEmpty()) { 0126 // If empty, look for the regular version (no layered changes) 0127 m_version = deployment.toObject()[QLatin1String("version")].toString(); 0128 } 0129 } 0130 }); 0131 0132 m_process->start(QStringLiteral("rpm-ostree"), {QStringLiteral("status"), QStringLiteral("--json")}); 0133 m_process->waitForFinished(); 0134 } 0135 0136 bool RpmOstreeNotifier::isValid() const 0137 { 0138 return QFile::exists(QStringLiteral("/run/ostree-booted")); 0139 } 0140 0141 void RpmOstreeNotifier::recheckSystemUpdateNeeded() 0142 { 0143 // Refuse to run on systems not managed by rpm-ostree 0144 if (!isValid()) { 0145 qWarning() << "rpm-ostree-notifier: Not starting on a system not managed by rpm-ostree"; 0146 return; 0147 } 0148 0149 qInfo() << "rpm-ostree-notifier: Checking for system update"; 0150 if (m_ostreeFormat->isClassic()) { 0151 checkSystemUpdateClassic(); 0152 } else if (m_ostreeFormat->isOCI()) { 0153 checkSystemUpdateOCI(); 0154 } 0155 } 0156 0157 void RpmOstreeNotifier::checkSystemUpdateClassic() 0158 { 0159 qInfo() << "rpm-ostree-notifier: Checking for system update (classic format)"; 0160 0161 m_process = new QProcess(this); 0162 m_stdout = QByteArray(); 0163 0164 // Display stderr 0165 connect(m_process, &QProcess::readyReadStandardError, [this]() { 0166 qWarning() << "rpm-ostree (error):" << m_process->readAllStandardError(); 0167 }); 0168 0169 // Display and store stdout 0170 connect(m_process, &QProcess::readyReadStandardOutput, [this]() { 0171 QByteArray message = m_process->readAllStandardOutput(); 0172 qInfo() << "rpm-ostree:" << message; 0173 m_stdout += message; 0174 }); 0175 0176 // Process command result 0177 connect(m_process, &QProcess::finished, [this](int exitCode, QProcess::ExitStatus exitStatus) { 0178 m_process->deleteLater(); 0179 m_process = nullptr; 0180 if (exitStatus != QProcess::NormalExit) { 0181 qWarning() << "rpm-ostree-notifier: Failed to check for system update"; 0182 return; 0183 } 0184 if (exitCode == 77) { 0185 // rpm-ostree will exit with status 77 when no updates are available 0186 qInfo() << "rpm-ostree-notifier: No updates available"; 0187 return; 0188 } 0189 if (exitCode != 0) { 0190 qWarning() << "rpm-ostree-notifier: Failed to check for system update. Exit code:" << exitCode; 0191 return; 0192 } 0193 0194 // We have an update available. Let's look if we already have a pending 0195 // deployment for the new version. First, look for the new version 0196 // string in rpm-ostree stdout 0197 QString newVersion, line; 0198 QString output = QString::fromUtf8(m_stdout); 0199 QTextStream stream(&output); 0200 while (stream.readLineInto(&line)) { 0201 if (line.contains(QLatin1String("Version: "))) { 0202 newVersion = line; 0203 break; 0204 } 0205 } 0206 0207 // Could not find the new version in rpm-ostree output. This is unlikely 0208 // to ever happen. 0209 if (newVersion.isEmpty()) { 0210 qInfo() << "rpm-ostree-notifier: Could not find the version for the update available"; 0211 } 0212 0213 // Process the string to get just the version "number". 0214 newVersion = newVersion.trimmed(); 0215 newVersion.remove(0, QStringLiteral("Version: ").length()); 0216 newVersion.remove(newVersion.size() - QStringLiteral(" (XXXX-XX-XXTXX:XX:XXZ)").length(), newVersion.size() - 1); 0217 qInfo() << "rpm-ostree-notifier: Found new version:" << newVersion; 0218 0219 // Have we already notified the user about this update? 0220 if (newVersion == m_updateVersion) { 0221 qInfo() << "rpm-ostree-notifier: New version has already been offered. Skipping."; 0222 return; 0223 } 0224 m_updateVersion = newVersion; 0225 0226 // Look for an existing deployment with this version 0227 checkForPendingDeployment(); 0228 }); 0229 0230 m_process->start(QStringLiteral("rpm-ostree"), {QStringLiteral("update"), QStringLiteral("--check")}); 0231 } 0232 0233 void RpmOstreeNotifier::checkSystemUpdateOCI() 0234 { 0235 qInfo() << "rpm-ostree-notifier: Checking for system update (OCI format)"; 0236 0237 m_process = new QProcess(this); 0238 m_stdout = QByteArray(); 0239 0240 // Display stderr 0241 connect(m_process, &QProcess::readyReadStandardError, [this]() { 0242 qWarning() << "skopeo (error):" << m_process->readAllStandardError(); 0243 }); 0244 0245 // Store stdout to process as JSON 0246 connect(m_process, &QProcess::readyReadStandardOutput, [this]() { 0247 m_stdout += m_process->readAllStandardOutput(); 0248 }); 0249 0250 // Process command result 0251 connect(m_process, &QProcess::finished, [this](int exitCode, QProcess::ExitStatus exitStatus) { 0252 m_process->deleteLater(); 0253 m_process = nullptr; 0254 if (exitStatus != QProcess::NormalExit) { 0255 qWarning() << "rpm-ostree-notifier: Failed to check for updates via skopeo"; 0256 return; 0257 } 0258 if (exitCode != 0) { 0259 // Unexpected error 0260 qWarning() << "rpm-ostree-notifier: Failed to check for updates via skopeo. Exit code:" << exitCode; 0261 return; 0262 } 0263 0264 // Parse stdout as JSON and look at the container image labels for the version 0265 const QJsonDocument jsonDocument = QJsonDocument::fromJson(m_stdout); 0266 if (!jsonDocument.isObject()) { 0267 qWarning() << "rpm-ostree-notifier: Could not parse 'rpm-ostree status' output as JSON"; 0268 return; 0269 } 0270 0271 // Get the version stored in .Labels.version 0272 const QString newVersion = jsonDocument.object().value(QLatin1String("Labels")).toObject().value(QLatin1String("version")).toString(); 0273 if (newVersion.isEmpty()) { 0274 qInfo() << "rpm-ostree-notifier: Could not get the version from the container labels"; 0275 return; 0276 } 0277 0278 QVersionNumber newVersionNumber = QVersionNumber::fromString(newVersion); 0279 QVersionNumber currentVersionNumber = QVersionNumber::fromString(m_version); 0280 if (newVersionNumber <= currentVersionNumber) { 0281 qInfo() << "rpm-ostree-notifier: No new version found"; 0282 return; 0283 } 0284 0285 // Have we already notified the user about this update? 0286 if (newVersion == m_updateVersion) { 0287 qInfo() << "rpm-ostree-notifier: New version has already been offered. Skipping."; 0288 return; 0289 } 0290 m_updateVersion = newVersion; 0291 0292 // Look for an existing deployment with this version 0293 checkForPendingDeployment(); 0294 }); 0295 0296 // This will fail on non-remote transports (oci, oci-archive, containers-storage) but that's 0297 // OK as we can not check for updates in those cases. 0298 m_process->start(QStringLiteral("skopeo"), 0299 {QStringLiteral("inspect"), QStringLiteral("docker://") + m_ostreeFormat->repo() + QStringLiteral(":") + m_ostreeFormat->tag()}); 0300 } 0301 0302 void RpmOstreeNotifier::checkForPendingDeployment() 0303 { 0304 qInfo() << "rpm-ostree-notifier: Looking at existing deployments"; 0305 m_process = new QProcess(this); 0306 m_stdout = QByteArray(); 0307 0308 // Display stderr 0309 connect(m_process, &QProcess::readyReadStandardError, [this]() { 0310 QByteArray message = m_process->readAllStandardError(); 0311 qWarning() << "rpm-ostree (error):" << message; 0312 }); 0313 0314 // Store stdout to process as JSON 0315 connect(m_process, &QProcess::readyReadStandardOutput, [this]() { 0316 QByteArray message = m_process->readAllStandardOutput(); 0317 m_stdout += message; 0318 }); 0319 0320 // Process command result 0321 connect(m_process, &QProcess::finished, [this](int exitCode, QProcess::ExitStatus exitStatus) { 0322 m_process->deleteLater(); 0323 m_process = nullptr; 0324 if (exitStatus != QProcess::NormalExit) { 0325 qWarning() << "rpm-ostree-notifier: Failed to check for existing deployments"; 0326 return; 0327 } 0328 if (exitCode != 0) { 0329 // Unexpected error 0330 qWarning() << "rpm-ostree-notifier: Failed to check for existing deployments. Exit code:" << exitCode; 0331 return; 0332 } 0333 0334 // Parse stdout as JSON and look at the deployments for a pending 0335 // deployment for the new version. 0336 const QJsonDocument jsonDocument = QJsonDocument::fromJson(m_stdout); 0337 if (!jsonDocument.isObject()) { 0338 qWarning() << "rpm-ostree-notifier: Could not parse 'rpm-ostree status' output as JSON"; 0339 return; 0340 } 0341 const QJsonArray deployments = jsonDocument.object().value(QLatin1String("deployments")).toArray(); 0342 if (deployments.isEmpty()) { 0343 qWarning() << "rpm-ostree-notifier: Could not find the deployments in 'rpm-ostree status' JSON output"; 0344 return; 0345 } 0346 QString version; 0347 for (const QJsonValue &deployment : deployments) { 0348 version = deployment.toObject()[QLatin1String("base-version")].toString(); 0349 if (version.isEmpty()) { 0350 version = deployment.toObject()[QLatin1String("version")].toString(); 0351 } 0352 if (version.isEmpty()) { 0353 qInfo() << "rpm-ostree-notifier: Could not read version for deployment:" << deployment; 0354 continue; 0355 } 0356 if (version == m_updateVersion) { 0357 qInfo() << "rpm-ostree-notifier: Found an existing deployment for the update available"; 0358 if (!m_needsReboot) { 0359 qInfo() << "rpm-ostree-notifier: Notifying that a reboot is needed"; 0360 m_needsReboot = true; 0361 Q_EMIT needsRebootChanged(); 0362 } 0363 return; 0364 } 0365 } 0366 0367 // Reaching here means that no deployment has been found for the new version. 0368 qInfo() << "rpm-ostree-notifier: Notifying that a new update is available"; 0369 m_hasUpdates = true; 0370 Q_EMIT foundUpdates(); 0371 0372 // TODO: Look for security updates fixed by this new deployment 0373 }); 0374 0375 m_process->start(QStringLiteral("rpm-ostree"), {QStringLiteral("status"), QStringLiteral("--json")}); 0376 } 0377 0378 bool RpmOstreeNotifier::hasSecurityUpdates() 0379 { 0380 return false; 0381 } 0382 0383 bool RpmOstreeNotifier::needsReboot() const 0384 { 0385 return m_needsReboot; 0386 } 0387 0388 bool RpmOstreeNotifier::hasUpdates() 0389 { 0390 return m_hasUpdates; 0391 }