ilua
ilua copied to clipboard
Ctrl-C Interrupt Support
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:
- 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.
- 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.
- 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.
- 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)
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
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 ^\
).
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.
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.
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.