diff --git a/CMakeLists.txt b/CMakeLists.txt index eb27238..5d64b6b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,53 +1,54 @@ cmake_minimum_required(VERSION 3.0) project(distroreleasenotifier) set(QT_MIN_VERSION "5.7.0") set(KF_MIN_VERSION "5.18.0") find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE) set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) include(KDEInstallDirs) include(KDECMakeSettings) include(KDECompilerSettings NO_POLICY_SCOPE) include(ECMInstallIcons) include(ECMQtDeclareLoggingCategory) include(FeatureSummary) find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core Gui Widgets) find_package(KF5 ${KF_MIN_VERSION} REQUIRED COMPONENTS CoreAddons I18n NetworkManagerQt Notifications + WidgetsAddons KIO ) # update flavor is derived from the installed metapackage(s) by pkg.split('-')[-1].captialize # it is therefore less accurate and good looking than /etc/os-release option(NAME_FROM_FLAVOR "Get distro name from update flavor [default is from /etc/os-release]" OFF) option(INSTALL_PREVIEW_UPGRADE "Whether to install a tiny helper binary to make pre-release upgrades more accessible. Also set PREVIEW_UPGRADE_NAME." OFF) set(PREVIEW_UPGRADE_NAME "distro-preview-upgrade" CACHE STRING "Name to install the upgrade preview helper as.") add_subdirectory(src) install(FILES distro-release-notifier.desktop DESTINATION ${KDE_INSTALL_AUTOSTARTDIR}) install(FILES releasechecker DESTINATION ${DATA_INSTALL_DIR}/distro-release-notifier PERMISSIONS OWNER_EXECUTE OWNER_READ OWNER_WRITE GROUP_EXECUTE GROUP_READ WORLD_EXECUTE WORLD_READ ) # Make it possible to use the po files fetched by the fetch-translations step ki18n_install(po) feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index d565e0b..7812555 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,39 +1,41 @@ if (${INSTALL_PREVIEW_UPGRADE}) add_subdirectory(preview) endif() configure_file(config.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config.h) set(distroreleasenotifier_SRCS main.cpp dbusinterface.cpp distroreleasenotifier.cpp notifier.cpp screensaverinhibitor.cpp upgraderwatcher.cpp + upgraderprocess.cpp ) ecm_qt_declare_logging_category(distroreleasenotifier_SRCS HEADER debug.h IDENTIFIER NOTIFIER CATEGORY_NAME org.kde.distro-release-notifier) qt5_generate_dbus_interface( dbusinterface.h ${CMAKE_CURRENT_BINARY_DIR}/org.kde.DistroReleaseNotifier.xml OPTIONS -a ) qt5_add_dbus_adaptor(distroreleasenotifier_SRCS ${CMAKE_CURRENT_BINARY_DIR}/org.kde.DistroReleaseNotifier.xml dbusinterface.h DBusInterface) add_executable(distro-release-notifier ${distroreleasenotifier_SRCS}) target_link_libraries(distro-release-notifier KF5::CoreAddons KF5::I18n KF5::NetworkManagerQt KF5::Notifications + KF5::WidgetsAddons Qt5::Network Qt5::Widgets ) install(TARGETS distro-release-notifier ${KDE_INSTALL_TARGETS_DEFAULT_ARGS}) diff --git a/src/distroreleasenotifier.cpp b/src/distroreleasenotifier.cpp index 6af9448..999b015 100644 --- a/src/distroreleasenotifier.cpp +++ b/src/distroreleasenotifier.cpp @@ -1,214 +1,211 @@ /* SPDX-FileCopyrightText: 2018 Jonathan Riddell SPDX-FileCopyrightText: 2018 Harald Sitter SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #include "distroreleasenotifier.h" #include #include #include #include #include #include #include #include #include "config.h" #include "dbusinterface.h" #include "debug.h" #include "notifier.h" +#include "upgraderprocess.h" DistroReleaseNotifier::DistroReleaseNotifier(QObject *parent) : QObject(parent) , m_dbus(new DBusInterface(this)) , m_checkerProcess(nullptr) , m_notifier(new Notifier(this)) , m_hasChecked(false) { // check after 10 seconds auto networkTimer = new QTimer(this); networkTimer->setSingleShot(true); networkTimer->setInterval(10 * 1000); connect(networkTimer, &QTimer::timeout, this, &DistroReleaseNotifier::releaseUpgradeCheck); networkTimer->start(); auto dailyTimer = new QTimer(this); dailyTimer->setInterval(24 * 60 * 60 * 1000); // refresh once every day connect(dailyTimer, &QTimer::timeout, this, &DistroReleaseNotifier::forceCheck); dailyTimer->start(); auto networkNotifier = NetworkManager::notifier(); connect(networkNotifier, &NetworkManager::Notifier::connectivityChanged, this, [networkTimer](NetworkManager::Connectivity connectivity) { if (connectivity == NetworkManager::Connectivity::Full) { // (re)start the timer. The timer will make sure we collect up // multiple signals arriving in quick succession into a single // check. networkTimer->start(); } }); connect(m_dbus, &DBusInterface::useDevelChanged, this, &DistroReleaseNotifier::forceCheck); connect(m_dbus, &DBusInterface::pollingRequested, this, &DistroReleaseNotifier::forceCheck); connect(m_notifier, &Notifier::activateRequested, this, &DistroReleaseNotifier::releaseUpgradeActivated); } DistroReleaseNotifier::~DistroReleaseNotifier() { } void DistroReleaseNotifier::releaseUpgradeCheck() { if (m_hasChecked) { // Don't check again if we had a successful check again. We don't wanna // be spamming the user with the notification. This is reset eventually // by a timer to remind the user. return; } const QString checkerFile = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("distro-release-notifier/releasechecker")); if (checkerFile.isEmpty()) { qCWarning(NOTIFIER) << "Couldn't find the releasechecker" << checkerFile << QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation); return; } if (m_checkerProcess) { // Guard against multiple polls from dbus qCDebug(NOTIFIER) << "Check still running"; return; } qCDebug(NOTIFIER) << "Running releasechecker"; m_checkerProcess = new QProcess(this); m_checkerProcess->setProcessChannelMode(QProcess::ForwardedErrorChannel); QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); // Force utf-8. In case the system has bogus encoding configured we'll still // be able to properly decode. env.insert("PYTHONIOENCODING", "utf-8"); if (m_dbus->useDevel()) { env.insert("USE_DEVEL", "1"); } m_checkerProcess->setProcessEnvironment(env); connect(m_checkerProcess, QOverload::of(&QProcess::finished), this, &DistroReleaseNotifier::checkReleaseUpgradeFinished); m_checkerProcess->start(QStringLiteral("/usr/bin/python3"), QStringList() << checkerFile); } void DistroReleaseNotifier::checkReleaseUpgradeFinished(int exitCode) { m_hasChecked = true; auto process = m_checkerProcess; m_checkerProcess->deleteLater(); m_checkerProcess = nullptr; const QByteArray checkerOutput = process->readAllStandardOutput(); // Make sure clearly invalid output doesn't get run through qjson at all. if (exitCode != 0 || checkerOutput.isEmpty()) { if (exitCode != 32) { // 32 is special exit on no new release qCWarning(NOTIFIER()) << "Failed to run releasechecker"; } else { qCDebug(NOTIFIER()) << "No new release found"; } return; } qCDebug(NOTIFIER) << checkerOutput; auto document = QJsonDocument::fromJson(checkerOutput); Q_ASSERT(document.isObject()); auto map = document.toVariant().toMap(); auto flavor = map.value(QStringLiteral("flavor")).toString(); m_version = map.value(QStringLiteral("new_dist_version")).toString(); m_name = NAME_FROM_FLAVOR ? flavor : KOSRelease().name(); // Download eol notification QNetworkAccessManager *manager = new QNetworkAccessManager(this); connect(manager, &QNetworkAccessManager::finished, this, &DistroReleaseNotifier::replyFinished); auto request = QNetworkRequest(QUrl("https://releases.neon.kde.org/eol.json")); request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); manager->get(request); } /* * Parses the eol.json file which is in a JSON hash of format release_version: eol_date * e.g. {"16.04": "2018-10-02"} */ void DistroReleaseNotifier::replyFinished(QNetworkReply *reply) { qCDebug(NOTIFIER) << reply->error(); if (reply->error() != QNetworkReply::NoError) { qCWarning(NOTIFIER) << reply->errorString(); } const QString versionId = KOSRelease().versionId(); const QByteArray eolOutput = reply->readAll(); const auto document = QJsonDocument::fromJson(eolOutput); if (!document.isObject()) { qCWarning(NOTIFIER) << "EOL reply failed to parse as document" << eolOutput; m_notifier->show(m_name, m_version, QDate()); return; } const auto map = document.toVariant().toMap(); auto dateString = map.value(versionId).toString(); if (qEnvironmentVariableIsSet("MOCK_RELEASE")) { // If this is a mock we'll construct the date string artifically. // Otherwise we'd have to run a server-side generator which is a bit // more tricky and detatches the code so if the format changes we may // easily forget. if (qEnvironmentVariableIsSet("MOCK_EOL")) { // already eol dateString = QDate::currentDate().addDays(-1).toString("yyyy-MM-dd"); } else { // eol in 3 days dateString = QDate::currentDate().addDays(3).toString("yyyy-MM-dd"); } } qCDebug(NOTIFIER) << "versionId:" << versionId; qCDebug(NOTIFIER) << "dateString" << dateString; m_notifier->show(m_name, m_version, QDate::fromString(dateString, Qt::ISODate)); return; } void DistroReleaseNotifier::releaseUpgradeActivated() { - // pkexec is being difficult. It will refuse to auth a startDetached service - // because it won't have a parent and parentless commands are not allowed - // to auth. - // Instead hold on to the process. - // For future reference: another approach is to sh -c and hold - // do-release-upgrade as a fork of that sh. - auto process = new QProcess(this); - process->setProcessChannelMode(QProcess::ForwardedChannels); - connect(process, QOverload::of(&QProcess::finished), - this, [process](){ process->deleteLater(); }); - auto args = QStringList({ - QStringLiteral("-m"), QStringLiteral("desktop"), - QStringLiteral("-f"), QStringLiteral("DistUpgradeViewKDE") - }); - if (m_dbus->useDevel()) { - args << "--devel-release"; + if (m_pendingUpgrader) { + // There's a time window between the user clicking upgrade and + // the UI registering on dbus. We don't know what's the state of + // things and consider the process pending. Should it fail we'll + // display the error via UpgraderProcess. + qCDebug(NOTIFIER) << "Upgrader requested but still waiting for one"; + return; } - process->start(QStringLiteral("do-release-upgrade"), args); + + m_pendingUpgrader = new UpgraderProcess; + m_pendingUpgrader->setUseDevel(m_dbus->useDevel()); + connect(m_pendingUpgrader, &UpgraderProcess::notPending, + this, [this]() { m_pendingUpgrader = nullptr; }); + m_pendingUpgrader->run(); // returns once we are sure the process is up and running } void DistroReleaseNotifier::forceCheck() { m_hasChecked = false; releaseUpgradeCheck(); } diff --git a/src/distroreleasenotifier.h b/src/distroreleasenotifier.h index 2d28d8a..3e13012 100644 --- a/src/distroreleasenotifier.h +++ b/src/distroreleasenotifier.h @@ -1,52 +1,57 @@ /* SPDX-FileCopyrightText: 2018 Jonathan Riddell SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ #ifndef DISTRORELEASENOTIFIER_H #define DISTRORELEASENOTIFIER_H #include class DBusInterface; class Notifier; class QProcess; class QNetworkReply; +class UpgraderProcess; class DistroReleaseNotifier : public QObject { Q_OBJECT public: /** * Default Constructor */ DistroReleaseNotifier(QObject *parent = nullptr); /** * Default Destructor */ ~DistroReleaseNotifier() override; private Q_SLOTS: void checkReleaseUpgradeFinished(int exitCode); void releaseUpgradeCheck(); void releaseUpgradeActivated(); void forceCheck(); void replyFinished(QNetworkReply *reply); private: DBusInterface *m_dbus; QProcess *m_checkerProcess; Notifier *m_notifier; // This acts as a safe guard. We listen to network device connections // to check on network connections. This can get super annoying for users // if we do in fact act on this a lot of times. So, instead this var // tracks if we ever had a successful check and if so prevents any further // checks from even running. bool m_hasChecked; QString m_name; QString m_version; + + // Upgrader is started but not yet on dbus = pending. + // This process auto-deleted itself. + UpgraderProcess *m_pendingUpgrader = nullptr; }; #endif // DISTRORELEASENOTIFIER_H diff --git a/src/upgraderprocess.cpp b/src/upgraderprocess.cpp new file mode 100644 index 0000000..0083e8d --- /dev/null +++ b/src/upgraderprocess.cpp @@ -0,0 +1,117 @@ +/* + SPDX-FileCopyrightText: 2018-2020 Harald Sitter + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "upgraderprocess.h" + +#include +#include +#include + +#include +#include +#include +#include +#include +#include + +#include "debug.h" +#include "upgraderwatcher.h" + +void UpgraderProcess::setUseDevel(bool useDevel) +{ + m_useDevel = useDevel; +} + +void UpgraderProcess::run() +{ + auto process = new QProcess(this); + connect(process, QOverload::of(&QProcess::finished), + this, [this](){ + m_waiting = false; + emit notPending(); + deleteLater(); + }); + + process->setProcessChannelMode(QProcess::MergedChannels); + connect(process, &QProcess::readyReadStandardOutput, + this, [this, process]() { + if (!NOTIFIER().isDebugEnabled() && !m_waiting) { + return; + } + + const QString newOutput = process->readAllStandardOutput(); + // route this through format string so newlines are preserved + qCDebug(NOTIFIER, "do-release-upgrader: %s\n", newOutput.toUtf8().constData()); + if (m_waiting) { + m_output += newOutput; + } + }); + + // Monitor dbus for the higher level UIs to appear. + // If the proc finishes before the dbus magic happens it likely crapped out in early + // checks which have zero UI backing, meaning we need to display stdout in a dialog. + auto unexpectedConnection = connect(process, QOverload::of(&QProcess::finished), + this, &UpgraderProcess::onUnexpectedFinish); + connect(UpgraderWatcher::self(), &UpgraderWatcher::upgraderRunning, + this, [this, unexpectedConnection]() { + m_waiting = false; + emit notPending(); + disconnect(unexpectedConnection); + }); + + // pkexec is being difficult. It will refuse to auth a startDetached service + // because it won't have a parent and parentless commands are not allowed + // to auth. + // Instead hold on to the process. + // For future reference: another approach is to sh -c and hold + // do-release-upgrade as a fork of that sh. + auto args = QStringList({ + QStringLiteral("-m"), QStringLiteral("desktop"), + QStringLiteral("-f"), QStringLiteral("DistUpgradeViewKDE") + }); + if (m_useDevel) { + args << "--devel-release"; + } + + qCDebug(NOTIFIER) << "Starting do-release-upgrade"; + process->start(QStringLiteral("do-release-upgrade"), args); +} + +void UpgraderProcess::onUnexpectedFinish(int code) +{ + // If the process finished within some seconds something is probably broken + // in the bootstrap. Display the output. + // Notably the upgrader exit(1) when it detects pending updates on apt and will only + // inform the user through stdout. It's very crappy UX. + + if (code == 0) { + qCWarning(NOTIFIER) << "Unexpected early exit but ignoring because it was code 0!"; + return; + } + + qCDebug(NOTIFIER) << "Probable failure" << code; + + QDialog dialog; + dialog.setWindowIcon(QIcon::fromTheme(QStringLiteral("system-software-update"))); + dialog.setWindowTitle(i18nc("@title/window upgrade failure dialog", "Upgrade Failed")); + auto layout = new QVBoxLayout; + dialog.setLayout(layout); + + auto title = new KTitleWidget(&dialog); + title->setText(i18nc("@title title widget above process output", + "Upgrade failed with the following output:")); + layout->addWidget(title); + + auto editor = new QTextEdit(&dialog); + editor->setReadOnly(true); + editor->setText(m_output.replace(QStringLiteral("ubuntu"), QStringLiteral("neon"), Qt::CaseInsensitive)); + layout->addWidget(editor); + + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Close, &dialog); + layout->addWidget(buttonBox); + connect(buttonBox, &QDialogButtonBox::rejected, &dialog, &QDialog::reject); + + dialog.exec(); +} diff --git a/src/upgraderprocess.h b/src/upgraderprocess.h new file mode 100644 index 0000000..f39bbcb --- /dev/null +++ b/src/upgraderprocess.h @@ -0,0 +1,38 @@ +/* + SPDX-FileCopyrightText: 2020 Harald Sitter + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#ifndef UPGRADERPROCESS_H +#define UPGRADERPROCESS_H + +#include + +/** + * Runs the upgrader. Possibly displayes UI if the upgrader craps out unexpectedly during + * startup. + */ +class UpgraderProcess : public QObject +{ + Q_OBJECT +public: + using QObject::QObject; + + void setUseDevel(bool useDevel); + void run(); + +signals: + // Either the process finished or it registered on dbus. + // Notifier should consider this launch concluded. + void notPending(); + +private slots: + void onUnexpectedFinish(int code); + +private: + bool m_useDevel = false; + bool m_waiting = true; // only true while we wait for the proc to fail + QString m_output; +}; + +#endif // UPGRADERPROCESS_H