Skip to content

Commit

Permalink
lua: fix SIGINT handling
Browse files Browse the repository at this point in the history
Tarantool console quits if you type Ctrl+C.
This patch fixes console behavior on sending SIGINT.
Console discards the current input on typing Ctrl+C
and invites user to the new line.

In daemon mode the process will exit after receiving SIGINT.

Test gh-2717 should be skipped on release build, cause it
uses error injection which is enabled only on debug mode.

Fixes #2717

@TarantoolBot document
Title: Use Ctrl+C to discard the input in console

The signal SIGINT discards the input in console mode.
When tarantool executes with -e flag or runs as a daemon, SIGINT
kills the process and tarantool complains about it in log.
  • Loading branch information
vr009 committed Mar 17, 2022
1 parent b99ac0b commit 4baadc8
Show file tree
Hide file tree
Showing 8 changed files with 310 additions and 7 deletions.
4 changes: 4 additions & 0 deletions changelogs/unreleased/gh-2717-noquit-on-sigint.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
## bugfix/lua

* Fixed the behavior of tarantool console on SIGINT. Now Ctrl+C discards
the current input and prints the new prompt (gh-2717).
49 changes: 49 additions & 0 deletions src/box/lua/console.c
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
#include "iostream.h"
#include "lua/msgpack.h"
#include "lua-yaml/lyaml.h"
#include "main.h"
#include "serialize_lua.h"
#include <lua.h>
#include <lauxlib.h>
Expand Down Expand Up @@ -226,13 +227,37 @@ console_push_line(char *line)
free(line);
}

static bool sigint_called;
/*
* The pointer to interactive fiber is needed to wake it up
* when SIGINT handler is called.
*/
static struct fiber *interactive_fb;

/*
* The sigint callback for console mode.
*/
static void
console_sigint_handler(ev_loop *loop, struct ev_signal *w, int revents)
{
(void)loop;
(void)w;
(void)revents;

sigint_called = true;
fiber_wakeup(interactive_fb);
}

