From ec55e40d5e8b45609b48a268aaaceb9cd7c6bf75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Florian=20M=C3=A4rkl?= Date: Fri, 23 Feb 2018 16:24:19 +0100 Subject: [PATCH] Support interrupting nested IPyKernel --- src/python/cutter_ipykernel.py | 40 ++++++++++++++++++++++++++++----- src/python/cutter_jupyter.py | 19 ++++++++++------ src/utils/JupyterConnection.cpp | 5 +++++ src/utils/NestedIPyKernel.cpp | 18 ++++++++++++--- src/utils/PythonAPI.cpp | 19 ++++++++++++++++ 5 files changed, 85 insertions(+), 16 deletions(-) diff --git a/src/python/cutter_ipykernel.py b/src/python/cutter_ipykernel.py index 242feef3..aaf90535 100644 --- a/src/python/cutter_ipykernel.py +++ b/src/python/cutter_ipykernel.py @@ -1,42 +1,70 @@ import logging import threading +import signal +import cutter_internal 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 __init__(self, thread, app): + self._thread = thread + self._app = app + def send_signal(self, signum): - pass + if not self._thread.is_alive(): + return + + if signum == signal.SIGKILL or signum == signal.SIGTERM: + self._app.io_loop.stop() + elif signum == signal.SIGINT and self._app.kernel.interruptable: + self._app.log.debug("Sending KeyboardInterrupt to ioloop thread.") + cutter_internal.thread_set_async_exc(self._thread.ident, KeyboardInterrupt()) def poll(self): - return None + if self._thread.is_alive(): + return None + else: + return 0 class CutterIPythonKernel(IPythonKernel): + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.interruptable = False + def pre_handler_hook(self): + self.interruptable = True pass def post_handler_hook(self): + self.interruptable = False pass class CutterIPKernelApp(IPKernelApp): def init_signal(self): + # This would call signal.signal(signal.SIGINT, signal.SIG_IGN) + # Not needed in supinterpreter. + pass + + def log_connection_info(self): + # Just skip this. It would only pollute Cutter's output. pass def launch_ipykernel(argv): + app = CutterIPKernelApp.instance() + def run_kernel(): - app = CutterIPKernelApp.instance() app.kernel_class = CutterIPythonKernel - #app.log_level = logging.DEBUG + app.log_level = logging.DEBUG app.initialize(argv[3:]) app.start() thread = threading.Thread(target=run_kernel) thread.start() - return IPyKernelInterfaceKernel() + return IPyKernelInterfaceKernel(thread, app) diff --git a/src/python/cutter_jupyter.py b/src/python/cutter_jupyter.py index 967b452e..a07ed148 100644 --- a/src/python/cutter_jupyter.py +++ b/src/python/cutter_jupyter.py @@ -1,7 +1,8 @@ +import signal +import time from jupyter_client.ioloop import IOLoopKernelManager from notebook.notebookapp import * -import signal import cutter_internal @@ -10,6 +11,7 @@ class IPyKernelInterfaceJupyter: self._id = id def send_signal(self, signum): + print("sending signal " + str(signum) + " to kernel") cutter_internal.kernel_interface_send_signal(self._id, signum) def kill(self): @@ -22,17 +24,23 @@ class IPyKernelInterfaceJupyter: return cutter_internal.kernel_interface_poll(self._id) def wait(self, timeout=None): + if timeout is not None: + start_time = time.process_time() + else: + start_time = None + while timeout is None or time.process_time() - start_time < timeout: + if self.poll() is not None: + return + time.sleep(0.1) pass class CutterInternalIPyKernelManager(IOLoopKernelManager): def start_kernel(self, **kw): - # 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() @@ -45,9 +53,6 @@ class CutterInternalIPyKernelManager(IOLoopKernelManager): 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! id = cutter_internal.launch_ipykernel(kernel_cmd, env=env, **kw) self.kernel = IPyKernelInterfaceJupyter(id) # self._launch_kernel(kernel_cmd, env=env, **kw) diff --git a/src/utils/JupyterConnection.cpp b/src/utils/JupyterConnection.cpp index 5d3a67c8..a12fe045 100644 --- a/src/utils/JupyterConnection.cpp +++ b/src/utils/JupyterConnection.cpp @@ -96,6 +96,11 @@ void JupyterConnection::start() if (!cutterJupyterModule) { createCutterJupyterModule(); + + if(!cutterJupyterModule) + { + return; + } } PyEval_RestoreThread(pyThreadState); diff --git a/src/utils/NestedIPyKernel.cpp b/src/utils/NestedIPyKernel.cpp index 9e2bc9b5..c066a8ad 100644 --- a/src/utils/NestedIPyKernel.cpp +++ b/src/utils/NestedIPyKernel.cpp @@ -2,6 +2,7 @@ #include #include +#include #include "cutter.h" #include "NestedIPyKernel.h" @@ -66,7 +67,11 @@ NestedIPyKernel::~NestedIPyKernel() void NestedIPyKernel::sendSignal(long signum) { auto parentThreadState = PyThreadState_Swap(threadState); - PyObject_CallMethod(kernel, "send_signal", "l", signum); + auto ret = PyObject_CallMethod(kernel, "send_signal", "l", signum); + if (!ret) + { + PyErr_Print(); + } PyThreadState_Swap(parentThreadState); } @@ -75,9 +80,16 @@ QVariant NestedIPyKernel::poll() QVariant ret; auto parentThreadState = PyThreadState_Swap(threadState); PyObject *pyRet = PyObject_CallMethod(kernel, "poll", nullptr); - if(PyLong_Check(pyRet)) + if(pyRet) { - ret = (qlonglong)PyLong_AsLong(pyRet); + if(PyLong_Check(pyRet)) + { + ret = (qlonglong)PyLong_AsLong(pyRet); + } + } + else + { + PyErr_Print(); } PyThreadState_Swap(parentThreadState); return ret; diff --git a/src/utils/PythonAPI.cpp b/src/utils/PythonAPI.cpp index d9f0b708..4f41dfcc 100644 --- a/src/utils/PythonAPI.cpp +++ b/src/utils/PythonAPI.cpp @@ -123,11 +123,30 @@ PyObject *api_internal_kernel_interface_poll(PyObject *, PyObject *args) } } +PyObject *api_internal_thread_set_async_exc(PyObject *, PyObject *args) +{ + long id; + PyObject *exc; + + if (!PyArg_ParseTuple(args, "lO", &id, &exc)) + { + qWarning() << "Invalid args passed to api_internal_set_async_exc()."; + return nullptr; + } + + printf("setasyncexc %ld\n", id); + int ret = PyThreadState_SetAsyncExc(id, exc); + printf("%d threads affected\n", ret); + + return PyLong_FromLong(ret); +} + PyMethodDef CutterInternalMethods[] = { {"launch_ipykernel", (PyCFunction)api_internal_launch_ipykernel, METH_VARARGS | METH_KEYWORDS, "Launch an IPython Kernel in a subinterpreter"}, {"kernel_interface_send_signal", (PyCFunction)api_internal_kernel_interface_send_signal, METH_VARARGS, ""}, {"kernel_interface_poll", (PyCFunction)api_internal_kernel_interface_poll, METH_VARARGS, ""}, + {"thread_set_async_exc", (PyCFunction)api_internal_thread_set_async_exc, METH_VARARGS, ""}, {NULL, NULL, 0, NULL} };