From 2c95f890ef97a5996480f5da9634b0addc18e428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olivier=20Teuli=C3=A8re?= Date: Sat, 19 Jan 2013 00:16:12 +0100 Subject: [PATCH] Check for new versions, about once a week. This can be deactivated in the preferences. --- configure.in | 2 +- qt/Makefile.am | 2 + qt/main_window.cpp | 10 +- qt/prefs_dialog.cpp | 7 ++ qt/prefs_dialog.h | 1 + qt/ui/prefs_dialog.ui | 17 ++++ qt/update_checker.cpp | 209 ++++++++++++++++++++++++++++++++++++++++++ qt/update_checker.h | 71 ++++++++++++++ 8 files changed, 317 insertions(+), 2 deletions(-) create mode 100644 qt/update_checker.cpp create mode 100644 qt/update_checker.h diff --git a/configure.in b/configure.in index 45f0418..86a22e4 100644 --- a/configure.in +++ b/configure.in @@ -169,7 +169,7 @@ AC_ARG_ENABLE([qt],AS_HELP_STRING([--enable-qt], [Qt interface support (default disabled)])) qt_ok=0 AS_IF([test "${enable_qt}" != "no"], - [PKG_CHECK_MODULES(QT, [QtCore QtGui >= 4.2.0], + [PKG_CHECK_MODULES(QT, [QtCore QtGui QtNetwork >= 4.2.0], [qt_ok=1 AC_PATH_PROG(MOC, moc, moc,`$PKG_CONFIG --variable=exec_prefix QtCore`/bin) AC_PATH_PROG(UIC, uic, uic,`$PKG_CONFIG --variable=exec_prefix QtCore`/bin) diff --git a/qt/Makefile.am b/qt/Makefile.am index d5614cb..881a3cf 100644 --- a/qt/Makefile.am +++ b/qt/Makefile.am @@ -90,6 +90,7 @@ eliot_SOURCES = \ training_widget.cpp training_widget.h \ player_widget.cpp player_widget.h \ prefs_dialog.cpp prefs_dialog.h \ + update_checker.cpp update_checker.h \ aux_window.cpp aux_window.h \ main_window.cpp main_window.h \ main.cpp @@ -137,6 +138,7 @@ nodist_eliot_SOURCES = \ topping_widget.moc.cpp \ training_widget.moc.cpp \ prefs_dialog.moc.cpp \ + update_checker.moc.cpp \ aux_window.moc.cpp \ main_window.moc.cpp \ resources.cpp diff --git a/qt/main_window.cpp b/qt/main_window.cpp index 54eeb0f..f918ac9 100644 --- a/qt/main_window.cpp +++ b/qt/main_window.cpp @@ -70,6 +70,7 @@ #include "timer_widget.h" #include "dic_wizard.h" #include "aux_window.h" +#include "update_checker.h" #include "qtcommon.h" @@ -217,6 +218,12 @@ MainWindow::MainWindow(QWidget *iParent) } } emit dicChanged(m_dic); + + // Check for updates + UpdateChecker *checker = new UpdateChecker(this); + QObject::connect(checker, SIGNAL(notifyInfo(QString)), + this, SLOT(displayInfoMsg(QString))); + checker->checkForUpdate(); } @@ -1422,7 +1429,8 @@ void MainWindow::onHelpAbout() "published by the Free Software Foundation; either version 2 of " \ "the License, or (at your option) any later version."); msg += "

"; - // Translate the URL, because the web site is translated in French + // TRANSLATORS: If the website is translated in your language, + // feel free to adapt the URL. QString url = _q("http://www.nongnu.org/eliot/en/"); msg += _q("Web site: %1").arg(QString("%2").arg(url).arg(url)); // QMessageBox::about() doesn't add the nice information icon, so we create diff --git a/qt/prefs_dialog.cpp b/qt/prefs_dialog.cpp index b2f4422..5914049 100644 --- a/qt/prefs_dialog.cpp +++ b/qt/prefs_dialog.cpp @@ -44,6 +44,7 @@ const QString PrefsDialog::kINTF_TIMER_TOTAL_DURATION = "Interface/TimerTotalDur const QString PrefsDialog::kINTF_TIMER_ALERT_DURATION = "Interface/TimerAlertDuration"; const QString PrefsDialog::kINTF_TIMER_BEEPS = "Interface/TimerBeeps"; const QString PrefsDialog::kINTF_TIMER_AUTO_START = "Interface/TimerAutoStart"; +const QString PrefsDialog::kINTF_CHECK_FOR_UPDATES = "Interface/CheckForUpdates"; const QString PrefsDialog::kARBIT_AUTO_MASTER = "Arbitration/AutoAssignMaster"; const QString PrefsDialog::kARBIT_LINK_7P1 = "Arbitration/LinkRackWith7P1"; const QString PrefsDialog::kDEFAULT_DEF_SITE = "http://fr.wiktionary.org/wiki/%w"; @@ -87,6 +88,9 @@ PrefsDialog::PrefsDialog(QWidget *iParent) "reaches the alert level, and when it reaches 0.")); checkBoxTimerAutoStart->setToolTip(_q("If checked, the timer will be reinitialized and restarted\n" "automatically every time that the main rack changes.")); + checkBoxIntfCheckUpdates->setToolTip(_q("If checked, Eliot will connect to the Internet from time to time\n" + "(about once a week) to check if new versions are available.\n" + "New versions are never installed automatically, you just get a notification.")); spinBoxTrainSearchLimit->setToolTip(_q("Maximum number of results returned by a search.\n" "The returned results will always be the best ones.\n" "Use 0 to disable the limit (warning: searches yielding many " @@ -138,6 +142,8 @@ PrefsDialog::PrefsDialog(QWidget *iParent) checkBoxTimerBeeps->setChecked(timerBeeps); bool timerAutoStart = qs.value(kINTF_TIMER_AUTO_START, false).toBool(); checkBoxTimerAutoStart->setChecked(timerAutoStart); + bool checkForUpdates = qs.value(kINTF_CHECK_FOR_UPDATES, true).toBool(); + checkBoxIntfCheckUpdates->setChecked(checkForUpdates); // Duplicate settings checkBoxDuplRefuseInvalid->setChecked(Settings::Instance().getBool("duplicate.reject-invalid")); @@ -258,6 +264,7 @@ void PrefsDialog::updateSettings() shouldEmitUpdate = true; qs.setValue(kINTF_TIMER_AUTO_START, checkBoxTimerAutoStart->isChecked()); } + qs.setValue(kINTF_CHECK_FOR_UPDATES, checkBoxIntfCheckUpdates->isChecked()); // Duplicate settings Settings::Instance().setBool("duplicate.reject-invalid", diff --git a/qt/prefs_dialog.h b/qt/prefs_dialog.h index 3cfb15c..5f694b9 100644 --- a/qt/prefs_dialog.h +++ b/qt/prefs_dialog.h @@ -47,6 +47,7 @@ public: static const QString kINTF_TIMER_ALERT_DURATION; static const QString kINTF_TIMER_BEEPS; static const QString kINTF_TIMER_AUTO_START; + static const QString kINTF_CHECK_FOR_UPDATES; static const QString kARBIT_AUTO_MASTER; static const QString kARBIT_LINK_7P1; diff --git a/qt/ui/prefs_dialog.ui b/qt/ui/prefs_dialog.ui index c297a69..060b6d5 100644 --- a/qt/ui/prefs_dialog.ui +++ b/qt/ui/prefs_dialog.ui @@ -184,6 +184,13 @@ + + + + _("Check periodially for new versions") + + + @@ -691,11 +698,13 @@ spinBoxTimerTotal spinBoxTimerAlert checkBoxTimerBeeps + checkBoxTimerAutoStart lineEditDefSite lineEditIntfDicPath pushButtonIntfDicBrowse checkBoxIntfShowPoints checkBoxIntfAlignHistory + checkBoxIntfCheckUpdates checkBoxDuplRefuseInvalid spinBoxDuplSoloPlayers spinBoxDuplSoloValue @@ -704,10 +713,18 @@ checkBoxArbitAutoMaster checkBoxArbitFillRack checkBoxArbitLink7P1 + checkBoxArbitSoloAuto + spinBoxArbitSoloPlayers + spinBoxArbitSoloValue spinBoxArbitPenaltyValue + spinBoxArbitWarnLimit + spinBoxArbitSearchLimit + checkBoxToppingElapsedPenalty + spinBoxToppingExpPenalty checkBoxConfoStartGame checkBoxConfoLoadGame checkBoxConfoLoadDic + checkBoxConfoReplayTurn checkBoxConfoQuitGame checkBoxConfoArbitReplaceMaster checkBoxConfoArbitLowMaster diff --git a/qt/update_checker.cpp b/qt/update_checker.cpp new file mode 100644 index 0000000..8b7565c --- /dev/null +++ b/qt/update_checker.cpp @@ -0,0 +1,209 @@ +/***************************************************************************** + * Eliot + * Copyright (C) 2013 Olivier Teulière + * Authors: Olivier Teulière + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + *****************************************************************************/ + +#include +#include +#include + +#include + +#include +#include +#include + +#include "config.h" + +#include "update_checker.h" +#include "prefs_dialog.h" +#include "qtcommon.h" + + +using namespace std; + +INIT_LOGGER(qt, UpdateChecker); + +#define SETTING_KEY "Interface/NextUpdateCheck" +#define URL "http://www.nongnu.org/eliot/latest-version" + + +UpdateChecker::UpdateChecker(QObject *parent) + : QObject(parent) +{ +} + + +void UpdateChecker::checkForUpdate() +{ + QSettings qs; + + // Do nothing if the updates are deactivated in the preferences + if (!qs.value(PrefsDialog::kINTF_CHECK_FOR_UPDATES, true).toBool()) + return; + + QString dateStr = qs.value(SETTING_KEY, "").toString(); + QDate nextCheckDate = QDate::fromString(dateStr, Qt::ISODate); + + // If the next date for the check is in the future, nothing to do + if (nextCheckDate.isValid() && nextCheckDate > QDate::currentDate()) + return; + + emit notifyInfo(_q("Checking for updates...")); + + QNetworkAccessManager *networkManager = new QNetworkAccessManager(this); + QObject::connect(networkManager, SIGNAL(finished(QNetworkReply*)), + this, SLOT(updateCheckFinished(QNetworkReply*))); + networkManager->get(QNetworkRequest(QUrl(URL))); +} + + +void UpdateChecker::updateCheckFinished(QNetworkReply *iReply) +{ + // Check at most once a week + const int nbDaysToWait = 7; + + // Save the new check date + QDate nextCheckDate = QDate::currentDate().addDays(nbDaysToWait); + QSettings qs; + qs.setValue(SETTING_KEY, nextCheckDate.toString(Qt::ISODate)); + + if (iReply->error() == QNetworkReply::NoError) + { + LOG_INFO("Update file retrieved successfully"); + QByteArray data = iReply->readAll().trimmed(); + bool newer = isNewer(data); + + if (newer) + { + LOG_INFO("New version available: " << lfq(data)); + showNewVersion(data); + } + else + emit notifyInfo(_q("Update check completed, no new version available")); + } + else + { + LOG_ERROR("Could not retrieve update file: " << lfq(iReply->errorString())); + emit notifyInfo(_q("Update check failed. Please check your internet connection")); + } + + // Delete the reply + iReply->deleteLater(); +} + + +UpdateChecker::VersionNumber UpdateChecker::parseVersionNumber(QString iVersion) const +{ + VersionNumber vn; + + // A version number has the following form: 1.12a-git (where 'a' is an + // optional letter, and -git is optional as well) + // Regexp to the rescue! + QRegExp re("^(\\d+)\\.(\\d+)([a-z])?(-git.*)?$"); + re.setPatternSyntax(QRegExp::RegExp2); + if (re.indexIn(iVersion) == -1) + { + LOG_ERROR("Error parsing version number: " << lfq(iVersion)); + vn.major = -1; + return vn; + } + vn.major = re.cap(1).toInt(); + vn.minor = re.cap(2).toInt(); + vn.letter = 0; + if (re.pos(3) != -1) + vn.letter = re.cap(3)[0].toAscii(); + vn.suffix = re.cap(4); + + LOG_DEBUG("Parsed version number: " << vn.major << "." << vn.minor << + (vn.letter ? string(1, vn.letter) : "") << lfq(vn.suffix) << + " (from '" << lfq(iVersion) << "')"); + return vn; +} + + +bool UpdateChecker::isNewer(QString iNewVersion) const +{ + // Before doing a lot of work, check the most common case first + if (iNewVersion == PACKAGE_VERSION) + return false; + + // Parse the 2 version strings + const VersionNumber &currVN = parseVersionNumber(PACKAGE_VERSION); + const VersionNumber &newVN = parseVersionNumber(iNewVersion); + if (currVN.major == -1 || newVN.major == -1) + return false; + + // We have the following order: + // - 1.* < 2.* + // - 1.12* < 1.12a* < 1.12b* < ... < 1.13* + // - 1.12 < 1.13-git < 1.13 + + // Compare the major numbers + if (newVN.major > currVN.major) + return true; + if (newVN.major < currVN.major) + return false; + + // Compare the minor numbers + if (newVN.minor > currVN.minor) + return true; + if (newVN.minor < currVN.minor) + return false; + + // Compare the letters + if (newVN.letter != 0 || currVN.letter != 0) + { + if (newVN.letter == 0 || currVN.letter == 0) + return currVN.letter == 0; + if (newVN.letter > currVN.letter) + return true; + if (newVN.letter < currVN.letter) + return false; + } + + // Compare the suffixes + if (newVN.suffix != "" || currVN.suffix != "") + { + if (newVN.suffix == "" || currVN.suffix == "") + return newVN.suffix == ""; + } + + // If we reach this point, the 2 version numbers have (different) suffixes. + // This is not expected, so we are conservative... + LOG_WARN("Cannot compare version numbers: " << + PACKAGE_VERSION << " and " << lfq(iNewVersion)); + return false; +} + + +void UpdateChecker::showNewVersion(QString iVersion) const +{ + const QString url = _q("http://www.nongnu.org/eliot/en/"); + // TRANSLATORS: Here %1 represents a version number. + QString msg = _q("Eliot %1 is available.").arg(iVersion); + msg += "
" + _q("You can download it from %1.") + .arg(QString("%2").arg(url).arg(url)); + msg += "

" + _q("This message will be displayed at most once a week."); + QMessageBox infoBox(QMessageBox::Information, _q("New version available"), + msg, QMessageBox::Ok); + infoBox.setTextFormat(Qt::RichText); + infoBox.exec(); +} + + diff --git a/qt/update_checker.h b/qt/update_checker.h new file mode 100644 index 0000000..fd7220e --- /dev/null +++ b/qt/update_checker.h @@ -0,0 +1,71 @@ +/***************************************************************************** + * Eliot + * Copyright (C) 2013 Olivier Teulière + * Authors: Olivier Teulière + * + * This program 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 2 of the License, or + * (at your option) any later version. + * + * This program 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 this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + *****************************************************************************/ + +#ifndef UPDATE_CHECKER_H_ +#define UPDATE_CHECKER_H_ + +#include +#include + +#include "logging.h" + +class QNetworkReply; + + +/** + * Utility class to check if a new version of Eliot is available + */ +class UpdateChecker: public QObject +{ + Q_OBJECT; + DEFINE_LOGGER(); + + /** + * Structured representation of a version number + */ + struct VersionNumber + { + int major; + int minor; + char letter; // ASCII value, or 0 if no letter is present + QString suffix; // May be an empty string + }; + +public: + UpdateChecker(QObject *parent); + + void checkForUpdate(); + +signals: + void notifyInfo(QString msg); + +private slots: + void updateCheckFinished(QNetworkReply*); + +private: + + VersionNumber parseVersionNumber(QString iVersion) const; + bool isNewer(QString iNewVersion) const; + void showNewVersion(QString iVersion) const; + +}; + +#endif +