diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,6 +77,7 @@ qt5_wrap_ui(UI_FILES src/binarystreamreadersettings.ui src/asciireadersettings.ui src/framedreadersettings.ui + src/updatecheckdialog.ui ) if (WIN32) @@ -126,6 +127,9 @@ add_executable(${PROGRAM_NAME} WIN32 src/framedreadersettings.cpp src/plotmanager.cpp src/numberformat.cpp + src/updatechecker.cpp + src/versionnumber.cpp + src/updatecheckdialog.cpp misc/windows_icon.rc ${UI_FILES} ${RES_FILES} @@ -136,7 +140,7 @@ target_link_libraries(${PROGRAM_NAME} ${QWT_LIBRARY} ${QTCOLORWIDGETS_LIBRARIES} ) -qt5_use_modules(${PROGRAM_NAME} Widgets SerialPort) +qt5_use_modules(${PROGRAM_NAME} Widgets SerialPort Network) if (BUILD_QWT) add_dependencies(${PROGRAM_NAME} QWT) @@ -169,6 +173,9 @@ set(VERSION_REVISION "0") include(GetVersion) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_STRING=\\\"${VERSION_STRING}\\\" ") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_MAJOR=${VERSION_MAJOR} ") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_MINOR=${VERSION_MINOR} ") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_PATCH=${VERSION_PATCH} ") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -DVERSION_REVISION=\\\"${VERSION_REVISION}\\\" ") # add make run target diff --git a/serialplot.pro b/serialplot.pro --- a/serialplot.pro +++ b/serialplot.pro @@ -68,7 +68,9 @@ SOURCES += \ src/framedreader.cpp \ src/plotmanager.cpp \ src/numberformat.cpp \ - src/recordpanel.cpp + src/recordpanel.cpp \ + src/updatechecker.cpp \ + src/updatecheckdialog.cpp HEADERS += \ src/mainwindow.h \ @@ -108,7 +110,9 @@ HEADERS += \ src/plotmanager.h \ src/setting_defines.h \ src/numberformat.h \ - src/recordpanel.h + src/recordpanel.h \ + src/updatechecker.h \ + src/updatecheckdialog.h FORMS += \ src/mainwindow.ui \ @@ -124,7 +128,8 @@ FORMS += \ src/framedreadersettings.ui \ src/binarystreamreadersettings.ui \ src/asciireadersettings.ui \ - src/recordpanel.ui + src/recordpanel.ui \ + src/updatecheckdialog.ui INCLUDEPATH += qmake/ src/ diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -63,7 +63,8 @@ MainWindow::MainWindow(QWidget *parent) snapshotMan(this, &channelMan), commandPanel(&serialPort), dataFormatPanel(&serialPort, &channelMan, &recorder), - recordPanel(&recorder, &channelMan) + recordPanel(&recorder, &channelMan), + updateCheckDialog(this) { ui->setupUi(this); @@ -112,6 +113,9 @@ MainWindow::MainWindow(QWidget *parent) QObject::connect(ui->actionHelpAbout, &QAction::triggered, &aboutDialog, &QWidget::show); + QObject::connect(ui->actionCheckUpdate, &QAction::triggered, + &updateCheckDialog, &QWidget::show); + QObject::connect(ui->actionReportBug, &QAction::triggered, [](){QDesktopServices::openUrl(QUrl(BUG_REPORT_URL));}); @@ -546,6 +550,7 @@ void MainWindow::saveAllSettings(QSettin plotMan->saveSettings(settings); commandPanel.saveSettings(settings); recordPanel.saveSettings(settings); + updateCheckDialog.saveSettings(settings); } void MainWindow::loadAllSettings(QSettings* settings) @@ -558,6 +563,7 @@ void MainWindow::loadAllSettings(QSettin plotMan->loadSettings(settings); commandPanel.loadSettings(settings); recordPanel.loadSettings(settings); + updateCheckDialog.loadSettings(settings); } void MainWindow::saveMWSettings(QSettings* settings) diff --git a/src/mainwindow.h b/src/mainwindow.h --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -45,6 +45,7 @@ #include "snapshotmanager.h" #include "plotmanager.h" #include "datarecorder.h" +#include "updatecheckdialog.h" namespace Ui { class MainWindow; @@ -85,6 +86,7 @@ private: DataFormatPanel dataFormatPanel; RecordPanel recordPanel; PlotControlPanel plotControlPanel; + UpdateCheckDialog updateCheckDialog; bool isDemoRunning(); /// Stores settings for all modules diff --git a/src/mainwindow.ui b/src/mainwindow.ui --- a/src/mainwindow.ui +++ b/src/mainwindow.ui @@ -90,7 +90,7 @@ 0 0 653 - 27 + 25 @@ -99,6 +99,7 @@ + @@ -142,7 +143,9 @@ false - + + + Pause @@ -156,7 +159,9 @@ - + + + Clear @@ -221,6 +226,11 @@ Load Settings from a File + + + &Check Update + + diff --git a/src/setting_defines.h b/src/setting_defines.h --- a/src/setting_defines.h +++ b/src/setting_defines.h @@ -30,6 +30,7 @@ const char SettingGroup_Channels[] = "Ch const char SettingGroup_Plot[] = "Plot"; const char SettingGroup_Commands[] = "Commands"; const char SettingGroup_Record[] = "Record"; +const char SettingGroup_UpdateCheck[] = "UpdateCheck"; // mainwindow setting keys const char SG_MainWindow_Size[] = "size"; @@ -103,4 +104,8 @@ const char SG_Record_Header[] const char SG_Record_Separator[] = "separator"; const char SG_Record_DisableBuffering[] = "disableBuffering"; +// update check settings keys +const char SG_UpdateCheck_Periodic[] = "periodicCheck"; +const char SG_UpdateCheck_LastCheck[] = "lastCheck"; + #endif // SETTING_DEFINES_H diff --git a/src/updatecheckdialog.cpp b/src/updatecheckdialog.cpp new file mode 100644 --- /dev/null +++ b/src/updatecheckdialog.cpp @@ -0,0 +1,105 @@ +/* + Copyright © 2017 Hasan Yavuz Özderya + + This file is part of serialplot. + + serialplot is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + serialplot is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with serialplot. If not, see . +*/ + +#include "setting_defines.h" +#include "updatecheckdialog.h" +#include "ui_updatecheckdialog.h" + +UpdateCheckDialog::UpdateCheckDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::UpdateCheckDialog) +{ + ui->setupUi(this); + + // by default start from yesterday, so that we check at first run + lastCheck = QDate::currentDate().addDays(-1); + + connect(&updateChecker, &UpdateChecker::checkFailed, + [this](QString errorMessage) + { + lastCheck = QDate::currentDate(); + ui->label->setText(QString("Update check failed.\n") + errorMessage); + }); + + connect(&updateChecker, &UpdateChecker::checkFinished, + [this](bool found, QString newVersion, QString downloadUrl) + { + QString text; + if (!found) + { + text = "There is no update yet."; + } + else + { + show(); +#ifdef UPDATE_TYPE_PKGMAN + text = QString("There is a new version: %1. " + "Use your package manager to update" + " or click to download.")\ + .arg(newVersion).arg(downloadUrl); +#else + text = QString("Found update to version %1. Click to download.")\ + .arg(newVersion).arg(downloadUrl); +#endif + } + + lastCheck = QDate::currentDate(); + ui->label->setText(text); + }); +} + +UpdateCheckDialog::~UpdateCheckDialog() +{ + delete ui; +} + +void UpdateCheckDialog::showEvent(QShowEvent *event) +{ + updateChecker.checkUpdate(); + ui->label->setText("Checking update..."); +} + +void UpdateCheckDialog::closeEvent(QShowEvent *event) +{ + if (updateChecker.isChecking()) updateChecker.cancelCheck(); +} + +void UpdateCheckDialog::saveSettings(QSettings* settings) +{ + settings->beginGroup(SettingGroup_UpdateCheck); + settings->setValue(SG_UpdateCheck_Periodic, ui->cbPeriodic->isChecked()); + settings->setValue(SG_UpdateCheck_LastCheck, lastCheck.toString(Qt::ISODate)); + settings->endGroup(); +} + +void UpdateCheckDialog::loadSettings(QSettings* settings) +{ + settings->beginGroup(SettingGroup_UpdateCheck); + ui->cbPeriodic->setChecked(settings->value(SG_UpdateCheck_Periodic, + ui->cbPeriodic->isChecked()).toBool()); + auto lastCheckS = settings->value(SG_UpdateCheck_LastCheck, lastCheck.toString(Qt::ISODate)).toString(); + lastCheck = QDate::fromString(lastCheckS, Qt::ISODate); + settings->endGroup(); + + // start the periodic update if required + if (ui->cbPeriodic->isChecked() && lastCheck < QDate::currentDate()) + { + updateChecker.checkUpdate(); + } +} diff --git a/src/updatecheckdialog.h b/src/updatecheckdialog.h new file mode 100644 --- /dev/null +++ b/src/updatecheckdialog.h @@ -0,0 +1,54 @@ +/* + Copyright © 2017 Hasan Yavuz Özderya + + This file is part of serialplot. + + serialplot is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + serialplot is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with serialplot. If not, see . +*/ + +#ifndef UPDATECHECKDIALOG_H +#define UPDATECHECKDIALOG_H + +#include +#include +#include +#include "updatechecker.h" + +namespace Ui { +class UpdateCheckDialog; +} + +class UpdateCheckDialog : public QDialog +{ + Q_OBJECT + +public: + explicit UpdateCheckDialog(QWidget *parent = 0); + ~UpdateCheckDialog(); + + /// Stores update settings into a `QSettings`. + void saveSettings(QSettings* settings); + /// Loads update settings from a `QSettings`. + void loadSettings(QSettings* settings); + +private: + Ui::UpdateCheckDialog *ui; + UpdateChecker updateChecker; + QDate lastCheck; + + void showEvent(QShowEvent *event); + void closeEvent(QShowEvent *event); +}; + +#endif // UPDATECHECKDIALOG_H diff --git a/src/updatecheckdialog.ui b/src/updatecheckdialog.ui new file mode 100644 --- /dev/null +++ b/src/updatecheckdialog.ui @@ -0,0 +1,71 @@ + + + UpdateCheckDialog + + + + 0 + 0 + 400 + 148 + + + + Check Update + + + + + + Checking update... + + + true + + + + + + + Updates will be checked only once a day at first start of the application + + + Check updates periodically + + + true + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + + buttonBox + clicked(QAbstractButton*) + UpdateCheckDialog + close() + + + 199 + 125 + + + 199 + 73 + + + + + diff --git a/src/updatechecker.cpp b/src/updatechecker.cpp new file mode 100644 --- /dev/null +++ b/src/updatechecker.cpp @@ -0,0 +1,222 @@ +/* + Copyright © 2017 Hasan Yavuz Özderya + + This file is part of serialplot. + + serialplot is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + serialplot is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with serialplot. If not, see . +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include "updatechecker.h" + +// This link returns the list of downloads in JSON format. Note that we only use +// the first page because results are sorted new to old. +const char BB_DOWNLOADS_URL[] = "https://api.bitbucket.org/2.0/repositories/hyozd/serialplot/downloads?fields=values.name,values.links.self.href"; + +UpdateChecker::UpdateChecker(QObject *parent) : + QObject(parent), nam(this) +{ + activeReply = NULL; + + connect(&nam, &QNetworkAccessManager::finished, + this, &UpdateChecker::onReqFinished); +} + +bool UpdateChecker::isChecking() const +{ + return activeReply != NULL && !activeReply->isFinished(); +} + +void UpdateChecker::checkUpdate() +{ + if (isChecking()) return; + + auto req = QNetworkRequest(QUrl(BB_DOWNLOADS_URL)); + activeReply = nam.get(req); +} + +void UpdateChecker::cancelCheck() +{ + if (activeReply != NULL) activeReply->abort(); +} + +void UpdateChecker::onReqFinished(QNetworkReply* reply) +{ + if (reply->error() != QNetworkReply::NoError) + { + emit checkFailed(QString("Network error: ") + reply->errorString()); + } + else + { + QJsonParseError error; + auto data = QJsonDocument::fromJson(reply->readAll(), &error); + if (error.error != QJsonParseError::NoError) + { + emit checkFailed(QString("JSon parsing error: ") + error.errorString()); + } + else + { + QList files; + if (!parseData(data, files)) + { + // TODO: emit detailed data contents for logging + emit checkFailed("Data parsing error."); + } + else + { + FileInfo updateFile; + if (findUpdate(files, updateFile)) + { + emit checkFinished( + true, updateFile.version.toString(), updateFile.link); + } + else + { + emit checkFinished(false, "", ""); + } + } + } + } + reply->deleteLater(); + activeReply = NULL; +} + +bool UpdateChecker::parseData(const QJsonDocument& data, QList& files) const +{ + /* Data is expected to be in this form: + + { + "values": [ + { + "name": "serialplot-0.9.1-x86_64.AppImage", + "links": { + "self": { + "href": "https://api.bitbucket.org/2.0/repositories/hyOzd/serialplot/downloads/serialplot-0.9.1-x86_64.AppImage" + } + } + }, ... ] + } + */ + + if (!data.isObject()) return false; + + auto values = data.object()["values"]; + if (values == QJsonValue::Undefined || !values.isArray()) return false; + + for (auto value : values.toArray()) + { + if (!value.isObject()) return false; + + auto name = value.toObject().value("name"); + if (name.isUndefined() || !name.isString()) + return false; + + auto links = value.toObject().value("links"); + if (links.isUndefined() || !links.isObject()) + return false; + + auto self = links.toObject().value("self"); + if (self.isUndefined() || !self.isObject()) + return false; + + auto href = self.toObject().value("href"); + if (href.isUndefined() || !href.isString()) + return false; + + FileInfo finfo; + finfo.name = name.toString(); + finfo.link = href.toString(); + finfo.hasVersion = VersionNumber::extract(name.toString(), finfo.version); + + if (finfo.name.contains("amd64") || + finfo.name.contains("x86_64") || + finfo.name.contains("win64")) + { + finfo.arch = FileArch::amd64; + } + else if (finfo.name.contains("win32") || + finfo.name.contains("i386")) + { + finfo.arch = FileArch::i386; + } + else + { + finfo.arch = FileArch::unknown; + } + + files += finfo; + } + + return true; +} + +bool UpdateChecker::findUpdate(const QList& files, FileInfo& foundFile) const +{ + QList fflist; + + // filter the file list according to extension and version number + for (int i = 0; i < files.length(); i++) + { + // file type to look +#if defined(Q_OS_WIN) + const char ext[] = ".exe"; +#else // of course linux + const char ext[] = ".appimage"; +#endif + + // file architecture to look +#if defined(Q_PROCESSOR_X86_64) + const FileArch arch = FileArch::amd64; +#elif defined(Q_PROCESSOR_X86_32) + const FileArch arch = FileArch::i386; +#elif defined(Q_PROCESSOR_ARM) + const FileArch arch = FileArch::arm; +#else + #error Unknown architecture for update file detection. +#endif + + // filter the file list + auto file = files[i]; + if (file.name.contains(ext, Qt::CaseInsensitive) && + file.arch == arch && + file.hasVersion && file.version > CurrentVersion) + { + fflist += file; + } + } + + // sort and find most up to date file + if (!fflist.empty()) + { + std::sort(fflist.begin(), fflist.end(), + [](const FileInfo& a, const FileInfo& b) + { + return a.version > b.version; + }); + + foundFile = fflist[0]; + return true; + } + else + { + return false; + } +} diff --git a/src/updatechecker.h b/src/updatechecker.h new file mode 100644 --- /dev/null +++ b/src/updatechecker.h @@ -0,0 +1,77 @@ +/* + Copyright © 2017 Hasan Yavuz Özderya + + This file is part of serialplot. + + serialplot is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + serialplot is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with serialplot. If not, see . +*/ + +#ifndef UPDATECHECKER_H +#define UPDATECHECKER_H + +#include +#include +#include +#include + +#include "versionnumber.h" + +class UpdateChecker : public QObject +{ + Q_OBJECT +public: + explicit UpdateChecker(QObject *parent = 0); + + bool isChecking() const; + +signals: + void checkFinished(bool found, QString newVersion, QString downloadUrl); + void checkFailed(QString errorMessage); + +public slots: + void checkUpdate(); + void cancelCheck(); + +private: + enum class FileArch + { + unknown, + i386, + amd64, + arm + }; + + struct FileInfo + { + QString name; + QString link; + bool hasVersion; + VersionNumber version; + FileArch arch; + }; + + QNetworkAccessManager nam; + QNetworkReply* activeReply; + + /// Parses json and creates a list of files + bool parseData(const QJsonDocument& data, QList& files) const; + /// Finds the update file in the file list. Returns `-1` if no new version + /// is found. + bool findUpdate(const QList& files, FileInfo& foundFile) const; + +private slots: + void onReqFinished(QNetworkReply* reply); +}; + +#endif // UPDATECHECKER_H diff --git a/src/versionnumber.cpp b/src/versionnumber.cpp new file mode 100644 --- /dev/null +++ b/src/versionnumber.cpp @@ -0,0 +1,103 @@ +/* + Copyright © 2017 Hasan Yavuz Özderya + + This file is part of serialplot. + + serialplot is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + serialplot is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with serialplot. If not, see . +*/ + +#include + +#include "versionnumber.h" + +VersionNumber::VersionNumber(unsigned mj, unsigned mn, unsigned pt) +{ + major = mj; + minor = mn; + patch = pt; +} + +QString VersionNumber::toString() const +{ + return QString("%1.%2.%3").arg(major).arg(minor).arg(patch); +} + +bool VersionNumber::extract(const QString& str, VersionNumber& number) +{ + QRegularExpression regexp("(?:[-_vV \\t]|^)(?\\d+)" + "(?:\\.(?\\d+))(?:\\.(?\\d+))?[-_ \\t]?"); + auto match = regexp.match(str, 0, QRegularExpression::PartialPreferCompleteMatch); + + if (!(match.hasMatch() || match.hasPartialMatch())) return false; + + number.major = match.captured("major").toUInt(); + + auto zeroIfNull = [](QString str) -> unsigned + { + if (str.isNull()) return 0; + return str.toUInt(); + }; + + number.minor = zeroIfNull(match.captured("minor")); + number.patch = zeroIfNull(match.captured("patch")); + + return true; +} + +bool operator==(const VersionNumber& lhs, const VersionNumber& rhs) +{ + return lhs.major == rhs.major && + lhs.minor == rhs.minor && + lhs.patch == rhs.patch; +} + +bool operator<(const VersionNumber& lhs, const VersionNumber& rhs) +{ + if (lhs.major < rhs.major) + { + return true; + } + else if (lhs.major == rhs.major) + { + if (lhs.minor < rhs.minor) + { + return true; + } + else if (lhs.minor == rhs.minor) + { + if (lhs.patch < rhs.patch) return true; + } + } + return false; +} + +bool operator>(const VersionNumber& lhs, const VersionNumber& rhs) +{ + if (lhs.major > rhs.major) + { + return true; + } + else if (lhs.major == rhs.major) + { + if (lhs.minor > rhs.minor) + { + return true; + } + else if (lhs.minor == rhs.minor) + { + if (lhs.patch > rhs.patch) return true; + } + } + return false; +} diff --git a/src/versionnumber.h b/src/versionnumber.h new file mode 100644 --- /dev/null +++ b/src/versionnumber.h @@ -0,0 +1,46 @@ +/* + Copyright © 2017 Hasan Yavuz Özderya + + This file is part of serialplot. + + serialplot is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + serialplot is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with serialplot. If not, see . +*/ + +#ifndef VERSIONNUMBER_H +#define VERSIONNUMBER_H + +#include + +struct VersionNumber +{ + unsigned major = 0; + unsigned minor = 0; + unsigned patch = 0; + + VersionNumber(unsigned mj=0, unsigned mn=0, unsigned pt=0); + + /// Convert version number to string. + QString toString() const; + + /// Extracts the version number from given string. + static bool extract(const QString& str, VersionNumber& number); +}; + +bool operator==(const VersionNumber& lhs, const VersionNumber& rhs); +bool operator<(const VersionNumber& lhs, const VersionNumber& rhs); +bool operator>(const VersionNumber& lhs, const VersionNumber& rhs); + +const VersionNumber CurrentVersion(VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH); + +#endif // VERSIONNUMBER_H