/ src / emc / pythonplugin / python_plugin.cc
python_plugin.cc
  1  /*    This is a component of LinuxCNC
  2   *    Copyright 2011, 2012, 2013 Jeff Epler <jepler@dsndata.com>, Michael
  3   *    Haberler <git@mah.priv.at>, Sebastian Kuzminsky <seb@highlab.com>
  4   *
  5   *    This program is free software; you can redistribute it and/or modify
  6   *    it under the terms of the GNU General Public License as published by
  7   *    the Free Software Foundation; either version 2 of the License, or
  8   *    (at your option) any later version.
  9   *
 10   *    This program is distributed in the hope that it will be useful,
 11   *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 12   *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 13   *    GNU General Public License for more details.
 14   *
 15   *    You should have received a copy of the GNU General Public License
 16   *    along with this program; if not, write to the Free Software
 17   *    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
 18   */
 19  #include "python_plugin.hh"
 20  #include "inifile.hh"
 21  
 22  #include <stdio.h>
 23  #include <stdlib.h>
 24  #include <unistd.h>
 25  #include <set>
 26  
 27  #define BOOST_PYTHON_MAX_ARITY 4
 28  #include <boost/python/exec.hpp>
 29  #include <boost/python/extract.hpp>
 30  #include <boost/python/import.hpp>
 31  
 32  namespace bp = boost::python;
 33  
 34  #define MAX_ERRMSG_SIZE 256
 35  
 36  #define ERRMSG(fmt, args...)					\
 37      do {							\
 38          char msgbuf[MAX_ERRMSG_SIZE];				\
 39          snprintf(msgbuf, sizeof(msgbuf) -1,  fmt, ##args);	\
 40          error_msg = std::string(msgbuf);			\
 41      } while(0)
 42  
 43  #define logPP(level, fmt, ...)						\
 44      do {								\
 45          ERRMSG(fmt, ## __VA_ARGS__);					\
 46  	if (log_level >= level) {					\
 47  	    fprintf(stderr, fmt, ## __VA_ARGS__);			\
 48  	    fprintf(stderr,"\n");					\
 49  	}								\
 50      } while (0)
 51  
 52  static const char *strstore(const char *s);
 53  
 54  // boost python versions from 1.58 to 1.61 (the latest at the time of
 55  // writing) all have a bug in boost::python::execfile that results in a
 56  // double free.  Work around it by using the Python implementation of
 57  // execfile instead.
 58  // The bug was introduced at https://github.com/boostorg/python/commit/fe24ab9dd5440562e27422cd38f7de03356bfd16
 59  bp::object working_execfile(const char *filename, bp::object globals, bp::object locals) {
 60      return bp::import("__builtin__").attr("execfile")(filename, globals, locals);
 61  }
 62  
 63  int PythonPlugin::run_string(const char *cmd, bp::object &retval, bool as_file)
 64  {
 65      reload();
 66      try {
 67  	if (as_file)
 68  	    retval = working_execfile(cmd, main_namespace, main_namespace);
 69  	else
 70  	    retval = bp::exec(cmd, main_namespace, main_namespace);
 71  	status = PLUGIN_OK;
 72      }
 73      catch (bp::error_already_set) {
 74  	if (PyErr_Occurred()) {
 75  	    exception_msg = handle_pyerror();
 76  	} else
 77  	    exception_msg = "unknown exception";
 78  	status = PLUGIN_EXCEPTION;
 79  	bp::handle_exception();
 80  	PyErr_Clear();
 81      }
 82      if (status == PLUGIN_EXCEPTION) {
 83  	logPP(0, "run_string(%s): \n%s",
 84  	      cmd, exception_msg.c_str());
 85      }
 86      return status;
 87  }
 88  
 89  int PythonPlugin::call_method(bp::object method, bp::object &retval) 
 90  {
 91  
 92      logPP(1, "call_method()");
 93      if (status < PLUGIN_OK)
 94  	return status;
 95  
 96      try {
 97  	retval = method(); 
 98  	status = PLUGIN_OK;
 99      }
100      catch (bp::error_already_set) {
101  	if (PyErr_Occurred()) {
102  	   exception_msg = handle_pyerror();
103  	} else
104  	    exception_msg = "unknown exception";
105  	status = PLUGIN_EXCEPTION;
106  	bp::handle_exception();
107  	PyErr_Clear();
108      }
109      if (status == PLUGIN_EXCEPTION) {
110  	logPP(0, "call_method(): %s", exception_msg.c_str());
111      }
112      return status;
113  
114  }
115  
116  int PythonPlugin::call(const char *module, const char *callable,
117  		       bp::object tupleargs, bp::object kwargs, bp::object &retval)
118  {
119      bp::object function;
120  
121      if (callable == NULL)
122  	return PLUGIN_NO_CALLABLE;
123  
124      reload();
125  
126      if (status < PLUGIN_OK)
127  	return status;
128  
129      try {
130  	if (module == NULL) {  // default to function in toplevel module
131  	    function = main_namespace[callable];
132  	} else {
133  	    bp::object submod =  main_namespace[module];
134  	    bp::object submod_namespace = submod.attr("__dict__");
135  	    function = submod_namespace[callable];
136  	}
137  	// this wont work with boost-python1.34 - needs 1.40
138  	//retval = function(*tupleargs, **kwargs);
139  
140  	// this does
141  	PyObject *rv = PyObject_Call(function.ptr(), tupleargs.ptr(), kwargs.ptr());
142  	if (PyErr_Occurred()) 
143  	    bp::throw_error_already_set();
144  	if (rv) 
145  	    retval = bp::object(bp::borrowed(rv));
146  	else
147  	    retval = bp::object();
148  	status = PLUGIN_OK;
149      }
150      catch (bp::error_already_set) {
151  	if (PyErr_Occurred()) {
152  	   exception_msg = handle_pyerror();
153  	} else
154  	    exception_msg = "unknown exception";
155  	status = PLUGIN_EXCEPTION;
156  	bp::handle_exception();
157  	PyErr_Clear();
158      }
159      if (status == PLUGIN_EXCEPTION) {
160  	logPP(0, "call(%s%s%s): \n%s",
161  	      module ? module : "",
162  	      module ? "." : "",
163  	      callable, exception_msg.c_str());
164      }
165      return status;
166  }
167  
168  bool PythonPlugin::is_callable(const char *module,
169  			       const char *funcname)
170  {
171      bool unexpected = false;
172      bool result = false;
173      bp::object function;
174  
175      reload();
176      if ((status != PLUGIN_OK) ||
177  	(funcname == NULL)) {
178  	return false;
179      }
180      try {
181  	if (module == NULL) {  // default to function in toplevel module
182  	   function = main_namespace[funcname];
183  	} else {
184  	    bp::object submod =  main_namespace[module];
185  	    bp::object submod_namespace = submod.attr("__dict__");
186  	    function = submod_namespace[funcname];
187  	}
188  	result = PyCallable_Check(function.ptr());
189      }
190      catch (bp::error_already_set) {
191  	// KeyError expected if not callable
192  	if (!PyErr_ExceptionMatches(PyExc_KeyError)) {
193  	    // something else, strange
194  	    exception_msg = handle_pyerror();
195  	    unexpected = true;
196  	}
197  	result = false;
198  	PyErr_Clear();
199      }
200      if (unexpected)
201  	logPP(0, "is_callable(%s%s%s): unexpected exception:\n%s",
202  	      module ? module : "", module ? "." : "",
203  	      funcname,exception_msg.c_str());
204  
205      if (log_level)
206  	logPP(4, "is_callable(%s%s%s) = %s",
207  	      module ? module : "", module ? "." : "",
208  	      funcname,result ? "TRUE":"FALSE");
209      return result;
210  }
211  
212  // this should be moved to an inotify-based solution and be done with it
213  int PythonPlugin::reload()
214  {
215      struct stat st;
216      if (!reload_on_change)
217  	return PLUGIN_OK;
218  
219      if (stat(abs_path, &st)) {
220  	logPP(0, "reload: stat(%s) returned %s", abs_path, strerror(errno));
221  	status = PLUGIN_STAT_FAILED;
222  	return status;
223      }
224      if (st.st_mtime > module_mtime) {
225  	module_mtime = st.st_mtime;
226  	initialize();
227  	logPP(1, "reload():  %s reloaded, status=%d", toplevel, status);
228      } else {
229  	logPP(5, "reload: no-op");
230  	status = PLUGIN_OK;
231      }
232      return status;
233  }
234  
235  // decode a Python exception into a string.
236  // Free function usable without working plugin instance.
237  std::string handle_pyerror()
238  {
239      PyObject *exc, *val, *tb;
240      bp::object formatted_list, formatted;
241  
242      PyErr_Fetch(&exc, &val, &tb);
243      bp::handle<> hexc(exc), hval(bp::allow_null(val)), htb(bp::allow_null(tb));
244      bp::object traceback(bp::import("traceback"));
245      if (!tb) {
246  	bp::object format_exception_only(traceback.attr("format_exception_only"));
247  	formatted_list = format_exception_only(hexc, hval);
248      } else {
249  	bp::object format_exception(traceback.attr("format_exception"));
250  	formatted_list = format_exception(hexc, hval, htb);
251      }
252      formatted = bp::str("\n").join(formatted_list);
253      return bp::extract<std::string>(formatted);
254  }
255  
256  int PythonPlugin::initialize()
257  {
258      std::string msg;
259      if (Py_IsInitialized()) {
260  	try {
261  	    bp::object module = bp::import("__main__");
262  	    main_namespace = module.attr("__dict__");
263  
264  	    for(unsigned i = 0; i < inittab_entries.size(); i++) {
265  		main_namespace[inittab_entries[i]] = bp::import(inittab_entries[i].c_str());
266  	    }
267  	    if (toplevel) // only execute a file if there's one configured.
268  		bp::object result = working_execfile(abs_path,
269  						  main_namespace,
270  						  main_namespace);
271  	    status = PLUGIN_OK;
272  	}
273  	catch (bp::error_already_set) {
274  	    if (PyErr_Occurred()) {
275  		exception_msg = handle_pyerror();
276  	    } else
277  		exception_msg = "unknown exception";
278  	    bp::handle_exception();
279  	    status = PLUGIN_INIT_EXCEPTION;
280  	    PyErr_Clear();
281  	}
282  	if (status == PLUGIN_INIT_EXCEPTION) {
283  	    logPP(-1, "initialize: module '%s' init failed: \n%s",
284  		  abs_path, exception_msg.c_str());
285  	}
286      } else {
287  	logPP(-1, "initialize: Plugin not initialized");
288  	status = PLUGIN_PYTHON_NOT_INITIALIZED;
289      }
290      return status;
291  }
292  
293  PythonPlugin::PythonPlugin(struct _inittab *inittab) :
294      status(0),
295      module_mtime(0),
296      reload_on_change(0),
297      toplevel(0),
298      abs_path(0),
299      log_level(0)
300  {
301      Py_SetProgramName((char *) abs_path);
302  
303      if ((inittab != NULL) &&
304  	PyImport_ExtendInittab(inittab)) {
305  	logPP(-1, "cant extend inittab");
306  	status = PLUGIN_INITTAB_FAILED;
307  	return;
308      }
309      Py_Initialize();
310      initialize();
311  }
312  
313  
314  int PythonPlugin::configure(const char *iniFilename,
315  			   const char *section) 
316  {
317      IniFile inifile;
318      const char *inistring;
319  
320      if (section == NULL) {
321  	logPP(1, "no section");
322  	status = PLUGIN_NO_SECTION;
323  	return status;
324      }
325      if ((iniFilename == NULL) &&
326  	((iniFilename = getenv("INI_FILE_NAME")) == NULL)) {
327  	logPP(-1, "no inifile");
328  	status = PLUGIN_NO_INIFILE;
329  	return status;
330      }
331      if (inifile.Open(iniFilename) == false) {
332            logPP(-1, "Unable to open inifile:%s:\n", iniFilename);
333  	  status = PLUGIN_BAD_INIFILE;
334  	  return status;
335      }
336  
337      char real_path[PATH_MAX];
338      if ((inistring = inifile.Find("TOPLEVEL", section)) != NULL) {
339  	toplevel = strstore(inistring);
340  
341  	if ((inistring = inifile.Find("RELOAD_ON_CHANGE", section)) != NULL)
342  	    reload_on_change = (atoi(inistring) > 0);
343  
344  	if (realpath(toplevel, real_path) == NULL) {
345  	    logPP(-1, "cant resolve path to '%s'", toplevel);
346  	    status = PLUGIN_BAD_PATH;
347  	    return status;
348  	}
349  	struct stat st;
350  	if (stat(real_path, &st)) {
351  	    logPP(1, "stat(%s) returns %s", real_path, strerror(errno));
352  	    status = PLUGIN_STAT_FAILED;
353  	    return status;
354  	}
355  	abs_path = strstore(real_path);
356  	module_mtime = st.st_mtime;      // record timestamp
357  
358      } else {
359          if (getcwd(real_path, PATH_MAX) == NULL) {
360              logPP(1, "path too long");
361              status = PLUGIN_PATH_TOO_LONG;
362              return status;
363          }
364  	abs_path = strstore(real_path);
365      }
366  
367      if ((inistring = inifile.Find("LOG_LEVEL", section)) != NULL)
368  	log_level = atoi(inistring);
369      else log_level = 0;
370  
371      char pycmd[PATH_MAX];
372      int n = 1;
373      int lineno;
374      while (NULL != (inistring = inifile.Find("PATH_PREPEND", "PYTHON",
375  					     n, &lineno))) {
376  	sprintf(pycmd, "import sys\nsys.path.insert(0,\"%s\")", inistring);
377  	logPP(1, "%s:%d: executing '%s'",iniFilename, lineno, pycmd);
378  
379  	if (PyRun_SimpleString(pycmd)) {
380  	    logPP(-1, "%s:%d: exception running '%s'",iniFilename, lineno, pycmd);
381  	    exception_msg = "exception running:" + std::string((const char*)pycmd);
382  	    status = PLUGIN_EXCEPTION_DURING_PATH_PREPEND;
383  	    return status;
384  	}
385  	n++;
386      }
387      n = 1;
388      while (NULL != (inistring = inifile.Find("PATH_APPEND", "PYTHON",
389  					     n, &lineno))) {
390  	sprintf(pycmd, "import sys\nsys.path.append(\"%s\")", inistring);
391  	logPP(1, "%s:%d: executing '%s'",iniFilename, lineno, pycmd);
392  	if (PyRun_SimpleString(pycmd)) {
393  	    logPP(-1, "%s:%d: exception running '%s'",iniFilename, lineno, pycmd);
394  	    exception_msg = "exception running " + std::string((const char*)pycmd);
395  	    status = PLUGIN_EXCEPTION_DURING_PATH_APPEND;
396  	    return status;
397  	}
398  	n++;
399      }
400      logPP(3,"PythonPlugin: Python  '%s'",  Py_GetVersion());
401      return initialize();
402  }
403  
404  // the externally visible singleton instance
405  PythonPlugin *python_plugin;
406  
407  
408  // first caller wins
409  // this splits instantiation from configuring PYTHONPATH, imports etc
410  PythonPlugin *PythonPlugin::instantiate(struct _inittab *inittab)
411  {
412      if (python_plugin == NULL) {
413  	python_plugin = new PythonPlugin(inittab);
414      }
415      return (python_plugin->usable()) ? python_plugin : NULL;
416  }
417  
418  
419  static const char *strstore(const char *s)
420  {
421      static std::set<std::string> stringtable;
422      using namespace std;
423  
424      if (s == NULL)
425          throw invalid_argument("strstore(): NULL argument");
426      pair< set<string>::iterator, bool > pair = stringtable.insert(s);
427      return pair.first->c_str();
428  }