Readline: получить новое приглашение на SIGINT

У меня есть код, похожий на следующий, используя readline:

#include <errno.h>
#include <error.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <readline/readline.h>
#include <readline/history.h>

void handle_signals(int signo) {
  if (signo == SIGINT) {
    printf("You pressed Ctrl+C\n");
  }
}

int main (int argc, char **argv)
{
   //printf("path is: %s\n", path_string);
  char * input;
  char * shell_prompt = "i-shell> ";
  if (signal(SIGINT, handle_signals) == SIG_ERR) {
    printf("failed to register interrupts with kernel\n");
  }

  //set up custom completer and associated data strucutres
  setup_readline();

  while (1) 
  {
    input = readline(shell_prompt);
    if (!input)
      break;
    add_history(input);

    //do something with the code
    execute_command(input);

  }  
  return 0;
}

Я настроил его на перехват SIGINT (т.е. пользователь нажимает Ctrl+C), поэтому я могу сказать, что обработчик сигнала handle_signals() работает. Однако, когда управление возвращается к readline(), оно использует ту же строку текста, что и до ввода. Я бы хотел, чтобы readline «отменил» текущую строку текста и дал мне новую строку, как в оболочке BASH. Что-то вроде этого:

i-shell> bad_command^C
i-shell> _

Есть ли шанс заставить это работать? Кое-что в списке рассылки, которое я читал, упоминало об использовании longjmp(2), но это действительно не кажется хорошей идеей.


person Vishal Kotcherlakota    schedule 30.05.2013    source источник


Ответы (4)


Вы правильно думаете, что используете longjmp. Но поскольку longjmp будет в обработчике сигнала, вам нужно использовать sigsetjmp/siglongjmp.

В качестве быстрого примера использования вашего кода в качестве базы:

#include <setjmp.h>
#include <errno.h>
#include <error.h>
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <readline/readline.h>
#include <readline/history.h>

sigjmp_buf ctrlc_buf;

void handle_signals(int signo) {
  if (signo == SIGINT) {
    printf("You pressed Ctrl+C\n");
    siglongjmp(ctrlc_buf, 1);
  }
}

int my_cmd_loop(int argc, char **argv)
{
   //printf("path is: %s\n", path_string);
  char * input;
  char * shell_prompt = "i-shell> ";
  if (signal(SIGINT, handle_signals) == SIG_ERR) {
    printf("failed to register interrupts with kernel\n");
  }

  //set up custom completer and associated data strucutres
  setup_readline();

  while (1) 
  {
    while ( sigsetjmp( ctrlc_buf, 1 ) != 0 );

    input = readline(shell_prompt);
    if (!input)
      break;
    add_history(input);

    //do something with the code
    execute_command(input);

  }  
  return 0;
}

siglongjmp возвращает значение, отличное от 0 (в данном случае 1), для sigsetjmp, поэтому цикл while снова вызывает sigsetjmp (успешное возвращаемое значение sigsetjmp равно 0), а затем снова вызывает readline.

также может быть полезно установить rl_catch_signals = 1, а затем вызвать rl_set_signals(), чтобы обработка сигнала readline очистила все необходимые переменные перед передачей сигнала в вашу программу, где вы затем вернетесь к вызову readline во второй раз.

person jancheta    schedule 11.06.2013
comment
Вы не можете безопасно вызывать printf из обработчика сигнала. - person pat; 18.04.2014

Сначала меня смутил ответ jancheta, пока я не обнаружил, что цель siglongjmp - разблокировать полученный сигнал в маске сигнала перед выполнением прыжка. Сигнал блокируется на входе обработчика сигнала, чтобы обработчик не прерывал себя. Мы не хотим оставлять сигнал заблокированным, когда мы возобновим нормальное выполнение, и поэтому мы используем siglongjmp вместо longjmp. AIUI, это просто сокращение, мы могли бы также вызвать sigprocmask, а затем longjmp, что, похоже, делает glibc в siglongjmp.

