From 693fc1eb1fc76d7bd65ab508a6f85d029c8308d3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Florian=20M=C3=A4rkl?= <info@florianmaerkl.de>
Date: Fri, 23 Feb 2018 13:04:53 +0100
Subject: [PATCH] IPyKernel signal and poll

---
 src/python/cutter_ipykernel.py  |  4 +--
 src/python/cutter_jupyter.py    | 35 +++++++++++++------------
 src/utils/JupyterConnection.cpp |  7 ++++-
 src/utils/NestedIPyKernel.cpp   | 20 ++++++++++++---
 src/utils/NestedIPyKernel.h     |  3 ++-
 src/utils/PythonAPI.cpp         | 45 +++++++++++++++++++++++++++++----
 6 files changed, 84 insertions(+), 30 deletions(-)

diff --git a/src/python/cutter_ipykernel.py b/src/python/cutter_ipykernel.py
index 9dcf0ad0..242feef3 100644
--- a/src/python/cutter_ipykernel.py
+++ b/src/python/cutter_ipykernel.py
@@ -7,8 +7,8 @@ from ipykernel.ipkernel import IPythonKernel
 
 # TODO: Make this behave like a Popen instance and pipe it to IPyKernelInterfaceJupyter!
 class IPyKernelInterfaceKernel:
-    def kill(self):
-        print("No!! Not into the pit! It burns!!!")
+    def send_signal(self, signum):
+        pass
 
     def poll(self):
         return None
diff --git a/src/python/cutter_jupyter.py b/src/python/cutter_jupyter.py
index f261b436..967b452e 100644
--- a/src/python/cutter_jupyter.py
+++ b/src/python/cutter_jupyter.py
@@ -1,34 +1,32 @@
 
 from jupyter_client.ioloop import IOLoopKernelManager
 from notebook.notebookapp import *
+import signal
+import cutter_internal
 
 
-# TODO: this must behave like a Popen instance and pipe to IPyKernelInterfaceKernel!
 class IPyKernelInterfaceJupyter:
     def __init__(self, id):
         self._id = id
 
+    def send_signal(self, signum):
+        cutter_internal.kernel_interface_send_signal(self._id, signum)
+
     def kill(self):
-        print("DIE!! " + str(self._id))
+        self.send_signal(signal.SIGKILL)
+
+    def terminate(self):
+        self.send_signal(signal.SIGTERM)
 
     def poll(self):
-        return None
+        return cutter_internal.kernel_interface_poll(self._id)
+
+    def wait(self, timeout=None):
+        pass
 
 
 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()
 
@@ -50,15 +48,16 @@ class CutterInternalIPyKernelManager(IOLoopKernelManager):
         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
         id = cutter_internal.launch_ipykernel(kernel_cmd, env=env, **kw)
         self.kernel = IPyKernelInterfaceJupyter(id)
-        # self._launch_kernel(kernel_cmd, env=env,
-        #                    **kw)
+        # self._launch_kernel(kernel_cmd, env=env, **kw)
 
         self.start_restarter()
         self._connect_control_socket()
 
+    def signal_kernel(self, signum):
+        self.kernel.send_signal(signum)
+
 
 def kernel_manager_factory(kernel_name, **kwargs):
     if kernel_name in {"python", "python2", "python3"}:
diff --git a/src/utils/JupyterConnection.cpp b/src/utils/JupyterConnection.cpp
index e727b5dc..5d3a67c8 100644
--- a/src/utils/JupyterConnection.cpp
+++ b/src/utils/JupyterConnection.cpp
@@ -144,5 +144,10 @@ long JupyterConnection::startNestedIPyKernel(const QStringList &argv)
 
 NestedIPyKernel *JupyterConnection::getNestedIPyKernel(long id)
 {
-    return kernels[id];
+    auto it = kernels.find(id);
+    if(it == kernels.end())
+    {
+        return nullptr;
+    }
+    return *it;
 }
