Readline: Get a new prompt on SIGINT

2019-02-12 07:47发布

问题:

I've got code similar to the following, using 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;
}

I've got it set up to intercept SIGINT (i.e. user pressing Ctrl+C), so I can tell that the signal handler handle_signals() is working. However, when control returns to readline(), it's using the same line of text it was using prior to the input. What I'd like to happen is for readline to "cancel" the current line of text and give me a new line, much like the BASH shell. Something like so:

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

Any chance of getting this to work? Something on a mailing list I read mentioned using longjmp(2), but that really doesn't seem like a good idea.

回答1:

You are correct in your line of thinking to use longjmp. But because the longjmp would be in a signal handler, you need to use sigsetjmp/siglongjmp.

As a quick example using your code as a base:

#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 returns a value other than 0 (in this case a 1) to sigsetjmp so the while loop calls sigsetjmp again (a successful return value of sigsetjmp is 0) and will then call readline again.

it may also be helpful to set rl_catch_signals = 1 and then call rl_set_signals() so that the readline signal handling cleans up any variables it needs to before passing the signal to your program where you will then jump back to call readline a second time.



回答2:

Call rl_clear_signals().

This will disable the signal handlers libreadline installed. The one that handles SIGINT is responsible for the observed behaviour of restoring the prompt.

More details on how to manage readline()s signal handling can be read here.



回答3:

I was confused at first by jancheta's answer, until I discovered that the purpose of siglongjmp is to unblock the received signal in the signal mask, before doing the jump. The signal is blocked at the entry of the signal handler so that the handler doesn't interrupt itself. We don't want to leave the signal blocked when we resume normal execution, and that's why we use siglongjmp instead of longjmp. AIUI, this is just shorthand, we could also call sigprocmask followed by longjmp, which seems to be what glibc is doing in siglongjmp.

I thought it might be unsafe to do a jump because readline() calls malloc and free. If the signal is received while some async-signal-unsafe function like malloc or free is modifying global state, some corruption could result if we were to then jump out of the signal handler. But Readline installs its own signal handlers which are careful about this. They just set a flag and exit; when the Readline library gets control again (usually after an interrupted 'read()' call) it calls RL_CHECK_SIGNALS() which then forwards any pending signal to the client application using kill(). So it is safe to use siglongjmp() to exit a signal handler for a signal which interrupted a call to readline() - the signal is guaranteed not to have been received during an async-signal-unsafe function.

Actually, that's not entirely true because there are a few calls to malloc() and free() within rl_set_prompt(), which readline() calls just before rl_set_signals(). I wonder if this calling order should be changed. In any case the probability of race condition is very slim.

I looked at the Bash source code and it seems to jump out of its SIGINT handler.

Another Readline interface you can use is the callback interface. That is used by applications such as Python or R which need to listen on multiple file descriptors at once, for instance to tell if a plot window is being resized while the command line interface is active. They'll do this in a select() loop.

Here is a message from Chet Ramey which gives some ideas of what to do to obtain Bash-like behavior upon receiving SIGINT in the callback interface:

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

The messages suggests that you do something like this:

    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");

When your SIGINT is received, you could set a flag, and later check the flag in your select() loop - since the select() call will get interrupted by the signal with errno==EINTR. If you find that the flag has been set, execute the above code.

My opinion is that Readline should run something like the above fragment in its own SIGINT handling code. Currently it more or less executes just the first two lines, which is why stuff like incremental-search and keyboard macros are cancelled by ^C, but the line isn't cleared.

Another poster said "Call rl_clear_signals()", which still confuses me. I haven't tried it but I don't see how it would accomplish anything given that (1) Readline's signal handlers forward the signal to you anyway, and (2) readline() installs the signal handlers upon entry (and clears them when it exits), so they won't normally be active outside of Readline code.



回答4:

Creating a jump seems hacky and error-prone to me. The shell implementation I was adding this support to didn't allow for this change.

Luckily, readlinehas a clearer, alternative solution. My SIGINT handler looks like this:

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();
}

This took no other additional code elsewhere to get this working — no global variables, no setting jumps.