Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lua: avoid tarantool console quit if sigint signal sending #6633

Merged
merged 1 commit into from
Mar 17, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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)
locker marked this conversation as resolved.
Show resolved Hide resolved
{
(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);
Totktonada marked this conversation as resolved.
Show resolved Hide resolved
}
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);
locker marked this conversation as resolved.
Show resolved Hide resolved

#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
ligurio marked this conversation as resolved.
Show resolved Hide resolved
Totktonada marked this conversation as resolved.
Show resolved Hide resolved
locker marked this conversation as resolved.
Show resolved Hide resolved

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
ligurio marked this conversation as resolved.
Show resolved Hide resolved
fragile = {
"retries": 10,
"tests": {
Expand Down