From b9b903052c63de173b481d5eb86837adc6b80007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Wed, 21 Feb 2018 17:37:46 +0100 Subject: [PATCH] Run IPyKernel in subinterpreter and thus expose Cutter bindings to it --- src/python/cutter_ipykernel.py | 39 +++++++++++++++ src/python/cutter_jupyter.py | 74 ++++++++++++++++++++++++--- src/resources.qrc | 1 + src/utils/JupyterConnection.cpp | 1 + src/utils/PythonAPI.cpp | 89 ++++++++++++++++++++++++++++++++- src/utils/PythonAPI.h | 2 + 6 files changed, 198 insertions(+), 8 deletions(-) create mode 100644 src/python/cutter_ipykernel.py diff --git a/src/python/cutter_ipykernel.py b/src/python/cutter_ipykernel.py new file mode 100644 index 00000000..dace563c --- /dev/null +++ b/src/python/cutter_ipykernel.py @@ -0,0 +1,39 @@ + +import logging +import threading +from ipykernel.kernelapp import IPKernelApp +from ipykernel.ipkernel import IPythonKernel + + +# TODO: Make this behave like a Popen instance and pipe it to IPyKernelInterfaceJupyter! +class IPyKernelInterfaceKernel: + def poll(self): + return None + + +class CutterIPythonKernel(IPythonKernel): + def pre_handler_hook(self): + pass + + def post_handler_hook(self): + pass + + +class CutterIPKernelApp(IPKernelApp): + def init_signal(self): + pass + + +def launch_ipykernel(argv): + def run_kernel(): + app = CutterIPKernelApp.instance() + app.kernel_class = CutterIPythonKernel + app.log_level = logging.DEBUG + app.initialize(argv[3:]) + app.start() + + thread = threading.Thread(target=run_kernel) + thread.start() + + return IPyKernelInterfaceKernel() + diff --git a/src/python/cutter_jupyter.py b/src/python/cutter_jupyter.py index a1f92c22..5bd01647 100644 --- a/src/python/cutter_jupyter.py +++ b/src/python/cutter_jupyter.py @@ -1,8 +1,64 @@ -import sys -import threading -import time + +from jupyter_client.ioloop import IOLoopKernelManager from notebook.notebookapp import * -import cutter + + +# TODO: this must behave like a Popen instance and pipe to IPyKernelInterfaceKernel! +class IPyKernelInterfaceJupyter: + def poll(self): + return None + + +class CutterInternalIPyKernelManager(IOLoopKernelManager): + def start_kernel(self, **kw): + """Starts a kernel on this host in a separate process. + + If random ports (port=0) are being used, this method must be called + before the channels are created. + + Parameters + ---------- + `**kw` : optional + keyword arguments that are passed down to build the kernel_cmd + and launching the kernel (e.g. Popen kwargs). + """ + + # write connection file / get default ports + self.write_connection_file() + + # save kwargs for use in restart + self._launch_args = kw.copy() + # build the Popen cmd + extra_arguments = kw.pop('extra_arguments', []) + kernel_cmd = self.format_kernel_cmd(extra_arguments=extra_arguments) + env = kw.pop('env', os.environ).copy() + # Don't allow PYTHONEXECUTABLE to be passed to kernel process. + # If set, it can bork all the things. + env.pop('PYTHONEXECUTABLE', None) + if not self.kernel_cmd: + # If kernel_cmd has been set manually, don't refer to a kernel spec + # Environment variables from kernel spec are added to os.environ + env.update(self.kernel_spec.env or {}) + + # launch the kernel subprocess + self.log.debug("Starting kernel: %s", kernel_cmd) + + # TODO: kernel_cmd including python executable and so on is currently used for argv. Make a clean version! + import cutter_internal + cutter_internal.launch_ipykernel(kernel_cmd, env=env, **kw) + self.kernel = IPyKernelInterfaceJupyter() + # self._launch_kernel(kernel_cmd, env=env, + # **kw) + + self.start_restarter() + self._connect_control_socket() + + +def kernel_manager_factory(kernel_name, **kwargs): + if kernel_name in {"python", "python2", "python3"}: + return CutterInternalIPyKernelManager(kernel_name=kernel_name, **kwargs) + else: + return IOLoopKernelManager(kernel_name=kernel_name, **kwargs) class CutterNotebookApp(NotebookApp): @@ -13,6 +69,8 @@ class CutterNotebookApp(NotebookApp): def start(self): """ see NotebookApp.start() """ + self.kernel_manager.kernel_manager_factory = kernel_manager_factory + super(NotebookApp, self).start() self.write_server_info_file() @@ -48,7 +106,9 @@ def start_jupyter(): app = CutterNotebookApp() app.initialize() app.start() - print('TODO: Export cutter bindings to any kernel') - print(cutter.version()) - print(cutter.cmd('?e That is executed from radare2')) return app + + +if __name__ == "__main__": + app = start_jupyter() + print("Started " + app.url_with_token) diff --git a/src/resources.qrc b/src/resources.qrc index 5532f223..d7596bdd 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -62,5 +62,6 @@ img/cutter.svg python/cutter_jupyter.py + python/cutter_ipykernel.py diff --git a/src/utils/JupyterConnection.cpp b/src/utils/JupyterConnection.cpp index 70f16baa..7785c0cc 100644 --- a/src/utils/JupyterConnection.cpp +++ b/src/utils/JupyterConnection.cpp @@ -32,6 +32,7 @@ JupyterConnection::~JupyterConnection() void JupyterConnection::start() { PyImport_AppendInittab("cutter", &PyInit_api); + PyImport_AppendInittab("cutter_internal", &PyInit_api_internal); Py_Initialize(); PyEval_InitThreads(); diff --git a/src/utils/PythonAPI.cpp b/src/utils/PythonAPI.cpp index 45526018..576b2914 100644 --- a/src/utils/PythonAPI.cpp +++ b/src/utils/PythonAPI.cpp @@ -1,6 +1,9 @@ + #include "PythonAPI.h" #include "cutter.h" +#include + /* Return the number of arguments of the application command line */ PyObject *api_version(PyObject *self, PyObject *null) { @@ -34,8 +37,92 @@ PyModuleDef CutterModule = { NULL, NULL, NULL, NULL }; - PyObject *PyInit_api() { return PyModule_Create(&CutterModule); } + + +// ----------------------------- + + +PyObject *api_internal_launch_ipykernel(PyObject *self, PyObject *args, PyObject *kw) +{ + QStringList argv; + + { + PyObject *argvListObject; + + if (!PyArg_ParseTuple(args, "O", &argvListObject) + || !PyList_Check(argvListObject)) + { + qWarning() << "Invalid args passed to api_internal_launch_ipykernel()."; + return nullptr; + } + + for (int i=0; i + PyObject *PyInit_api(); +PyObject *PyInit_api_internal(); #endif // PYTHONAPI_H