Readline:在SIGINT上获得一个新的提示。

时间:2021-05-28 20:50:01

I've got code similar to the following, using readline:

我有类似于下面的代码,使用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:

我将它设置为拦截SIGINT(即用户按Ctrl+C),这样我就可以知道信号处理程序handle_signals()正在工作。但是,当控件返回readline()时,它使用的是在输入之前使用的同一行文本。我希望发生的是readline“取消”当前文本行,并给我一个新的行,就像BASH shell一样。类似这样:

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.

有机会让它工作吗?我在邮件列表中读到过使用longjmp(2)的内容,但这似乎不是一个好主意。

4 个解决方案

#1


5  

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.

您使用longjmp的想法是正确的。但是因为longjmp将在一个信号处理程序中,所以需要使用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 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 ( sigsetjmp( ctrlc_buf, 1 ) != 0 );

  while (1) 
  {
    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.

siglongjmp返回一个非0的值(在本例中为1)给sigsetjmp,因此while循环再次调用sigsetjmp (sigsetjmp的成功返回值为0),然后再次调用readline。

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.

设置rl_catch_signals = 1,然后调用rl_set_signals(),以便readline信号处理在将信号传递给程序之前清理它需要的任何变量,然后再次跳转回调用readline。

#2


4  

Call rl_clear_signals().

调用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.

这将禁用已安装的信号处理程序libreadline。处理SIGINT的函数负责恢复提示符的行为。

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

关于如何管理readline()信号处理的更多细节可以在这里阅读。

#3


4  

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.

一开始我对jancheta的答案感到困惑,直到我发现siglongjmp的目的是在执行跳转之前,先解除信号掩码中的接收信号。信号在信号处理程序的入口被阻塞,因此处理程序不会中断自己。我们不想让信号在恢复正常执行时被阻塞,这就是为什么我们使用siglongjmp而不是longjmp。AIUI,这只是一种简写,我们也可以将sigprocmask后跟longjmp,这似乎是glibc在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.

我认为做跳转可能不安全,因为readline()调用malloc和free。如果在接收到信号的同时,malloc或free等不安全的异步信号函数正在修改全局状态,那么如果我们跳出信号处理程序,可能会导致一些损坏。但是Readline安装了自己的信号处理程序,这些处理程序对此非常小心。他们只是设置了一个标志然后退出;当Readline库再次获得控制(通常在中断的“read()”调用之后)时,它调用RL_CHECK_SIGNALS(),然后使用kill()将任何挂起的信号转发给客户端应用程序。因此,使用siglongjmp()来为中断对readline()的调用的信号退出一个信号处理程序是安全的——在异步信号不安全函数期间,保证没有收到信号。

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.

实际上,这并不完全正确,因为在rl_set_prompt()中有一些对malloc()和free()的调用,readline()在rl_set_signals()之前调用。我想知道是否应该改变这个呼叫顺序。无论如何,竞态条件的概率非常小。

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

我查看了Bash源代码,它似乎跳出了它的SIGINT处理程序。

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.

您可以使用的另一个Readline接口是回调接口。应用程序(如Python或R)需要同时监听多个文件描述符,例如,在命令行接口处于活动状态时,判断是否对plot窗口进行了调整。它们将在select()循环中执行此操作。

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:

这是来自Chet Ramey的一条消息,它给出了如何在回调接口中接收到SIGINT时获得类似bashint的行为的一些想法:

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

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.

当接收到SIGINT时,您可以设置一个标志,然后在select()循环中检查标志——因为select()调用将被带有errno==EINTR的信号中断。如果您发现已经设置了标记,则执行上面的代码。

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.

我的观点是Readline应该在它自己的SIGINT处理代码中运行类似上面的片段。目前或多或少地执行前两行,这就是为什么像增量搜索和键盘宏由C ^取消,但不清除。

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.

另一张海报上写着“调用rl_clear_signals()”这让我很困惑。我还没有尝试过,但是我不知道它是如何完成的,因为(1)Readline的信号处理程序无论如何都会将信号转发给您,(2)Readline()在进入时安装信号处理程序(并在它退出时清除它们),所以它们通常不会在Readline代码之外活动。

#4


1  

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.

创建一个跳转看起来很容易出错,而且容易出错。我添加这个支持的shell实现不允许这种更改。

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

幸运的是,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();
}

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

