HAL  v4.5.0-234-g3c2e8eb18
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), mTriggerClear(false),
42  mThread(nullptr), 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  // help('modules') would import every installed package, some of them crash HAL (e.g. OpenGL.Tk opens a Tk window)
143  "import pydoc, pkgutil\n"
144  "def hal_listmodules(self, key=''):\n"
145  " names = set(sys.builtin_module_names) | {m.name for m in pkgutil.iter_modules()}\n"
146  " if key:\n"
147  " names = {n for n in names if key.lower() in n.lower()}\n"
148  " self.list(sorted(names))\n"
149  "pydoc.Helper.listmodules = hal_listmodules\n"
150  "import hal_py\n";
151 
152  py::exec(command, *context, *context);
153 
154  (*context)["netlist"] = gNetlistOwner; // assign the shared_ptr here, not the raw ptr
155 
156  if (gGuiApi)
157  {
158  (*context)["gui"] = gGuiApi;
159  }
160  }
161 
162  void PythonContext::initializeScript(py::dict *context)
163  {
164  // GIL must be held
165 
166  std::string command = "import __main__\n"
167  "import io, sys, threading\n"
168  "from hal_gui.console import reset\n"
169  "from hal_gui.console import clear\n"
170  "import hal_py\n";
171 
172  py::exec(command, *context, *context);
173 
174  (*context)["netlist"] = gNetlistOwner; // assign the shared_ptr here, not the raw ptr
175 
176  if (gGuiApi)
177  {
178  (*context)["gui"] = gGuiApi;
179  }
180  }
181 
182 
184  {
185  // GIL must be held
186 
187  // using namespace py::literals;
188 
189  new py::dict();
190  mContext = new py::dict(**py::globals());
191 
192  initializeContext(mContext);
193  (*mContext)["console"] = py::module::import("hal_gui.console");
194  (*mContext)["hal_gui"] = py::module::import("hal_gui");
195  }
196 
198  {
199  // GIL must be held
200  delete mContext;
201  mContext = nullptr;
202  }
203 
204  bool PythonContext::interpretBackground(QObject* caller, const QString& input, bool multiple_expressions)
205  {
206  if (input.isEmpty())
207  {
208  return false;
209  }
210 
211  if (input == "quit()")
212  {
213  forwardError("quit() cannot be used in this interpreter. Use console.reset() to restart it.\n");
214  return false;
215  }
216 
217  if (mThread)
218  {
219  forwardError("python thread already running. Please retry after other thread finished.\n");
220  return false;
221  }
222  log_info("python", "Python console execute: \"{}\".", input.toStdString());
223 
224  mInterpreterCaller = caller;
225  startThread(input,!multiple_expressions);
226  return true;
227  }
228 
229  void PythonContext::startThread(const QString& input, bool singleStatement)
230  {
231  mThread = new PythonThread(input,singleStatement,this);
232  connect(mThread,&QThread::finished,this,&PythonContext::handleThreadFinished);
233  connect(mThread,&PythonThread::stdOutput,this,&PythonContext::handleScriptOutput);
234  connect(mThread,&PythonThread::stdError,this,&PythonContext::handleScriptError);
235  connect(mThread,&PythonThread::requireInput,this,&PythonContext::handleInputRequired);
236  if (mConsole) mConsole->setReadOnly(true);
237  mLayoutLocker = new LayoutLocker;
238  mThread->start();
239  }
240 
242  {
243  if (input.isEmpty())
244  {
245  return;
246  }
247 
248  log_info("python", "Python console execute: \"{}\".", input.toStdString());
249 
250  // since we've released the GIL in the constructor, re-acquire it here before
251  // running some Python code on the main thread
252  PyGILState_STATE state = PyGILState_Ensure();
253  //PyEval_RestoreThread(mMainThreadState);
254 
255  // TODO should the console also be moved to threads? Maybe actually catch Ctrl+C there
256  // as a method to interrupt? Currently you can hang the GUI by running an endless loop
257  // from the console.
258 
259  try
260  {
261  pybind11::object rc;
262  rc = py::eval<py::eval_single_statement>(input.toStdString(), *mContext, *mContext);
263  if (!rc.is_none())
264  {
265  forwardStdout(QString::fromStdString(py::str(rc).cast<std::string>()));
266  }
267  handleReset();
268  }
269  catch (py::error_already_set& e)
270  {
271  qDebug() << "interpret error set";
272  forwardError(QString::fromStdString(std::string(e.what())));
273  e.restore();
274  PyErr_Clear();
275  }
276  catch (std::exception& e)
277  {
278  qDebug() << "interpret exception";
279  forwardError(QString::fromStdString(std::string(e.what())));
280  }
281 
282  // make sure we release the GIL, otherwise we interfere with threading
283  PyGILState_Release(state);
284  //mMainThreadState = PyEval_SaveThread();
285 
286  }
287 
288  void PythonContext::interpretScript(QObject* caller, const QString& input)
289  {
290  if (mThread)
291  {
292  log_warning("python", "Not executed, python script already running");
293  return;
294  }
295 
296  //log_info("python", "Python editor execute script:\n{}\n", input.toStdString());
297 #ifdef HAL_STUDY
298  log_info("UserStudy", "Python editor execute script:\n{}\n", input.toStdString());
299 #endif
300  forwardStdout("\n");
301  forwardStdout("<Execute Python Editor content>");
302  forwardStdout("\n");
303 
304  mInterpreterCaller = caller;
305  startThread(input,false);
306  }
307 
308  void PythonContext::handleScriptOutput(const QString& txt)
309  {
310  if (!txt.isEmpty())
311  forwardStdout(txt);
312  }
313 
314  void PythonContext::handleScriptError(const QString& txt)
315  {
316  if (!txt.isEmpty())
317  forwardError(txt);
318  }
319 
320  void PythonContext::handleInputRequired(int type, const QString& prompt, const QVariant &defaultValue)
321  {
322  bool confirm;
323  if (mThread && !mThread->stdoutBuffer().isEmpty())
324  mConsole->handleStdout(mThread->flushStdout());
325 
326  switch (type) {
328  mConsole->handleStdout(prompt + "\n");
329  mConsole->setInputMode(true);
330  mConsole->displayPrompt();
331  mConsole->setReadOnly(false);
332  break;
334  {
335  QString userInput = QInputDialog::getText(qApp->activeWindow(), "Python Script Input", prompt, QLineEdit::Normal, defaultValue.toString(), &confirm);
336  if (!confirm) userInput.clear();
337  if (mThread) mThread->setInput(userInput);
338  break;
339  }
341  {
342  int userInput = QInputDialog::getInt(qApp->activeWindow(), "Python Script Input", prompt, defaultValue.toInt());
343  if (mThread) mThread->setInput(userInput);
344  break;
345  }
347  {
348  QSet<u32> gats;
349  for (const Gate* g : gNetlist->get_gates()) gats.insert(g->get_id());
351  PythonGateSelectionReceiver* pgs = new PythonGateSelectionReceiver(mThread,this);
352  GateDialog gd(gats, prompt, pgs, qApp->activeWindow());
353  Gate* gatSelect = (gd.exec() == QDialog::Accepted)
354  ? gNetlist->get_gate_by_id(gd.selectedId())
355  : nullptr;
356  if (!gd.pickerModeActivated() && mThread) mThread->setInput(QVariant::fromValue<void*>(gatSelect));
357  break;
358  }
360  {
361  QSet<u32> mods;
363  PythonModuleSelectionReceiver* pms = new PythonModuleSelectionReceiver(mThread,this);
364  ModuleDialog md({}, prompt, false, pms, qApp->activeWindow());
365  Module* modSelect = (md.exec() == QDialog::Accepted)
366  ? gNetlist->get_module_by_id(md.selectedId())
367  : nullptr;
368  if (!md.pickerModeActivated() && mThread) mThread->setInput(QVariant::fromValue<void*>(modSelect));
369  break;
370  }
372  {
373  QString userInput = QFileDialog::getOpenFileName(qApp->activeWindow(),prompt,".",defaultValue.toString());
374  if (mThread) mThread->setInput(userInput);
375  break;
376  }
377  default:
378  break;
379  }
380  }
381 
383  {
384  Gate* gatSelect = gats.isEmpty()
385  ? nullptr
386  : gNetlist->get_gate_by_id(*gats.constBegin());
387  if (mThread) mThread->setInput(QVariant::fromValue<void*>(gatSelect));
388  this->deleteLater();
389  }
390 
392  {
393  Module* modSelect = mods.isEmpty()
394  ? nullptr
396  if (mThread) mThread->setInput(QVariant::fromValue<void*>(modSelect));
397  this->deleteLater();
398  }
399 
400  void PythonContext::handleConsoleInputReceived(const QString& input)
401  {
402  mConsole->setInputMode(false);
403  mConsole->setReadOnly(true);
404  mThread->setInput(input);
405  }
406 
407  void PythonContext::handleThreadFinished()
408  {
409  if (mConsole) mConsole->setReadOnly(false);
410  PythonEditor* calledFromEditor = dynamic_cast<PythonEditor*>(mInterpreterCaller);
411  QString errmsg;
412  if (!mThread)
413  {
414  mThreadAborted = false;
415  return;
416  }
417  if (!mThreadAborted)
418  {
419  errmsg = mThread->errorMessage();
420  if (!mThread->stdoutBuffer().isEmpty())
421  forwardStdout(mThread->stdoutBuffer());
422  if (!calledFromEditor && !mThread->result().isEmpty() && errmsg.isEmpty())
423  forwardStdout(mThread->result());
424  }
425  else
426  {
427  forwardError("\nPython thread aborted\n");
428  mThreadAborted = false;
429  }
430 
431  mThread->deleteLater();
432  mThread = 0;
433  delete mLayoutLocker;
434 
435  if (!errmsg.isEmpty())
436  forwardError(errmsg);
437 
438  if (calledFromEditor)
439  {
440  calledFromEditor->handleThreadFinished();
441  if (mTriggerReset)
442  {
443  forwardError("\nreset() can only be used in the Python console!\n");
444  mTriggerReset = false;
445  }
446  if (mTriggerClear)
447  {
448  forwardError("\nclear() can only be used in the Python console!\n");
449  mTriggerClear = false;
450  }
451  }
452  else if (mConsole)
453  {
454  handleReset();
455  handleClear();
456  mConsole->handleThreadFinished();
457  }
458 
459  if (mConsole)
460  {
462  mConsole->displayPrompt();
463  }
464  }
465 
467  {
468  if (output != "\n")
469  {
470  log_info("python", "{}", utils::rtrim(output.toStdString(), "\r\n"));
471  }
472  if (mConsole)
473  {
474  mConsole->handleStdout(output);
475  }
476  }
477 
479  {
480  log_error("python", "{}", output.toStdString());
481  if (mConsole)
482  {
483  mConsole->handleError(output);
484  }
485  }
486 
487  std::vector<std::tuple<std::string, std::string>> PythonContext::complete(const QString& text, bool use_console_context)
488  {
489  PyGILState_STATE state = PyGILState_Ensure();
490 
491  std::vector<std::tuple<std::string, std::string>> ret_val;
492  try
493  {
494  auto namespaces = py::list();
495  if (use_console_context)
496  {
497  namespaces.append(*mContext);
498  namespaces.append(*mContext);
499  }
500  else
501  {
502  py::dict tmp_context = py::globals();
503  initializeContext(&tmp_context);
504  namespaces.append(tmp_context);
505  namespaces.append(tmp_context);
506  }
507  auto jedi = py::module::import("jedi");
508  py::object script = jedi.attr("Interpreter")(text.toStdString(), namespaces);
509  py::object list;
510  if (py::hasattr(script,"complete"))
511  list = script.attr("complete")();
512  else if (py::hasattr(script,"completions"))
513  list = script.attr("completions")();
514  else
515  log_warning("python", "Jedi autocompletion failed, neither complete() nor completions() found.");
516 
517  for (const auto& entry : list)
518  {
519  auto a = entry.attr("name_with_symbols").cast<std::string>();
520  auto b = entry.attr("complete").cast<std::string>();
521  ret_val.emplace_back(a, b);
522  }
523  }
524  catch (py::error_already_set& e)
525  {
526  forwardError(QString::fromStdString(std::string(e.what()) + "\n"));
527  e.restore();
528  PyErr_Clear();
529  }
530 
531  PyGILState_Release(state);
532 
533  return ret_val;
534  }
535 
537  {
538  // Use Python codeop which is designed for interactive interpreters.
539  // Returns: 0 = incomplete, 1 = complete, -1 = syntax error
540  PyGILState_STATE state = PyGILState_Ensure();
541 
542  int result;
543  try
544  {
545  py::module codeop = py::module::import("codeop");
546  py::object compiled = codeop.attr("compile_command")(text.toStdString(), "stdin", "exec");
547  result = compiled.is_none() ? 0 : 1;
548  }
549  catch (py::error_already_set& e)
550  {
551  result = -1;
552  e.restore();
553  PyErr_Clear();
554  }
555  catch (...)
556  {
557  result = -1;
558  }
559 
560  PyGILState_Release(state);
561  return result;
562  }
563 
564  void PythonContext::handleReset()
565  {
566  if (mTriggerReset)
567  {
568  PyGILState_STATE state = PyGILState_Ensure();
569  closePython();
570  initPython();
571  PyGILState_Release(state);
572  scheduleClear();
573  mTriggerReset = false;
574  }
575  }
576 
577  void PythonContext::handleClear()
578  {
579  if (mTriggerClear)
580  {
581  if (mConsole)
582  {
583  mConsole->clear();
584  }
585  mTriggerClear = false;
586  }
587  }
588 
590  {
591  mTriggerReset = true;
592  }
593 
595  {
596  mTriggerClear = true;
597  }
598 
600  {
601  PyGILState_STATE state = PyGILState_Ensure();
602  (*mContext)["netlist"] = gNetlistOwner; // assign the shared_ptr here, not the raw ptr
603  PyGILState_Release(state);
604  }
605 
606 } // namespace hal
Definition: gate.h:59
const std::vector< Gate * > & get_gates() const
Definition: netlist.cpp:206
Gate * get_gate_by_id(const u32 gate_id) const
Definition: netlist.cpp:195
Module * get_module_by_id(u32 module_id) const
Definition: netlist.cpp:615
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
bool interpretBackground(QObject *caller, const QString &input, bool multiple_expressions=false)
static void initializeContext(py::dict *context)
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
const Module * module(const Gate *g, const NodeBoxes &boxes)
T rtrim(const T &s, const char *to_remove=" \t\r\n")
Definition: utils.h:382
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:84
Netlist * gNetlist
Definition: gui_globals.h:69
GuiApi * gGuiApi
Definition: plugin_gui.cpp:87
std::shared_ptr< Netlist > gNetlistOwner
Definition: plugin_gui.cpp:80
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