16

我有类似于以下的代码,使用 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 shell 一样。像这样:

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

有没有机会让这个工作?我读到的邮件列表中提到了 using longjmp(2),但这似乎不是一个好主意。

4

4 回答 4

13

您使用 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 向 sigsetjmp 返回 0 以外的值(在本例中为 1),因此 while 循环再次调用 sigsetjmp(sigsetjmp 的成功返回值为 0),然后将再次调用 readline。

rl_catch_signals = 1设置然后调用也可能会有所帮助,rl_set_signals()以便 readline 信号处理在将信号传递给您的程序之前清理它需要的任何变量,然后您将跳回第二次调用 readline。

于 2013-06-11T01:02:06.400 回答
9

起初我对 jancheta 的回答感到困惑,直到我发现 的目的siglongjmp是在进行跳转之前解除信号掩码中接收到的信号的阻塞。信号在信号处理程序的入口处被阻塞,以便处理程序不会中断自己。当我们恢复正常执行时,我们不想让信号阻塞,这就是我们使用siglongjmp而不是longjmp. sigprocmaskAIUI ,这只是简写,我们也可以调用following,longjmp这似乎是glibc在做的siglongjmp

我认为进行跳转可能不安全,因为readline()调用mallocfree. 如果在某些异步信号不安全函数(如mallocfree正在修改全局状态)时接收到信号,如果我们随后跳出信号处理程序,可能会导致一些损坏。但是 Readline 安装了自己的信号处理程序,对此非常小心。他们只是设置一个标志并退出;当 Readline 库再次获得控制权时(通常在中断的 'read()' 调用之后),它会调用RL_CHECK_SIGNALS(),然后使用kill(). 因此,为siglongjmp()中断调用的信号退出信号处理程序是安全的readline()——保证在异步信号不安全函数期间不会收到信号。

实际上,这并不完全正确,因为有几个调用 tomalloc()free()within rl_set_prompt(),它们readline()调用了 before rl_set_signals()。我想知道这个调用顺序是否应该改变。无论如何,竞争条件的可能性非常小。

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

您可以使用的另一个 Readline 接口是回调接口。这由需要一次侦听多个文件描述符的应用程序(例如 Python 或 R)使用,例如在命令行界面处于活动状态时判断绘图窗口是否正在调整大小。他们会select()循环执行此操作。

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

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. 如果发现flag已经被设置,执行上面的代码。

我的观点是 Readline 应该在其自己的 SIGINT 处理代码中运行类似于上述片段的内容。目前它或多或少只执行前两行,这就是为什么像增量搜索和键盘宏这样的东西被 ^C 取消,但该行没有被清除。

另一张海报说“调用 rl_clear_signals()”,这仍然让我感到困惑。我没有尝试过,但我看不出它会如何完成任何事情,因为(1)Readline 的信号处理程序无论如何都会将信号转发给您,并且(2)readline()在进入时安装信号处理程序(并在退出时清除它们) ,因此它们通常不会在 Readline 代码之外处于活动状态。

于 2016-04-30T21:50:35.633 回答
7

对我来说,创建跳转似乎很笨拙且容易出错。我添加此支持的 shell 实现不允许进行此更改。

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

这不需要其他额外的代码来实现这个工作——没有全局变量,没有设置跳转。

于 2017-01-29T05:17:38.673 回答
4

打电话rl_clear_signals()

这将禁用libreadline已安装的信号处理程序。处理SIGINT者负责观察到的恢复提示的行为。

readline()可以在此处阅读有关如何管理信号处理的更多详细信息

于 2013-05-30T16:05:29.260 回答