diff --git a/src/Cutter.pro b/src/Cutter.pro index 86c85454..d5f5e2f0 100644 --- a/src/Cutter.pro +++ b/src/Cutter.pro @@ -312,7 +312,8 @@ SOURCES += \ common/PythonManager.cpp \ plugins/PluginManager.cpp \ common/BasicBlockHighlighter.cpp \ - dialogs/LinkTypeDialog.cpp + dialogs/LinkTypeDialog.cpp \ + common/UpdateWorker.cpp HEADERS += \ core/Cutter.h \ @@ -429,6 +430,7 @@ HEADERS += \ common/PythonManager.h \ plugins/PluginManager.h \ common/BasicBlockHighlighter.h \ + common/UpdateWorker.h \ dialogs/LinkTypeDialog.h FORMS += \ diff --git a/src/CutterApplication.cpp b/src/CutterApplication.cpp index 50298c17..cb4f8d4d 100644 --- a/src/CutterApplication.cpp +++ b/src/CutterApplication.cpp @@ -23,6 +23,10 @@ #include +#ifdef Q_OS_WIN +#include +#endif // Q_OS_WIN + CutterApplication::CutterApplication(int &argc, char **argv) : QApplication(argc, argv) { // Setup application information @@ -32,6 +36,13 @@ CutterApplication::CutterApplication(int &argc, char **argv) : QApplication(argc setLayoutDirection(Qt::LeftToRight); // WARN!!! Put initialization code below this line. Code above this line is mandatory to be run First + +#ifdef Q_OS_WIN + // Hack to force Cutter load internet connection related DLL's + QSslSocket s; + s.sslConfiguration(); +#endif // Q_OS_WIN + // Load translations if (!loadTranslations()) { qWarning() << "Cannot load translations"; diff --git a/src/Main.cpp b/src/Main.cpp index 62e23575..564d7a25 100644 --- a/src/Main.cpp +++ b/src/Main.cpp @@ -1,6 +1,8 @@ #include "CutterApplication.h" #include "core/MainWindow.h" +#include "common/UpdateWorker.h" +#include "CutterConfig.h" /** * @brief Migrate Settings used before Cutter 1.8 @@ -34,6 +36,18 @@ int main(int argc, char *argv[]) CutterApplication a(argc, argv); + if (Config()->getAutoUpdateEnabled()) { + UpdateWorker *updateWorker = new UpdateWorker; + QObject::connect(updateWorker, &UpdateWorker::checkComplete, + [=](const QString & version, const QString & error) { + if (error == "" && version != CUTTER_VERSION_FULL) { + updateWorker->showUpdateDialog(true); + } + updateWorker->deleteLater(); + }); + updateWorker->checkCurrentVersion(7000); + } + int ret = a.exec(); return ret; diff --git a/src/common/Configuration.cpp b/src/common/Configuration.cpp index 0977d469..f1d27e3a 100644 --- a/src/common/Configuration.cpp +++ b/src/common/Configuration.cpp @@ -136,6 +136,16 @@ void Configuration::resetAll() emit fontsUpdated(); } +bool Configuration::getAutoUpdateEnabled() const +{ + return s.value("autoUpdateEnabled", false).toBool(); +} + +void Configuration::setAutoUpdateEnabled(bool au) +{ + s.setValue("autoUpdateEnabled", au); +} + /** * @brief get the current Locale set in Cutter's user configuration * @return a QLocale object describes user's current locale diff --git a/src/common/Configuration.h b/src/common/Configuration.h index e7a9296b..3e48748e 100644 --- a/src/common/Configuration.h +++ b/src/common/Configuration.h @@ -46,6 +46,10 @@ public: void resetAll(); + // Auto update + bool getAutoUpdateEnabled() const; + void setAutoUpdateEnabled(bool au); + // Languages QLocale getCurrLocale() const; void setLocale(const QLocale &l); @@ -58,7 +62,7 @@ public: // Colors bool windowColorIsDark(); - void setLastThemeOf(const CutterQtTheme &currQtTheme, const QString& theme); + void setLastThemeOf(const CutterQtTheme &currQtTheme, const QString &theme); QString getLastThemeOf(const CutterQtTheme &currQtTheme) const; const QColor getColor(const QString &name) const; void setTheme(int theme); diff --git a/src/common/UpdateWorker.cpp b/src/common/UpdateWorker.cpp new file mode 100644 index 00000000..816bec0f --- /dev/null +++ b/src/common/UpdateWorker.cpp @@ -0,0 +1,211 @@ +#include "UpdateWorker.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include "common/Configuration.h" +#include "CutterConfig.h" + +UpdateWorker::UpdateWorker(QObject *parent) : + QObject(parent), latestVersion(""), pending(false) +{ + connect(&t, &QTimer::timeout, [this]() { + if (pending) { + disconnect(checkReply, nullptr, this, nullptr); + checkReply->close(); + checkReply->deleteLater(); + emit checkComplete("", tr("Time limit exceeded during version check. Please check your " + "internet connection and try again.")); + } + }); +} + +void UpdateWorker::checkCurrentVersion(time_t timeoutMs) +{ + QUrl url("https://api.github.com/repos/radareorg/cutter/releases/latest"); + QNetworkRequest request; + request.setUrl(url); + + t.setInterval(timeoutMs); + t.setSingleShot(true); + t.start(); + + checkReply = nm.get(request); + connect(checkReply, &QNetworkReply::finished, + this, &UpdateWorker::serveVersionCheckReply); + pending = true; +} + +void UpdateWorker::download(QString filename, QString version) +{ + downloadFile.setFileName(filename); + downloadFile.open(QIODevice::WriteOnly); + + QNetworkRequest request; + request.setAttribute(QNetworkRequest::FollowRedirectsAttribute, true); + QUrl url(QString("https://github.com/radareorg/cutter/releases/" + "download/v%1/%2").arg(version).arg(getRepositoryFileName())); + request.setUrl(url); + + downloadReply = nm.get(request); + connect(downloadReply, &QNetworkReply::downloadProgress, + this, &UpdateWorker::process); + connect(downloadReply, &QNetworkReply::finished, + this, &UpdateWorker::serveDownloadFinish); +} + +void UpdateWorker::showUpdateDialog(bool showDontCheckForUpdatesButton) +{ + QMessageBox mb; + mb.setWindowTitle(tr("Version control")); + mb.setText(tr("There is an update available for Cutter.
") + + "" + tr("Current version:") + " " CUTTER_VERSION_FULL "
" + + "" + tr("Latest version:") + " " + latestVersion + "

