File indexing completed on 2024-04-21 05:45:11

0001 /*
0002     SPDX-FileCopyrightText: 2018 Jonathan Riddell <jr@jriddell.org>
0003     SPDX-FileCopyrightText: 2018 Harald Sitter <sitter@kde.org>
0004     SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
0005 */
0006 
0007 #include "distroreleasenotifier.h"
0008 
0009 #include <NetworkManagerQt/Manager>
0010 #include <KOSRelease>
0011 
0012 #include <QDate>
0013 #include <QJsonDocument>
0014 #include <QNetworkReply>
0015 #include <QProcess>
0016 #include <QStandardPaths>
0017 #include <QTimer>
0018 
0019 #include "config.h"
0020 #include "dbusinterface.h"
0021 #include "debug.h"
0022 #include "notifier.h"
0023 #include "upgraderprocess.h"
0024 
0025 DistroReleaseNotifier::DistroReleaseNotifier(QObject *parent)
0026     : QObject(parent)
0027     , m_dbus(new DBusInterface(this))
0028     , m_checkerProcess(nullptr)
0029     , m_notifier(new Notifier(this))
0030     , m_hasChecked(false)
0031 {
0032     // check after 10 seconds
0033     auto networkTimer = new QTimer(this);
0034     networkTimer->setSingleShot(true);
0035     networkTimer->setInterval(10 * 1000);
0036     connect(networkTimer, &QTimer::timeout, this, &DistroReleaseNotifier::releaseUpgradeCheck);
0037     networkTimer->start();
0038 
0039     auto dailyTimer = new QTimer(this);
0040     dailyTimer->setInterval(24 * 60 * 60 * 1000); // refresh once every day
0041     connect(dailyTimer, &QTimer::timeout,
0042             this, &DistroReleaseNotifier::forceCheck);
0043     dailyTimer->start();
0044 
0045     auto networkNotifier = NetworkManager::notifier();
0046     connect(networkNotifier, &NetworkManager::Notifier::connectivityChanged,
0047             this, [networkTimer](NetworkManager::Connectivity connectivity) {
0048         if (connectivity == NetworkManager::Connectivity::Full) {
0049             // (re)start the timer. The timer will make sure we collect up
0050             // multiple signals arriving in quick succession into a single
0051             // check.
0052             networkTimer->start();
0053         }
0054     });
0055 
0056     connect(m_dbus, &DBusInterface::useDevelChanged,
0057             this, &DistroReleaseNotifier::forceCheck);
0058     connect(m_dbus, &DBusInterface::pollingRequested,
0059             this, &DistroReleaseNotifier::forceCheck);
0060 
0061     connect(m_notifier, &Notifier::activateRequested,
0062             this, &DistroReleaseNotifier::releaseUpgradeActivated);
0063 }
0064 
0065 void DistroReleaseNotifier::releaseUpgradeCheck()
0066 {
0067     if (m_hasChecked) {
0068         // Don't check again if we had a successful check again. We don't wanna
0069         // be spamming the user with the notification. This is reset eventually
0070         // by a timer to remind the user.
0071         return;
0072     }
0073 
0074     const QString checkerFile =
0075             QStandardPaths::locate(QStandardPaths::GenericDataLocation,
0076                                    QStringLiteral("distro-release-notifier/releasechecker"));
0077     if (checkerFile.isEmpty()) {
0078         qCWarning(NOTIFIER) << "Couldn't find the releasechecker"
0079                             << checkerFile
0080                             << QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
0081         return;
0082     }
0083 
0084     if (m_checkerProcess) {
0085         // Guard against multiple polls from dbus
0086         qCDebug(NOTIFIER) << "Check still running";
0087         return;
0088     }
0089 
0090     qCDebug(NOTIFIER) << "Running releasechecker";
0091 
0092     m_checkerProcess = new QProcess(this);
0093     m_checkerProcess->setProcessChannelMode(QProcess::ForwardedErrorChannel);
0094     QProcessEnvironment env = QProcessEnvironment::systemEnvironment();
0095     // Force utf-8. In case the system has bogus encoding configured we'll still
0096     // be able to properly decode.
0097     env.insert(QStringLiteral("PYTHONIOENCODING"), QStringLiteral("utf-8"));
0098     if (m_dbus->useDevel()) {
0099         env.insert(QStringLiteral("USE_DEVEL"), QStringLiteral("1"));
0100     }
0101     m_checkerProcess->setProcessEnvironment(env);
0102     connect(m_checkerProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished),
0103             this, &DistroReleaseNotifier::checkReleaseUpgradeFinished);
0104     m_checkerProcess->start(QStringLiteral("/usr/bin/python3"), QStringList() << checkerFile);
0105 }
0106 
0107 void DistroReleaseNotifier::checkReleaseUpgradeFinished(int exitCode)
0108 {
0109     m_hasChecked = true;
0110 
0111     auto process = m_checkerProcess;
0112     m_checkerProcess->deleteLater();
0113     m_checkerProcess = nullptr;
0114 
0115     const QByteArray checkerOutput = process->readAllStandardOutput();
0116 
0117     // Make sure clearly invalid output doesn't get run through qjson at all.
0118     if (exitCode != 0 || checkerOutput.isEmpty()) {
0119         if (exitCode != 32) { // 32 is special exit on no new release
0120             qCWarning(NOTIFIER()) << "Failed to run releasechecker";
0121         } else {
0122             qCDebug(NOTIFIER()) << "No new release found";
0123         }
0124         return;
0125     }
0126 
0127     qCDebug(NOTIFIER) << checkerOutput;
0128     auto document = QJsonDocument::fromJson(checkerOutput);
0129     Q_ASSERT(document.isObject());
0130     auto map = document.toVariant().toMap();
0131     auto flavor = map.value(QStringLiteral("flavor")).toString();
0132     m_version = map.value(QStringLiteral("new_dist_version")).toString();
0133     m_name = NAME_FROM_FLAVOR ? flavor : KOSRelease().name();
0134 
0135     // Download eol notification
0136     QNetworkAccessManager *manager = new QNetworkAccessManager(this);
0137     connect(manager, &QNetworkAccessManager::finished,
0138             this, &DistroReleaseNotifier::replyFinished);
0139 
0140     auto request = QNetworkRequest(QUrl(QStringLiteral("https://releases.neon.kde.org/eol.json")));
0141     request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true);
0142     manager->get(request);
0143 }
0144 
0145 /*
0146  * Parses the eol.json file which is in a JSON hash of format release_version: eol_date
0147  * e.g. {"16.04": "2018-10-02"}
0148  */
0149 void DistroReleaseNotifier::replyFinished(QNetworkReply *reply)
0150 {
0151     qCDebug(NOTIFIER) << reply->error();
0152     if (reply->error() != QNetworkReply::NoError) {
0153         qCWarning(NOTIFIER) << reply->errorString();
0154     }
0155     const QString versionId = KOSRelease().versionId();
0156     const QByteArray eolOutput = reply->readAll();
0157     const auto document = QJsonDocument::fromJson(eolOutput);
0158     if (!document.isObject()) {
0159         qCWarning(NOTIFIER) << "EOL reply failed to parse as document" << eolOutput;
0160         m_notifier->show(m_name, m_version, QDate());
0161         return;
0162     }
0163     const auto map = document.toVariant().toMap();
0164     auto dateString = map.value(versionId).toString();
0165     if (qEnvironmentVariableIsSet("MOCK_RELEASE")) {
0166         // If this is a mock we'll construct the date string artificially.
0167         // Otherwise we'd have to run a server-side generator which is a bit
0168         // more tricky and detaches the code so if the format changes we may
0169         // easily forget.
0170         if (qEnvironmentVariableIsSet("MOCK_EOL")) {
0171             // already eol
0172             dateString = QDate::currentDate().addDays(-1).toString(u"yyyy-MM-dd");
0173         } else {
0174             // eol in 3 days
0175             dateString = QDate::currentDate().addDays(3).toString(u"yyyy-MM-dd");
0176         }
0177     }
0178     qCDebug(NOTIFIER) << "versionId:" << versionId;
0179     qCDebug(NOTIFIER) << "dateString" << dateString;
0180     m_notifier->show(m_name, m_version,
0181                      QDate::fromString(dateString, Qt::ISODate));
0182     return;
0183 }
0184 
0185 void DistroReleaseNotifier::releaseUpgradeActivated()
0186 {
0187     if (m_pendingUpgrader) {
0188         // There's a time window between the user clicking upgrade and
0189         // the UI registering on dbus. We don't know what's the state of
0190         // things and consider the process pending. Should it fail we'll
0191         // display the error via UpgraderProcess.
0192         qCDebug(NOTIFIER) << "Upgrader requested but still waiting for one";
0193         return;
0194     }
0195 
0196     m_pendingUpgrader = new UpgraderProcess;
0197     m_pendingUpgrader->setUseDevel(m_dbus->useDevel());
0198     connect(m_pendingUpgrader, &UpgraderProcess::notPending,
0199             this, [this]() { m_pendingUpgrader = nullptr; });
0200     m_pendingUpgrader->run(); // returns once we are sure the process is up and running
0201 }
0202 
0203 void DistroReleaseNotifier::forceCheck()
0204 {
0205     m_hasChecked = false;
0206     releaseUpgradeCheck();
0207 }