guysv / ilua

Portable Lua kernel for Jupyter
GNU General Public License v2.0
115 stars 11 forks source link

Ctrl-C Interrupt Support #1

Open guysv opened 5 years ago

guysv commented 5 years ago

This is a tricky feature to implement because of the fact that the popular lua consoles (lua 5.3-5.1 & luajit) implement SIGINTs poorly

For example, lets take the reference implementation lua 5.3. here is the SIGINT handler:

static void laction (int i) {
  signal(i, SIG_DFL); /* if another SIGINT happens, terminate process */
  lua_sethook(globalL, lstop, LUA_MASKCALL | LUA_MASKRET | LUA_MASKCOUNT, 1);
}

The handler sets the default handler (which crashes) upon activation. Because we run in script mode (to execute ilua's interp.lua) that handler is only installed once. Because of that, If the same session will handle two SIGINTs, it will crash.

A secondary problem is that there are many lua consoles who are not even handling SIGINTs, and crash upon the signal.

There are couple of solutions I could think of:

  1. Run in console mode

Pipe the interp.script into the host lua stdin in interactive mode (after getting rid of the prompts) e.g.

lua -i -e "_PROMPT='' _PROMPT2=''" < interp.lua

This way the lua-error-throwing SIGINT handler is installed after every evaluation, and multiple signals could be caught

The down side is that we should not allow the user to read from stdin (io.input:read) because that will mess things up.

  1. Serialize the session

We could track every evaluation request, serialize a state in the python kernel, and on SIGINT kill the lua host, launch a new one and dump the state back. Not sure this is possible though.

  1. Proxy lua host's signal()

If we could replace signal() with a function that NO-OPs on SIG_DFL, we could use the lua-error-throwing handler multiple times i.e. Inject the lua host some library like this

void *__real_signal(int sig, void *func);

void *__wrap_signal(int sig, void *func)
{
    void *old_handler = __real_signal(sig, SIG_DFL);
    __real_signal(sig, old_handler);
    if (SIGINT == sig && SIG_DFL == func)
    {
        return old_handler;
    }
    else
    {
        return __real_signal(sig, func);
    }
}

The down-side of course is that implementing DLL injection for every platform (Windows especially) is hell.

  1. Not implementing Ctrl-C at all

We could call this a known issue, and stop bothering about it. If we were to offer a solution anyway, we could distribute a patched lua.c (that won't reinstall default interrupt upon signal)

guysv commented 5 years ago

The plot thickens:

In Windows, Ctrl-C actually creates a new thread for the handler That means that even if we could interrupt the lua interpreter with the lua-error-throwing signal handler, we will still not be able to interrupt native code.

This problem also meets other languages For example, try running this code in a python interpreter in windows:

import threading
threading.Event().wait()

Now try to ctrl-c: you can't.

EDIT: actually, this is a better explaination of why windows ctrl c handling is wierd: https://mail.python.org/pipermail/python-dev/2017-August/148800.html

hroncok commented 4 years ago

This is extremely useful for bad interpreters:

$ ilua -i true
Jupyter console 6.1.0

ILua 0.2.1
In [1]: a2020-05-11T15:13:42+0200 [ilua.kernel.ILuaKernel#critical] Uncought exception in message handler                                                                                                                        
    Traceback (most recent call last):
      File "/usr/lib64/python3.8/site-packages/twisted/internet/defer.py", line 1418, in _inlineCallbacks
        result = g.send(result)
      File "/usr/lib/python3.8/site-packages/ilua/kernelbase.py", line 196, in handle_message
        content = yield self.do_is_complete(**msg['content'])
      File "/usr/lib64/python3.8/site-packages/twisted/internet/defer.py", line 1613, in unwindGenerator
        return _cancellableInlineCallbacks(gen)
      File "/usr/lib64/python3.8/site-packages/twisted/internet/defer.py", line 1529, in _cancellableInlineCallbacks
        _inlineCallbacks(None, g, status)
    --- <exception caught here> ---
      File "/usr/lib/python3.8/site-packages/ilua/kernelbase.py", line 196, in handle_message
        content = yield self.do_is_complete(**msg['content'])
      File "/usr/lib64/python3.8/site-packages/twisted/internet/defer.py", line 1418, in _inlineCallbacks
        result = g.send(result)
      File "/usr/lib/python3.8/site-packages/ilua/kernel.py", line 179, in do_is_complete
        result = yield self.proto.sendRequest({"type": "is_complete",
    builtins.AttributeError: 'ILuaKernel' object has no attribute 'proto'

In [1]: a                                                                                                                                                                                                                        
/usr/lib/python3.8/site-packages/jupyter_console/ptshell.py:656: UserWarning: The kernel did not respond to an is_complete_request. Setting `use_kernel_is_complete` to False.
  warn('The kernel did not respond to an is_complete_request. '
2020-05-11T15:13:43+0200 [ilua.kernel.ILuaKernel#critical] Uncought exception in message handler
    Traceback (most recent call last):
      File "/usr/lib64/python3.8/site-packages/twisted/internet/defer.py", line 1418, in _inlineCallbacks
        result = g.send(result)
      File "/usr/lib/python3.8/site-packages/ilua/kernelbase.py", line 193, in handle_message
        content = yield self.do_execute(**msg['content'])
      File "/usr/lib64/python3.8/site-packages/twisted/internet/defer.py", line 1613, in unwindGenerator
        return _cancellableInlineCallbacks(gen)
      File "/usr/lib64/python3.8/site-packages/twisted/internet/defer.py", line 1529, in _cancellableInlineCallbacks
        _inlineCallbacks(None, g, status)
    --- <exception caught here> ---
      File "/usr/lib/python3.8/site-packages/ilua/kernelbase.py", line 193, in handle_message
        content = yield self.do_execute(**msg['content'])
      File "/usr/lib64/python3.8/site-packages/twisted/internet/defer.py", line 1418, in _inlineCallbacks
        result = g.send(result)
      File "/usr/lib/python3.8/site-packages/ilua/kernel.py", line 126, in do_execute
        result = yield self.proto.sendRequest({"type": "execute",
    builtins.AttributeError: 'ILuaKernel' object has no attribute 'proto'

^C2020-05-11T15:13:47+0200 [ilua.kernel.ILuaKernel#warn] ILua does not support keyboard interrupts

There seem to be no way out (except ^\).

guysv commented 4 years ago

Hmm. I guess we could try to kill/respawn the interpreter on a second ctrl-c.
It's hardly a fix to the problem, but at least it's more user-friendly. I'll give it a thought.

rhaberkorn commented 8 months ago

Some kind of SIGINT-killing at least on Linux would be very much appreciated. Even if the interpreter dies afterwards, this is still better than not being able to interrupt at all.

rhaberkorn commented 8 months ago

I can just tweak the following in kernel.py:

def do_interrupt(self):
    self.lua_process.signalProcess("INT")
    return {'status': 'ok'}

It appears we need to return a status after reading https://jupyter-client.readthedocs.io/en/latest/messaging.html#msging-interrupt Although it's apparently not critical.

This does work on Linux with interpreters respecting SIGINT. But it does leave the cell busy forever and the Lua error is never displayed. I can confirm that a Lua error is raised after the interruption and it still gets processed in ILuaKernel.do_execute(). I suggest that the request_interrupt message just gets in the way. Not sure how to send the reply_interrupt message only after the execution response.

So instead, I just set interrupt_mode to signal in kernel.json and ignore SIGINT via

signal.signal(signal.SIGINT, signal.SIG_IGN)

The signal still gets delivered to the child processes and thus to the Lua interpreter. This will allow loops to be interrupted properly, including displaying the Lua error. Cells will be idle after interruption. At least when running from the notebook (Web UI). Pressing CTRL+C in the Jupyter console apparently does not deliver the signal to the ILua kernel.

I am also not sure whether this change would be harmless on Windows.