Support interrupting nested IPyKernel

This commit is contained in:
Florian Märkl 2018-02-23 16:24:19 +01:00 committed by xarkes
parent 693fc1eb1f
commit ec55e40d5e
5 changed files with 85 additions and 16 deletions

View File

@ -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)

View File

@ -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)

View File

@ -96,6 +96,11 @@ void JupyterConnection::start()
if (!cutterJupyterModule)
{
createCutterJupyterModule();
if(!cutterJupyterModule)
{
return;
}
}
PyEval_RestoreThread(pyThreadState);

View File

@ -2,6 +2,7 @@
#include <Python.h>
#include <QFile>
#include <csignal>
#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;

View File

@ -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}
};