" + + tr("For update, please check the link:
") + + QString("" + "https://github.com/radareorg/cutter/releases/tag/v%1
").arg(latestVersion) + + tr("or click \"Download\" to download latest version of Cutter.")); + if (showDontCheckForUpdatesButton) { + mb.setStandardButtons(QMessageBox::Save | QMessageBox::Reset | QMessageBox::Ok); + mb.button(QMessageBox::Reset)->setText(tr("Don't check for updates")); + } else { + mb.setStandardButtons(QMessageBox::Save | QMessageBox::Ok); + } + mb.button(QMessageBox::Save)->setText(tr("Download")); + mb.setDefaultButton(QMessageBox::Ok); + int ret = mb.exec(); + if (ret == QMessageBox::Reset) { + Config()->setAutoUpdateEnabled(false); + } else if (ret == QMessageBox::Save) { + QString fullFileName = + QFileDialog::getSaveFileName(nullptr, + tr("Choose directory for downloading"), + QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + + QDir::separator() + getRepositoryFileName(), + QString("%1 (*.%1)").arg(getRepositeryExt())); + if (fullFileName != "") { + QProgressDialog progressDial(tr("Downloading update..."), + tr("Cancel"), + 0, 100); + connect(this, &UpdateWorker::downloadProcess, + [&progressDial](size_t curr, size_t total) { + progressDial.setValue(100.0f * curr / total); + }); + connect(&progressDial, &QProgressDialog::canceled, + this, &UpdateWorker::abortDownload); + connect(this, &UpdateWorker::downloadFinished, + &progressDial, &QProgressDialog::cancel); + connect(this, &UpdateWorker::downloadFinished, + [](QString filePath){ + QMessageBox info(QMessageBox::Information, + tr("Download finished!"), + tr("Latest version of Cutter was succesfully downloaded!"), + QMessageBox::Yes | QMessageBox::Open | QMessageBox::Ok, + nullptr); + info.button(QMessageBox::Open)->setText(tr("Open file")); + info.button(QMessageBox::Yes)->setText(tr("Open download folder")); + int r = info.exec(); + if (r == QMessageBox::Open) { + QDesktopServices::openUrl(filePath); + } else if (r == QMessageBox::Yes) { + auto path = filePath.split('/'); + path.removeLast(); + QDesktopServices::openUrl(path.join('/')); + } + }); + download(fullFileName, latestVersion); + // Calling show() before exec() is only way make dialog non-modal + // it seems wierd, but it works + progressDial.show(); + progressDial.exec(); + } + } +} + +void UpdateWorker::abortDownload() +{ + disconnect(downloadReply, &QNetworkReply::finished, + this, &UpdateWorker::serveDownloadFinish); + disconnect(downloadReply, &QNetworkReply::downloadProgress, + this, &UpdateWorker::process); + downloadReply->close(); + downloadReply->deleteLater(); + downloadFile.remove(); +} + +void UpdateWorker::serveVersionCheckReply() +{ + pending = false; + QString versionReply = ""; + QString errStr = ""; + if (checkReply->error()) { + errStr = checkReply->errorString(); + } else { + versionReply = QJsonDocument::fromJson(checkReply->readAll()).object().value("tag_name").toString(); + versionReply.remove('v'); + } + latestVersion = versionReply; + checkReply->close(); + checkReply->deleteLater(); + emit checkComplete(versionReply, errStr); +} + +void UpdateWorker::serveDownloadFinish() +{ + downloadReply->close(); + downloadReply->deleteLater(); + if (downloadReply->error()) { + emit downloadError(downloadReply->errorString()); + } else { + emit downloadFinished(downloadFile.fileName()); + } +} + +void UpdateWorker::process(size_t bytesReceived, size_t bytesTotal) +{ + downloadFile.write(downloadReply->readAll()); + emit downloadProcess(bytesReceived, bytesTotal); +} + +QString UpdateWorker::getRepositeryExt() const +{ +#ifdef Q_OS_LINUX + return "AppImage"; +#elif defined (Q_OS_WIN64) || defined (Q_OS_WIN32) + return "zip"; +#elif defined (Q_OS_MACOS) + return "dmg"; +#endif +} + +QString UpdateWorker::getRepositoryFileName() const +{ + QString downloadFileName; +#ifdef Q_OS_LINUX + downloadFileName = "Cutter-v%1-x%2.Linux.AppImage"; +#elif defined (Q_OS_WIN64) || defined (Q_OS_WIN32) + downloadFileName = "Cutter-v%1-x%2.Windows.zip"; +#elif defined (Q_OS_MACOS) + downloadFileName = "Cutter-v%1-x%2.macOS.dmg"; +#endif + downloadFileName = downloadFileName + .arg(latestVersion) + .arg(QSysInfo::buildAbi().split('-').at(2).contains("64") + ? "64" + : "32"); + + return downloadFileName; +} diff --git a/src/common/UpdateWorker.h b/src/common/UpdateWorker.h new file mode 100644 index 00000000..54790199 --- /dev/null +++ b/src/common/UpdateWorker.h @@ -0,0 +1,123 @@ +#ifndef UPDATEWORKER_H +#define UPDATEWORKER_H + +#include +#include +#include +#include + +class QNetworkReply; + +/** + * @class UpdateWorker + * @brief The UpdateWorker class is a class providing API to check for current Cutter version + * and download specific version of one. + */ + +class UpdateWorker : public QObject +{ + Q_OBJECT +public: + explicit UpdateWorker(QObject *parent = nullptr); + + /** + * @fn void UpdateWorker::checkCurrentVersion(time_t timeoutMs) + * + * Sends request to determine current version of Cutter. + * If there is no response in @a timeoutMs milliseconds, emits + * @fn UpdateWorker::checkComplete(const QString& currVerson, const QString& errorMsg) + * with timeout error message. + * + * + * @sa checkComplete(const QString& verson, const QString& errorMsg) + */ + + void checkCurrentVersion(time_t timeoutMs); + + /** + * @fn void UpdateWorker::download(QDir downloadPath, QString version) + * + * @brief Downloads provided @a version of Cutter into @a downloadDir. + * + * @sa downloadProcess(size_t bytesReceived, size_t bytesTotal) + */ + void download(QString filename, QString version); + + /** + * @fn void UpdateWorker::showUpdateDialog() + * + * Shows dialog that allows user to either download latest version of Cutter from website + * or download it by clicking on a button. This dialog also has "Don't check for updates" + * button which disables on-start update checks if @a showDontCheckForUpdatesButton is true. + * + * @sa downloadProcess(size_t bytesReceived, size_t bytesTotal) + */ + void showUpdateDialog(bool showDontCheckForUpdatesButton); + +public slots: + /** + * @fn void UpdateWorker::abortDownload() + * + * @brief Stops current process of downloading. + * + * @note UpdateWorker::downloadFinished(QString filename) is not send after this function. + * + * @sa download(QDir downloadDir, QString version) + */ + void abortDownload(); + +signals: + /** + * @fn UpdateWorker::checkComplete(const QString& verson, const QString& errorMsg) + * + * The signal is emitted when check has been done with an empty @a errorMsg string. + * In case of an error @a currVerson is empty and @a errorMsg contains description + * of error. + */ + void checkComplete(const QString &currVerson, const QString &errorMsg); + + /** + * @fn UpdateWorker::downloadProcess(size_t bytesReceived, size_t bytesTotal) + * + * The signal is emitted each time when some amount of bytes was downloaded. + * May be used as indicator of download progress. + */ + void downloadProcess(size_t bytesReceived, size_t bytesTotal); + + + /** + * @fn UpdateWorker::downloadFinished(QString filename) + * + * @brief The signal is emitted as soon as downloading completes. + */ + void downloadFinished(QString filename); + + /** + * @fn UpdateWorker::downloadError(QString errorStr) + * + * @brief The signal is emitted when error occures during download. + */ + void downloadError(QString errorStr); + +private slots: + void serveVersionCheckReply(); + + void serveDownloadFinish(); + + void process(size_t bytesReceived, size_t bytesTotal); + +private: + QString getRepositeryExt() const; + QString getRepositoryFileName() const; + +private: + QNetworkAccessManager nm; + QString latestVersion; + QTimer t; + bool pending; + QFile downloadFile; + QNetworkReply *downloadReply; + QNetworkReply *checkReply; +}; + +#endif // UPDATEWORKER_H diff --git a/src/dialogs/AboutDialog.cpp b/src/dialogs/AboutDialog.cpp index e8d52069..9a9e49a4 100644 --- a/src/dialogs/AboutDialog.cpp +++ b/src/dialogs/AboutDialog.cpp @@ -8,9 +8,11 @@ #include #include +#include #include #include #include +#include #include #include @@ -25,29 +27,32 @@ AboutDialog::AboutDialog(QWidget *parent) : ui->logoSvgWidget->load(Config()->getLogoFile()); QString aboutString("

Cutter

" - + tr("Version") + " " CUTTER_VERSION_FULL "
" - + tr("Using r2-") + R2_GITTAP - + "

" + tr("Optional Features:") + "
" - + QString("Jupyter: %1
").arg( + + tr("Version") + " " CUTTER_VERSION_FULL "
" + + tr("Using r2-") + R2_GITTAP + + "

" + tr("Optional Features:") + "
" + + QString("Jupyter: %1
").arg( #ifdef CUTTER_ENABLE_JUPYTER - "ON" + "ON" #else - "OFF" + "OFF" #endif - ) - + QString("QtWebEngine: %2

").arg( + ) + + QString("QtWebEngine: %2

").arg( #ifdef CUTTER_ENABLE_QTWEBENGINE - "ON" + "ON" #else - "OFF" + "OFF" #endif - ) - + "

" + tr("License") + "

" - + tr("This Software is released under the GNU General Public License v3.0") - + "

" + tr("Authors") + "

" - "xarkes, thestr4ng3r, ballessay
" - "Based on work by Hugo Teso <hugo.teso@gmail.org> (originally Iaito)."); + ) + + "

" + tr("License") + "

" + + tr("This Software is released under the GNU General Public License v3.0") + + "

" + tr("Authors") + "

" + "xarkes, thestr4ng3r, ballessay
" + "Based on work by Hugo Teso <hugo.teso@gmail.org> (originally Iaito)."); ui->label->setText(aboutString); + + QSignalBlocker s(ui->updatesCheckBox); + ui->updatesCheckBox->setChecked(Config()->getAutoUpdateEnabled()); } AboutDialog::~AboutDialog() {} @@ -75,9 +80,7 @@ void AboutDialog::on_showPluginsButton_clicked() void AboutDialog::on_checkForUpdatesButton_clicked() { - QUrl url("https://api.github.com/repos/radareorg/cutter/releases/latest"); - QNetworkRequest request; - request.setUrl(url); + UpdateWorker updateWorker; QProgressDialog waitDialog; QProgressBar *bar = new QProgressBar(&waitDialog); @@ -86,56 +89,25 @@ void AboutDialog::on_checkForUpdatesButton_clicked() waitDialog.setBar(bar); waitDialog.setLabel(new QLabel(tr("Checking for updates..."), &waitDialog)); - QNetworkAccessManager nm; - - QTimer timeoutTimer; - timeoutTimer.setSingleShot(true); - timeoutTimer.setInterval(7000); - - connect(&nm, &QNetworkAccessManager::finished, &timeoutTimer, &QTimer::stop); - connect(&nm, &QNetworkAccessManager::finished, &waitDialog, &QProgressDialog::cancel); - connect(&nm, &QNetworkAccessManager::finished, this, &AboutDialog::serveVersionCheckReply); - - QNetworkReply *reply = nm.get(request); - timeoutTimer.start(); - - connect(&timeoutTimer, &QTimer::timeout, []() { - QMessageBox mb; - mb.setIcon(QMessageBox::Critical); - mb.setStandardButtons(QMessageBox::Ok); - mb.setWindowTitle(tr("Timeout error!")); - mb.setText(tr("Please check your internet connection and try again.")); - mb.exec(); + connect(&updateWorker, &UpdateWorker::checkComplete, &waitDialog, &QProgressDialog::cancel); + connect(&updateWorker, &UpdateWorker::checkComplete, + [&updateWorker](const QString & version, const QString & error) { + if (error != "") { + QMessageBox::critical(nullptr, tr("Error!"), error); + } else { + if (version == CUTTER_VERSION_FULL) { + QMessageBox::information(nullptr, tr("Version control"), tr("Cutter is up to date!")); + } else { + updateWorker.showUpdateDialog(false); + } + } }); + updateWorker.checkCurrentVersion(7000); waitDialog.exec(); - delete reply; } -void AboutDialog::serveVersionCheckReply(QNetworkReply *reply) +void AboutDialog::on_updatesCheckBox_stateChanged(int state) { - QString currVersion = ""; - QMessageBox mb; - mb.setStandardButtons(QMessageBox::Ok); - if (reply->error()) { - mb.setIcon(QMessageBox::Critical); - mb.setWindowTitle(tr("Error!")); - mb.setText(reply->errorString()); - } else { - currVersion = QJsonDocument::fromJson(reply->readAll()).object().value("tag_name").toString(); - currVersion.remove('v'); - - mb.setWindowTitle(tr("Version control")); - mb.setIcon(QMessageBox::Information); - if (currVersion == CUTTER_VERSION_FULL) { - mb.setText(tr("You have latest version and no need to update!")); - } else { - mb.setText("" + tr("Current version:") + " " CUTTER_VERSION_FULL "
" - + "" + tr("Latest version:") + " " + currVersion + "

" - + tr("For update, please check the link:") - + "" - + "https://github.com/radareorg/cutter/releases"); - } - } - mb.exec(); + Config()->setAutoUpdateEnabled(!Config()->getAutoUpdateEnabled()); } diff --git a/src/dialogs/AboutDialog.h b/src/dialogs/AboutDialog.h index 669757aa..3d094736 100644 --- a/src/dialogs/AboutDialog.h +++ b/src/dialogs/AboutDialog.h @@ -21,8 +21,20 @@ private slots: void on_buttonBox_rejected(); void on_showVersionButton_clicked(); void on_showPluginsButton_clicked(); + + /** + * @fn AboutDialog::on_checkForUpdatesButton_clicked() + * + * @brief Initiates process of checking for updates. + */ void on_checkForUpdatesButton_clicked(); - void serveVersionCheckReply(QNetworkReply *reply); + + /** + * @fn AboutDialog::on_updatesCheckBox_stateChanged(int state) + * + * @brief Changes value of autoUpdateEnabled option in settings. + */ + void on_updatesCheckBox_stateChanged(int state); private: std::unique_ptr ui; diff --git a/src/dialogs/AboutDialog.ui b/src/dialogs/AboutDialog.ui index 96b0695f..dea81d4c 100644 --- a/src/dialogs/AboutDialog.ui +++ b/src/dialogs/AboutDialog.ui @@ -76,6 +76,19 @@ + + + + + 0 + 0 + + + + Check for updates on start + + + diff --git a/src/dialogs/WelcomeDialog.cpp b/src/dialogs/WelcomeDialog.cpp index a70f47ef..6359467e 100644 --- a/src/dialogs/WelcomeDialog.cpp +++ b/src/dialogs/WelcomeDialog.cpp @@ -20,8 +20,9 @@ WelcomeDialog::WelcomeDialog(QWidget *parent) : ui->logoSvgWidget->load(Config()->getLogoFile()); ui->versionLabel->setText("" + tr("Version ") + CUTTER_VERSION_FULL + ""); ui->themeComboBox->setCurrentIndex(Config()->getTheme()); - ui->themeComboBox->setFixedWidth(200); - ui->themeComboBox->view()->setFixedWidth(200); + + QSignalBlocker s(ui->updatesCheckBox); + ui->updatesCheckBox->setChecked(Config()->getAutoUpdateEnabled()); QStringList langs = Config()->getAvailableTranslations(); ui->languageComboBox->addItems(langs); @@ -97,3 +98,8 @@ void WelcomeDialog::on_continueButton_clicked() { accept(); } + +void WelcomeDialog::on_updatesCheckBox_stateChanged(int state) +{ + Config()->setAutoUpdateEnabled(!Config()->getAutoUpdateEnabled()); +} diff --git a/src/dialogs/WelcomeDialog.h b/src/dialogs/WelcomeDialog.h index 6f8c423a..1ac8627b 100644 --- a/src/dialogs/WelcomeDialog.h +++ b/src/dialogs/WelcomeDialog.h @@ -30,6 +30,7 @@ private slots: void onLanguageComboBox_currentIndexChanged(int index); void on_checkUpdateButton_clicked(); void on_continueButton_clicked(); + void on_updatesCheckBox_stateChanged(int state); private: Ui::WelcomeDialog *ui; diff --git a/src/dialogs/WelcomeDialog.ui b/src/dialogs/WelcomeDialog.ui index a43859d0..4f7c1d38 100644 --- a/src/dialogs/WelcomeDialog.ui +++ b/src/dialogs/WelcomeDialog.ui @@ -7,7 +7,7 @@ 0 0 806 - 488 + 620 @@ -122,84 +122,118 @@ - - - QLayout::SetFixedSize - - - 0 - - - 9 - - - + + + + + Qt::Horizontal + + + + 40 + 20 + + + - - - - - 0 - 0 - + + + + 9 - - - 0 - 0 - - - - - 16777215 - 16777215 - - - - 0 - - - - 160 - 16 - + + QLayout::SetMaximumSize - - Native Theme - + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + About + + - - Dark Theme - + + + + 0 + 0 + + + + + 0 + 0 + + + + + 16777215 + 16777215 + + + + 0 + + + + 16 + 16 + + + + + Native Theme + + + + + Dark Theme + + + - + + + + + + + Check for updates on start + + + + - - - - - 0 - 0 - + + + + Qt::Horizontal - + - 0 - 0 + 40 + 20 - - - 16777215 - 16777215 - - - - About - - +