HAL  v4.5.0-83-g30c8f0afc
The Hardware Analyzer - a comprehensive reverse engineering and manipulation framework for gate-level netlists.
python_context.cpp
Go to the documentation of this file.
2 
3 #include "gui/gui_globals.h"
11 #include "hal_core/utilities/log.h"
13 
14 #include <QDir>
15 #include <QDebug>
16 #include <QInputDialog>
17 #include <QApplication>
18 #include <QFileDialog>
19 #include <fstream>
21 
22 // Following is needed for PythonContext::checkCompleteStatement
23 #include "hal_config.h"
24 
25 #include <Python.h>
26 #include <compile.h>
27 #include <errcode.h>
28 
29 #if PY_VERSION_HEX < 0x030900a0 // Python 3.9.0
30 
31 #include <grammar.h>
32 #include <node.h>
33 #include <parsetok.h>
34 extern grammar _PyParser_Grammar;
35 
36 #endif
37 
38 namespace hal
39 {
41  : QObject(parent), mContext(nullptr), mSender(nullptr), mTriggerReset(false), mThread(nullptr),
42  mThreadAborted(false), mInterpreterCaller(nullptr)
43  {
44  py::initialize_interpreter();
45  initPython();
46 
47  // The Python interpreter is not thread safe, so it implements an internal
48  // Global Interpreter Lock that means only one thread can actively be
49  // executing Python code at a time (though the interpreter can switch between
50  // threads, for example if one thread is blocked for I/O).
51  // Take care to always handle the GIL correctly, or you will cause deadlocks,
52  // weird issues with threads that are running but Python believes that they
53  // don't exist, etc.
54  // DO NOT EVER run Python code while not holding the GIL!
55  // Wherever possible, use the PyGILState_Ensure / PyGILState_Release API to
56  // acquire/release the GIL.
57 
58  // We must release the GIL, otherwise any spawning thread will deadlock.
59  mMainThreadState = PyEval_SaveThread();
60 
61  }
62 
64  {
65  PyEval_RestoreThread(mMainThreadState);
66 
67  closePython();
68  py::finalize_interpreter();
69  }
70 
72  {
73  mConsole = c; // may be nullptr
74  if (c)
75  {
76  connect(mConsole,&PythonConsole::inputReceived,this,&PythonContext::handleConsoleInputReceived);
77  }
78  }
79 
81  {
82  return mThread;
83  }
84 
86  {
87  if (!mThread) return;
88  mThreadAborted = true;
89  mThread->interrupt();
90  }
91 
93  {
94  if (!mThread) return;
95  abortThread();
96  mThread->wait();
97  }
98 
99  void PythonContext::initializeContext(py::dict* context)
100  {
101  // GIL must be held
102 
103  std::string command = "import __main__\n"
104  "import io, sys, threading, builtins\n";
105  for (auto path : utils::get_plugin_directories())
106  {
107  command += "sys.path.append('" + path.string() + "')\n";
108  }
109  command += "sys.path.append('" + utils::get_library_directory().string()
110  + "')\n"
111  "from hal_gui.console import reset\n"
112  "from hal_gui.console import clear\n"
113  "class StdOutCatcher(io.TextIOBase):\n"
114  " def __init__(self):\n"
115  " pass\n"
116  " def write(self, stuff):\n"
117  " from hal_gui import console\n"
118  " if threading.current_thread() is threading.main_thread():\n"
119  " console.redirector.write_stdout(stuff)\n"
120  " else:\n"
121  " console.redirector.thread_stdout(stuff)\n"
122  "class StdErrCatcher(io.TextIOBase):\n"
123  " def __init__(self):\n"
124  " pass\n"
125  " def write(self, stuff):\n"
126  " from hal_gui import console\n"
127  " if threading.current_thread() is threading.main_thread():\n"
128  " console.redirector.write_stderr(stuff)\n"
129  " else:\n"
130  " console.redirector.thread_stderr(stuff)\n"
131  "def redirect_input(prompt=\"Please enter input:\"):\n"
132  " if threading.current_thread() is threading.main_thread():\n"
133  " return \"input in main thread not supported\"\n"
134  " else:\n"
135  " return console.redirector.thread_stdin(prompt)\n"
136  "sys.stdout = StdOutCatcher()\n"
137  "sys.__stdout__ = sys.stdout\n"
138  "sys.stderr = StdErrCatcher()\n"
139  "sys.__stderr__ = sys.stderr\n"
140  "builtins.input = redirect_input\n"
141  "builtins.raw_input = redirect_input\n"
142  "import hal_py\n";
143 
144  py::exec(command, *context, *context);
145 
146  (*context)["netlist"] = gNetlistOwner; // assign the shared_ptr here, not the raw ptr
147 
148  if (gGuiApi)
149  {
150  (*context)["gui"] = gGuiApi;
151  }
152  }
153 
154  void PythonContext::initializeScript(py::dict *context)
155  {
156  // GIL must be held
157 
158  std::string command = "import __main__\n"
159  "import io, sys, threading\n"
160  "from hal_gui.console import reset\n"
161  "from hal_gui.console import clear\n"
162  "import hal_py\n";
163 
164  py::exec(command, *context, *context);
165 
166  (*context)["netlist"] = gNetlistOwner; // assign the shared_ptr here, not the raw ptr
167 
168  if (gGuiApi)
169  {
170  (*context)["gui"] = gGuiApi;
171  }
172  }
173 
174 
176  {
177  // GIL must be held
178 
179  // using namespace py::literals;
180 
181  new py::dict();
182  mContext = new py::dict(**py::globals());
183 
184  initializeContext(mContext);
185  (*mContext)["console"] = py::module::import("hal_gui.console");
186  (*mContext)["hal_gui"] = py::module::import("hal_gui");
187  }
188 
190  {
191  // GIL must be held
192  delete mContext;
193  mContext = nullptr;
194  }
195 
196  void PythonContext::interpretBackground(QObject* caller, const QString& input, bool multiple_expressions)
197  {
198  if (input.isEmpty())
199  {
200  return;
201  }
202 
203  if (input == "quit()")
204  {
205  forwardError("quit() cannot be used in this interpreter. Use console.reset() to restart it.\n");
206  return;
207  }
208 
209  if (input == "help()")
210  {
211  forwardError("help() cannot be used in this interpreter.\n");
212  return;
213  }
214 
215  if (input == "license()")
216  {
217  forwardError("license() cannot be used in this interpreter.\n");
218  return;
219  }
220 
221  if (mThread)
222  {
223  forwardError("python thread already running. Please retry after other thread finished.\n");
224  return;
225  }
226  log_info("python", "Python console execute: \"{}\".", input.toStdString());
227 
228  mInterpreterCaller = caller;
229  startThread(input,!multiple_expressions);
230  }
231 
232  void PythonContext::startThread(const QString& input, bool singleStatement)
233  {
234  mThread = new PythonThread(input,singleStatement,this);
235  connect(mThread,&QThread::finished,this,&PythonContext::handleThreadFinished);
236  connect(mThread,&PythonThread::stdOutput,this,&PythonContext::handleScriptOutput);
237  connect(mThread,&PythonThread::stdError,this,&PythonContext::handleScriptError);
238  connect(mThread,&PythonThread::requireInput,this,&PythonContext::handleInputRequired);
239  if (mConsole) mConsole->setReadOnly(true);
240  mLayoutLocker = new LayoutLocker;
241  mThread->start();
242  }
243 
245  {
246  if (input.isEmpty())
247  {
248  return;
249  }
250 
251  log_info("python", "Python console execute: \"{}\".", input.toStdString());
252 
253  // since we've released the GIL in the constructor, re-acquire it here before
254  // running some Python code on the main thread
255  PyGILState_STATE state = PyGILState_Ensure();
256  //PyEval_RestoreThread(mMainThreadState);
257 
258  // TODO should the console also be moved to threads? Maybe actually catch Ctrl+C there
259  // as a method to interrupt? Currently you can hang the GUI by running an endless loop
260  // from the console.
261 
262  try
263  {
264  pybind11::object rc;
265  rc = py::eval<py::eval_single_statement>(input.toStdString(), *mContext, *mContext);
266  if (!rc.is_none())
267  {
268  forwardStdout(QString::fromStdString(py::str(rc).cast<std::string>()));
269  }
270  handleReset();
271  }
272  catch (py::error_already_set& e)
273  {
274  qDebug() << "interpret error set";
275  forwardError(QString::fromStdString(std::string(e.what())));
276  e.restore();
277  PyErr_Clear();
278  }
279  catch (std::exception& e)
280  {
281  qDebug() << "interpret exception";
282  forwardError(QString::fromStdString(std::string(e.what())));
283  }
284 
285  // make sure we release the GIL, otherwise we interfere with threading
286  PyGILState_Release(state);
287  //mMainThreadState = PyEval_SaveThread();
288 
289  }
290 
291  void PythonContext::interpretScript(QObject* caller, const QString& input)
292  {
293  if (mThread)
294  {
295  log_warning("python", "Not executed, python script already running");
296  return;
297  }
298 
299  //log_info("python", "Python editor execute script:\n{}\n", input.toStdString());
300 #ifdef HAL_STUDY
301  log_info("UserStudy", "Python editor execute script:\n{}\n", input.toStdString());
302 #endif
303  forwardStdout("\n");
304  forwardStdout("<Execute Python Editor content>");
305  forwardStdout("\n");
306 
307  mInterpreterCaller = caller;
308  startThread(input,false);
309  }
310 
311  void PythonContext::handleScriptOutput(const QString& txt)
312  {
313  if (!txt.isEmpty())
314  forwardStdout(txt);
315  }
316 
317  void PythonContext::handleScriptError(const QString& txt)
318  {
319  if (!txt.isEmpty())
320  forwardError(txt);
321  }
322 
323  void PythonContext::handleInputRequired(int type, const QString& prompt, const QVariant &defaultValue)
324  {
325  bool confirm;
326  if (mThread && !mThread->stdoutBuffer().isEmpty())
327  mConsole->handleStdout(mThread->flushStdout());
328 
329  switch (type) {
331  mConsole->handleStdout(prompt + "\n");
332  mConsole->setInputMode(true);
333  mConsole->displayPrompt();
334  mConsole->setReadOnly(false);
335  break;
337  {
338  QString userInput = QInputDialog::getText(qApp->activeWindow(), "Python Script Input", prompt, QLineEdit::Normal, defaultValue.toString(), &confirm);
339  if (!confirm) userInput.clear();
340  if (mThread) mThread->setInput(userInput);
341  break;
342  }
344  {
345  int userInput = QInputDialog::getInt(qApp->activeWindow(), "Python Script Input", prompt, defaultValue.toInt());
346  if (mThread) mThread->setInput(userInput);
347  break;
348  }
350  {
351  QSet<u32> gats;
352  for (const Gate* g : gNetlist->get_gates()) gats.insert(g->get_id());
354  PythonGateSelectionReceiver* pgs = new PythonGateSelectionReceiver(mThread,this);
355  GateDialog gd(gats, prompt, pgs, qApp->activeWindow());
356  Gate* gatSelect = (gd.exec() == QDialog::Accepted)
357  ? gNetlist->get_gate_by_id(gd.selectedId())
358  : nullptr;
359  if (!gd.pickerModeActivated() && mThread) mThread->setInput(QVariant::fromValue<void*>(gatSelect));
360  break;
361  }
363  {
364  QSet<u32> mods;
366  PythonModuleSelectionReceiver* pms = new PythonModuleSelectionReceiver(mThread,this);
367  ModuleDialog md({}, prompt, false, pms, qApp->activeWindow());
368  Module* modSelect = (md.exec() == QDialog::Accepted)
369  ? gNetlist->get_module_by_id(md.selectedId())
370  : nullptr;
371  if (!md.pickerModeActivated() && mThread) mThread->setInput(QVariant::fromValue<void*>(modSelect));
372  break;
373  }
375  {
376  QString userInput = QFileDialog::getOpenFileName(qApp->activeWindow(),prompt,".",defaultValue.toString());
377  if (mThread) mThread->setInput(userInput);
378  break;
379  }
380  default:
381  break;
382  }
383  }
384 
386  {
387  Gate* gatSelect = gats.isEmpty()
388  ? nullptr
389  : gNetlist->get_gate_by_id(*gats.constBegin());
390  if (mThread) mThread->setInput(QVariant::fromValue<void*>(gatSelect));
391  this->deleteLater();
392  }
393 
395  {
396  Module* modSelect = mods.isEmpty()
397  ? nullptr
399  if (mThread) mThread->setInput(QVariant::fromValue<void*>(modSelect));
400  this->deleteLater();
401  }
402 
403  void PythonContext::handleConsoleInputReceived(const QString& input)
404  {
405  mConsole->setInputMode(false);
406  mConsole->setReadOnly(true);
407  mThread->setInput(input);
408  }
409 
410  void PythonContext::handleThreadFinished()
411  {
412  if (mConsole) mConsole->setReadOnly(false);
413  PythonEditor* calledFromEditor = dynamic_cast<PythonEditor*>(mInterpreterCaller);
414  QString errmsg;
415  if (!mThread)
416  {
417  mThreadAborted = false;
418  return;
419  }
420  if (!mThreadAborted)
421  {
422  errmsg = mThread->errorMessage();
423  if (!mThread->stdoutBuffer().isEmpty())
424  forwardStdout(mThread->stdoutBuffer());
425  if (!calledFromEditor && !mThread->result().isEmpty() && errmsg.isEmpty())
426  forwardStdout(mThread->result());
427  }
428  else
429  {
430  forwardError("\nPython thread aborted\n");
431  mThreadAborted = false;
432  }
433 
434  mThread->deleteLater();
435  mThread = 0;
436  delete mLayoutLocker;
437 
438  if (!errmsg.isEmpty())
439  forwardError(errmsg);
440 
441  if (calledFromEditor)
442  {
443  calledFromEditor->handleThreadFinished();
444  if (mTriggerReset)
445  {
446  forwardError("\nreset() can only be used in the Python console!\n");
447  mTriggerReset = false;
448  }
449  if (mTriggerClear)
450  {
451  forwardError("\nclear() can only be used in the Python console!\n");
452  mTriggerClear = false;
453  }
454  }
455  else if (mConsole)
456  {
457  handleReset();
458  handleClear();
459  mConsole->handleThreadFinished();
460  }
461 
462  if (mConsole)
463  {
465  mConsole->displayPrompt();
466  }
467  }
468 
470  {
471  if (output != "\n")
472  {
473  log_info("python", "{}", utils::rtrim(output.toStdString(), "\r\n"));
474  }
475  if (mConsole)
476  {
477  mConsole->handleStdout(output);
478  }
479  }
480 
482  {
483  log_error("python", "{}", output.toStdString());
484  if (mConsole)
485  {
486  mConsole->handleError(output);
487  }
488  }
489 
490  std::vector<std::tuple<std::string, std::string>> PythonContext::complete(const QString& text, bool use_console_context)
491  {
492  PyGILState_STATE state = PyGILState_Ensure();
493 
494  std::vector<std::tuple<std::string, std::string>> ret_val;
495  try
496  {
497  auto namespaces = py::list();
498  if (use_console_context)
499  {
500  namespaces.append(*mContext);
501  namespaces.append(*mContext);
502  }
503  else
504  {
505  py::dict tmp_context = py::globals();
506  initializeContext(&tmp_context);
507  namespaces.append(tmp_context);
508  namespaces.append(tmp_context);
509  }
510  auto jedi = py::module::import("jedi");
511  py::object script = jedi.attr("Interpreter")(text.toStdString(), namespaces);
512  py::object list;
513  if (py::hasattr(script,"complete"))
514  list = script.attr("complete")();
515  else if (py::hasattr(script,"completions"))
516  list = script.attr("completions")();
517  else
518  log_warning("python", "Jedi autocompletion failed, neither complete() nor completions() found.");
519 
520  for (const auto& entry : list)
521  {
522  auto a = entry.attr("name_with_symbols").cast<std::string>();
523  auto b = entry.attr("complete").cast<std::string>();
524  ret_val.emplace_back(a, b);
525  }
526  }
527  catch (py::error_already_set& e)
528  {
529  forwardError(QString::fromStdString(std::string(e.what()) + "\n"));
530  e.restore();
531  PyErr_Clear();
532  }
533 
534  PyGILState_Release(state);
535 
536  return ret_val;
537  }
538 
540  {
541 
542  PyGILState_STATE state = PyGILState_Ensure();
543 
544  #if PY_VERSION_HEX < 0x030900a0 // Python 3.9.0
545  // PEG not yet available, use PyParser
546 
547  node* n;
548  perrdetail e;
549 
550  n = PyParser_ParseString(text.toStdString().c_str(), &_PyParser_Grammar, Py_file_input, &e);
551  if (n == NULL)
552  {
553  if (e.error == E_EOF)
554  {
555  PyGILState_Release(state);
556  return 0;
557  }
558  PyGILState_Release(state);
559  return -1;
560  }
561 
562  PyNode_Free(n);
563  PyGILState_Release(state);
564  return 1;
565 
566  #else
567 
568  // attempt to parse Python expression into AST using PEG
569  PyCompilerFlags flags {PyCF_ONLY_AST, PY_MINOR_VERSION};
570  PyObject* o = Py_CompileStringExFlags(text.toStdString().c_str(), "stdin", Py_file_input, &flags, 0);
571  // if parsing failed (-> not a complete statement), nullptr is returned
572  // (apparently no need to PyObject_Free(o) here)
573  PyGILState_Release(state);
574  return o != nullptr;
575 
576  #endif
577  }
578 
579  void PythonContext::handleReset()
580  {
581  if (mTriggerReset)
582  {
583  PyGILState_STATE state = PyGILState_Ensure();
584  closePython();
585  initPython();
586  PyGILState_Release(state);
587  scheduleClear();
588  mTriggerReset = false;
589  }
590  }
591 
592  void PythonContext::handleClear()
593  {
594  if (mTriggerClear)
595  {
596  if (mConsole)
597  {
598  mConsole->clear();
599  }
600  mTriggerClear = false;
601  }
602  }
603 
605  {
606  mTriggerReset = true;
607  }
608 
610  {
611  mTriggerClear = true;
612  }
613 
615  {
616  PyGILState_STATE state = PyGILState_Ensure();
617  (*mContext)["netlist"] = gNetlistOwner; // assign the shared_ptr here, not the raw ptr
618  PyGILState_Release(state);
619  }
620 
621 } // namespace hal
Definition: gate.h:58
const std::vector< Gate * > & get_gates() const
Definition: netlist.cpp:204
Gate * get_gate_by_id(const u32 gate_id) const
Definition: netlist.cpp:193
Module * get_module_by_id(u32 module_id) const
Definition: netlist.cpp:613
An interactable python console.
void setInputMode(bool state)
void inputReceived(QString input)
virtual void clear() override
virtual void handleStdout(const QString &output) override
virtual void handleError(const QString &output) override
std::vector< std::tuple< std::string, std::string > > complete(const QString &text, bool use_console_context)
PythonContext(QObject *parent=nullptr)
int checkCompleteStatement(const QString &text)
PythonThread * pythonThread() const
static void initializeContext(py::dict *context)
void interpretBackground(QObject *caller, const QString &input, bool multiple_expressions=false)
void setConsole(PythonConsole *console)
void interpretScript(QObject *caller, const QString &input)
void forwardError(const QString &output)
void interpretForeground(const QString &input)
void forwardStdout(const QString &output)
static void initializeScript(py::dict *context)
void handleGatesPicked(const QSet< u32 > &gats) override
void handleModulesPicked(const QSet< u32 > &gats) override
void stdError(QString txt)
QString errorMessage() const
Definition: python_thread.h:87
QString result() const
Definition: python_thread.h:88
void setInput(const QVariant &inp)
void stdOutput(QString txt)
void requireInput(int type, QString prompt, QVariant defaultValue)
const QString & stdoutBuffer() const
int numberSelectedGates() const
int numberSelectedModules() const
#define log_error(channel,...)
Definition: log.h:78
#define log_info(channel,...)
Definition: log.h:70
#define log_warning(channel,...)
Definition: log.h:76
T rtrim(const T &s, const char *to_remove=" \t\r\n")
Definition: utils.h:338
std::filesystem::path get_library_directory()
Definition: utils.cpp:129
std::vector< std::filesystem::path > get_plugin_directories()
Definition: utils.cpp:195
Definition: defines.h:45
SelectionRelay * gSelectionRelay
Definition: plugin_gui.cpp:83
Netlist * gNetlist
Definition: gui_globals.h:69
GuiApi * gGuiApi
Definition: plugin_gui.cpp:86
std::shared_ptr< Netlist > gNetlistOwner
Definition: plugin_gui.cpp:79
PinType type
grammar _PyParser_Grammar
QString getOpenFileName(QWidget *parent, const QString &caption, const QString &dir, const QString &filter, QString *selectedFilter, QFileDialog::Options options)
int getInt(QWidget *parent, const QString &title, const QString &label, int value, int min, int max, int step, bool *ok, Qt::WindowFlags flags)
QString getText(QWidget *parent, const QString &title, const QString &label, QLineEdit::EchoMode mode, const QString &text, bool *ok, Qt::WindowFlags flags, Qt::InputMethodHints inputMethodHints)
QMetaObject::Connection connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
void deleteLater()
QSet::const_iterator constBegin() const const
QSet::iterator insert(const T &value)
bool isEmpty() const const
void clear()
QString fromStdString(const std::string &str)
bool isEmpty() const const
std::string toStdString() const const
void setReadOnly(bool ro)
void finished()
void start(QThread::Priority priority)
bool wait(QDeadlineTimer deadline)
int toInt(bool *ok) const const
QString toString() const const