/* implements readline() Lua API */
static int
lbox_console_readline(struct lua_State *L)
{
const char *prompt = NULL;
int top;
int completion = 0;
interactive_fb = fiber();
sigint_cb_t old_cb = set_sigint_cb(console_sigint_handler);
sigint_called = false;

if (lua_gettop(L) > 0) {
switch (lua_type(L, 1)) {
Expand Down Expand Up @@ -284,12 +309,35 @@ lbox_console_readline(struct lua_State *L)
while (top == lua_gettop(L)) {
while (coio_wait(STDIN_FILENO, COIO_READ,
TIMEOUT_INFINITY) == 0) {
if (sigint_called) {
const char *line_end = "^C\n";
ssize_t rc = write(STDOUT_FILENO, line_end,
strlen(line_end));
(void)rc;
/*
* Discard current input and disable search
* mode.
*/
RL_UNSETSTATE(RL_STATE_ISEARCH |
RL_STATE_NSEARCH |
RL_STATE_SEARCH);
rl_on_new_line();
rl_replace_line("", 0);
lua_pushstring(L, "");

readline_L = NULL;
sigint_called = false;
set_sigint_cb(old_cb);
return 1;
}
/*
* Make sure the user of interactive
* console has not hanged us, otherwise
* we might spin here forever eating
* the whole cpu time.
*/
if (fiber_is_cancelled())
set_sigint_cb(old_cb);
luaL_testcancel(L);
}
rl_callback_read_char();
Expand All @@ -299,6 +347,7 @@ lbox_console_readline(struct lua_State *L)
/* Incidents happen. */
#pragma GCC poison readline_L
rl_attempted_completion_function = NULL;
set_sigint_cb(old_cb);
luaL_testcancel(L);
return 1;
}
Expand Down
16 changes: 15 additions & 1 deletion src/main.cc
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,20 @@ signal_cb(ev_loop *loop, struct ev_signal *w, int revents)
tarantool_exit(0);
}

/*
* Handle SIGINT like SIGTERM by default, but allow to overwrite the behavior.
* Used by console.
*/
static sigint_cb_t signal_sigint_cb = signal_cb;

sigint_cb_t
set_sigint_cb(sigint_cb_t new_sigint_cb)
{
sigint_cb_t old_cb = signal_sigint_cb;
ev_set_cb(&ev_sigs[1], new_sigint_cb);
return old_cb;
}

static void
signal_sigwinch_cb(ev_loop *loop, struct ev_signal *w, int revents)
{
Expand Down Expand Up @@ -253,7 +267,7 @@ signal_init(void)
crash_signal_init();

ev_signal_init(&ev_sigs[0], sig_checkpoint, SIGUSR1);
ev_signal_init(&ev_sigs[1], signal_cb, SIGINT);
ev_signal_init(&ev_sigs[1], signal_sigint_cb, SIGINT);
ev_signal_init(&ev_sigs[2], signal_cb, SIGTERM);
ev_signal_init(&ev_sigs[3], signal_sigwinch_cb, SIGWINCH);
ev_signal_init(&ev_sigs[4], say_logrotate, SIGHUP);
Expand Down
15 changes: 15 additions & 0 deletions src/main.h
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,21 @@ tarantool_exit(int);
void
load_cfg(void);

/* The type of sigint callback's pointer. */
struct ev_loop;
struct ev_signal;
typedef void
(*sigint_cb_t)(struct ev_loop *loop, struct ev_signal *w, int revents);

/*
* This function is needed for setting the new sigint callback.
* Returns the pointer to the old callback function.
* The main scenario is to replace the current callback
* and having an opportunity to set the old one back.
*/
sigint_cb_t
set_sigint_cb(sigint_cb_t new_sigint_cb);

#if defined(__cplusplus)
} /* extern "C" */
#endif /* defined(__cplusplus) */
Expand Down
204 changes: 204 additions & 0 deletions test/app-tap/gh-2717-no-quit-sigint.test.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
#!/usr/bin/env tarantool

local tap = require('tap')
local popen = require('popen')
local process_timeout = require('process_timeout')
local fio = require('fio')
local clock = require('clock')
local fiber = require('fiber')

--
-- gh-2717: tarantool console quit on sigint.
--
local file_read_timeout = 60.0
local file_read_interval = 0.2
local file_open_timeout = 60.0
local prompt = 'tarantool> '

local TARANTOOL_PATH = arg[-1]
local test = tap.test('gh-2717-no-quit-sigint')

test:plan(6)
local cmd = 'ERRINJ_STDIN_ISATTY=1 ' .. TARANTOOL_PATH .. ' -i 2>&1'
local ph = popen.new({cmd}, {
shell = true,
setsid = true,
group_signal = true,
stdout = popen.opts.PIPE,
stderr = popen.opts.DEVNULL,
stdin = popen.opts.PIPE,
})
assert(ph, 'process is not up')

local start_time = clock.monotonic()
local time_quota = 10.0

local output = ''
while output:find(prompt) == nil
and clock.monotonic() - start_time < time_quota do
output = output .. ph:read({timeout = 1.0})
end
assert(clock.monotonic() - start_time < time_quota, 'time_quota is violated')
ph:signal(popen.signal.SIGINT)

while output:find(prompt .. '^C\n---\n...\n\n' .. prompt) == nil and
clock.monotonic() - start_time < time_quota do
local data = ph:read({timeout = 1.0})
if data ~= nil then
output = output .. data
end
end
assert(clock.monotonic() - start_time < time_quota, 'time_quota is violated')

test:unlike(ph:info().status.state, popen.state.EXITED,
'SIGINT doesn\'t kill tarantool in interactive mode')
test:like(output, prompt .. '^C\n---\n...\n\n' .. prompt,
'Ctrl+C discards the input and calls the new prompt')

ph:shutdown({stdin = true})
ph:close()

--
-- gh-2717: testing daemonized tarantool on signaling INT
-- and nested console output.
--
local log_file = fio.abspath('tarantool.log')
local pid_file = fio.abspath('tarantool.pid')
local xlog_file = fio.abspath('00000000000000000000.xlog')
local snap_file = fio.abspath('00000000000000000000.snap')
local sock = fio.abspath('tarantool.soc')

local user_grant = ' box.schema.user.grant(\'guest\', \'super\')'
local arg = ' -e \"box.cfg{pid_file=\''
.. pid_file .. '\', log=\'' .. log_file .. '\', listen=\'unix/:'
.. sock .. '\'}' .. user_grant .. '\"'

start_time = clock.monotonic()
time_quota = 10.0

os.remove(log_file)
os.remove(pid_file)
os.remove(xlog_file)
os.remove(snap_file)

local ph_server = popen.shell(TARANTOOL_PATH .. arg, 'r')

local f = process_timeout.open_with_timeout(log_file, file_open_timeout)
assert(f, 'error while opening ' .. log_file)

cmd = 'ERRINJ_STDIN_ISATTY=1 ' .. TARANTOOL_PATH .. ' -i 2>&1'
local ph_client = popen.new({cmd}, {
shell = true,
setsid = true,
group_signal = true,
stdout = popen.opts.PIPE,
stderr = popen.opts.DEVNULL,
stdin = popen.opts.PIPE,
})
assert(ph_client, 'the nested console is not up')

ph_client:write('require("console").connect(\'unix/:' .. sock .. '\')\n')

local client_data = ''
while string.endswith(client_data, 'unix/:' .. sock .. '>') == nil
and clock.monotonic() - start_time < time_quota do
local cur_data = ph_client:read({timeout = 3.0})
if cur_data ~= nil and cur_data ~= '' then
client_data = client_data .. cur_data
end
end
assert(clock.monotonic() - start_time < time_quota, 'time_quota is violated')

start_time = clock.monotonic()
ph_client:signal(popen.signal.SIGINT)
test:unlike(ph_client:info().status.state, popen.state.EXITED,
'SIGINT doesn\'t kill nested tarantool in interactive mode')
while string.endswith(client_data, 'C\n---\n...\n\nunix/:' .. sock .. '>') == nil
and clock.monotonic() - start_time < time_quota do
local cur_data = ph_client:read({timeout = 3.0})
if cur_data ~= nil and cur_data ~= '' then
client_data = client_data .. cur_data
end
end
assert(clock.monotonic() - start_time < time_quota, 'time_quota is violated')

ph_server:signal(popen.signal.SIGINT)

local status = ph_server:wait(nil, popen.signal.SIGINT)
test:unlike(status.state, popen.state.ALIVE,
'SIGINT terminates tarantool in daemon mode')

start_time = clock.monotonic()
local data = ''
while data:match('got signal 2') == nil
and clock.monotonic() - start_time < time_quota do
data = data .. process_timeout.read_with_timeout(f,
file_read_timeout,
file_read_interval)
end
assert(data:match('got signal 2'), 'there is no one note about SIGINT')
assert(clock.monotonic() - start_time < time_quota, 'time_quota is violated')

f:close()
ph_server:close()
ph_client:close()
os.remove(log_file)
os.remove(pid_file)
os.remove(xlog_file)
os.remove(snap_file)

--
-- Testing case when the client and instance are called in the same console.
--
cmd = 'ERRINJ_STDIN_ISATTY=1 ' .. TARANTOOL_PATH .. ' -i 2>&1'
ph = popen.new({cmd}, {
shell = true,
setsid = true,
group_signal = true,
stdout = popen.opts.PIPE,
stderr = popen.opts.DEVNULL,
stdin = popen.opts.PIPE,
})
assert(ph, 'process is not up')

start_time = clock.monotonic()
time_quota = 10.0

output = ''
while output:find(prompt) == nil
and clock.monotonic() - start_time < time_quota do
data = ph:read({timeout = 1.0})
if data ~= nil then
output = output .. data
end
end
assert(clock.monotonic() - start_time < time_quota, 'time_quota is violated')

prompt = 'unix/:' .. sock .. '> '
ph:write('_ = require(\'console\').listen(\'' .. sock .. '\')\n',
{timeout = 1.0})
ph:write('_ = require(\'console\').connect(\'' .. sock .. '\')\n',
{timeout = 1.0})

fiber.sleep(0.2)
ph:signal(popen.signal.SIGINT)

start_time = clock.monotonic()
while string.endswith(output, prompt .. '^C\n---\n...\n\n' .. prompt) == false
and clock.monotonic() - start_time < time_quota do
local data = ph:read({timeout = 1.0})
if data ~= nil then
output = output .. data
end
end
assert(clock.monotonic() - start_time < time_quota, 'time_quota is violated')

test:ok(string.endswith(output, prompt .. '^C\n---\n...\n\n' .. prompt),
'Ctrl+C discards the input and calls the new prompt')
test:unlike(ph:info().status.state, popen.state.EXITED,
'SIGINT doesn\'t kill tarantool in interactive mode')

ph:shutdown({stdin = true})
ph:close()

os.exit(test:check() and 0 or 1)
2 changes: 1 addition & 1 deletion test/app-tap/suite.ini
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ description = application server tests (TAP)
lua_libs = lua/require_mod.lua lua/serializer_test.lua lua/process_timeout.lua
is_parallel = True
use_unix_sockets_iproto = True
release_disabled = gh-5040-inter-mode-isatty-via-errinj.test.lua
release_disabled = gh-5040-inter-mode-isatty-via-errinj.test.lua gh-2717-no-quit-sigint.test.lua
fragile = {
"retries": 10,
"tests": {
Expand Down

0 comments on commit 4baadc8

Please sign in to comment.