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 }