FS#13993 - rbutl push decompression onto a separate thread (Alessio Lenzi)

During Rockbox installation or update, the progress window currently
becomes unresponsive to screen-reader navigation while archive
inspection, free-space calculation and extraction are in progress. A
blind user cannot reliably move through or read the status messages and
therefore cannot determine what the utility is doing, how far the
installation has progressed, or whether an error has occurred.

The cause is that these operations are performed synchronously in the
GUI thread after a package has been downloaded. Although the
installation itself continues, the user interface cannot process
keyboard input and accessibility events reliably until the operation
finishes.

The attached patch moves the package installation work to a low-priority
QThread. Downloading remains asynchronous as before, while progress and
log signals from archive extraction are delivered back to the GUI thread
through Qt connections. This keeps the progress window fully navigable
with a screen reader throughout the installation.

Change-Id: I9a36a736e5b4bf98de8c3a71151679a4c1333dc7
This commit is contained in:
Solomon Peachy 2026-09-01 07:36:48 -04:00
parent 3664373ce7
commit 3e996ec73e
2 changed files with 147 additions and 45 deletions

View file

@ -23,13 +23,118 @@
#include "ziputil.h"
#include "Logger.h"
class ZipInstallThread : public QThread
{
public:
enum Result {
Success,
OpenFailed,
NotEnoughSpace,
ExtractionFailed,
CopyFailed
};
explicit ZipInstallThread(ZipInstaller *owner)
: QThread(owner), m_owner(owner), m_unzip(true),
m_result(OpenFailed), m_requiredSpace(0)
{
}
void setArchive(const QString& file, const QString& mountpoint)
{
m_file = file;
m_mountpoint = mountpoint;
m_unzip = true;
}
void setCopy(const QString& file, const QString& mountpoint,
const QString& target)
{
m_file = file;
m_mountpoint = mountpoint;
m_target = target;
m_unzip = false;
}
Result result(void) const { return m_result; }
QStringList installedFiles(void) const { return m_installedFiles; }
protected:
void run(void) override
{
if(m_unzip) {
ZipUtil zip(nullptr);
connect(&zip, &ZipUtil::logProgress,
m_owner, &ZipInstaller::logProgress);
connect(&zip, &ZipUtil::logItem,
m_owner, &ZipInstaller::logItem);
if(!zip.open(m_file, QuaZip::mdUnzip)) {
m_result = OpenFailed;
return;
}
m_requiredSpace = zip.totalUncompressedSize(
Utils::filesystemClusterSize(m_mountpoint));
if((qint64)Utils::filesystemFree(m_mountpoint)
< m_requiredSpace + 1000000) {
zip.close();
m_result = NotEnoughSpace;
return;
}
m_installedFiles = zip.files();
if(!zip.extractArchive(m_mountpoint)) {
m_result = ExtractionFailed;
zip.close();
return;
}
zip.close();
}
else {
QString destfile = m_mountpoint + "/" + m_target;
QString path = QFileInfo(destfile).absolutePath();
if(!QDir().mkpath(path)) {
m_result = CopyFailed;
return;
}
QFile(destfile).remove();
if(!QFile::copy(m_file, destfile)) {
m_result = CopyFailed;
return;
}
m_installedFiles.append(m_target);
}
m_result = Success;
}
private:
ZipInstaller *m_owner;
QString m_file;
QString m_mountpoint;
QString m_target;
QStringList m_installedFiles;
bool m_unzip;
Result m_result;
qint64 m_requiredSpace;
};
ZipInstaller::ZipInstaller(QObject* parent) :
QObject(parent),
m_unzip(true), m_usecache(false), m_getter(nullptr)
m_unzip(true), m_usecache(false), m_getter(nullptr),
m_installThread(nullptr)
{
}
ZipInstaller::~ZipInstaller()
{
if(m_installThread && m_installThread->isRunning()) {
m_installThread->wait();
}
}
void ZipInstaller::install()
{
LOG_INFO() << "initializing installation";
@ -106,7 +211,6 @@ void ZipInstaller::installStart()
void ZipInstaller::downloadDone(QNetworkReply::NetworkError error)
{
LOG_INFO() << "download done, error:" << error;
QStringList zipContents; // needed later
// update progress bar
emit logProgress(1, 1);
@ -128,64 +232,57 @@ void ZipInstaller::downloadDone(QNetworkReply::NetworkError error)
else {
emit logItem(tr("Download finished."),LOGOK);
}
QCoreApplication::processEvents();
if(m_unzip) {
// unzip downloaded file
LOG_INFO() << "about to unzip" << m_file << "to" << m_mountpoint;
emit logItem(tr("Extracting file."), LOGINFO);
QCoreApplication::processEvents();
ZipUtil zip(this);
connect(&zip, &ZipUtil::logProgress, this, &ZipInstaller::logProgress);
connect(&zip, &ZipUtil::logItem, this, &ZipInstaller::logItem);
zip.open(m_file, QuaZip::mdUnzip);
// check for free space. Make sure after installation will still be
// some room for operating (also includes calculation mistakes due to
// cluster sizes on the player).
if((qint64)Utils::filesystemFree(m_mountpoint)
< (zip.totalUncompressedSize(
Utils::filesystemClusterSize(m_mountpoint))
+ 1000000)) {
emit logItem(tr("Not enough disk space! Aborting."), LOGERROR);
emit logProgress(1, 1);
emit done(true);
return;
}
zipContents = zip.files();
if(!zip.extractArchive(m_mountpoint)) {
emit logItem(tr("Extraction failed!"), LOGERROR);
emit logProgress(1, 1);
emit done(true);
return;
}
zip.close();
m_installThread = new ZipInstallThread(this);
m_installThread->setArchive(m_file, m_mountpoint);
}
else {
if (m_target.isEmpty())
m_target = QUrl(m_url).fileName();
QString destfile = m_mountpoint + "/" + m_target;
// only copy the downloaded file to the output location / name
emit logItem(tr("Installing file."), LOGINFO);
LOG_INFO() << "saving downloaded file (no extraction) to" << destfile;
// Keep the temporary file materialized while the worker copies it.
m_downloadFile->open();
m_installThread = new ZipInstallThread(this);
m_installThread->setCopy(m_file, m_mountpoint, m_target);
}
m_downloadFile->open(); // copy fails if file is not opened (filename issue?)
// make sure the required path is existing
QString path = QFileInfo(destfile).absolutePath();
QDir p;
p.mkpath(path);
// QFile::copy() doesn't overwrite files, so remove old one first
// TODO: compare old and new file and fail if those are different.
QFile(destfile).remove();
if(!m_downloadFile->copy(destfile)) {
connect(m_installThread, &QThread::finished,
this, &ZipInstaller::installFinished);
m_installThread->start(QThread::LowPriority);
}
void ZipInstaller::installFinished()
{
ZipInstallThread *thread = m_installThread;
m_installThread = nullptr;
ZipInstallThread::Result result = thread->result();
QStringList zipContents = thread->installedFiles();
thread->deleteLater();
emit logProgress(1, 1);
switch(result) {
case ZipInstallThread::Success:
break;
case ZipInstallThread::NotEnoughSpace:
emit logItem(tr("Not enough disk space! Aborting."), LOGERROR);
emit done(true);
return;
case ZipInstallThread::CopyFailed:
emit logItem(tr("Installing file failed."), LOGERROR);
emit done(true);
return;
}
// add file to log
zipContents.append(m_target);
case ZipInstallThread::OpenFailed:
case ZipInstallThread::ExtractionFailed:
emit logItem(tr("Extraction failed!"), LOGERROR);
emit done(true);
return;
}
if(m_logver.isEmpty()) {
// if no version info is set use the timestamp of the server file.
m_logver = m_getter->timestamp().toString(Qt::ISODate);

View file

@ -28,6 +28,8 @@
#include "httpget.h"
#include "Logger.h"
class ZipInstallThread;
/** Install a file or zip.
* Downloads file(s) from a given URL, and installs by either extracting or
* copying it to the target path set by setMountpoint().
@ -37,6 +39,7 @@ class ZipInstaller : public QObject
Q_OBJECT
public:
ZipInstaller(QObject* parent);
~ZipInstaller();
void install(void);
void setMountPoint(QString& mountpoint) {m_mountpoint = mountpoint;}
void setUrl(QString& url){m_urllist = QStringList(url);}
@ -59,6 +62,7 @@ public slots:
private slots:
void downloadDone(QNetworkReply::NetworkError error);
void installFinished(void);
void installStart(void);
void installContinue(void);
@ -79,6 +83,7 @@ private:
HttpGet *m_getter;
QTemporaryFile *m_downloadFile;
ZipInstallThread *m_installThread;
};