diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index 90e724793..4049d0cdf 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -49,6 +49,7 @@ search/searchdownloadhandler.h search/searchhandler.h search/searchpluginmanager.h utils/bytearray.h +utils/foreignapps.h utils/fs.h utils/gzip.h utils/misc.h @@ -117,6 +118,7 @@ search/searchdownloadhandler.cpp search/searchhandler.cpp search/searchpluginmanager.cpp utils/bytearray.cpp +utils/foreignapps.cpp utils/fs.cpp utils/gzip.cpp utils/misc.cpp diff --git a/src/base/base.pri b/src/base/base.pri index c84c49c93..1357fc754 100644 --- a/src/base/base.pri +++ b/src/base/base.pri @@ -64,6 +64,7 @@ HEADERS += \ $$PWD/types.h \ $$PWD/unicodestrings.h \ $$PWD/utils/bytearray.h \ + $$PWD/utils/foreignapps.h \ $$PWD/utils/fs.h \ $$PWD/utils/gzip.h \ $$PWD/utils/misc.h \ @@ -127,6 +128,7 @@ SOURCES += \ $$PWD/torrentfilter.cpp \ $$PWD/tristatebool.cpp \ $$PWD/utils/bytearray.cpp \ + $$PWD/utils/foreignapps.cpp \ $$PWD/utils/fs.cpp \ $$PWD/utils/gzip.cpp \ $$PWD/utils/misc.cpp \ diff --git a/src/base/search/searchdownloadhandler.cpp b/src/base/search/searchdownloadhandler.cpp index e3d853e5b..6dfdb75f8 100644 --- a/src/base/search/searchdownloadhandler.cpp +++ b/src/base/search/searchdownloadhandler.cpp @@ -30,8 +30,8 @@ #include +#include "../utils/foreignapps.h" #include "../utils/fs.h" -#include "../utils/misc.h" #include "searchpluginmanager.h" SearchDownloadHandler::SearchDownloadHandler(const QString &siteUrl, const QString &url, SearchPluginManager *manager) @@ -48,7 +48,7 @@ SearchDownloadHandler::SearchDownloadHandler(const QString &siteUrl, const QStri url }; // Launch search - m_downloadProcess->start(Utils::Misc::pythonExecutable(), params, QIODevice::ReadOnly); + m_downloadProcess->start(Utils::ForeignApps::pythonInfo().executableName, params, QIODevice::ReadOnly); } void SearchDownloadHandler::downloadProcessFinished(int exitcode) diff --git a/src/base/search/searchhandler.cpp b/src/base/search/searchhandler.cpp index efb14a29e..8547087be 100644 --- a/src/base/search/searchhandler.cpp +++ b/src/base/search/searchhandler.cpp @@ -32,8 +32,8 @@ #include #include +#include "../utils/foreignapps.h" #include "../utils/fs.h" -#include "../utils/misc.h" #include "searchpluginmanager.h" namespace @@ -70,7 +70,7 @@ SearchHandler::SearchHandler(const QString &pattern, const QString &category, co }; // Launch search - m_searchProcess->setProgram(Utils::Misc::pythonExecutable()); + m_searchProcess->setProgram(Utils::ForeignApps::pythonInfo().executableName); m_searchProcess->setArguments(params + m_pattern.split(" ")); #if (QT_VERSION >= QT_VERSION_CHECK(5, 6, 0)) diff --git a/src/base/search/searchpluginmanager.cpp b/src/base/search/searchpluginmanager.cpp index de0a4b577..892b3186a 100644 --- a/src/base/search/searchpluginmanager.cpp +++ b/src/base/search/searchpluginmanager.cpp @@ -46,6 +46,7 @@ #include "base/net/downloadmanager.h" #include "base/preferences.h" #include "base/profile.h" +#include "base/utils/foreignapps.h" #include "base/utils/fs.h" #include "base/utils/misc.h" #include "searchdownloadhandler.h" @@ -63,7 +64,7 @@ namespace QPointer SearchPluginManager::m_instance = nullptr; SearchPluginManager::SearchPluginManager() - : m_updateUrl(QString("http://searchplugins.qbittorrent.org/%1/engines/").arg(Utils::Misc::pythonVersion() >= 3 ? "nova3" : "nova")) + : m_updateUrl(QString("http://searchplugins.qbittorrent.org/%1/engines/").arg(Utils::ForeignApps::pythonInfo().version.majorNumber() >= 3 ? "nova3" : "nova")) { Q_ASSERT(!m_instance); // only one instance is allowed m_instance = this; @@ -322,7 +323,7 @@ QString SearchPluginManager::pluginsLocation() QString SearchPluginManager::engineLocation() { QString folder = "nova"; - if (Utils::Misc::pythonVersion() >= 3) + if (Utils::ForeignApps::pythonInfo().version.majorNumber() >= 3) folder = "nova3"; const QString location = Utils::Fs::expandPathAbs(specialFolderLocation(SpecialFolder::Data) + folder); QDir locationDir(location); @@ -370,7 +371,7 @@ void SearchPluginManager::updateNova() // create nova directory if necessary QDir searchDir(engineLocation()); - QString novaFolder = Utils::Misc::pythonVersion() >= 3 ? "searchengine/nova3" : "searchengine/nova"; + QString novaFolder = Utils::ForeignApps::pythonInfo().version.majorNumber() >= 3 ? "searchengine/nova3" : "searchengine/nova"; QFile packageFile(searchDir.absoluteFilePath("__init__.py")); packageFile.open(QIODevice::WriteOnly | QIODevice::Text); packageFile.close(); @@ -436,7 +437,7 @@ void SearchPluginManager::update() QStringList params; params << Utils::Fs::toNativePath(engineLocation() + "/nova2.py"); params << "--capabilities"; - nova.start(Utils::Misc::pythonExecutable(), params, QIODevice::ReadOnly); + nova.start(Utils::ForeignApps::pythonInfo().executableName, params, QIODevice::ReadOnly); nova.waitForStarted(); nova.waitForFinished(); diff --git a/src/base/utils/foreignapps.cpp b/src/base/utils/foreignapps.cpp new file mode 100644 index 000000000..af45243e1 --- /dev/null +++ b/src/base/utils/foreignapps.cpp @@ -0,0 +1,100 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Mike Tzou + * Copyright (C) 2006 Christophe Dumez + * + * 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 Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#include "foreignapps.h" + +#include +#include +#include + +#include "base/logger.h" + +using namespace Utils::ForeignApps; + +namespace +{ + bool testPythonInstallation(const QString &exeName, PythonInfo &info) + { + QProcess proc; + proc.start(exeName, {"--version"}, QIODevice::ReadOnly); + if (proc.waitForFinished() && (proc.exitCode() == QProcess::NormalExit)) { + QByteArray procOutput = proc.readAllStandardOutput(); + if (procOutput.isEmpty()) + procOutput = proc.readAllStandardError(); + procOutput = procOutput.simplified(); + + // Software 'Anaconda' installs its own python interpreter + // and `python --version` returns a string like this: + // "Python 3.4.3 :: Anaconda 2.3.0 (64-bit)" + const QList outputSplit = procOutput.split(' '); + if (outputSplit.size() <= 1) + return false; + + try { + info = {exeName, outputSplit[1]}; + } + catch (const std::runtime_error &err) { + return false; + } + + LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Python detected, version: %1").arg(info.version), Log::INFO); + return true; + } + + return false; + } +} + +bool Utils::ForeignApps::PythonInfo::isValid() const +{ + return (!executableName.isEmpty() && version.isValid()); +} + +PythonInfo Utils::ForeignApps::pythonInfo() +{ + static PythonInfo pyInfo; + if (!pyInfo.isValid()) { +#if defined(Q_OS_UNIX) + // On Unix-Like Systems python2 and python3 should always exist + // https://legacy.python.org/dev/peps/pep-0394/ + if (testPythonInstallation("python3", pyInfo)) + return pyInfo; + if (testPythonInstallation("python2", pyInfo)) + return pyInfo; +#endif + // Look for "python" in Windows and in UNIX if "python2" and "python3" are + // not detected. + if (testPythonInstallation("python", pyInfo)) + return pyInfo; + + LogMsg(QCoreApplication::translate("Utils::ForeignApps", "Python not detected"), Log::INFO); + } + + return pyInfo; +} diff --git a/src/base/utils/foreignapps.h b/src/base/utils/foreignapps.h new file mode 100644 index 000000000..ca427c4a6 --- /dev/null +++ b/src/base/utils/foreignapps.h @@ -0,0 +1,52 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2018 Mike Tzou + * Copyright (C) 2006 Christophe Dumez + * + * 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 Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +#pragma once + +#include + +#include "base/utils/version.h" + +namespace Utils +{ + namespace ForeignApps + { + struct PythonInfo + { + using Version = Utils::Version; + + bool isValid() const; + + QString executableName; + Version version; + }; + + PythonInfo pythonInfo(); + } +} diff --git a/src/base/utils/misc.cpp b/src/base/utils/misc.cpp index 000d937db..aa9159bd4 100644 --- a/src/base/utils/misc.cpp +++ b/src/base/utils/misc.cpp @@ -237,105 +237,7 @@ QPoint Utils::Misc::screenCenter(const QWidget *w) QRect r = desktop->availableGeometry(scrn); return QPoint(r.x() + (r.width() - w->frameSize().width()) / 2, r.y() + (r.height() - w->frameSize().height()) / 2); } - -#endif - -/** - * Detects the python version. - */ -int Utils::Misc::pythonVersion() -{ - static int version = -1; - if (version < 0) { - QString versionComplete = pythonVersionComplete().trimmed(); - QStringList splitted = versionComplete.split('.'); - if (splitted.size() > 1) { - int highVer = splitted.at(0).toInt(); - if ((highVer == 2) || (highVer == 3)) - version = highVer; - } - } - return version; -} - -/** - * Detects the python executable by calling "python --version". - */ -QString Utils::Misc::pythonExecutable() -{ - static QString executable; - if (executable.isEmpty()) { - QProcess pythonProc; -#if defined(Q_OS_UNIX) - /* - * On Unix-Like Systems python2 and python3 should always exist - * http://legacy.python.org/dev/peps/pep-0394/ - */ - pythonProc.start("python3", {"--version"}, QIODevice::ReadOnly); - if (pythonProc.waitForFinished() && (pythonProc.exitCode() == 0)) { - executable = "python3"; - return executable; - } - pythonProc.start("python2", {"--version"}, QIODevice::ReadOnly); - if (pythonProc.waitForFinished() && (pythonProc.exitCode() == 0)) { - executable = "python2"; - return executable; - } #endif - // Look for "python" in Windows and in UNIX if "python2" and "python3" are - // not detected. - pythonProc.start("python", {"--version"}, QIODevice::ReadOnly); - if (pythonProc.waitForFinished() && (pythonProc.exitCode() == 0)) - executable = "python"; - else - Logger::instance()->addMessage(QCoreApplication::translate("misc", "Python not detected"), Log::INFO); - } - return executable; -} - -/** - * Returns the complete python version - * eg 2.7.9 - * Make sure to have setup python first - */ -QString Utils::Misc::pythonVersionComplete() -{ - static QString version; - if (version.isEmpty()) { - if (pythonExecutable().isEmpty()) - return version; - QProcess pythonProc; - pythonProc.start(pythonExecutable(), {"--version"}, QIODevice::ReadOnly); - if (pythonProc.waitForFinished() && (pythonProc.exitCode() == 0)) { - QByteArray output = pythonProc.readAllStandardOutput(); - if (output.isEmpty()) - output = pythonProc.readAllStandardError(); - - // Software 'Anaconda' installs its own python interpreter - // and `python --version` returns a string like this: - // `Python 3.4.3 :: Anaconda 2.3.0 (64-bit)` - const QList outSplit = output.split(' '); - if (outSplit.size() > 1) { - version = outSplit.at(1).trimmed(); - Logger::instance()->addMessage(QCoreApplication::translate("misc", "Python version: %1").arg(version), Log::INFO); - } - - // If python doesn't report a 3-piece version e.g. 3.6.1 - // then fill the missing pieces with zero - const QStringList verSplit = version.split('.', QString::SkipEmptyParts); - if (verSplit.size() < 3) { - for (int i = verSplit.size(); i < 3; ++i) { - if (version.endsWith('.')) - version.append('0'); - else - version.append(".0"); - } - Logger::instance()->addMessage(QCoreApplication::translate("misc", "Normalized Python version: %1").arg(version), Log::INFO); - } - } - } - return version; -} QString Utils::Misc::unitString(Utils::Misc::SizeUnit unit) { diff --git a/src/base/utils/misc.h b/src/base/utils/misc.h index 8af5c54fc..028bc146a 100644 --- a/src/base/utils/misc.h +++ b/src/base/utils/misc.h @@ -80,10 +80,6 @@ namespace Utils QString boostVersionString(); QString libtorrentVersionString(); - int pythonVersion(); - QString pythonExecutable(); - QString pythonVersionComplete(); - QString unitString(SizeUnit unit); // return the best user friendly storage unit (B, KiB, MiB, GiB, TiB) diff --git a/src/base/utils/version.h b/src/base/utils/version.h index 19213cb35..71b797eb4 100644 --- a/src/base/utils/version.h +++ b/src/base/utils/version.h @@ -58,7 +58,7 @@ namespace Utils template constexpr Version(Other ... components) - : m_components {{components ...}} + : m_components {{static_cast(components) ...}} { } @@ -129,6 +129,11 @@ namespace Utils return res; } + constexpr bool isValid() const + { + return (*this != ThisType {}); + } + constexpr bool operator==(const ThisType &other) const { return (m_components == other.m_components); diff --git a/src/gui/mainwindow.cpp b/src/gui/mainwindow.cpp index a4263b9fe..dbe5a85ef 100644 --- a/src/gui/mainwindow.cpp +++ b/src/gui/mainwindow.cpp @@ -66,6 +66,7 @@ #include "base/rss/rss_folder.h" #include "base/rss/rss_session.h" #include "base/settingsstorage.h" +#include "base/utils/foreignapps.h" #include "base/utils/fs.h" #include "base/utils/misc.h" #include "aboutdialog.h" @@ -1742,45 +1743,42 @@ void MainWindow::on_actionRSSReader_triggered() void MainWindow::on_actionSearchWidget_triggered() { if (!m_hasPython && m_ui->actionSearchWidget->isChecked()) { - int pythonVersion = Utils::Misc::pythonVersion(); + int majorVersion = Utils::ForeignApps::pythonInfo().version.majorNumber(); // Check if python is already in PATH - if (pythonVersion > 0) + if (majorVersion > 0) { // Prevent translators from messing with PATH Logger::instance()->addMessage(tr("Python found in %1: %2", "Python found in PATH: /usr/local/bin:/usr/bin:/etc/bin") .arg("PATH", qgetenv("PATH").constData()), Log::INFO); + } #ifdef Q_OS_WIN - else if (addPythonPathToEnv()) - pythonVersion = Utils::Misc::pythonVersion(); + else if (addPythonPathToEnv()) { + majorVersion = Utils::ForeignApps::pythonInfo().version.majorNumber(); + } #endif + else { + QMessageBox::information(this, tr("Undetermined Python version"), tr("Couldn't determine your Python version. Search engine disabled.")); + m_ui->actionSearchWidget->setChecked(false); + Preferences::instance()->setSearchEnabled(false); + return; + } bool res = false; - if ((pythonVersion == 2) || (pythonVersion == 3)) { + if ((majorVersion == 2) || (majorVersion == 3)) { // Check Python minimum requirement: 2.7.9 / 3.3.0 - QString version = Utils::Misc::pythonVersionComplete(); - QStringList splitted = version.split('.'); - if (splitted.size() > 2) { - int middleVer = splitted.at(1).toInt(); - int lowerVer = splitted.at(2).toInt(); - if (((pythonVersion == 2) && (middleVer < 7)) - || ((pythonVersion == 2) && (middleVer == 7) && (lowerVer < 9)) - || ((pythonVersion == 3) && (middleVer < 3))) { - QMessageBox::information(this, tr("Old Python Interpreter"), tr("Your Python version (%1) is outdated. Please upgrade to latest version for search engines to work.\nMinimum requirement: 2.7.9 / 3.3.0.").arg(version)); - m_ui->actionSearchWidget->setChecked(false); - Preferences::instance()->setSearchEnabled(false); - return; - } - else { - res = true; - } - } - else { - QMessageBox::information(this, tr("Undetermined Python version"), tr("Couldn't determine your Python version (%1). Search engine disabled.").arg(version)); + using Version = Utils::ForeignApps::PythonInfo::Version; + const Version pyVersion = Utils::ForeignApps::pythonInfo().version; + + if (((majorVersion == 2) && (pyVersion < Version {2, 7, 9})) + || ((majorVersion == 3) && (pyVersion < Version {3, 3, 0}))) { + QMessageBox::information(this, tr("Old Python Interpreter"), tr("Your Python version (%1) is outdated. Please upgrade to latest version for search engines to work.\nMinimum requirement: 2.7.9 / 3.3.0.").arg(pyVersion)); m_ui->actionSearchWidget->setChecked(false); Preferences::instance()->setSearchEnabled(false); return; } + + res = true; } if (res) { @@ -2085,7 +2083,7 @@ void MainWindow::pythonDownloadSuccess(const QString &url, const QString &filePa m_hasPython = addPythonPathToEnv(); if (m_hasPython) { // Make it print the version to Log - Utils::Misc::pythonVersion(); + Utils::ForeignApps::pythonInfo(); m_ui->actionSearchWidget->setChecked(true); displaySearchTab(true); } diff --git a/src/gui/search/searchwidget.cpp b/src/gui/search/searchwidget.cpp index efac66a5f..ccff90752 100644 --- a/src/gui/search/searchwidget.cpp +++ b/src/gui/search/searchwidget.cpp @@ -56,8 +56,8 @@ #include "base/preferences.h" #include "base/search/searchpluginmanager.h" #include "base/search/searchhandler.h" +#include "base/utils/foreignapps.h" #include "base/utils/fs.h" -#include "base/utils/misc.h" #include "addnewtorrentdialog.h" #include "guiiconprovider.h" #include "mainwindow.h" @@ -285,7 +285,7 @@ void SearchWidget::giveFocusToSearchInput() // Function called when we click on search button void SearchWidget::on_searchButton_clicked() { - if (Utils::Misc::pythonVersion() < 0) { + if (Utils::ForeignApps::pythonInfo().version.majorNumber() <= 0) { m_mainWindow->showNotificationBaloon(tr("Search Engine"), tr("Please install Python to use the Search Engine.")); return; }