Initial import

tsimonq2/null-packages-fix
Aaron Rainbolt 12 months ago
commit 29c1eb8abb

@ -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()

@ -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`.

@ -0,0 +1,315 @@
#include "aptmanager.h"
#include <QFile>
#include <QProcess>
#include <QProgressBar>
#include <QRegularExpression>
#include <QPlainTextEdit>
#include <QSystemTrayIcon>
#include <QTextStream>
#include <QThread>
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<int, QProcess::ExitStatus>::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<double>(internalUpdateProgress) / (((internalUpdateInfo[0].count() + internalUpdateInfo[1].count()) * 4) + internalUpdateInfo[2].count())) * 100;
if (percentageDone > 100.0) {
percentageDone = 100.0;
}
emit progressUpdated(static_cast<int>(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<QStringList> 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<QStringList> 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;
}

@ -0,0 +1,47 @@
#ifndef APTMANAGER_H
#define APTMANAGER_H
#include <QObject>
#include <QStringList>
class QProcess;
class QProgressBar;
class QPlainTextEdit;
class QSystemTrayIcon;
class AptManager : public QObject
{
Q_OBJECT
public:
AptManager(QObject *parent = nullptr);
static QList<QStringList> 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<QStringList> internalUpdateInfo;
QStringList instUnpackList;
QStringList instConfigList;
QStringList upgradeUnpackList;
QStringList upgradeConfigList;
QStringList removeList;
quint32 numPackagesToPrep;
quint32 internalUpdateProgress;
};
#endif // APTMANAGER_H

@ -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);
}

@ -0,0 +1,31 @@
#ifndef CONFFILEHANDLERDIALOG_H
#define CONFFILEHANDLERDIALOG_H
#include <QDialog>
#include <QStringList>
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<ConffileWidget *> conffileList;
QStringList replaceConffileList;
};
#endif // CONFFILEHANDLERDIALOG_H

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ConffileHandlerDialog</class>
<widget class="QDialog" name="ConffileHandlerDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>937</width>
<height>399</height>
</rect>
</property>
<property name="windowTitle">
<string>Configuration File Conflicts</string>
</property>
<property name="windowIcon">
<iconset resource="resources.qrc">
<normaloff>:/res/images/update.svg</normaloff>:/res/images/update.svg</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Some of the newly installed updates have updated configuration files. &lt;/p&gt;&lt;p&gt;Please choose what to do with these files.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="horizontalScrollBarPolicy">
<enum>Qt::ScrollBarAsNeeded</enum>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>917</width>
<height>294</height>
</rect>
</property>
<layout class="QVBoxLayout" name="conffileStack">
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>190</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="doneButton">
<property name="text">
<string>Done</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="resources.qrc"/>
</resources>
<connections/>
</ui>

@ -0,0 +1,45 @@
#include "conffilewidget.h"
#include "diffdisplaydialog.h"
#include "ui_conffilewidget.h"
#include <QProcess>
#include <QDebug>
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();
}

@ -0,0 +1,28 @@
#ifndef CONFFILEWIDGET_H
#define CONFFILEWIDGET_H
#include <QWidget>
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

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ConffileWidget</class>
<widget class="QWidget" name="ConffileWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>557</width>
<height>43</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="filenameLabel">
<property name="text">
<string>Filename</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>181</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QRadioButton" name="keepRadioButton">
<property name="text">
<string>Keep old</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="replaceRadioButton">
<property name="text">
<string>Replace with new</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="showDiffButton">
<property name="text">
<string>Show diff</string>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

@ -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);
}

@ -0,0 +1,25 @@
#ifndef DIFFDISPLAYDIALOG_H
#define DIFFDISPLAYDIALOG_H
#include <QDialog>
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

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DiffDisplayDialog</class>
<widget class="QDialog" name="DiffDisplayDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>704</width>
<height>553</height>
</rect>
</property>
<property name="windowTitle">
<string>Dialog</string>
</property>
<property name="windowIcon">
<iconset resource="resources.qrc">
<normaloff>:/res/images/update.svg</normaloff>:/res/images/update.svg</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>&lt;html&gt;&lt;head/&gt;&lt;body&gt;&lt;p&gt;Lines that start with a &amp;quot;+&amp;quot; only exist in the &lt;span style=&quot; font-weight:700;&quot;&gt;new&lt;/span&gt; file.&lt;/p&gt;&lt;p&gt;Lines that start with a &amp;quot;-&amp;quot; only exist in the &lt;span style=&quot; font-weight:700;&quot;&gt;old&lt;/span&gt; file.&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="diffView">
<property name="styleSheet">
<string notr="true">font: 9pt &quot;Monospace&quot;;</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QPushButton" name="doneButton">
<property name="text">
<string>Done</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="resources.qrc"/>
</resources>
<connections/>
</ui>

@ -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

@ -0,0 +1,50 @@
#include "orchestrator.h"
#include "mainwindow.h"
#include "conffilehandlerdialog.h"
#include <QApplication>
#include <QDialog>
#include <QLocale>
#include <QTranslator>
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();
}

@ -0,0 +1,130 @@
#include "aptmanager.h"
#include "conffilehandlerdialog.h"
#include "mainwindow.h"
#include "./ui_mainwindow.h"
#include <QSystemTrayIcon>
#include <QTextCursor>
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<QStringList> 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();
}

@ -0,0 +1,43 @@
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include "aptmanager.h"
#include <QCloseEvent>
#include <QMainWindow>
#include <QStringList>
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<QStringList> 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

@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>462</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Lubuntu Update</string>
</property>
<property name="windowIcon">
<iconset resource="resources.qrc">
<normaloff>:/res/images/update.svg</normaloff>:/res/images/update.svg</iconset>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="statLabel">
<property name="text">
<string>0 package(s) will be updated. 0 of these updates are security-related.</string>
</property>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="packageView">
<column>
<property name="text">
<string>Packages</string>
</property>
</column>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>0</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QPlainTextEdit" name="logView">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>100</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">background-color: rgb(0, 0, 0);
color: rgb(255, 255, 255);
font: 9pt &quot;Monospace&quot;;</string>
</property>
<property name="readOnly">
<bool>true</bool>
</property>
<property name="plainText">
<string/>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="installButton">
<property name="text">
<string>Install Updates</string>
</property>
<property name="icon">
<iconset resource="resources.qrc">
<normaloff>:/res/images/update.svg</normaloff>:/res/images/update.svg</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="detailsButton">
<property name="text">
<string>Details</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="closeButton">
<property name="text">
<string>Close</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
</widget>
<resources>
<include location="resources.qrc"/>
</resources>
<connections/>
</ui>

@ -0,0 +1,53 @@
#include "orchestrator.h"
#include "mainwindow.h"
#include "aptmanager.h"
#include <QIcon>
#include <QSystemTrayIcon>
#include <QTimer>
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<QStringList> updateInfo)
{
if (!updaterWindow.isVisible()) {
updaterWindow.setUpdateInfo(updateInfo);
updaterWindow.show();
}
}
void Orchestrator::handleUpdatesInstalled()
{
trayIcon->hide();
}

@ -0,0 +1,30 @@
#ifndef ORCHESTRATOR_H
#define ORCHESTRATOR_H
#include "mainwindow.h"
#include <QObject>
#include <QStringList>
class QTimer;
class QSystemTrayIcon;
class Orchestrator : public QObject
{
Q_OBJECT
public:
explicit Orchestrator(QObject *parent = nullptr);
private slots:
void checkForUpdates();
void displayUpdater(QList<QStringList> updateInfo);
void handleUpdatesInstalled();
private:
QTimer *checkTimer;
QSystemTrayIcon *trayIcon;
QList<QStringList> updateInfo;
MainWindow updaterWindow;
};
#endif // ORCHESTRATOR_H

@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="512"
height="512"
version="1.1"
viewBox="0 0 512 512"
id="svg2"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="update.svg"
inkscape:export-filename="/home/wolf/Documents/lubuntu/brand2/installer.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#">
<metadata
id="metadata22">
<rdf:RDF>
<cc:Work
rdf:about="" />
</rdf:RDF>
</metadata>
<defs
id="defs20" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="990"
id="namedview18"
showgrid="false"
inkscape:zoom="1.1196294"
inkscape:cx="-86.189236"
inkscape:cy="159.42776"
inkscape:current-layer="svg2"
inkscape:showpageshadow="2"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1" />
<path
style="opacity:0.2;stroke-width:8"
d="M 240,28 C 115.904,28 16,127.904 16,252 16,376.096 115.904,476 240,476 364.096,476 464,376.096 464,252 464,127.904 364.096,28 240,28 Z"
id="path4"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sssss" />
<path
style="fill:#0068c8;fill-opacity:1;stroke-width:8"
d="M 240,20 C 115.904,20 16,119.904 16,244 16,368.096 115.904,468 240,468 364.096,468 464,368.096 464,244 464,119.904 364.096,20 240,20 Z"
id="path8"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sssss" />
<path
style="opacity:0.2;fill:#ffffff;stroke-width:8"
d="M 240,20 C 115.904,20 16,119.904 16,244 c 0,0.9076 0.09024,1.7929 0.140624,2.6875 C 18.95297,125.07345 117.68582,28 240,28 362.31418,28 461.04703,125.07345 463.85938,246.6875 463.90976,245.7929 464,244.9076 464,244 464,119.904 364.096,20 240,20 Z"
id="path10"
inkscape:connector-curvature="0"
sodipodi:nodetypes="sscscss" />
<path
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:8"
d="m 269.10347,107.39168 c -9.58334,0.052 -19.15657,2.46279 -27.58809,7.02393 -10.37721,5.61374 -18.98083,14.45497 -24.31028,24.98107 -5.32933,10.5261 -7.36063,22.69091 -5.7428,34.37791 1.61796,11.68688 6.87763,22.8437 14.86611,31.52617 -4.15065,-7.72078 -6.11736,-16.6004 -5.61919,-25.352 0.498,-8.75161 3.46028,-17.34966 8.4603,-24.54958 5.00003,-7.19992 12.02214,-12.97864 20.04849,-16.50235 8.02634,-3.5237 17.03332,-4.78058 25.71761,-3.58829 12.16805,1.67061 23.59308,8.23918 31.15796,17.915 H 427.31586 L 320.95438,138.56533 c -8.84824,-16.80207 -26.3556,-28.73511 -45.22943,-30.82646 -2.19885,-0.24384 -4.41004,-0.358 -6.62148,-0.3476 z M 78.488688,128.10836 c -7.791208,15.39202 -10.872648,33.12817 -8.73124,50.24618 2.141536,17.11814 9.497488,33.54929 20.840408,46.54752 6.347568,7.27398 13.950844,13.41352 22.326724,18.20966 L 89.206224,236.4429 c -1.14392,13.37072 1.486312,27.04414 7.507952,39.03684 6.021644,11.9927 15.418094,22.26642 26.825214,29.33483 16.93333,10.49277 38.25673,13.62972 57.49371,8.45513 -19.27005,0.13744 -38.36684,-9.43316 -49.79108,-24.95216 -6.44627,-8.7568 -10.499,-19.25735 -11.60919,-30.07409 l 112.61463,18.41743 -91.04559,-25.5992 c -7.28384,-3.53667 -13.93943,-8.37329 -19.49352,-14.26626 -7.6798,-8.14852 -13.25918,-18.26142 -16.05775,-29.10333 -3.20426,-12.41408 -2.72932,-25.75263 1.34702,-37.90836 l 134.28625,75.28264 z m 218.992062,58.78806 c -2.65746,5.51168 -4.50614,11.41167 -5.46667,17.45471 -2.97872,18.74258 2.59508,37.67646 8.14995,55.82322 5.55474,18.14676 11.21699,37.02968 8.41556,55.79963 -2.09303,14.02373 -8.93642,27.15127 -18.51211,37.60837 -9.57582,10.45711 -21.79536,18.31537 -34.9251,23.6683 -21.63584,8.82099 -45.96869,10.89326 -68.78476,5.85861 v 62.39477 l 18.76217,-44.53762 c 24.31626,5.54565 50.34774,3.30244 73.35679,-6.32163 16.4716,-6.88956 31.50268,-17.61592 42.34369,-31.80242 10.84087,-14.18637 17.3123,-31.91616 16.85473,-49.76474 -0.54992,-21.4518 -10.68913,-41.3322 -20.50086,-60.41653 -9.81187,-19.0842 -19.75279,-39.1183 -19.86173,-60.57697 -0.01,-1.73078 0.052,-3.46106 0.16864,-5.1877 z"
id="path4489"
inkscape:connector-curvature="0" />
<circle
style="fill:#1caff3;stroke-width:8"
cx="368"
cy="356"
r="128"
id="circle12" />
<path
style="fill:#ffffff;stroke-width:8"
d="m 368.2784,292 47.7248,64 h -32 v 64 h -32 v -64 h -32 z"
id="path14" />
<path
style="opacity:0.2;stroke-width:8"
d="M 240.20313,360 A 128,128.01515 0 0 0 240,363.98485 128,128.01515 0 0 0 368,492 128,128.01515 0 0 0 496,363.98485 128,128.01515 0 0 0 495.85938,360.04688 128,128.01515 0 0 1 368,483.99906 128,128.01515 0 0 1 240.20313,360 Z"
id="path16" />
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

@ -0,0 +1,5 @@
<RCC>
<qresource prefix="/">
<file>res/images/update.svg</file>
</qresource>
</RCC>

@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE TS>
<TS version="2.1" language="en_US"></TS>
Loading…
Cancel
Save