1 /******************************************************************************
2   This source file is part of the Avogadro project.
3   This source code is released under the 3-Clause BSD License, (see "LICENSE").
4 ******************************************************************************/
5 
6 #include "pythonscript.h"
7 
8 #include "avogadropython.h"
9 
10 #include <QtCore/QDebug>
11 #include <QtCore/QLocale>
12 #include <QtCore/QProcess>
13 #include <QtCore/QSettings>
14 
15 namespace Avogadro {
16 namespace QtGui {
17 
PythonScript(const QString & scriptFilePath_,QObject * parent_)18 PythonScript::PythonScript(const QString& scriptFilePath_, QObject* parent_)
19   : QObject(parent_), m_debug(!qgetenv("AVO_PYTHON_SCRIPT_DEBUG").isEmpty()),
20     m_scriptFilePath(scriptFilePath_), m_process(nullptr)
21 {
22   setDefaultPythonInterpretor();
23 }
24 
PythonScript(QObject * parent_)25 PythonScript::PythonScript(QObject* parent_)
26   : QObject(parent_), m_debug(!qgetenv("AVO_PYTHON_SCRIPT_DEBUG").isEmpty()),
27     m_process(nullptr)
28 {
29   setDefaultPythonInterpretor();
30 }
31 
~PythonScript()32 PythonScript::~PythonScript() {}
33 
setScriptFilePath(const QString & scriptFile)34 void PythonScript::setScriptFilePath(const QString& scriptFile)
35 {
36   m_scriptFilePath = scriptFile;
37 }
38 
setDefaultPythonInterpretor()39 void PythonScript::setDefaultPythonInterpretor()
40 {
41   m_pythonInterpreter = qgetenv("AVO_PYTHON_INTERPRETER");
42   if (m_pythonInterpreter.isEmpty()) {
43     m_pythonInterpreter =
44       QSettings().value(QStringLiteral("interpreters/python")).toString();
45   }
46   if (m_pythonInterpreter.isEmpty())
47     m_pythonInterpreter = pythonInterpreterPath;
48 }
49 
execute(const QStringList & args,const QByteArray & scriptStdin)50 QByteArray PythonScript::execute(const QStringList& args,
51                                  const QByteArray& scriptStdin)
52 {
53   clearErrors();
54   QProcess proc;
55 
56   // Merge stdout and stderr
57   proc.setProcessChannelMode(QProcess::MergedChannels);
58 
59   // Add debugging flag if needed.
60   QStringList realArgs(args);
61   if (m_debug)
62     realArgs.prepend(QStringLiteral("--debug"));
63 
64   // Add the global language / locale to *all* calls
65   realArgs.append("--lang");
66   realArgs.append(QLocale::system().name());
67 
68   // Start script
69   realArgs.prepend(m_scriptFilePath);
70   if (m_debug) {
71     qDebug() << "Executing" << m_pythonInterpreter
72              << realArgs.join(QStringLiteral(" ")) << "<" << scriptStdin;
73   }
74   proc.start(m_pythonInterpreter, realArgs);
75 
76   // Write scriptStdin to the process's stdin
77   if (!scriptStdin.isNull()) {
78     if (!proc.waitForStarted(5000)) {
79       m_errors << tr("Error running script '%1 %2': Timed out waiting for "
80                      "start (%3).")
81                     .arg(m_pythonInterpreter,
82                          realArgs.join(QStringLiteral(" ")),
83                          processErrorString(proc));
84       return QByteArray();
85     }
86 
87     qint64 len = proc.write(scriptStdin);
88     if (len != static_cast<qint64>(scriptStdin.size())) {
89       m_errors << tr("Error running script '%1 %2': failed to write to stdin "
90                      "(len=%3, wrote %4 bytes, QProcess error: %5).")
91                     .arg(m_pythonInterpreter)
92                     .arg(realArgs.join(QStringLiteral(" ")))
93                     .arg(scriptStdin.size())
94                     .arg(len)
95                     .arg(processErrorString(proc));
96       return QByteArray();
97     }
98     proc.closeWriteChannel();
99   }
100 
101   if (!proc.waitForFinished(5000)) {
102     m_errors << tr("Error running script '%1 %2': Timed out waiting for "
103                    "finish (%3).")
104                   .arg(m_pythonInterpreter, realArgs.join(QStringLiteral(" ")),
105                        processErrorString(proc));
106     return QByteArray();
107   }
108 
109   if (proc.exitStatus() != QProcess::NormalExit || proc.exitCode() != 0) {
110     m_errors << tr("Error running script '%1 %2': Abnormal exit status %3 "
111                    "(%4: %5)\n\nOutput:\n%6")
112                   .arg(m_pythonInterpreter)
113                   .arg(realArgs.join(QStringLiteral(" ")))
114                   .arg(proc.exitCode())
115                   .arg(processErrorString(proc))
116                   .arg(proc.errorString())
117                   .arg(QString(proc.readAll()));
118     return QByteArray();
119   }
120 
121   QByteArray result(proc.readAll());
122 
123   if (m_debug)
124     qDebug() << "Output:" << result;
125 
126   return result;
127 }
128 
asyncExecute(const QStringList & args,const QByteArray & scriptStdin)129 void PythonScript::asyncExecute(const QStringList& args,
130                                 const QByteArray& scriptStdin)
131 {
132   clearErrors();
133   if (m_process != nullptr) {
134     // bad news
135     m_process->terminate();
136     disconnect(m_process, SIGNAL(finished()), this, SLOT(processsFinished()));
137     m_process->deleteLater();
138   }
139   m_process = new QProcess(parent());
140 
141   // Merge stdout and stderr
142   m_process->setProcessChannelMode(QProcess::MergedChannels);
143 
144   // Add debugging flag if needed.
145   QStringList realArgs(args);
146   if (m_debug)
147     realArgs.prepend(QStringLiteral("--debug"));
148 
149   // Add the global language / locale to *all* calls
150   realArgs.append("--lang");
151   realArgs.append(QLocale::system().name());
152 
153   // Start script
154   realArgs.prepend(m_scriptFilePath);
155   if (m_debug) {
156     qDebug() << "Executing" << m_pythonInterpreter
157              << realArgs.join(QStringLiteral(" ")) << "<" << scriptStdin;
158   }
159   m_process->start(m_pythonInterpreter, realArgs);
160 
161   // Write scriptStdin to the process's stdin
162   if (!scriptStdin.isNull()) {
163     if (!m_process->waitForStarted(5000)) {
164       m_errors << tr("Error running script '%1 %2': Timed out waiting for "
165                      "start (%3).")
166                     .arg(m_pythonInterpreter,
167                          realArgs.join(QStringLiteral(" ")),
168                          processErrorString(*m_process));
169       return;
170     }
171 
172     qint64 len = m_process->write(scriptStdin);
173     if (len != static_cast<qint64>(scriptStdin.size())) {
174       m_errors << tr("Error running script '%1 %2': failed to write to stdin "
175                      "(len=%3, wrote %4 bytes, QProcess error: %5).")
176                     .arg(m_pythonInterpreter)
177                     .arg(realArgs.join(QStringLiteral(" ")))
178                     .arg(scriptStdin.size())
179                     .arg(len)
180                     .arg(processErrorString(*m_process));
181       return;
182     }
183     m_process->closeWriteChannel();
184   }
185 
186   // let the script run
187   connect(m_process, SIGNAL(finished(int, QProcess::ExitStatus)), this,
188           SLOT(processFinished(int, QProcess::ExitStatus)));
189 }
190 
processFinished(int exitCode,QProcess::ExitStatus exitStatus)191 void PythonScript::processFinished(int exitCode,
192                                    QProcess::ExitStatus exitStatus)
193 {
194   emit finished();
195 }
196 
asyncResponse()197 QByteArray PythonScript::asyncResponse()
198 {
199   if (m_process == nullptr || m_process->state() == QProcess::Running) {
200     return QByteArray(); // wait
201   }
202 
203   return m_process->readAll();
204 }
205 
processErrorString(const QProcess & proc) const206 QString PythonScript::processErrorString(const QProcess& proc) const
207 {
208   QString result;
209   switch (proc.error()) {
210     case QProcess::FailedToStart:
211       result = tr("Script failed to start.");
212       break;
213     case QProcess::Crashed:
214       result = tr("Script crashed.");
215       break;
216     case QProcess::Timedout:
217       result = tr("Script timed out.");
218       break;
219     case QProcess::ReadError:
220       result = tr("Read error.");
221       break;
222     case QProcess::WriteError:
223       result = tr("Write error.");
224       break;
225     default:
226     case QProcess::UnknownError:
227       result = tr("Unknown error.");
228       break;
229   }
230   return result;
231 }
232 
233 } // namespace QtGui
234 } // namespace Avogadro
235