diff --git a/src/utils/NestedIPyKernel.cpp b/src/utils/NestedIPyKernel.cpp
index cb97ce00..9e2bc9b5 100644
--- a/src/utils/NestedIPyKernel.cpp
+++ b/src/utils/NestedIPyKernel.cpp
@@ -45,6 +45,8 @@ NestedIPyKernel *NestedIPyKernel::start(const QStringList &argv)
 
 NestedIPyKernel::NestedIPyKernel(PyObject *cutterIPykernelModule, const QStringList &argv)
 {
+    threadState = PyThreadState_Get();
+
     auto launchFunc = PyObject_GetAttrString(cutterIPykernelModule, "launch_ipykernel");
 
     PyObject *argvListObject = PyList_New(argv.size());
@@ -59,12 +61,24 @@ NestedIPyKernel::NestedIPyKernel(PyObject *cutterIPykernelModule, const QStringL
 
 NestedIPyKernel::~NestedIPyKernel()
 {
-
 }
 
-void NestedIPyKernel::kill()
+void NestedIPyKernel::sendSignal(long signum)
 {
     auto parentThreadState = PyThreadState_Swap(threadState);
-    PyObject_CallMethod(kernel, "kill", nullptr);
+    PyObject_CallMethod(kernel, "send_signal", "l", signum);
     PyThreadState_Swap(parentThreadState);
 }
+
+QVariant NestedIPyKernel::poll()
+{
+    QVariant ret;
+    auto parentThreadState = PyThreadState_Swap(threadState);
+    PyObject *pyRet = PyObject_CallMethod(kernel, "poll", nullptr);
+    if(PyLong_Check(pyRet))
+    {
+        ret = (qlonglong)PyLong_AsLong(pyRet);
+    }
+    PyThreadState_Swap(parentThreadState);
+    return ret;
+}
\ No newline at end of file
diff --git a/src/utils/NestedIPyKernel.h b/src/utils/NestedIPyKernel.h
index 316b6bad..12751f87 100644
--- a/src/utils/NestedIPyKernel.h
+++ b/src/utils/NestedIPyKernel.h
@@ -17,7 +17,8 @@ public:
     static NestedIPyKernel *start(const QStringList &argv);
     ~NestedIPyKernel();
 
-    void kill();
+    void sendSignal(long signum);
+    QVariant poll();
 
 private:
     NestedIPyKernel(PyObject *cutterIPykernelModule, const QStringList &argv);
diff --git a/src/utils/PythonAPI.cpp b/src/utils/PythonAPI.cpp
index 3e1cd7c5..d9f0b708 100644
--- a/src/utils/PythonAPI.cpp
+++ b/src/utils/PythonAPI.cpp
@@ -74,25 +74,60 @@ PyObject *api_internal_launch_ipykernel(PyObject *self, PyObject *args, PyObject
     return PyLong_FromLong(id);
 }
 
-PyObject *api_internal_kernel_interface_kill(PyObject *, PyObject *args)
+PyObject *api_internal_kernel_interface_send_signal(PyObject *, PyObject *args)
+{
+    long id;
+    long signum;
+
+    if (!PyArg_ParseTuple(args, "ll", &id, &signum))
+    {
+        qWarning() << "Invalid args passed to api_internal_kernel_interface_send_signal().";
+        return nullptr;
+    }
+
+    NestedIPyKernel *kernel = Jupyter()->getNestedIPyKernel(id);
+    if(kernel)
+    {
+        kernel->sendSignal(signum);
+    }
+
+    Py_RETURN_NONE;
+}
+
+PyObject *api_internal_kernel_interface_poll(PyObject *, PyObject *args)
 {
     long id;
 
     if (!PyArg_ParseTuple(args, "l", &id))
     {
-        qWarning() << "Invalid args passed to api_internal_kernel_interface_kill().";
+        qWarning() << "Invalid args passed to api_internal_kernel_interface_poll().";
         return nullptr;
     }
 
-    Jupyter()->getNestedIPyKernel(id)->kill();
+    NestedIPyKernel *kernel = Jupyter()->getNestedIPyKernel(id);
+    if(!kernel)
+    {
+        return PyLong_FromLong(0);
+    }
 
-    Py_RETURN_NONE;
+    QVariant v = kernel->poll();
+    bool ok;
+    auto ret = static_cast<long>(v.toLongLong(&ok));
+    if(ok)
+    {
+        return PyLong_FromLong(ret);
+    }
+    else
+    {
+        Py_RETURN_NONE;
+    }
 }
 
 PyMethodDef CutterInternalMethods[] = {
     {"launch_ipykernel", (PyCFunction)api_internal_launch_ipykernel, METH_VARARGS | METH_KEYWORDS,
     "Launch an IPython Kernel in a subinterpreter"},
-    {"kernel_interface_kill", (PyCFunction)api_internal_kernel_interface_kill, METH_VARARGS, ""},
+    {"kernel_interface_send_signal", (PyCFunction)api_internal_kernel_interface_send_signal, METH_VARARGS, ""},
+    {"kernel_interface_poll", (PyCFunction)api_internal_kernel_interface_poll, METH_VARARGS, ""},
     {NULL, NULL, 0, NULL}
 };