commit 29c1eb8abb70ac7fd19603b6ed65a2be80f15c9b Author: Aaron Rainbolt Date: Thu Jan 4 15:22:53 2024 -0600 Initial import diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..b9fd8a0 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,65 @@ +cmake_minimum_required(VERSION 3.5) + +project(lubuntu-update VERSION 0.1 LANGUAGES CXX) + +set(CMAKE_AUTOUIC ON) +set(CMAKE_AUTOMOC ON) +set(CMAKE_AUTORCC ON) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets LinguistTools) +find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets LinguistTools) + +set(TS_FILES translations/lubuntu-update_en_US.ts) + +set(PROJECT_SOURCES + main.cpp + mainwindow.cpp + mainwindow.h + mainwindow.ui + orchestrator.h + orchestrator.cpp + aptmanager.h + aptmanager.cpp + conffilewidget.h + conffilewidget.cpp + conffilewidget.ui + diffdisplaydialog.h + diffdisplaydialog.cpp + diffdisplaydialog.ui + conffilehandlerdialog.h + conffilehandlerdialog.cpp + conffilehandlerdialog.ui + ${TS_FILES} +) + +if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) + qt_add_executable(lubuntu-update + MANUAL_FINALIZATION + ${PROJECT_SOURCES} + resources.qrc + ) + + qt_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES}) +else() + add_executable(lubuntu-update + ${PROJECT_SOURCES} + resources.qrc + ) + + qt5_create_translation(QM_FILES ${CMAKE_SOURCE_DIR} ${TS_FILES}) +endif() + +target_link_libraries(lubuntu-update PRIVATE Qt${QT_VERSION_MAJOR}::Widgets) + +install(TARGETS lubuntu-update + BUNDLE DESTINATION . + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} +) + +if(QT_VERSION_MAJOR EQUAL 6) + qt_finalize_executable(lubuntu-update) +endif() diff --git a/README.md b/README.md new file mode 100644 index 0000000..6505502 --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +# Lubuntu Update + +Copyright (c) 2023-2024 Lubuntu Contributors. Licensed under the GNU General Public License version 2, or (at your option) any later version. + +Build dependencies are Qt 5.15 and cmake, runtime dependencies are apt, apt-get, and diff. + +To build: + +```bash +cd lubuntu-update +mkdir build +cd build +cmake .. +make -j$(nproc) +``` + +To use, copy the lubuntu-update-backend script to /usr/bin/lubuntu-update-backend, then compile and run the updater. It is highly recommended that you **do not** install the updater all the way into your system with `sudo make install` or similar. + +It is highly recommended that you use a Lubuntu virtual machine for testing and development. Use the latest development release if at all possible, unless you know you need to test an earlier release. + +Qt Creator is recommended for editing the code. It is present in Ubuntu's official repos and can be installed using `sudo apt install qtcreator`. diff --git a/aptmanager.cpp b/aptmanager.cpp new file mode 100644 index 0000000..4768702 --- /dev/null +++ b/aptmanager.cpp @@ -0,0 +1,315 @@ +#include "aptmanager.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +AptManager::AptManager(QObject *parent) + : QObject(parent) +{ +} + +void AptManager::applyFullUpgrade() +{ + internalUpdateProgress = 0; + internalUpdateInfo = getUpdateInfo(); + + // these six vars are used to track how far along things are for updating the progress bar + instUnpackList = instConfigList = internalUpdateInfo[0]; + upgradeUnpackList = upgradeConfigList = internalUpdateInfo[1]; + removeList = internalUpdateInfo[2]; + numPackagesToPrep = instUnpackList.count() + upgradeUnpackList.count(); + + aptProcess = new QProcess(); + aptProcess->setProgram("/usr/bin/lxqt-sudo"); + // Note that the lubuntu-update-backend script sets LC_ALL=C in it already, so we don't need to add that here. + aptProcess->setArguments(QStringList() << "/usr/bin/lubuntu-update-backend" << "doupdate"); + aptProcess->setProcessChannelMode(QProcess::MergedChannels); + QObject::connect(aptProcess, &QProcess::readyRead, this, &AptManager::handleProcessBuffer); + QObject::connect(aptProcess, QOverload::of(&QProcess::finished), this, &AptManager::handleProcessBuffer); + aptProcess->start(); +} + +void AptManager::keepConffile(QString conffile) +{ + aptProcess->write("keep\n"); + aptProcess->write(conffile.toUtf8() + "\n"); + aptProcess->waitForBytesWritten(); +} + +void AptManager::replaceConffile(QString conffile) +{ + aptProcess->write("replace\n"); + aptProcess->write(conffile.toUtf8() + "\n"); + aptProcess->waitForBytesWritten(); +} + +void AptManager::doneWithConffiles() +{ + aptProcess->write("done\n"); + aptProcess->waitForBytesWritten(); + aptProcess->closeWriteChannel(); +} + +void AptManager::handleProcessBuffer() +{ + int maxWaitTime = 20; + while (!aptProcess->canReadLine() && maxWaitTime > 0) { + // this is horrible why doesn't QProcess have canReadLine signal + QThread::msleep(20); + maxWaitTime--; + } + + QRegularExpression dlRegex("^Get:\\d+"); + char lineBuf[2048]; + + while(aptProcess->canReadLine()) { + aptProcess->readLine(lineBuf, 2048); + QString line = QString(lineBuf); + emit logLineReady(line); // this tells the main window to print the line to the log view + QRegularExpressionMatch dlLineMatch = dlRegex.match(line); + + // yes, this gave me a headache also + if (dlLineMatch.hasMatch()) { // Increments the progress counter for each package downloaded + internalUpdateProgress++; + } else if (line.count() >= 25 && line.left(24) == "Preparing to unpack .../" && numPackagesToPrep != 0) { + internalUpdateProgress++; // Increments the progress counter for each package that is "prepared to unpack" + numPackagesToPrep--; + } else if (line.count() >= 10 && line.left(9) == "Unpacking") { + /* + * Increments the progress counter for each package that is unpacked + * The package name may be suffixed with ":amd64" or some other + * :architecture string, so we have to split that off + * This and the subsequent progress updaters are complex since + * we don't want any line that starts with "Unpacking" or some + * other simple string to cause a progress increment. What if a + * maintainer script says it's unpacking something? + */ + QStringList parts = line.split(' '); + QString packageName; + if (parts.count() >= 2) { + packageName = parts[1].split(':')[0]; // strip off a trailing :amd64 if it's there + if (instUnpackList.removeAll(packageName) || upgradeUnpackList.removeAll(packageName)) { + internalUpdateProgress++; + } + } + } else if (line.count() >= 11 && line.left(10) == "Setting up") { + QStringList parts = line.split(' '); + QString packageName; + if (parts.count() >= 3) { + packageName = parts[2].split(':')[0]; + if (instConfigList.removeAll(packageName) || upgradeConfigList.removeAll(packageName)) { + internalUpdateProgress++; + } + } + } else if (line.count() >= 9 && line.left(8) == "Removing") { + QStringList parts = line.split(' '); + QString packageName; + if (parts.count() >= 2) { + packageName = parts[1].split(':')[0]; + if (removeList.removeAll(packageName)) { + internalUpdateProgress++; + } + } + } else if (line == "Lubuntu Update !!! CONFIGURATION FILE LIST START\r\n") { + // oh boy, conffile processing. Stop everything and get the full list of conffiles, busy-waiting as needed. + QStringList conffileList; + while (true) { + while (!aptProcess->canReadLine()) { + QThread::msleep(20); + } + aptProcess->readLine(lineBuf, 2048); + QString confLine = QString(lineBuf); + confLine = confLine.left(confLine.count() - 2); + if (confLine == "Lubuntu Update !!! CONFIGURATION FILE LIST END") { + emit conffileListReady(conffileList); // this triggers the main window to show the conffile handler window + break; + } else { + conffileList.append(confLine); + } + } + } + + double percentageDone = (static_cast(internalUpdateProgress) / (((internalUpdateInfo[0].count() + internalUpdateInfo[1].count()) * 4) + internalUpdateInfo[2].count())) * 100; + if (percentageDone > 100.0) { + percentageDone = 100.0; + } + emit progressUpdated(static_cast(percentageDone)); + } + + if (aptProcess->state() == QProcess::NotRunning) { + emit progressUpdated(100); // just in case the progress bar didn't fill all the way due to a previous partial download + emit updateComplete(); + aptProcess->deleteLater(); + } +} + +QList AptManager::getUpdateInfo() +{ + /* + * We use `apt-get -s full-upgrade` here rather than `apt list + * --upgradable` because it tells us not only what packages need upgraded, + * but also which packages need installed and removed, as well as which + * packages have been held back. The only thing this doesn't tell us is + * which packages are security updates, and for that we do use `apt list + * --upgradable`. + */ + + QList output; + + QProcess *checker = new QProcess(); + QProcessEnvironment checkerEnv; + checkerEnv.insert("LC_ALL", "C"); + checker->setProcessEnvironment(checkerEnv); + checker->setProgram("/usr/bin/apt-get"); + checker->setArguments(QStringList() << "-s" << "full-upgrade"); + checker->start(); + + if (checker->waitForFinished(60000)) { + QString stdoutString = QString(checker->readAllStandardOutput()); + QTextStream stdoutStream(&stdoutString); + QString stdoutLine; + + /* + * output[0] = packages to install + * output[1] = packages to upgrade + * output[2] = packages to remove + * output[3] = held packages + * output[5] = security-related updates, this is populated at the end + */ + + for (int i = 0; i < 4;i++) { + output.append(QStringList()); + } + + bool gettingInstallPackages = false; + bool gettingUpgradePackages = false; + bool gettingUninstallPackages = false; + bool gettingHeldPackages = false; + bool gettingPackageList = false; + + while (stdoutStream.readLineInto(&stdoutLine)) { + if (!gettingPackageList) { + parseAptLine(stdoutLine, &gettingInstallPackages, &gettingUpgradePackages, &gettingUninstallPackages, &gettingHeldPackages, &gettingPackageList); + } else { + /* + * Each line of output apt-get gives when displaying package + * lists is intended by two spaces. A package name will always + * consist of at least one character, so if there are less + * than three characters or the line isn't indented with two + * spaces, we know we're no longer reading a package list. + */ + + if (stdoutLine.count() < 3 || stdoutLine.left(2) != " ") { + gettingInstallPackages = false; + gettingUpgradePackages = false; + gettingUninstallPackages = false; + gettingHeldPackages = false; + gettingPackageList = false; + + parseAptLine(stdoutLine, &gettingInstallPackages, &gettingUpgradePackages, &gettingUninstallPackages, &gettingHeldPackages, &gettingPackageList); + } else { + QString packageLine = stdoutLine.trimmed(); + QStringList packageNames = packageLine.split(' '); + + if (gettingInstallPackages) { + for (int i = 0;i < packageNames.count();i++) { + output[0].append(packageNames[i]); + } + } else if (gettingUpgradePackages) { + for (int i = 0;i < packageNames.count();i++) { + output[1].append(packageNames[i]); + } + } else if (gettingUninstallPackages) { + for (int i = 0;i < packageNames.count();i++) { + output[2].append(packageNames[i]); + } + } else if (gettingHeldPackages) { + for (int i = 0;i < packageNames.count();i++) { + output[3].append(packageNames[i]); + } + } + } + } + } + + output.append(getSecurityUpdateList()); + } + checker->terminate(); + checker->deleteLater(); + return output; +} + +void AptManager::parseAptLine(QString line, bool *gettingInstallPackages, bool *gettingUpgradePackages, bool *gettingUninstallPackages, bool *gettingHeldPackages, bool *gettingPackageList) +{ + *gettingPackageList = true; + + if (line == "The following NEW packages will be installed:") { + *gettingInstallPackages = true; + } else if (line == "The following packages will be upgraded:") { + *gettingUpgradePackages = true; + } else if (line == "The following packages will be REMOVED:") { + *gettingUninstallPackages = true; + } else if (line == "The following packages have been kept back:") { + *gettingHeldPackages = true; + } else { + *gettingPackageList = false; + } +} + +QStringList AptManager::getSecurityUpdateList() +{ + QStringList updateList; + QString distroName; + + // Find the distro name + QFile lsbReleaseFile("/etc/lsb-release"); + lsbReleaseFile.open(QIODevice::ReadOnly); + QString contents = QString(lsbReleaseFile.readAll()); + lsbReleaseFile.close(); + QTextStream distroFinder(&contents); + QString distroLine; + while (distroFinder.readLineInto(&distroLine)) { + // The line has to be at least 18 characters long - 16 for the string "DISTRIB_CODENAME", one for the = sign, and one for a codename with a length of at least one. + if (distroLine.count() >= 18 && distroLine.left(16) == "DISTRIB_CODENAME") { + QStringList distroParts = distroLine.split('='); + if (distroParts.count() >= 2) { + distroName = distroParts[1]; + } + } + } + + // Now check for security updates + QProcess *checker = new QProcess(); + QProcessEnvironment checkerEnv; + checkerEnv.insert("LC_ALL", "C"); + checker->setProcessEnvironment(checkerEnv); + checker->setProgram("/usr/bin/apt"); + checker->setArguments(QStringList() << "list" << "--upgradable"); + checker->start(); + + if (checker->waitForFinished(60000)) { + QRegularExpression regex(QString("%1-security").arg(distroName)); + QString stdoutString = checker->readAllStandardOutput(); + QTextStream stdoutStream(&stdoutString); + QString stdoutLine; + + while (stdoutStream.readLineInto(&stdoutLine)) { + QRegularExpressionMatch match = regex.match(stdoutLine); + if (match.hasMatch()) { + // The package name we want is followed immediately by a single forward slash, so we can use that as our delimiter. + QStringList updateParts = stdoutLine.split('/'); + updateList.append(updateParts[0]); + } + } + } + + checker->terminate(); + checker->deleteLater(); + return updateList; +} diff --git a/aptmanager.h b/aptmanager.h new file mode 100644 index 0000000..19d16cb --- /dev/null +++ b/aptmanager.h @@ -0,0 +1,47 @@ +#ifndef APTMANAGER_H +#define APTMANAGER_H + +#include +#include + +class QProcess; +class QProgressBar; +class QPlainTextEdit; +class QSystemTrayIcon; + +class AptManager : public QObject +{ + Q_OBJECT + +public: + AptManager(QObject *parent = nullptr); + static QList getUpdateInfo(); + void applyFullUpgrade(); + void keepConffile(QString conffile); + void replaceConffile(QString conffile); + void doneWithConffiles(); + +signals: + void updateComplete(); + void progressUpdated(int progress); + void logLineReady(QString logLine); + void conffileListReady(QStringList conffileList); + +private slots: + void handleProcessBuffer(); + +private: + static void parseAptLine(QString line, bool *gettingInstallPackages, bool *gettingUpgradePackages, bool *gettingUninstallPackages, bool *gettingHeldPackages, bool *gettingPackageList); + static QStringList getSecurityUpdateList(); + QProcess *aptProcess; + QList internalUpdateInfo; + QStringList instUnpackList; + QStringList instConfigList; + QStringList upgradeUnpackList; + QStringList upgradeConfigList; + QStringList removeList; + quint32 numPackagesToPrep; + quint32 internalUpdateProgress; +}; + +#endif // APTMANAGER_H diff --git a/conffilehandlerdialog.cpp b/conffilehandlerdialog.cpp new file mode 100644 index 0000000..64934e5 --- /dev/null +++ b/conffilehandlerdialog.cpp @@ -0,0 +1,37 @@ +#include "conffilehandlerdialog.h" +#include "conffilewidget.h" +#include "ui_conffilehandlerdialog.h" + +ConffileHandlerDialog::ConffileHandlerDialog(const QStringList &conffiles, QWidget *parent) : + QDialog(parent), + ui(new Ui::ConffileHandlerDialog) +{ + ui->setupUi(this); + for (QString conffile : conffiles) { + ConffileWidget *conffileWidget = new ConffileWidget(conffile); + conffileList.append(conffileWidget); + ui->conffileStack->insertWidget(ui->conffileStack->count() - 1, conffileWidget); + } + + connect(ui->doneButton, &QPushButton::clicked, this, &ConffileHandlerDialog::onDoneClicked); +} + +ConffileHandlerDialog::~ConffileHandlerDialog() +{ + delete ui; +} + +QStringList ConffileHandlerDialog::getReplaceConffileList() +{ + return replaceConffileList; +} + +void ConffileHandlerDialog::onDoneClicked() +{ + for (ConffileWidget *w : conffileList) { + if (w->doReplace()) { + replaceConffileList.append(w->filename()); + } + } + this->done(0); +} diff --git a/conffilehandlerdialog.h b/conffilehandlerdialog.h new file mode 100644 index 0000000..b93d771 --- /dev/null +++ b/conffilehandlerdialog.h @@ -0,0 +1,31 @@ +#ifndef CONFFILEHANDLERDIALOG_H +#define CONFFILEHANDLERDIALOG_H + +#include +#include + +class ConffileWidget; + +namespace Ui { +class ConffileHandlerDialog; +} + +class ConffileHandlerDialog : public QDialog +{ + Q_OBJECT + +public: + explicit ConffileHandlerDialog(const QStringList &conffiles, QWidget *parent = nullptr); + ~ConffileHandlerDialog(); + QStringList getReplaceConffileList(); + +private slots: + void onDoneClicked(); + +private: + Ui::ConffileHandlerDialog *ui; + QList conffileList; + QStringList replaceConffileList; +}; + +#endif // CONFFILEHANDLERDIALOG_H diff --git a/conffilehandlerdialog.ui b/conffilehandlerdialog.ui new file mode 100644 index 0000000..98012d5 --- /dev/null +++ b/conffilehandlerdialog.ui @@ -0,0 +1,93 @@ + + + ConffileHandlerDialog + + + + 0 + 0 + 937 + 399 + + + + Configuration File Conflicts + + + + :/res/images/update.svg:/res/images/update.svg + + + + + + <html><head/><body><p>Some of the newly installed updates have updated configuration files. </p><p>Please choose what to do with these files.</p></body></html> + + + + + + + Qt::ScrollBarAsNeeded + + + true + + + + + 0 + 0 + 917 + 294 + + + + + + + Qt::Vertical + + + + 20 + 190 + + + + + + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Done + + + + + + + + + + + + diff --git a/conffilewidget.cpp b/conffilewidget.cpp new file mode 100644 index 0000000..c716a07 --- /dev/null +++ b/conffilewidget.cpp @@ -0,0 +1,45 @@ +#include "conffilewidget.h" +#include "diffdisplaydialog.h" +#include "ui_conffilewidget.h" + +#include + +#include + +ConffileWidget::ConffileWidget(QString filename, QWidget *parent) : + QWidget(parent), + ui(new Ui::ConffileWidget) +{ + ui->setupUi(this); + internalFilename = filename; + ui->filenameLabel->setText(filename); + ui->keepRadioButton->setChecked(true); + connect(ui->showDiffButton, &QPushButton::clicked, this, &ConffileWidget::onDiffClicked); +} + +ConffileWidget::~ConffileWidget() +{ + delete ui; +} + +QString ConffileWidget::filename() +{ + return internalFilename; +} + +bool ConffileWidget::doReplace() +{ + return ui->replaceRadioButton->isChecked(); +} + +void ConffileWidget::onDiffClicked() +{ + QProcess diffProcess; + diffProcess.setProgram("/usr/bin/diff"); + diffProcess.setArguments(QStringList() << "-u" << internalFilename << internalFilename + ".dpkg-dist"); + diffProcess.start(); + diffProcess.waitForFinished(); + QString result = diffProcess.readAllStandardOutput(); + DiffDisplayDialog ddw(internalFilename, result); + ddw.exec(); +} diff --git a/conffilewidget.h b/conffilewidget.h new file mode 100644 index 0000000..9a7a76f --- /dev/null +++ b/conffilewidget.h @@ -0,0 +1,28 @@ +#ifndef CONFFILEWIDGET_H +#define CONFFILEWIDGET_H + +#include + +namespace Ui { +class ConffileWidget; +} + +class ConffileWidget : public QWidget +{ + Q_OBJECT + +public: + explicit ConffileWidget(QString filename, QWidget *parent = nullptr); + ~ConffileWidget(); + QString filename(); + bool doReplace(); + +private slots: + void onDiffClicked(); + +private: + Ui::ConffileWidget *ui; + QString internalFilename; +}; + +#endif // CONFFILEWIDGET_H diff --git a/conffilewidget.ui b/conffilewidget.ui new file mode 100644 index 0000000..8a014bd --- /dev/null +++ b/conffilewidget.ui @@ -0,0 +1,62 @@ + + + ConffileWidget + + + + 0 + 0 + 557 + 43 + + + + Form + + + + + + Filename + + + + + + + Qt::Horizontal + + + + 181 + 20 + + + + + + + + Keep old + + + + + + + Replace with new + + + + + + + Show diff + + + + + + + + diff --git a/diffdisplaydialog.cpp b/diffdisplaydialog.cpp new file mode 100644 index 0000000..b43140a --- /dev/null +++ b/diffdisplaydialog.cpp @@ -0,0 +1,22 @@ +#include "diffdisplaydialog.h" +#include "ui_diffdisplaydialog.h" + +DiffDisplayDialog::DiffDisplayDialog(QString filename, QString diff, QWidget *parent) : + QDialog(parent), + ui(new Ui::DiffDisplayDialog) +{ + ui->setupUi(this); + this->setWindowTitle(filename + " diff"); + ui->diffView->setPlainText(diff); + connect(ui->doneButton, &QPushButton::clicked, this, &DiffDisplayDialog::onDoneClicked); +} + +DiffDisplayDialog::~DiffDisplayDialog() +{ + delete ui; +} + +void DiffDisplayDialog::onDoneClicked() +{ + this->done(0); +} diff --git a/diffdisplaydialog.h b/diffdisplaydialog.h new file mode 100644 index 0000000..e3d06db --- /dev/null +++ b/diffdisplaydialog.h @@ -0,0 +1,25 @@ +#ifndef DIFFDISPLAYDIALOG_H +#define DIFFDISPLAYDIALOG_H + +#include + +namespace Ui { +class DiffDisplayDialog; +} + +class DiffDisplayDialog : public QDialog +{ + Q_OBJECT + +public: + explicit DiffDisplayDialog(QString filename, QString diff, QWidget *parent = nullptr); + ~DiffDisplayDialog(); + +private slots: + void onDoneClicked(); + +private: + Ui::DiffDisplayDialog *ui; +}; + +#endif // DIFFDISPLAYDIALOG_H diff --git a/diffdisplaydialog.ui b/diffdisplaydialog.ui new file mode 100644 index 0000000..f690a48 --- /dev/null +++ b/diffdisplaydialog.ui @@ -0,0 +1,68 @@ + + + DiffDisplayDialog + + + + 0 + 0 + 704 + 553 + + + + Dialog + + + + :/res/images/update.svg:/res/images/update.svg + + + + + + <html><head/><body><p>Lines that start with a &quot;+&quot; only exist in the <span style=" font-weight:700;">new</span> file.</p><p>Lines that start with a &quot;-&quot; only exist in the <span style=" font-weight:700;">old</span> file.</p></body></html> + + + + + + + font: 9pt "Monospace"; + + + true + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Done + + + + + + + + + + + + diff --git a/lubuntu-update-backend b/lubuntu-update-backend new file mode 100755 index 0000000..1768a30 --- /dev/null +++ b/lubuntu-update-backend @@ -0,0 +1,92 @@ +#!/bin/bash +# Backend script for Lubuntu Update (does text processing and update installation, can be run as root and must be in order to install updates) + +set -e +export LC_ALL='C' + +if [ "$1" = 'pkgver' ]; then + shift + while [ "$1" != '' ]; do + source="$(apt-cache show "$1" | grep 'Source:' | cut -d' ' -f2)" + version="$(apt-cache show lubuntu-default-settings | grep Version: | head -n1 | cut -d' ' -f2)" + if [ "$source" = '' ]; then + echo "$1" + else + echo "$source" + fi + echo "$version" + shift + done +elif [ "$1" = 'doupdate' ]; then + # Prepare to be able to grep through the logs + rm /run/lubuntu-update-apt-log || true + touch /run/lubuntu-update-apt-log + chmod 0600 /run/lubuntu-update-apt-log # prevent non-root from being able to trick the script into deleting arbitrary files later on + + # Repair an interrupted upgrade if necessary + dpkg --configure -a + + # Run the real update + DEBIAN_FRONTEND='kde' apt-get -o Dpkg::Options::='--force-confdef' -o Dpkg::Options::='--force-confold' -o Apt::Color='0' -o Dpkg::Use-Pty='0' -y dist-upgrade |& tee /run/lubuntu-update-apt-log + + # Find all the conffiles + mapfile conffileRawList <<< "$(grep -P "^Configuration file \'.*\'$" '/run/lubuntu-update-apt-log')" + if [ "$(echo "${conffileRawList[0]}" | head -c1)" != 'C' ]; then # Empty or invalid list, we're done + exit 0 + fi + + conffileList=() + counter=0 + while [ "$counter" -lt "${#conffileRawList[@]}" ]; do + # Cut off "Configuration file '" from the start and "'" plus a couple trailing characters from the end + conffileList[counter]="$(echo "${conffileRawList[$counter]}" | tail -c+21 | head -c-3)" + counter=$((counter+1)) + done + + echo "Lubuntu Update !!! CONFIGURATION FILE LIST START"; + counter=0 + while [ "$counter" -lt "${#conffileList[@]}" ]; do + echo "${conffileList[$counter]}" + counter=$((counter+1)) + done + echo "Lubuntu Update !!! CONFIGURATION FILE LIST END"; + + # If we make it this far, there were conffiles to deal with + breakLoop='no' + gotCommand='no' + commandName='' + while [ "$breakLoop" = 'no' ]; do + read -r inputVal + if [ "$gotCommand" = 'no' ]; then + if [ "$inputVal" = 'done' ]; then + breakLoop='yes' + else + commandName="$inputVal" + gotCommand='yes' + fi + else + if [ "$commandName" = 'replace' ]; then # Replace an existing file + counter=0 + while [ "$counter" -lt "${#conffileList[@]}" ]; do + if [ "$inputVal" = "${conffileList[$counter]}" ]; then + mv "$inputVal.dpkg-dist" "$inputVal" + break + fi + counter=$((counter+1)) + done + elif [ "$commandName" = 'keep' ]; then # Keep an existing file + counter=0 + while [ "$counter" -lt "${#conffileList[@]}" ]; do + if [ "$inputVal" = "${conffileList[$counter]}" ]; then + rm "$inputVal.dpkg-dist" + break + fi + counter=$((counter+1)) + done + fi + gotCommand='no' + fi + done + + echo 'Update installation complete.' +fi diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..5d4a342 --- /dev/null +++ b/main.cpp @@ -0,0 +1,50 @@ +#include "orchestrator.h" +#include "mainwindow.h" +#include "conffilehandlerdialog.h" + +#include +#include +#include +#include + +int main(int argc, char *argv[]) +{ + QApplication a(argc, argv); + + QTranslator translator; + const QStringList uiLanguages = QLocale::system().uiLanguages(); + for (const QString &locale : uiLanguages) { + const QString baseName = "lubuntu-update_" + QLocale(locale).name(); + if (translator.load(":/i18n/" + baseName)) { + a.installTranslator(&translator); + break; + } + } + // Don't want the updater to stop just because the user closed it :P + a.setQuitOnLastWindowClosed(false); + + /* + * As this is a background process, we don't pop up any window upon + * startup. An Orchestrator object periodically checks to see if new + * updates have been detected, and offers them to the user (by displaying + * a tray icon) if so. The user can click on this tray icon to see the + * updater window. + * + * Orchestrator's constructor automatically starts the update checker, so + * there's no need to do anything with this except create it and then + * start the event loop. + */ + + Orchestrator *o = new Orchestrator(); + + /* + * This is an artifact from testing the conffile handler window. You can + * uncomment this and rebuild lubuntu-update in order to test the conffile + * handler UI and develop it further. + * + * ConffileHandlerDialog cfhd(QStringList() << "/home/user/testfile"); + * cfhd.show(); + */ + + return a.exec(); +} diff --git a/mainwindow.cpp b/mainwindow.cpp new file mode 100644 index 0000000..506fbeb --- /dev/null +++ b/mainwindow.cpp @@ -0,0 +1,130 @@ +#include "aptmanager.h" +#include "conffilehandlerdialog.h" +#include "mainwindow.h" +#include "./ui_mainwindow.h" + +#include +#include + +MainWindow::MainWindow(QWidget *parent) + : QMainWindow(parent) + , ui(new Ui::MainWindow) +{ + ui->setupUi(this); + aptManager = new AptManager(); + + // The progress bar and log view are shown after the user chooses to begin installing updates + ui->progressBar->setVisible(false); + ui->logView->setVisible(false); + + connect(ui->installButton, &QPushButton::clicked, this, &MainWindow::onInstallButtonClicked); + connect(ui->closeButton, &QPushButton::clicked, this, &MainWindow::onCloseButtonClicked); + connect(aptManager, &AptManager::updateComplete, this, &MainWindow::onUpdateCompleted); + connect(aptManager, &AptManager::progressUpdated, this, &MainWindow::onProgressUpdate); + connect(aptManager, &AptManager::logLineReady, this, &MainWindow::onLogLineReady); + connect(aptManager, &AptManager::conffileListReady, this, &MainWindow::onConffileListReady); +} + +MainWindow::~MainWindow() +{ + delete ui; +} + +void MainWindow::setUpdateInfo(QList updateInfo) +{ + ui->packageView->clear(); + // The progress bar and log view are shown after the user chooses to begin installing updates + ui->progressBar->setVisible(false); + ui->logView->setVisible(false); + ui->installButton->setEnabled(true); + ui->detailsButton->setEnabled(true); + ui->closeButton->setEnabled(true); + + for (int i = 0;i < 4;i++) { + QTreeWidgetItem *installItem; + switch (i) { + case 0: + installItem = new QTreeWidgetItem(QStringList() << "To be installed"); + break; + case 1: + installItem = new QTreeWidgetItem(QStringList() << "To be upgraded"); + break; + case 2: + installItem = new QTreeWidgetItem(QStringList() << "To be removed"); + break; + case 3: + installItem = new QTreeWidgetItem(QStringList() << "Held back (usually temporarily)"); + break; + } + + for (int j = 0;j < updateInfo[i].count();j++) { + // TODO: Add security update detection here - security updates should be marked in some way + installItem->addChild(new QTreeWidgetItem(QStringList() << updateInfo[i][j])); + } + + ui->packageView->addTopLevelItem(installItem); + } + ui->statLabel->setText(QString("%1 package(s) will be updated. %2 of these updates are security-related.") + .arg(QString::number(updateInfo[0].count() + updateInfo[1].count() + updateInfo[2].count()), + QString::number(updateInfo[4].count()))); +} + +void MainWindow::closeEvent(QCloseEvent *event) +{ + /* + * Don't allow the user to close the window with the close button if we've + * disabled the normal "Close" button. + */ + + if (!ui->closeButton->isEnabled()) { + event->ignore(); + } +} + +void MainWindow::onInstallButtonClicked() +{ + ui->progressBar->setVisible(true); + ui->logView->setVisible(true); + ui->installButton->setEnabled(false); + ui->detailsButton->setEnabled(false); + ui->closeButton->setEnabled(false); + aptManager->applyFullUpgrade(); +} + +void MainWindow::onCloseButtonClicked() +{ + hide(); +} + +void MainWindow::onUpdateCompleted() +{ + ui->closeButton->setEnabled(true); + ui->progressBar->setVisible(false); + ui->statLabel->setText("Update installation complete."); + emit updatesInstalled(); // this tells the orchestrator to hide the tray icon +} + +void MainWindow::onProgressUpdate(int progress) +{ + ui->progressBar->setValue(progress); +} + +void MainWindow::onLogLineReady(QString logLine) +{ + ui->logView->textCursor().insertText(logLine); + ui->logView->moveCursor(QTextCursor::End); +} + +void MainWindow::onConffileListReady(QStringList conffileList) +{ + ConffileHandlerDialog chd(conffileList); + chd.exec(); + for (QString single : chd.getReplaceConffileList()) { + conffileList.removeAll(single); + aptManager->replaceConffile(single); + } + for (QString single : conffileList) { + aptManager->keepConffile(single); + } + aptManager->doneWithConffiles(); +} diff --git a/mainwindow.h b/mainwindow.h new file mode 100644 index 0000000..f2ef2b7 --- /dev/null +++ b/mainwindow.h @@ -0,0 +1,43 @@ +#ifndef MAINWINDOW_H +#define MAINWINDOW_H + +#include "aptmanager.h" + +#include +#include +#include + +class QSystemTrayIcon; + +QT_BEGIN_NAMESPACE +namespace Ui { class MainWindow; } +QT_END_NAMESPACE + +class MainWindow : public QMainWindow +{ + Q_OBJECT + +public: + MainWindow(QWidget *parent = nullptr); + ~MainWindow(); + void setUpdateInfo(QList updateInfo); + +signals: + void updatesInstalled(); + +protected slots: + void closeEvent(QCloseEvent *event) override; + +private slots: + void onInstallButtonClicked(); + void onCloseButtonClicked(); + void onUpdateCompleted(); + void onProgressUpdate(int progress); + void onLogLineReady(QString logLine); + void onConffileListReady(QStringList conffileList); + +private: + Ui::MainWindow *ui; + AptManager *aptManager; +}; +#endif // MAINWINDOW_H diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..cb0935b --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,111 @@ + + + MainWindow + + + + 0 + 0 + 462 + 600 + + + + Lubuntu Update + + + + :/res/images/update.svg:/res/images/update.svg + + + + + + + 0 package(s) will be updated. 0 of these updates are security-related. + + + + + + + + Packages + + + + + + + + 0 + + + false + + + + + + + + 0 + 0 + + + + + 16777215 + 100 + + + + background-color: rgb(0, 0, 0); +color: rgb(255, 255, 255); +font: 9pt "Monospace"; + + + true + + + + + + + + + + + + Install Updates + + + + :/res/images/update.svg:/res/images/update.svg + + + + + + + Details + + + + + + + Close + + + + + + + + + + + + + diff --git a/orchestrator.cpp b/orchestrator.cpp new file mode 100644 index 0000000..c2a651d --- /dev/null +++ b/orchestrator.cpp @@ -0,0 +1,53 @@ +#include "orchestrator.h" +#include "mainwindow.h" +#include "aptmanager.h" + +#include +#include +#include + +Orchestrator::Orchestrator(QObject *parent) + : QObject{parent} +{ + checkTimer = new QTimer(); // every time this triggers, the apt database is checked for new updates + trayIcon = new QSystemTrayIcon(); // this is shown to the user to offer updates + + connect(checkTimer, &QTimer::timeout, this, &Orchestrator::checkForUpdates); + connect(trayIcon, &QSystemTrayIcon::activated, this, [this](){this->displayUpdater(updateInfo);}); + connect(&updaterWindow, &MainWindow::updatesInstalled, this, &Orchestrator::handleUpdatesInstalled); + + checkTimer->start(21600000); // check four times a day, at least one of those times unattended-upgrades should have refreshed the apt database + + checkForUpdates(); // check immediately after launch, which usually will be immediately after boot or login +} + +/* + * Checks the apt database to see if updated software is available, and places + * the results in the updateInfo variable. If updated software is available, + * show the system tray icon and trigger a notification. + */ +void Orchestrator::checkForUpdates() +{ + updateInfo = AptManager::getUpdateInfo(); + if (!updateInfo[0].isEmpty() || !updateInfo[1].isEmpty() || !updateInfo[2].isEmpty() || !updateInfo[3].isEmpty()) { // no need to check updateInfo[4], it will only ever contain security updates already listed in updateInfo[1] + trayIcon->setIcon(QPixmap(":/res/images/update.svg")); + trayIcon->show(); + // Yes, we do intentionally use updateInfo[1], then updateInfo[0], then updateInfo[2]. The updateInfo array is populated in a different order than the one we display in. + trayIcon->showMessage("", + QString("Updates available!\n\n%1 to upgrade, %2 to install, and %3 to remove.\n\nClick the tray icon to install the updates.") + .arg(QString::number(updateInfo[1].count()), QString::number(updateInfo[0].count()), QString::number(updateInfo[2].count()))); + } +} + +void Orchestrator::displayUpdater(QList updateInfo) +{ + if (!updaterWindow.isVisible()) { + updaterWindow.setUpdateInfo(updateInfo); + updaterWindow.show(); + } +} + +void Orchestrator::handleUpdatesInstalled() +{ + trayIcon->hide(); +} diff --git a/orchestrator.h b/orchestrator.h new file mode 100644 index 0000000..64864f1 --- /dev/null +++ b/orchestrator.h @@ -0,0 +1,30 @@ +#ifndef ORCHESTRATOR_H +#define ORCHESTRATOR_H + +#include "mainwindow.h" + +#include +#include + +class QTimer; +class QSystemTrayIcon; + +class Orchestrator : public QObject +{ + Q_OBJECT +public: + explicit Orchestrator(QObject *parent = nullptr); + +private slots: + void checkForUpdates(); + void displayUpdater(QList updateInfo); + void handleUpdatesInstalled(); + +private: + QTimer *checkTimer; + QSystemTrayIcon *trayIcon; + QList updateInfo; + MainWindow updaterWindow; +}; + +#endif // ORCHESTRATOR_H diff --git a/res/images/update.svg b/res/images/update.svg new file mode 100644 index 0000000..85b5322 --- /dev/null +++ b/res/images/update.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + diff --git a/resources.qrc b/resources.qrc new file mode 100644 index 0000000..806b389 --- /dev/null +++ b/resources.qrc @@ -0,0 +1,5 @@ + + + res/images/update.svg + + diff --git a/translations/lubuntu-update_en_US.ts b/translations/lubuntu-update_en_US.ts new file mode 100644 index 0000000..edd0d34 --- /dev/null +++ b/translations/lubuntu-update_en_US.ts @@ -0,0 +1,3 @@ + + +