这就不需要其他代码了——没有全局变量,没有设置跳转。

#1


5  

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.

您使用longjmp的想法是正确的。但是因为longjmp将在一个信号处理程序中,所以需要使用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 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 ( sigsetjmp( ctrlc_buf, 1 ) != 0 );

  while (1) 
  {
    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.

siglongjmp返回一个非0的值(在本例中为1)给sigsetjmp,因此while循环再次调用sigsetjmp (sigsetjmp的成功返回值为0),然后再次调用readline。

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.

设置rl_catch_signals = 1,然后调用rl_set_signals(),以便readline信号处理在将信号传递给程序之前清理它需要的任何变量,然后再次跳转回调用readline。

#2


4  

Call rl_clear_signals().

调用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.

这将禁用已安装的信号处理程序libreadline。处理SIGINT的函数负责恢复提示符的行为。

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

关于如何管理readline()信号处理的更多细节可以在这里阅读。

#3


4  

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.

一开始我对jancheta的答案感到困惑,直到我发现siglongjmp的目的是在执行跳转之前,先解除信号掩码中的接收信号。信号在信号处理程序的入口被阻塞,因此处理程序不会中断自己。我们不想让信号在恢复正常执行时被阻塞,这就是为什么我们使用siglongjmp而不是longjmp。AIUI,这只是一种简写,我们也可以将sigprocmask后跟longjmp,这似乎是glibc在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.

我认为做跳转可能不安全,因为readline()调用malloc和free。如果在接收到信号的同时,malloc或free等不安全的异步信号函数正在修改全局状态,那么如果我们跳出信号处理程序,可能会导致一些损坏。但是Readline安装了自己的信号处理程序,这些处理程序对此非常小心。他们只是设置了一个标志然后退出;当Readline库再次获得控制(通常在中断的“read()”调用之后)时,它调用RL_CHECK_SIGNALS(),然后使用kill()将任何挂起的信号转发给客户端应用程序。因此,使用siglongjmp()来为中断对readline()的调用的信号退出一个信号处理程序是安全的——在异步信号不安全函数期间,保证没有收到信号。

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.

实际上,这并不完全正确,因为在rl_set_prompt()中有一些对malloc()和free()的调用,readline()在rl_set_signals()之前调用。我想知道是否应该改变这个呼叫顺序。无论如何,竞态条件的概率非常小。

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

我查看了Bash源代码,它似乎跳出了它的SIGINT处理程序。

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.

您可以使用的另一个Readline接口是回调接口。应用程序(如Python或R)需要同时监听多个文件描述符,例如,在命令行接口处于活动状态时,判断是否对plot窗口进行了调整。它们将在select()循环中执行此操作。

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:

这是来自Chet Ramey的一条消息,它给出了如何在回调接口中接收到SIGINT时获得类似bashint的行为的一些想法:

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

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.

当接收到SIGINT时,您可以设置一个标志,然后在select()循环中检查标志——因为select()调用将被带有errno==EINTR的信号中断。如果您发现已经设置了标记,则执行上面的代码。

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.

我的观点是Readline应该在它自己的SIGINT处理代码中运行类似上面的片段。目前或多或少地执行前两行,这就是为什么像增量搜索和键盘宏由C ^取消,但不清除。

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.

另一张海报上写着“调用rl_clear_signals()”这让我很困惑。我还没有尝试过,但是我不知道它是如何完成的,因为(1)Readline的信号处理程序无论如何都会将信号转发给您,(2)Readline()在进入时安装信号处理程序(并在它退出时清除它们),所以它们通常不会在Readline代码之外活动。

#4


1  

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.

创建一个跳转看起来很容易出错,而且容易出错。我添加这个支持的shell实现不允许这种更改。

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

幸运的是,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();
}

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

这就不需要其他代码了——没有全局变量,没有设置跳转。

相关文章