Я подумал, что прыгать может быть небезопасно, потому что readline() вызывает malloc и free. Если сигнал получен, когда какая-либо небезопасная для асинхронного сигнала функция, такая как malloc или free, изменяет глобальное состояние, это может привести к некоторому повреждению, если мы затем выскочим из обработчика сигнала. Но Readline устанавливает свои собственные обработчики сигналов, которые заботятся об этом. Они просто устанавливают флаг и уходят; когда библиотека Readline снова получает управление (обычно после прерванного вызова read()), она вызывает RL_CHECK_SIGNALS(), который затем пересылает любой ожидающий сигнал клиентскому приложению, используя kill(). Таким образом, безопасно использовать siglongjmp() для выхода из обработчика сигнала, который прервал вызов readline() - сигнал гарантированно не был получен во время функции, небезопасной для асинхронного сигнала.

На самом деле это не совсем так, потому что есть несколько вызовов malloc() и free() внутри rl_set_prompt(), который readline() вызывает непосредственно перед rl_set_signals(). Интересно, следует ли изменить этот порядок вызова? В любом случае вероятность состояния гонки очень мала.

Я посмотрел на исходный код Bash, и он, кажется, выпрыгивает из обработчика SIGINT.

Другой интерфейс Readline, который вы можете использовать, — это интерфейс обратного вызова. Это используется такими приложениями, как Python или R, которым необходимо одновременно прослушивать несколько файловых дескрипторов, например, чтобы узнать, изменяется ли размер окна графика, когда активен интерфейс командной строки. Они сделают это в цикле select().

Вот сообщение от Chet Ramey, которое дает некоторые идеи о том, что нужно сделать, чтобы получить поведение, подобное Bash, при получении сигнала SIGINT в интерфейсе обратного вызова:

https://lists.gnu.org/archive/html/bug-readline/2016-04/msg00071.html

В сообщениях предлагается сделать что-то вроде этого:

    rl_free_line_state ();
    rl_cleanup_after_signal ();
    RL_UNSETSTATE(RL_STATE_ISEARCH|RL_STATE_NSEARCH|RL_STATE_VIMOTION|RL_STATE_NUMERICARG|RL_STATE_MULTIKEY);
    rl_line_buffer[rl_point = rl_end = rl_mark = 0] = 0;
    printf("\n");

Когда ваш SIGINT получен, вы можете установить флаг, а позже проверить флаг в цикле select(), поскольку вызов select() будет прерван сигналом с errno==EINTR. Если вы обнаружите, что флаг установлен, выполните приведенный выше код.

Я считаю, что Readline должен запускать что-то вроде приведенного выше фрагмента в своем собственном коде обработки SIGINT. В настоящее время он более или менее выполняет только первые две строки, поэтому такие вещи, как добавочный поиск и макросы клавиатуры, отменяются с помощью ^C, но строка не очищается.

Другой постер гласил: «Вызовите rl_clear_signals()», что до сих пор меня смущает. Я не пробовал, но не вижу, как это может что-то сделать, учитывая, что (1) обработчики сигналов Readline все равно пересылают сигнал вам и (2) readline() устанавливает обработчики сигналов при входе (и очищает их при выходе). ), поэтому обычно они не будут активны вне кода Readline.

person Metamorphic    schedule 30.04.2016

Создание прыжка кажется мне хакерским и подверженным ошибкам. Реализация оболочки, в которую я добавлял эту поддержку, не допускала этого изменения.

К счастью, у readline есть более четкое альтернативное решение. Мой обработчик SIGINT выглядит так:

static void
int_handler(int status) {
    printf("\n"); // Move to a new line
    rl_on_new_line(); // Regenerate the prompt on a newline
    rl_replace_line("", 0); // Clear the previous text
    rl_redisplay();
}

Для того, чтобы это заработало, не требовалось никакого другого дополнительного кода — никаких глобальных переменных, никаких переходов к настройкам.

person James Taylor    schedule 29.01.2017

Звоните rl_clear_signals().

Это отключит установленные обработчики сигналов libreadline. Тот, который обрабатывает SIGINT, отвечает за наблюдаемое поведение восстановления подсказки.

Подробнее о том, как управлять обработкой сигнала readline()s, можно прочитать здесь.

person alk    schedule 30.05.2013