Skip to content

Commit

Permalink
Python Startup 1 of 2
Browse files Browse the repository at this point in the history
.. look for python in path and check version and module path, then
   use this when initialising to ensure the installed modules
   are used, not the local copy.

.. needs testing on Windows and need to enable the user to specify
   the location of Python so users don't need to modify PATH.
  • Loading branch information
liversedge committed Feb 9, 2018
1 parent be5df09 commit 710cddf
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 62 deletions.
5 changes: 5 additions & 0 deletions src/Core/DataFilter.cpp
Expand Up @@ -3027,6 +3027,8 @@ Result Leaf::eval(DataFilterRuntime *df, Leaf *leaf, float x, RideItem *m, RideF
double
DataFilterRuntime::runPythonScript(Context *context, QString script)
{
if (python == NULL) return(0);

// get the lock
pythonMutex.lock();

Expand Down Expand Up @@ -3069,6 +3071,9 @@ DataFilterRuntime::runPythonScript(Context *context, QString script)
double
DataFilterRuntime::runRScript(Context *context, QString script)
{

if (rtool == NULL) return(0);

// get the lock
RMutex.lock();

Expand Down
35 changes: 35 additions & 0 deletions src/Core/Utils.cpp
Expand Up @@ -16,8 +16,12 @@
* Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/

#include "Utils.h"
#include <QTextEdit>
#include <QString>
#include <QStringList>
#include <QDebug>
#include <QDir>

namespace Utils
{
Expand Down Expand Up @@ -103,6 +107,37 @@ QString jsonunprotect(const QString &string)
return s;
}

QStringList
searchPath(QString path, QString binary, bool isexec)
{
// search the path provided for a file named binary
// if isexec is true it must be an executable file
// which means on Windows its an exe or com
// on Linux/Mac means it has executable bit set

QStringList returning, extend; // extensions needed

#ifdef Q_OS_WIN
if (isexec) {
extend << ".exe" << ".com";
} else {
extend << "";
}
#else
extend << "";
#endif
foreach(QString dir, path.split(PATHSEP)) {

foreach(QString ext, extend) {
QString filename(dir + QDir::separator() + binary + ext);

// exists and executable (or not if we don't care) and not already found (dupe paths)
if (QFileInfo(filename).exists() && (!isexec || QFileInfo(filename).isExecutable()) && !returning.contains(filename))
returning << filename;
}
}
return returning;
}

};

9 changes: 9 additions & 0 deletions src/Core/Utils.h
Expand Up @@ -21,12 +21,21 @@

// Common shared utility functions

#ifdef Q_OS_WIN
#define PATHSEP ";"
#else
#define PATHSEP ":"
#endif
class QString;
class QStringList;

namespace Utils
{
QString xmlprotect(const QString &string);
QString unprotect(const QString &buffer);
QString jsonprotect(const QString &buffer);
QString jsonunprotect(const QString &buffer);
QStringList searchPath(QString path, QString binary, bool isexec=true);
};


Expand Down
225 changes: 163 additions & 62 deletions src/Python/PythonEmbed.cpp
Expand Up @@ -20,10 +20,12 @@
*/

#include "PythonEmbed.h"
#include "Utils.h"
#include "Settings.h"
#include <stdexcept>

#include <QMessageBox>
#include <QProcess>

// Python header - using a C preprocessor hack
// GC_PYTHON_INCDIR is from gcconfig.pri, typically python3.6
Expand All @@ -45,85 +47,184 @@ PythonEmbed::~PythonEmbed()
{
}

bool PythonEmbed::pythonInstalled(QString &pyhome, QString &pypath)
{
// where to check
QString path = QProcessEnvironment::systemEnvironment().value("PATH", "");

// what is is typically installed as ?
QStringList binarynames;
binarynames << "python3" << "python";

// what we found
QStringList installnames;

// lets search
foreach(QString name, binarynames) {
installnames = Utils::searchPath(path, name, true);
if (installnames.count() >0) break;
}

// if we failed, its not installed
if (installnames.count()==0) return false;

// lets just use the first one we found
QString pythonbinary = installnames[0];
pyhome=pythonbinary;

// get the version and path via an interaction
QProcess py;
py.setProgram(pythonbinary);

// set the arguments
QStringList args;
args << "-c";
args << QString("import sys\n"
"print('ZZ',sys.version_info.major,'ZZ')\n"
"print('ZZ', '%1'.join(sys.path), 'ZZ')\n"
"quit()\n").arg(PATHSEP);
py.setArguments(args);
py.start();

// failed to start python
if (py.waitForStarted(500) == false) {
fprintf(stderr, "Failed to start: %s\n", pythonbinary.toStdString().c_str());
py.terminate();
return false;
}

// wait for output, should be rapid
if (py.waitForReadyRead(2000)==false) {
fprintf(stderr, "Didn't get output: %s\n", pythonbinary.toStdString().c_str());
py.terminate();
return false;
}

// get output
QString output = py.readAll();

// close if it didn't already
if (py.waitForFinished(500)==false) {
fprintf(stderr, "forced terminate of %s\n", pythonbinary.toStdString().c_str());
py.terminate();
}

// scan output
QRegExp contents("^ZZ(.*)ZZ\nZZ(.*)ZZ\n$");
if (contents.exactMatch(output)) {
QString vmajor=contents.cap(1);
QString path=contents.cap(2);

// check its version 3
if (vmajor.toInt() != 3) {
fprintf(stderr, "%s is not version 3, it's version %d\n", pythonbinary.toStdString().c_str(), vmajor.toInt());
return false;
}

// now get python path
pypath = path;
return true;

} else {

// didn't understand !
fprintf(stderr, "Python output doesn't parse: %s\n", output.toStdString().c_str());
}

// by default we return false (pessimistic)
return false;
}

PythonEmbed::PythonEmbed(const bool verbose, const bool interactive) : verbose(verbose), interactive(interactive)
{
loaded = false;
threadid=-1;
name = QString("GoldenCheetah");

// tell python our program name
Py_SetProgramName((wchar_t*) name.toStdString().c_str());
// is python3 installed?
if (pythonInstalled(pyhome, pypath)) {

// our own module
PyImport_AppendInittab("goldencheetah", PyInit_goldencheetah);
// tell python our program name
Py_SetProgramName((wchar_t*) name.toStdString().c_str());

// need to load the interpreter etc
Py_InitializeEx(0);
// our own module
PyImport_AppendInittab("goldencheetah", PyInit_goldencheetah);

// set the module path in the same way the interpreter would
PyObject *sys = PyImport_ImportModule("sys");
// need to load the interpreter etc
Py_InitializeEx(0);

// did module import fail (python not installed)
if (sys == NULL) {
// set path - allocate storage for it...
wchar_t *here = new wchar_t(pypath.length()+1);
pypath.toWCharArray(here);
here[pypath.length()]=0;
PySys_SetPath(here);

// abort embedding
QMessageBox msg(QMessageBox::Information, QObject::tr("Python not installed"),
QObject::tr("Python v3.6 or higher is required for Python.\nPython disabled in preferences."));
appsettings->setValue(GC_EMBED_PYTHON, false);
loaded=false;
msg.exec();
return;
}
// set the module path in the same way the interpreter would
PyObject *sys = PyImport_ImportModule("sys");

// did module import fail (python not installed properly?)
if (sys != NULL) {

PyObject *path = PyObject_GetAttrString(sys, "path");
PyList_Append(path, PyUnicode_FromString("."));
//PyObject *path = PyObject_GetAttrString(sys, "path");
//PyList_Append(path, PyUnicode_FromString("."));

// get version
version = QString(Py_GetVersion());
version.replace("\n", " ");
// get version
version = QString(Py_GetVersion());
version.replace("\n", " ");

fprintf(stderr, "Python loaded [%s]\n", version.toStdString().c_str());
fprintf(stderr, "Python loaded [%s]\n", version.toStdString().c_str());

// our base code - traps stdout and loads goldencheetan module
// mapping all the bindings to a GC object.
std::string stdOutErr = ("import sys\n"
// our base code - traps stdout and loads goldencheetan module
// mapping all the bindings to a GC object.
std::string stdOutErr = ("import sys\n"
#ifdef Q_OS_LINUX
"import os\n"
"sys.setdlopenflags(os.RTLD_NOW | os.RTLD_DEEPBIND)\n"
"import os\n"
"sys.setdlopenflags(os.RTLD_NOW | os.RTLD_DEEPBIND)\n"
#endif
"class CatchOutErr:\n"
" def __init__(self):\n"
" self.value = ''\n"
" def write(self, txt):\n"
" self.value += txt\n"
"catchOutErr = CatchOutErr()\n"
"sys.stdout = catchOutErr\n"
"sys.stderr = catchOutErr\n"
"import goldencheetah\n"
"GC=goldencheetah.Bindings()\n");

PyRun_SimpleString(stdOutErr.c_str()); //invoke code to redirect

// now load the library
QFile lib(":python/library.py");
if (lib.open(QFile::ReadOnly)) {
QString libstring=lib.readAll();
lib.close();
PyRun_SimpleString(libstring.toLatin1().constData());
}


// setup trapping of output
PyObject *pModule = PyImport_AddModule("__main__"); //create main module
catcher = static_cast<void*>(PyObject_GetAttrString(pModule,"catchOutErr"));
clear = static_cast<void*>(PyObject_GetAttrString(static_cast<PyObject*>(catcher), "__init__"));
PyErr_Print(); //make python print any errors
PyErr_Clear(); //and clear them !

// prepare for threaded processing
PyEval_InitThreads();
mainThreadState = PyEval_SaveThread();
loaded = true;
"class CatchOutErr:\n"
" def __init__(self):\n"
" self.value = ''\n"
" def write(self, txt):\n"
" self.value += txt\n"
"catchOutErr = CatchOutErr()\n"
"sys.stdout = catchOutErr\n"
"sys.stderr = catchOutErr\n"
"import goldencheetah\n"
"GC=goldencheetah.Bindings()\n");

PyRun_SimpleString(stdOutErr.c_str()); //invoke code to redirect

// now load the library
QFile lib(":python/library.py");
if (lib.open(QFile::ReadOnly)) {
QString libstring=lib.readAll();
lib.close();
PyRun_SimpleString(libstring.toLatin1().constData());
}


// setup trapping of output
PyObject *pModule = PyImport_AddModule("__main__"); //create main module
catcher = static_cast<void*>(PyObject_GetAttrString(pModule,"catchOutErr"));
clear = static_cast<void*>(PyObject_GetAttrString(static_cast<PyObject*>(catcher), "__init__"));
PyErr_Print(); //make python print any errors
PyErr_Clear(); //and clear them !

// prepare for threaded processing
PyEval_InitThreads();
mainThreadState = PyEval_SaveThread();
loaded = true;
return;
} // sys != NULL
} // pythonInstalled == true

// if we get here loading failed
QMessageBox msg(QMessageBox::Information, QObject::tr("Python not installed or in path"),
QObject::tr("Python v3.6 or higher is required for Python.\nPython disabled in preferences."));
appsettings->setValue(GC_EMBED_PYTHON, false);
loaded=false;
msg.exec();
return;
}

// run on called thread
Expand Down
3 changes: 3 additions & 0 deletions src/Python/PythonEmbed.h
Expand Up @@ -38,6 +38,9 @@ class PythonEmbed {
PythonEmbed(const bool verbose=false, const bool interactive=false);
~PythonEmbed();

static bool pythonInstalled(QString &home, QString &pypath);
QString pyhome, pypath;

// scripts can set a result value
double result;

Expand Down

0 comments on commit 710cddf

Please sign in to comment.