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), но это действительно не похоже на хорошую идею.

Ответ 1

Вы правы в своем мышлении, чтобы использовать 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 rl_set_signals() все переменные, которые ему нужны, перед передачей сигнала в вашу программу, где вы затем перейдете к вызову readline во второй раз.

Ответ 3

Сначала я был смущен jancheta answer, пока не обнаружил, что целью 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().

Вот сообщение от Чет Рейми, в котором даются некоторые идеи о том, что делать, чтобы получить поведение 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.

Ответ 4

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

К счастью, 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();
}

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