diff --git a/companion/src/progresswidget.cpp b/companion/src/progresswidget.cpp index 333e7b556..ecbc6892d 100644 --- a/companion/src/progresswidget.cpp +++ b/companion/src/progresswidget.cpp @@ -212,3 +212,10 @@ void ProgressWidget::forceKeepOpen(bool value) { emit keepOpen(value); } + +void ProgressWidget::refresh() +{ + ui->info->update(); + ui->progressBar->update(); + ui->textEdit->update(); +} diff --git a/companion/src/progresswidget.h b/companion/src/progresswidget.h index e4ab6f3d3..0ea6e8746 100644 --- a/companion/src/progresswidget.h +++ b/companion/src/progresswidget.h @@ -53,6 +53,7 @@ class ProgressWidget : public QWidget void stop(); void clearDetails() const; void forceKeepOpen(bool value); + void refresh(); signals: void detailsToggled(); diff --git a/companion/src/storage/minizinterface.cpp b/companion/src/storage/minizinterface.cpp index 4ac1ce4fd..380cbdce5 100644 --- a/companion/src/storage/minizinterface.cpp +++ b/companion/src/storage/minizinterface.cpp @@ -27,14 +27,17 @@ #include #include #include +#include MinizInterface::MinizInterface(ProgressWidget * progress, const ProgressCalcMethod progressMethod, const int & logLevel) : + QObject(nullptr), progress(progress), progressMethod(progressMethod), progressValue(0), logLevel(logLevel), path(""), - archiveFile("") + archiveFile(""), + stopping(false) { reportProgress(tr("Progress calculation method: file %1").arg(progressMethod == PCM_COUNT ? tr("count") : tr("size")), QtDebugMsg); } @@ -171,8 +174,10 @@ bool MinizInterface::addFileToArchive(const QString & path) bool MinizInterface::unzipArchiveToPath(const QString & archiveFile, const QString & destinationPath) { + QFileInfo file(archiveFile); + if (progress) - progress->setInfo(tr("Decompressing %1").arg(archiveFile)); + progress->setInfo(tr("Decompressing %1").arg(file.fileName())); else qDebug() << QString("Decompressing %1 to %2").arg(archiveFile, destinationPath); @@ -212,6 +217,12 @@ bool MinizInterface::unzipArchiveToPath(const QString & archiveFile, const QStri } for (int i = 0; i < fileCount; i++) { + if (isStopping()) { + mz_zip_reader_end(&zip_archive); + reportProgress(tr("Decompression interrupted"), QtWarningMsg); + return false; + } + if (!mz_zip_reader_file_stat(&zip_archive, i, &file_stat)) { mz_zip_reader_end(&zip_archive); reportProgress(tr("Unable to obtain file status for index: %1").arg(i), QtFatalMsg); @@ -254,6 +265,8 @@ bool MinizInterface::unzipArchiveToPath(const QString & archiveFile, const QStri progress->setValue(progressValue); reportProgress(tr("Extracted file: %1").arg(file_stat.m_filename), QtDebugMsg); + // to assist in responsiveness and allow stop signal to be received and processed + QApplication::processEvents(); } } @@ -290,3 +303,8 @@ void MinizInterface::reportProgress(const QString & text, const int & type, bool qDebug() << text; } } + +void MinizInterface::stop() +{ + stopping = true; +} diff --git a/companion/src/storage/minizinterface.h b/companion/src/storage/minizinterface.h index 2f24999bd..71e50a3a8 100644 --- a/companion/src/storage/minizinterface.h +++ b/companion/src/storage/minizinterface.h @@ -28,9 +28,9 @@ class ProgressWidget; -class MinizInterface +class MinizInterface : public QObject { - Q_DECLARE_TR_FUNCTIONS(MinizInterface) + Q_OBJECT public: enum ProgressCalcMethod { @@ -44,6 +44,9 @@ class MinizInterface bool zipPathToFile(const QString & sourcePath, const QString & archiveFile, bool append = true); bool unzipArchiveToPath(const QString & archiveFile, const QString & destinationPath); + public slots: + void stop(); + private: ProgressWidget *progress; ProgressCalcMethod progressMethod; @@ -51,10 +54,12 @@ class MinizInterface int logLevel; QString path; QString archiveFile; + bool stopping; mz_zip_archive zip_archive; bool addFileToArchive(const QString & path); bool createDirectory(const QString & path); void reportProgress(const QString & text, const int & type = QtInfoMsg, bool richText = false); + bool isStopping() { return stopping; } }; diff --git a/companion/src/updates/repoassets.cpp b/companion/src/updates/repoassets.cpp index 71c6cf168..d4370fa8a 100644 --- a/companion/src/updates/repoassets.cpp +++ b/companion/src/updates/repoassets.cpp @@ -106,6 +106,15 @@ bool AssetsRawItemModel::setDownloadName(const int id, const QString name) return setValue(id, RIMR_DownloadName, QVariant(name)); } +bool AssetsRawItemModel::resetFlags() +{ + for (int i = 0; i < rowCount(); i++) { + item(i)->setData(0, RIMR_Flags); + } + + return true; +} + /* AssetsFilteredItemModel */ @@ -320,3 +329,8 @@ bool RepoAssets::setDownloadName(const QString name) { return m_rawItemModel->setDownloadName(id(), name); } + +bool RepoAssets::resetFlags() +{ + return m_rawItemModel->resetFlags(); +} diff --git a/companion/src/updates/repoassets.h b/companion/src/updates/repoassets.h index a004bdaed..f07462636 100644 --- a/companion/src/updates/repoassets.h +++ b/companion/src/updates/repoassets.h @@ -39,6 +39,7 @@ class AssetsRawItemModel : public RepoRawItemModel bool setDownloadUrl(const int id, const QString url); bool setDownloadName(const int id, const QString name); + bool resetFlags(); private: QObject *m_parentRepo; // cannot use Repo class from repo.h as compile error due to cyclic headers @@ -93,16 +94,22 @@ class RepoAssets : public QObject, public RepoMetaData bool getJson(const QString assetName, QJsonDocument * json); const QString name() const; const QString name(const int row); + bool setCopyFilter(const QString filter); + bool setSubDirectory(const QString path); const QString subDirectory() const; + bool setDownloadUrl(const QString url); const QString downloadUrl() const; const QString downloadUrl(const int row); + bool setDownloadName(const QString name); const QString downloadName() const; const QString downloadName(const int row); + bool resetFlags(); + signals: void idChanged(int id); diff --git a/companion/src/updates/updatecloudbuild.cpp b/companion/src/updates/updatecloudbuild.cpp index 52989db52..6398b619c 100644 --- a/companion/src/updates/updatecloudbuild.cpp +++ b/companion/src/updates/updatecloudbuild.cpp @@ -25,11 +25,10 @@ #include "eeprominterface.h" #include "flashfirmwaredialog.h" -constexpr char STATUS_UNKNOWN[] {"STATUS_UNKNOWN"}; -constexpr char STATUS_WAITING_FOR_BUILD[] {"WAITING_FOR_BUILD"}; -constexpr char STATUS_BUILD_IN_PROGRESS[] {"BUILD_IN_PROGRESS"}; -constexpr char STATUS_BUILD_SUCCESS[] {"BUILD_SUCCESS"}; -// constexpr char STATUS_BUILD_ERROR[] {"BUILD_ERROR"}; +// these must match text strings in server response +constexpr char CLOUD_BUILD_WAITING[] {"WAITING_FOR_BUILD"}; +constexpr char CLOUD_BUILD_IN_PROGRESS[] {"BUILD_IN_PROGRESS"}; +constexpr char CLOUD_BUILD_SUCCESS[] {"BUILD_SUCCESS"}; UpdateCloudBuild::UpdateCloudBuild(QWidget * parent) : UpdateInterface(parent, CID_CloudBuild, tr("CloudBuild"), Repo::REPO_TYPE_BUILD, @@ -48,6 +47,38 @@ UpdateCloudBuild::~UpdateCloudBuild() delete m_docResp; } +QString UpdateCloudBuild::statusToString(const int status) +{ + switch (status) { + case STATUS_WAITING: + return tr("waiting"); + case STATUS_IN_PROGRESS: + return tr("in progress"); + case STATUS_SUCCESS: + return tr("success"); + case STATUS_ERROR: + return tr("error"); + case STATUS_TIMEOUT: + return tr("timeout"); + case STATUS_CANCELLED: + return tr("cancelled"); + default: + return tr("unknown"); + } +} + +int UpdateCloudBuild::cloudStatusToInt(const QString status) +{ + if (status == QString(CLOUD_BUILD_WAITING)) + return STATUS_WAITING; + else if (status == QString(CLOUD_BUILD_IN_PROGRESS)) + return STATUS_IN_PROGRESS; + else if (status == QString(CLOUD_BUILD_SUCCESS)) + return STATUS_SUCCESS; + else + return STATUS_UNKNOWN; +} + void UpdateCloudBuild::assetSettingsInit() { if (!isSettingsIndexValid()) @@ -189,8 +220,8 @@ int UpdateCloudBuild::asyncInstall() bool UpdateCloudBuild::buildFlaggedAsset(const int row) { + m_jobStatus = STATUS_UNKNOWN; m_buildFlags.clear(); - m_jobStatus.clear(); m_profileOpts.clear(); m_radio = repo()->assets()->name(row); @@ -289,7 +320,7 @@ bool UpdateCloudBuild::buildFlaggedAsset(const int row) if (params()->logLevel == QtDebugMsg) network()->saveJsonDocToFile(docBody, QString("%1/%2_body.txt").arg(m_logDir).arg(m_radio)); - network()->submitRequest(tr("Submit firmware build"), repo()->urlJobs(), docBody, m_docResp); + network()->submitRequest(tr("Submit build request"), repo()->urlJobs(), docBody, m_docResp); if (params()->logLevel == QtDebugMsg) network()->saveBufferToFile(QString("%1/%2_buffer.txt").arg(m_logDir).arg(m_radio)); @@ -318,15 +349,18 @@ bool UpdateCloudBuild::buildFlaggedAsset(const int row) return false; } else { - m_jobStatus = obj.value("status").toString(QString(STATUS_UNKNOWN)); + m_jobStatus = cloudStatusToInt(obj.value("status").toString()); } if (isStatusInProgress()) waitForBuildFinish(); - status()->reportProgress(tr("Firmware build finished status: %1").arg(m_jobStatus)); + status()->reportProgress(tr("Build finish status: %1").arg(statusToString(m_jobStatus))); - if (m_jobStatus == QString(STATUS_BUILD_SUCCESS) && setAssetDownload()) + // ensure progress bar not in wait state + status()->setMaximum(100); + + if (m_jobStatus == STATUS_SUCCESS && setAssetDownload()) return true; return false; @@ -334,22 +368,25 @@ bool UpdateCloudBuild::buildFlaggedAsset(const int row) void UpdateCloudBuild::cancel() { - status()->reportProgress(tr("Build firmware cancelled"), QtWarningMsg); + // this does not cancel the build job on the server but only interrupts the client side + status()->reportProgress(tr("Build cancelled"), QtWarningMsg); + m_jobStatus = STATUS_CANCELLED; cleanup(); } void UpdateCloudBuild::checkStatus() { if (m_buildStartTime.secsTo(QTime::currentTime()) > 180) { - status()->reportProgress(tr("Build firmware timeout"), QtWarningMsg); + status()->reportProgress(tr("Build timeout. Retry later."), QtWarningMsg); + m_jobStatus = STATUS_TIMEOUT; cleanup(); + return; } getStatus(); if (!isStatusInProgress()) cleanup(); - } void UpdateCloudBuild::cleanup() @@ -363,7 +400,7 @@ void UpdateCloudBuild::cleanup() bool UpdateCloudBuild::getStatus() { - m_jobStatus = QString(STATUS_UNKNOWN); + m_jobStatus = STATUS_UNKNOWN; QJsonDocument *docBody = new QJsonDocument(*m_objBody); if (m_docResp) @@ -376,9 +413,9 @@ bool UpdateCloudBuild::getStatus() network()->saveJsonDocToFile(m_docResp, QString("%1/%2_status_%3.txt").arg(m_logDir).arg(m_radio).arg(QTime::currentTime().toString("hhmmss"))); if (m_docResp->isObject()) - m_jobStatus = m_docResp->object().value("status").toString(QString(STATUS_UNKNOWN)); + m_jobStatus = cloudStatusToInt(m_docResp->object().value("status").toString()); - if (m_jobStatus == QString(STATUS_UNKNOWN)) { + if (m_jobStatus == STATUS_UNKNOWN) { status()->reportProgress(tr("Build status unknown"), QtCriticalMsg); return false; } @@ -388,7 +425,7 @@ bool UpdateCloudBuild::getStatus() bool UpdateCloudBuild::isStatusInProgress() { - if (m_jobStatus == QString(STATUS_WAITING_FOR_BUILD) || m_jobStatus == QString(STATUS_BUILD_IN_PROGRESS)) + if (m_jobStatus == STATUS_WAITING || m_jobStatus == STATUS_IN_PROGRESS) return true; return false; @@ -451,15 +488,16 @@ bool UpdateCloudBuild::stringExists(const QJsonObject & parent, const QString ch void UpdateCloudBuild::waitForBuildFinish() { - status()->progressMessage(tr("Waiting for firmware build to finish...")); + status()->progressMessage(tr("Waiting for build to finish...")); + status()->setMaximum(0); // sets progress bar to wait state m_timer.setInterval(2000); // check status every 2 seconds - connect(this, &UpdateInterface::stop, this, &UpdateCloudBuild::cancel); + connect(this, &UpdateInterface::stopping, this, &UpdateCloudBuild::cancel); connect(&m_timer, &QTimer::timeout, this, &UpdateCloudBuild::checkStatus); m_timer.start(); m_eventLoop.exec(); - disconnect(this, &UpdateInterface::stop, this, &UpdateCloudBuild::cancel); - disconnect(&m_timer, nullptr); + disconnect(&m_timer, &QTimer::timeout, this, &UpdateCloudBuild::checkStatus); + disconnect(this, &UpdateInterface::stopping, this, &UpdateCloudBuild::cancel); } diff --git a/companion/src/updates/updatecloudbuild.h b/companion/src/updates/updatecloudbuild.h index a63dbcc2e..ea783be5f 100644 --- a/companion/src/updates/updatecloudbuild.h +++ b/companion/src/updates/updatecloudbuild.h @@ -31,6 +31,16 @@ class UpdateCloudBuild: public UpdateInterface Q_DECLARE_TR_FUNCTIONS(UpdateCloudBuild) public: + enum ProcessStatus { + STATUS_UNKNOWN, + STATUS_WAITING, + STATUS_IN_PROGRESS, + STATUS_SUCCESS, + STATUS_ERROR, + STATUS_TIMEOUT, + STATUS_CANCELLED + }; + Q_ENUM(ProcessStatus) explicit UpdateCloudBuild(QWidget * parent); virtual ~UpdateCloudBuild(); @@ -53,7 +63,7 @@ class UpdateCloudBuild: public UpdateInterface QString m_logDir; QString m_buildFlags; QTime m_buildStartTime; - QString m_jobStatus; + int m_jobStatus; QStringList m_profileOpts; QString m_radio; @@ -69,4 +79,6 @@ class UpdateCloudBuild: public UpdateInterface bool isStatusInProgress(); bool setAssetDownload(); void waitForBuildFinish(); + static QString statusToString(const int status); + static int cloudStatusToInt(const QString status); }; diff --git a/companion/src/updates/updateinterface.cpp b/companion/src/updates/updateinterface.cpp index 6081cc4ca..12fa7942b 100644 --- a/companion/src/updates/updateinterface.cpp +++ b/companion/src/updates/updateinterface.cpp @@ -130,11 +130,16 @@ int UpdateInterface::build() bool UpdateInterface::buildFlaggedAssets() { + if (!isOkay()) + return true; + m_repo->assets()->setFilterFlags(UPDFLG_Build); m_status->reportProgress(tr("Asset filter applied: %1 - %2 found").arg(updateFlagToString(UPDFLG_Build)).arg(m_repo->assets()->count()), QtDebugMsg); for (int i = 0; i < m_repo->assets()->count(); ++i) { - if (!buildFlaggedAsset(i)) + if (!isOkay()) + return true; + else if (!buildFlaggedAsset(i)) return false; } @@ -211,9 +216,11 @@ bool UpdateInterface::copyFiles() int cnt = 0; + processEvents(); + //m_status->reportProgress(tr("Calculating number of files"), QtDebugMsg); - while (itcnt.hasNext()) + while (isOkay() && itcnt.hasNext()) { QFileInfo src(itcnt.next()); @@ -229,7 +236,9 @@ bool UpdateInterface::copyFiles() cnt = 0; - while (itfiles.hasNext()) + processEvents(); + + while (isOkay() && itfiles.hasNext()) { QFileInfo src(itfiles.next()); @@ -254,6 +263,7 @@ bool UpdateInterface::copyFiles() m_status->reportProgress(tr("Copied %1 to %2").arg(src.fileName()).arg(dest.fileName()), QtDebugMsg); cnt++; m_status->setValue(cnt); + processEvents(); } if (!cnt) { @@ -264,17 +274,23 @@ bool UpdateInterface::copyFiles() m_status->setValue(m_status->maximum()); m_status->reportProgress(tr("Files copied: %1").arg(cnt), QtInfoMsg); + if (!isOkay()) + return false; + return true; } bool UpdateInterface::copyFlaggedAssets() { + if (!isOkay()) + return false; + m_repo->assets()->setFilterFlags(UPDFLG_CopyDest); m_status->reportProgress(tr("Asset filter applied: %1 - %2 found").arg(updateFlagToString(UPDFLG_CopyDest)).arg(m_repo->assets()->count()), QtDebugMsg); for (int i = 0; i < m_repo->assets()->count(); ++i) { m_repo->assets()->getSetId(i); - if (!copyAsset()) + if ( !isOkay() || !copyAsset()) return false; } @@ -307,7 +323,9 @@ bool UpdateInterface::copyStructure() int cnt = 0; - while (itdirs.hasNext()) + processEvents(); + + while (isOkay() && itdirs.hasNext()) { QFileInfo src(itdirs.next()); @@ -323,6 +341,7 @@ bool UpdateInterface::copyStructure() m_status->reportProgress(tr("Check/create directory: %1").arg(dir), QtDebugMsg); cnt++; + processEvents(); } m_status->reportProgress(tr("Directories checked/created: %1").arg(cnt), QtInfoMsg); @@ -334,9 +353,11 @@ bool UpdateInterface::copyStructure() cnt = 0; + processEvents(); + //m_status->reportProgress(tr("Calculating number of files"), QtDebugMsg); - while (itcnt.hasNext()) + while (isOkay() && itcnt.hasNext()) { QFileInfo src(itcnt.next()); @@ -352,7 +373,9 @@ bool UpdateInterface::copyStructure() cnt = 0; - while (itfiles.hasNext()) + processEvents(); + + while (isOkay() && itfiles.hasNext()) { QFileInfo src(itfiles.next()); @@ -379,20 +402,24 @@ bool UpdateInterface::copyStructure() m_status->reportProgress(tr("Copied %1 to %2").arg(src.fileName()).arg(dest.fileName()), QtDebugMsg); cnt++; m_status->setValue(cnt); + processEvents(); } m_status->setValue(m_status->maximum()); m_status->reportProgress(tr("Files copied: %1").arg(cnt), QtInfoMsg); + if (!isOkay()) + return false; + return true; } int UpdateInterface::copyToDestination() { - m_status->progressMessage(tr("Copying to destination")); + m_status->progressMessage(tr("Copying files to destination")); if (!copyFlaggedAssets()) { - m_status->reportProgress(tr("Unable to copy assets"), QtDebugMsg); + m_status->reportProgress(tr("Unable to copy files"), QtDebugMsg); return false; } @@ -401,10 +428,10 @@ int UpdateInterface::copyToDestination() int UpdateInterface::decompress() { - m_status->progressMessage(tr("Decompressing assets")); + m_status->progressMessage(tr("Decompressing files")); if (!decompressFlaggedAssets()) { - m_status->reportProgress(tr("Unable to decompress flagged assets"), QtDebugMsg); + m_status->reportProgress(tr("Unable to decompress flagged files"), QtDebugMsg); return false; } @@ -418,11 +445,16 @@ const QString UpdateInterface::decompressDir() const bool UpdateInterface::decompressFlaggedAssets() { + if (!isOkay()) + return true; + m_repo->assets()->setFilterFlags(UPDFLG_Decompress); m_status->reportProgress(tr("Asset filter applied: %1 - %2 found").arg(updateFlagToString(UPDFLG_Decompress)).arg(m_repo->assets()->count()), QtDebugMsg); for (int i = 0; i < m_repo->assets()->count(); ++i) { - if (!decompressAsset(i)) + if (!isOkay()) + return true; + else if (!decompressAsset(i)) return false; } @@ -454,6 +486,8 @@ bool UpdateInterface::decompressAsset(int row) bool UpdateInterface::decompressArchive(const QString & archivePath, const QString & destDir) { + bool ret = true; + QString dest = destDir; if (!dest.endsWith("/")) @@ -463,12 +497,16 @@ bool UpdateInterface::decompressArchive(const QString & archivePath, const QStri MinizInterface mIface(m_status->progress(), MinizInterface::PCM_SIZE, m_params->logLevel); + connect(this, &UpdateInterface::stopping, &mIface, &MinizInterface::stop, Qt::DirectConnection); + if (!mIface.unzipArchiveToPath(archivePath, dest)) { m_status->reportProgress(tr("Failed to decompress %1").arg(archivePath), QtCriticalMsg); - return false; + ret = false; } - return true; + disconnect(this, &UpdateInterface::stopping, &mIface, &MinizInterface::stop); + + return ret; } const QString UpdateInterface::downloadDir() const @@ -478,11 +516,16 @@ const QString UpdateInterface::downloadDir() const bool UpdateInterface::downloadFlaggedAssets() { + if (!isOkay()) + return true; + m_repo->assets()->setFilterFlags(UPDFLG_Download); m_status->reportProgress(tr("Asset filter applied: %1 - %2 found").arg(updateFlagToString(UPDFLG_Download)).arg(m_repo->assets()->count()), QtDebugMsg); for (int i = 0; i < m_repo->assets()->count(); ++i) { - if (!downloadFlaggedAsset(i)) + if (!isOkay()) + return true; + else if (!downloadFlaggedAsset(i)) return false; } @@ -499,7 +542,7 @@ int UpdateInterface::download() m_status->progressMessage(tr("Downloading...")); if (!downloadFlaggedAssets()) { - m_status->reportProgress(tr("Unable to download flagged assets"), QtDebugMsg); + m_status->reportProgress(tr("Unable to download flagged files"), QtDebugMsg); return false; } @@ -540,6 +583,9 @@ bool UpdateInterface::flagAssets() int UpdateInterface::housekeeping() { + if (!isOkay()) + return PROC_RESULT_CANCELLED; + m_status->progressMessage(tr("Housekeeping")); int cnt = 0; @@ -667,10 +713,16 @@ UpdateNetwork* const UpdateInterface::network() const void UpdateInterface::onStatusCancelled() { - m_status->setMaximum(0); + m_result = PROC_RESULT_CANCELLED; m_stopping = true; - emit stop(); - m_status->reportProgress(tr("Update cancelled"), QtWarningMsg); + m_status->setValue(100); + m_status->reportProgress(tr("Update cancelled by user"), QtWarningMsg); + emit stopping(); + + if (m_eventLoop.isRunning()) { + m_eventLoop.processEvents(); + m_eventLoop.quit(); + } } UpdateParameters* const UpdateInterface::params() const @@ -709,6 +761,10 @@ int UpdateInterface::preparation() if (!m_repo->assets()->retrieveMetaDataAll()) return false; + // clear filters from previous runs + if (!m_repo->assets()->resetFlags()) + return false; + m_status->setValue(++cnt); if (!flagAssets()) @@ -719,6 +775,11 @@ int UpdateInterface::preparation() return true; } +void UpdateInterface::processEvents() +{ + m_eventLoop.processEvents(); +} + void UpdateInterface::radioProfileChanged() { resetEnvironment(); @@ -809,57 +870,73 @@ bool UpdateInterface::retrieveRepoJsonFile(const QString & filename, QJsonDocume void UpdateInterface::runAsyncInstall() { + m_status->setValue(0); m_result = asyncInstall(); - if (m_result == PROC_RESULT_FAIL) m_status->reportProgress(tr("%1 start async failed").arg(m_name), QtCriticalMsg); + if (m_result == PROC_RESULT_FAIL) m_status->reportProgress(tr("%1 start async %2").arg(m_name).arg(resultToString()), QtCriticalMsg); + m_status->setValue(m_status->maximum()); emit finished(); } void UpdateInterface::runBuild() { + m_status->setValue(0); m_result = build(); - if (m_result == PROC_RESULT_FAIL) m_status->reportProgress(tr("%1 preparation failed").arg(m_name), QtCriticalMsg); + if (m_result == PROC_RESULT_FAIL) m_status->reportProgress(tr("%1 preparation %2").arg(m_name).arg(resultToString()), QtCriticalMsg); + m_status->setValue(m_status->maximum()); emit finished(); } void UpdateInterface::runCopyToDestination() { + m_status->setValue(0); m_result = copyToDestination(); - if (m_result == PROC_RESULT_FAIL) m_status->reportProgress(tr("%1 copy to destination failed").arg(m_name), QtCriticalMsg); + if (m_result == PROC_RESULT_FAIL) m_status->reportProgress(tr("%1 copy to destination %2").arg(m_name).arg(resultToString()), QtCriticalMsg); + m_status->setValue(m_status->maximum()); emit finished(); } void UpdateInterface::runDecompress() { + m_status->setValue(0); m_result = decompress(); - if (m_result == PROC_RESULT_FAIL) m_status->reportProgress(tr("%1 decompress failed").arg(m_name), QtCriticalMsg); + if (m_result == PROC_RESULT_FAIL) m_status->reportProgress(tr("%1 decompress %2").arg(m_name).arg(resultToString()), QtCriticalMsg); + m_status->setValue(m_status->maximum()); emit finished(); } void UpdateInterface::runDownload() { + m_status->setValue(0); m_result = download(); - if (m_result == PROC_RESULT_FAIL) m_status->reportProgress(tr("%1 download failed").arg(m_name), QtCriticalMsg); + if (m_result == PROC_RESULT_FAIL) m_status->reportProgress(tr("%1 download %2").arg(m_name).arg(resultToString()), QtCriticalMsg); + m_status->setValue(m_status->maximum()); emit finished(); } void UpdateInterface::runHousekeeping() { + m_status->setValue(0); m_result = housekeeping(); - if (m_result == PROC_RESULT_FAIL) m_status->reportProgress(tr("%1 housekeeping failed").arg(m_name), QtCriticalMsg); + if (m_result == PROC_RESULT_FAIL) m_status->reportProgress(tr("%1 housekeeping %2").arg(m_name).arg(resultToString()), QtCriticalMsg); + m_status->setValue(m_status->maximum()); emit finished(); } void UpdateInterface::runPreparation() { + m_status->setValue(0); m_result = preparation(); - if (m_result == PROC_RESULT_FAIL) m_status->reportProgress(tr("%1 preparation failed").arg(m_name), QtCriticalMsg); + if (m_result == PROC_RESULT_FAIL) m_status->reportProgress(tr("%1 preparation %2").arg(m_name).arg(resultToString()), QtCriticalMsg); + m_status->setValue(m_status->maximum()); emit finished(); } void UpdateInterface::runReleaseSettingsSave() { + m_status->setValue(0); m_result = releaseSettingsSave(); - if (m_result == PROC_RESULT_FAIL) m_status->reportProgress(tr("%1 save release settings failed").arg(m_name), QtCriticalMsg); + if (m_result == PROC_RESULT_FAIL) m_status->reportProgress(tr("%1 save release settings %2").arg(m_name).arg(resultToString()), QtCriticalMsg); + m_status->setValue(m_status->maximum()); emit finished(); } @@ -1021,72 +1098,92 @@ bool UpdateInterface::update(ProgressWidget * progress) // and then display close button status()->keepOpen(true); + // handle Cancel button pressed connect(m_status, &UpdateStatus::cancelled, this, &UpdateInterface::onStatusCancelled); - // need to create a separate thread for the processes so that the cancel request can be handled asap - QEventLoop loop; + //===================================================================================================== + // EXTREMELY IMPORTANT !!!!!! + // + // Every process within the event loop MUST emit finished() otherwise the event loop will not exit + //===================================================================================================== + connect(this, &UpdateInterface::finished, [&]() { + if (m_eventLoop.isRunning()) + m_eventLoop.quit(); + }); - // EXTREMELY IMPORTANT: every process within the event loop MUST emit finished() otherwise the loop will not exit - // Note: finished does not indicate the exit status - connect(this, &UpdateInterface::finished, [&]() { if (loop.isRunning()) loop.quit(); }); - - if (okToRun()) { + // sub-processes should also perform this test to return as early as possible on receiving cancel request + if (isOkay()) { // pause before starting next process to allow time for queued events to be processed ie like the cancel request - QTimer::singleShot(100, this, &UpdateInterface::runPreparation); - // NOTE: this will not exit until the finished signal is detected and processed - loop.exec(); + QTimer::singleShot(500, this, &UpdateInterface::runPreparation); + // NOTE: this will not exit until the finished or cancelled signal is detected and processed + m_eventLoop.exec(); } - if (okToRun()) { - QTimer::singleShot(100, this, &UpdateInterface::runBuild); - loop.exec(); + if (isOkay()) { + QTimer::singleShot(500, this, &UpdateInterface::runBuild); + m_eventLoop.exec(); } - if (okToRun()) { - QTimer::singleShot(100, this, &UpdateInterface::runDownload); - loop.exec(); + if (isOkay()) { + QTimer::singleShot(500, this, &UpdateInterface::runDownload); + m_eventLoop.exec(); } - if (okToRun()) { - QTimer::singleShot(100, this, &UpdateInterface::runDecompress); - loop.exec(); + if (isOkay()) { + QTimer::singleShot(500, this, &UpdateInterface::runDecompress); + m_eventLoop.exec(); } - if (okToRun()) { - QTimer::singleShot(100, this, &UpdateInterface::runCopyToDestination); - loop.exec(); + if (isOkay()) { + QTimer::singleShot(500, this, &UpdateInterface::runCopyToDestination); + m_eventLoop.exec(); } // perform before async install in case Companion restarted - if (okToRun()) { - QTimer::singleShot(100, this, &UpdateInterface::runReleaseSettingsSave); - loop.exec(); + if (isOkay()) { + QTimer::singleShot(500, this, &UpdateInterface::runReleaseSettingsSave); + m_eventLoop.exec(); } - if (okToRun()) { - QTimer::singleShot(100, this, &UpdateInterface::runAsyncInstall); - loop.exec(); + if (isOkay()) { + QTimer::singleShot(500, this, &UpdateInterface::runAsyncInstall); + m_eventLoop.exec(); } - if (okToRun()) { - QTimer::singleShot(100, this, &UpdateInterface::runHousekeeping); - loop.exec(); + if (isOkay()) { + QTimer::singleShot(500, this, &UpdateInterface::runHousekeeping); + m_eventLoop.exec(); } + // remove event check from stack otherwise repeated running of update will add unnecessary duplicates disconnect(m_status, &UpdateStatus::cancelled, this, &UpdateInterface::onStatusCancelled); + m_status->reportProgress(tr("%1 update %2").arg(m_name).arg(resultToString()), QtInfoMsg); + + if (!m_status->progress()) + QMessageBox::information(m_status->progress(), CPN_STR_APP_NAME, tr("%1 update %2").arg(m_name).arg(resultToString())); + // allow the dialog to be closed by clicking the close button status()->keepOpen(false); - if (!okToRun()) - return false; + if (m_result != PROC_RESULT_SUCCESS) + m_status->setValue(m_status->maximum()); - m_status->reportProgress(tr("%1 update successful").arg(m_name), QtInfoMsg); + return m_result == PROC_RESULT_SUCCESS; +} - if (!m_status->progress()) - QMessageBox::information(m_status->progress(), CPN_STR_APP_NAME, tr("%1 update successful").arg(m_name)); - - return true; +const QString UpdateInterface::resultToString() const +{ + switch (m_result) { + case PROC_RESULT_FAIL: + return tr("failed"); + case PROC_RESULT_SUCCESS: + return tr("successful"); + case PROC_RESULT_CANCELLED: + return tr("cancelled"); + default: + return tr("unknown"); + } } const QString UpdateInterface::updateDir() const diff --git a/companion/src/updates/updateinterface.h b/companion/src/updates/updateinterface.h index c84387b83..103ce5b4a 100644 --- a/companion/src/updates/updateinterface.h +++ b/companion/src/updates/updateinterface.h @@ -107,7 +107,7 @@ class UpdateInterface : public QWidget bool update(ProgressWidget * progress = nullptr); signals: - void stop(); + void stopping(); void finished(); protected: @@ -156,6 +156,7 @@ class UpdateInterface : public QWidget private slots: void onStatusCancelled(); + void processEvents(); private: const ComponentIdentity m_id; @@ -169,6 +170,8 @@ class UpdateInterface : public QWidget QString m_updateDir; bool m_stopping; int m_result; + QEventLoop m_eventLoop; + QTimer m_timer; void appSettingsInit(); bool checkCreateDirectory(const QString & dirSetting, const UpdateFlags flag); @@ -177,7 +180,9 @@ class UpdateInterface : public QWidget bool setRunFolders(); bool validateFolder(QString & fldr); - bool okToRun() { return m_result == PROC_RESULT_SUCCESS && m_stopping == false; } + bool isOkay() { return m_result == PROC_RESULT_SUCCESS && !m_stopping; } + const QString resultToString() const; + void runAsyncInstall(); void runBuild(); void runCopyToDestination(); diff --git a/companion/src/updates/updatenetwork.cpp b/companion/src/updates/updatenetwork.cpp index baaf6ca43..cd7fb41c7 100644 --- a/companion/src/updates/updatenetwork.cpp +++ b/companion/src/updates/updatenetwork.cpp @@ -49,6 +49,30 @@ UpdateNetwork::~UpdateNetwork() delete m_file; } +void UpdateNetwork::cleanup() +{ + if (m_timer.isActive()) + m_timer.stop(); + + disconnect(&m_timer, &QTimer::timeout, this, &UpdateNetwork::updateProgress); + + if (m_eventLoop.isRunning()) + m_eventLoop.quit(); +} + +void UpdateNetwork::connectReplyCommon() +{ + connect(m_reply, QOverload::of(&QNetworkReply::errorOccurred), [=] (QNetworkReply::NetworkError code) { + m_success = false; + m_status->reportProgress(tr("Network error has occurred. Error code: %1").arg(code), QtCriticalMsg); + }); + + connect(m_reply, &QNetworkReply::sslErrors, [=]() { + m_success = false; + m_status->reportProgress(tr("Ssl library version: %1").arg(QSslSocket::sslLibraryVersionString()), QtCriticalMsg); + }); +} + void UpdateNetwork::convertBufferToJson(QJsonDocument * json) { m_success = false; @@ -64,6 +88,57 @@ void UpdateNetwork::convertBufferToJson(QJsonDocument * json) m_success = true; } +void UpdateNetwork::download(const DownloadDataType type, const QString & url, const char * acceptHeader, const QString & filePath) +{ + m_action = tr("Downloading"); + + if (!init(url)) + return; + + if (type == DDT_GitHub_SaveToFile || type == DDT_Build_SaveToFile) { + m_file = new QFile(filePath); + if (!m_file->open(QIODevice::WriteOnly)) { + m_status->reportProgress(tr("Unable to open the download file %1 for writing. Error: %2").arg(filePath).arg(m_file->errorString()), QtCriticalMsg); + return; + } + } + else + m_file = nullptr; + + if (type >= DDT_GitHub_First && type <= DDT_GitHub_Last) { + m_request.setRawHeader(QByteArray("X-GitHub-Api-Version"), GH_API_VERSION); + m_request.setRawHeader(QByteArray("Accept"), QByteArray(acceptHeader)); + } + + // returns a new QNetworkReply object + m_reply = m_manager.get(m_request); + + // connect to the signals of the new object + connectReplyCommon(); + + connect(m_reply, &QNetworkReply::downloadProgress, [=](const qint64 bytesRead, const qint64 totalBytes) { + m_status->setMaximum(totalBytes); + m_status->setValue(bytesRead); + }); + + connect(m_reply, &QNetworkReply::readyRead, [=]() { + if (type == DDT_GitHub_SaveToFile || type == DDT_Build_SaveToFile) { + m_file->write(m_reply->readAll()); + } + else { + const QByteArray qba = m_reply->readAll(); + m_buffer->append(qba); + } + }); + + connect(m_reply, &QNetworkReply::finished, [=]() { onGetFinished(m_reply, type); }); + + connect(m_status, &UpdateStatus::cancelled, this, &UpdateNetwork::cancelDownload); + + m_timer.start(); + m_eventLoop.exec(); +} + // static QString UpdateNetwork::downloadDataTypeToString(const DownloadDataType val) { @@ -140,6 +215,7 @@ void UpdateNetwork::downloadToFile(const DownloadDataType type, const QString & int reply = QMessageBox::question(m_status->progress(), CPN_STR_APP_NAME, tr("File %1 exists. Download again?").arg(fi.fileName()), QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No); if (reply == QMessageBox::Cancel) { + m_status->reportProgress(tr("Download cancelled by user"), QtWarningMsg); return; } else if (reply == QMessageBox::No) { @@ -162,82 +238,6 @@ void UpdateNetwork::downloadToFile(const DownloadDataType type, const QString & fi.absoluteFilePath()); } -void UpdateNetwork::download(const DownloadDataType type, const QString & url, const char * acceptHeader, const QString & filePath) -{ - m_status->setValue(0); - m_status->setMaximum(100); - - if (!init(url)) - return; - - if (type == DDT_GitHub_SaveToFile || type == DDT_Build_SaveToFile) { - m_file = new QFile(filePath); - if (!m_file->open(QIODevice::WriteOnly)) { - m_status->reportProgress(tr("Unable to open the download file %1 for writing. Error: %2").arg(filePath).arg(m_file->errorString()), QtCriticalMsg); - return; - } - } - else - m_file = nullptr; - - if (type >= DDT_GitHub_First && type <= DDT_GitHub_Last) { - m_request.setRawHeader(QByteArray("X-GitHub-Api-Version"), GH_API_VERSION); - m_request.setRawHeader(QByteArray("Accept"), QByteArray(acceptHeader)); - } - - // run GET inside event loop with timer interrupts to allow other events to run - QEventLoop loop; - - QTimer timer; - timer.setInterval(1000); - - connect(&timer, &QTimer::timeout, [&]() { - m_status->reportProgress(tr("Downloading..."), QtDebugMsg); - }); - - m_reply = m_manager.get(m_request); - - connect(m_reply, QOverload::of(&QNetworkReply::errorOccurred), [=] (QNetworkReply::NetworkError code) { - m_success = false; - m_status->reportProgress(tr("Network error has occurred. Error code: %1").arg(code), QtCriticalMsg); - }); - - connect(m_reply, &QNetworkReply::sslErrors, [=]() { - m_success = false; - m_status->reportProgress(tr("Ssl library version: %1").arg(QSslSocket::sslLibraryVersionString()), QtCriticalMsg); - }); - - connect(m_reply, &QNetworkReply::downloadProgress, [=](const qint64 bytesRead, const qint64 totalBytes) { - m_status->setMaximum(totalBytes); - m_status->setValue(bytesRead); - }); - - connect(m_reply, &QNetworkReply::readyRead, [=]() { - if (type == DDT_GitHub_SaveToFile || type == DDT_Build_SaveToFile) { - m_file->write(m_reply->readAll()); - } - else { - const QByteArray qba = m_reply->readAll(); - m_buffer->append(qba); - } - }); - - connect(m_reply, &QNetworkReply::finished, [&]() { - if (timer.isActive()) - timer.stop(); - - if (loop.isRunning()) - loop.quit(); - - disconnect(m_reply, nullptr); - - onDownloadFinished(m_reply, type); - }); - - timer.start(); - loop.exec(); -} - QByteArray * UpdateNetwork::getDownloadBuffer() { return m_buffer; @@ -245,10 +245,21 @@ QByteArray * UpdateNetwork::getDownloadBuffer() bool UpdateNetwork::init(const QString & urlStr) { + m_status->setValue(0); + m_status->setMaximum(100); + m_buffer->clear(); m_success = false; m_request = QNetworkRequest(); + if (m_reply) { + delete m_reply; + m_reply = nullptr; + } + + m_timer.setInterval(1000); + connect(&m_timer, &QTimer::timeout, this, &UpdateNetwork::updateProgress); + m_url.setUrl(urlStr); if (!m_url.isValid()) { @@ -269,8 +280,11 @@ const bool UpdateNetwork::isSuccess() return m_success; } -void UpdateNetwork::onDownloadFinished(QNetworkReply * reply, DownloadDataType type) +void UpdateNetwork::onGetFinished(QNetworkReply * reply, DownloadDataType type) { + cleanup(); + disconnect(m_status, &UpdateStatus::cancelled, this, &UpdateNetwork::cancelDownload); + m_status->setValue(m_status->maximum()); if (type == DDT_GitHub_SaveToFile || type == DDT_Build_SaveToFile) { @@ -292,64 +306,56 @@ void UpdateNetwork::onDownloadFinished(QNetworkReply * reply, DownloadDataType t delete m_file; m_file = nullptr; } + + if (m_reply) { + delete m_reply; + m_reply = nullptr; + } +} + +void UpdateNetwork::onPostFinished() +{ + cleanup(); + + if (m_reply->error()) { + m_success = false; + m_status->reportProgress(tr("POST error: %2\n%3").arg(m_reply->error()).arg(m_reply->errorString()), QtCriticalMsg); + } + + if (m_reply) { + delete m_reply; + m_reply = nullptr; + } + + m_success = true; } void UpdateNetwork::post(const QString & action, const QString & url, QJsonDocument * data) { - m_status->setValue(0); - m_status->setMaximum(0); // put progress bar in wait mode + m_action = action; if (!init(url)) return; + m_status->setMaximum(0); // put progress bar into wait mode as cannot guess how long request will run + m_request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - // run POST inside event loop with timer interrupts to allow other events to run - QEventLoop loop; - - QTimer timer; - timer.setInterval(1000); - - connect(&timer, &QTimer::timeout, [&]() { - m_status->reportProgress(QString("%1...").arg(action), QtDebugMsg); - }); - + // returns a new QNetworkReply object m_reply = m_manager.post(m_request, data->toJson(QJsonDocument::Compact)); - connect(m_reply, QOverload::of(&QNetworkReply::errorOccurred), [=] (QNetworkReply::NetworkError code) { - m_success = false; - m_status->reportProgress(tr("Network error has occurred. Error code: %1").arg(code), QtCriticalMsg); - }); - - connect(m_reply, &QNetworkReply::sslErrors, [=]() { - m_success = false; - m_status->reportProgress(tr("Ssl library version: %1").arg(QSslSocket::sslLibraryVersionString()), QtCriticalMsg); - }); + // connect to the signals of the new object + connectReplyCommon(); connect(m_reply, &QNetworkReply::readyRead, [=]() { const QByteArray qba = m_reply->readAll(); m_buffer->append(qba); }); - connect(m_reply, &QNetworkReply::finished, [&]() { - if (timer.isActive()) - timer.stop(); + connect(m_reply, &QNetworkReply::finished, this, &UpdateNetwork::onPostFinished); - if (loop.isRunning()) - loop.quit(); - - disconnect(m_reply, nullptr); - - if (m_reply->error()) { - m_success = false; - m_status->reportProgress(tr("POST error: %2\n%3").arg(m_reply->error()).arg(m_reply->errorString()), QtCriticalMsg); - } - - m_success = true; - }); - - timer.start(); - loop.exec(); + m_timer.start(); + m_eventLoop.exec(); } bool UpdateNetwork::saveBufferToFile(const QString & filePath) @@ -396,3 +402,14 @@ void UpdateNetwork::submitRequest(const QString & action, const QString & url, Q if (m_success) convertBufferToJson(response); } + +void UpdateNetwork::cancelDownload() +{ + m_status->reportProgress(tr("Download cancelled"), QtWarningMsg); + m_reply->abort(); +} + +void UpdateNetwork::updateProgress() +{ + m_status->reportProgress(QString("%1...").arg(m_action), QtDebugMsg); +} diff --git a/companion/src/updates/updatenetwork.h b/companion/src/updates/updatenetwork.h index 54d3e1e2d..4a9183a16 100644 --- a/companion/src/updates/updatenetwork.h +++ b/companion/src/updates/updatenetwork.h @@ -72,7 +72,10 @@ class UpdateNetwork : public QObject static QString downloadDataTypeToString(const DownloadDataType val); private slots: - void onDownloadFinished(QNetworkReply * reply, DownloadDataType ddt); + void onGetFinished(QNetworkReply * reply, DownloadDataType ddt); + void onPostFinished(); + void updateProgress(); + void cancelDownload(); private: UpdateStatus *m_status; @@ -83,6 +86,11 @@ class UpdateNetwork : public QObject QFile *m_file; QUrl m_url; bool m_success; + QEventLoop m_eventLoop; + QTimer m_timer; + QString m_action; bool init(const QString & url); + void connectReplyCommon(); + void cleanup(); }; diff --git a/companion/src/updates/updates.cpp b/companion/src/updates/updates.cpp index 3d879d0c6..759d06bff 100644 --- a/companion/src/updates/updates.cpp +++ b/companion/src/updates/updates.cpp @@ -28,6 +28,7 @@ #include "helpers.h" #include +#include Updates::Updates(QWidget * parent, UpdateFactories * updateFactories) : QWidget(parent), @@ -127,6 +128,9 @@ void Updates::runUpdate() ok = factories->updateAll(progressDialog.progress()); progressDialog.setProcessStopped(); progressDialog.progress()->setInfo(tr("Finished %1").arg(ok ? tr("successfully") : tr("with errors"))); + progressDialog.progress()->setValue(progressDialog.progress()->maximum()); + progressDialog.progress()->refresh(); + QApplication::processEvents(); progressDialog.exec(); if (ok)