【问题标题】:How to get cursor position in C when another thread is reading stdin当另一个线程正在读取标准输入时如何在 C 中获取光标位置
【发布时间】:2019-01-13 02:15:48
【问题描述】:

我正在开发一个 Linux (centOS7) 上的控制台 C/C++ 程序,其中一些信息需要显示在终端屏幕的顶部。当主线程处理标准输入时,另一个线程处理回调并在标准输出上显示状态。为避免破坏,回调状态仅显示在屏幕顶部,但必须将光标返回到原始位置。

我尝试了ANSI save/restore cursor,但它不像链接中指出的那样工作。虽然this stackoverflow solution 在单线程中工作,但它在多线程中不起作用,因为两个线程都会读取标准输入。我尝试了几种方法在获取当前光标位置时暂时禁用标准输入,但都失败了:

  • 在 termios.c_cflag 中禁用 CREAD -- tcsetattr() 返回错误(无效参数)
  • tcflow(TCIOFF)
  • dup()

我知道 ncurses 可以工作,但在我的应用程序中,我需要用 ncurses 包装器替换太多的 stdio 函数。有谁知道如何在一个线程正在读取标准输入的多线程环境中保存/恢复光标位置或获取当前位置?

【问题讨论】:

    标签: c linux terminal


    【解决方案1】:

    我知道 ncurses 可以工作,但在我的应用程序中,我需要用 ncurses 包装器替换的 stdio 函数太多。

    因此,您对解决问题不感兴趣,只是对解决问题感兴趣。您可以尝试的一种方法是

        flockfile(stdin);
        flockfile(stdout);
        flockfile(stderr);
        /* Write ("\033[6n") to standard input,
           and read the ("\033[" row ";" column "R") response */
        funlockfile(stderr);
        funlockfile(stdout);
        funlockfile(stdin);
    

    详情请参阅man 3 flockfile()。我们的想法是为所有三个标准流获取 C 库内部锁,这样任何其他对其执行 I/O 的线程都会阻塞,直到我们在该流上调用 funlockfile()

    这不会以任何方式影响到 STDIN_FILENO、STDOUT_FILENO 或 STDERR_FILENO 的低级 I/O。


    来自 rici 的评论让我意识到实际上有一种方法不涉及重写原始代码。

    使用辅助进程(或线程)处理标准输入、标准输出、标准错误和终端的所有 I/O。

    基本上,在程序的一开始,您就构建了三个管道和一个 Unix 域数据报套接字对,并创建了帮助程序。

    (如果使用辅助进程,可以将其做成外部可执行文件,使用ncurses编写,不影响父程序。)

    帮助程序通过管道和套接字对连接到父进程。 Parent 将 STDIN_FILENOSTDOUT_FILENOSTDERR_FILENO 描述符替换为管道末端(关闭它们各自的原始描述符)。这样,它只能从帮助程序中读取,并写入帮助程序,而不是直接写入原始流。

    Unix 域数据报套接字对允许父级从帮助程序查询当前光标位置(并且可能执行其他类似操作)。

    帮助器从两个父管道和原始标准输入中读取,并写入父管道之一和原始标准输出和错误。我个人会让辅助管道结束非阻塞,并使用select(),所以单线程辅助就足够了。

    【讨论】:

    • 如果您尝试flockfile(stdin) 并且有一个fread(或其他输入请求)在stdin 上挂起,我希望flockfile 阻止,而不是以某种方式抢占挂起的输入请求。
    • @rici:是的;还有一个输入在上述操作之前到达的问题,在这种情况下,read() 读取的不是终端响应,而是输入。如果不将整个 I/O 大修到标准输入,就无法避免这种情况。我只是懒得说明所有这些,因为 OP 对适当的解决方案不感兴趣,只对他们可以逃脱的最简单的创可贴感兴趣。由于它们规定的局限性,不存在真正的解决方案。我想知道删除这个答案是否会更好?事实上,它可能会误导其他人编写糟糕、脆弱的代码?
    • 实际上,一个实际的解决方案可能有效:在程序开始时,派生一个辅助线程,并将标准输入、输出和错误重定向到该线程。让该线程处理 I/O,包括解析终端响应。它做了很多额外的工作,但它避免了编辑现有代码。
    • 这当然是我会采用的解决方案。如果 helper 使用 ncurses 来维护一个状态面板和独立的控制台面板,那么甚至没有必要搞乱 cursir 查询。
    • 当然我有兴趣解决这个问题,但实际上有一些困难。由于有很多我不熟悉的遗留代码,我想对现有代码进行最小的更改。现有代码使用 readline 和 ansi 转义字符串来实现漂亮的终端显示,这一切都使 dup() 和 pipe() 损坏..
    【解决方案2】:

    在评论中,您提到了这一点

    现有代码使用 readline 和 ansi 转义字符串来实现漂亮的终端显示...

    您确实应该将这些信息放入问题中,因为它非常重要。

    您的代码库使用readline 的事实严重限制了您的可能性,因为readline 实际上不适用于ncurses。为了将程序转换为使用ncurses,您需要重新创建您所依赖的readline 的那些功能。可能有其他库可以帮助解决这个问题,但我不知道。

    另一方面,ncurses 能够将屏幕划分为不重叠的区域,并独立滚动这些区域。这正是您想要将状态消息保留在状态行中的应用程序所需要的。从大约十年前发布的 5.7 版开始,ncurses 具有原始线程支持(无论如何在 Linux 上),并且可以将不同的“窗口”(屏幕区域)分配给不同的线程。 man curs_threads 提供了一些信息。

    Ncurses 还提供易于使用的界面,可以替代控制台控制序列的使用。

    所以这可能是长期的解决方案,但这将是相当多的工作。与此同时,几乎不可能通过使用readline 库中内置的功能来做你想做的事。或者至少我能够编写一个概念验证,它在接受来自 readline 的用户输入的同时成功地维护了一个状态行。此解决方案的导入方面是 readline (几乎)始终处于活动状态;也就是说,有一个线程在调用 readline 并将缓冲区读取传递给处理线程的硬循环中。 (在我的 POC 实现中,如果调用 readline 的线程也处理输入并且输入处理需要大量时间,那么在进行输入处理时状态行将不会更新。)

    关键是rl_event_hook函数,它在等待输入时被readline周期性调用(大约每秒10次)。我对rl_event_hook 的实现如下所示:

    /* This function is never called directly. Enable it by setting:
     *     rl_event_hook = event_hook
     * before the first call to readline.
     */
    int event_hook(void) {
      char* msg = NULL;
      pthread_mutex_lock(&status_mutex_);
      if (status_line_) {
        msg = status_line_;
        status_line_ = NULL;
      }
      pthread_mutex_unlock(&status_mutex_);
      if (msg) {
        free(saved_msg_);
        saved_msg_ = msg;  /* Save for redisplay */
        /* Return value of `get_cursor` is a pointer to the `R` in the
         * input buffer, or NULL to indicate that the status reply 
         * couldn't be parsed.
         */
        char cursor_buf[2 + sizeof "x1b[999;999R"];
        char* action = get_cursor(cursor_buf, sizeof cursor_buf - 1);
        if (action) {
          set_cursor(1, 1);
          fputs(msg, stdout);
          clear_eol();
          *action = 'H';
          fputs(cursor_buf, stdout);
        }
      }
      return 0;
    }
    

    为了显示状态消息,您需要锁定互斥体并将status_line_ 设置为包含状态行的动态分配缓冲区:

    /* Set the status message, so that it will (soon) be shown */
    void show_status(char* msg) {
      pthread_mutex_lock(&status_mutex_);
      free(status_line_);
      status_line_ = msg;
      pthread_mutex_unlock(&status_mutex_);
    }
    

    由于readline 在读取换行符时(以及在某些其他情况下)不保留状态行,并且当您向其发送输出时不会阻止屏幕滚动,因此上述代码保留当前状态行在saved_msg_ 中,以便在必要时重新显示:

    /* Show the status message again */
    void reshow_status(void) {
      pthread_mutex_lock(&status_line_mutex_);
      msg_ = saved_msg_;
      saved_msg_ = NULL;
      pthread_mutex_unlock(&status_line_mutex_);
    }
    

    这是一个非常混乱的解决方案,可以说的最好的一点是,它主要是在一个与您的实际用例可能或可能没有任何关系的虚构上下文中工作的。 (这并不完美。至少有一个竞态条件,尽管它实际上并没有在我的测试代码中被触发,因为对reshow_status 的唯一调用是在调用readline 的线程中执行的,因此只调用该函数如果事件挂钩不可能运行。

    用户输入也可能与控制台的状态返回交错,但我认为这种情况非常罕见。我对get_cursor 的实现确实尝试处理在发送状态请求之后和收到状态回复之前到达的用户输入字符的可能性:

      fputs("\x1b[6n", stdout);
      int ch;
      while((ch = getchar()) != 0x1b) rl_stuff_char(ch);
    

    我没有对此进行彻底的测试,所以我只能说它似乎有效。 (rl_stuff_char 将一个字符插入到缓冲区中,以便下次 readline 的输入循环运行时使用。)

    【讨论】:

    • 感谢您的建议和详细解释!我知道将 readline 和 ncurses 结合起来并不容易,所以这就是我想避免 ncurses 除了包装所有打印件的另一个原因。您的解决方案大部分时间都有效,但有时 get cursor return 确实与用户输入混合在一起。似乎没有办法阻止竞态条件。
    • @zhao:那不是比赛条件;竞争条件是 free()ing saved_msg_ 而没有持有锁。它实际上很容易修复,我想我应该这样做。使用伪 tty 可以避免混合输入,因为这可以让您在控制台发出状态回复所需的几微秒内真正关闭用户输入。我还考虑过滤掉状态回复不会产生的字符,但最后我决定放弃解析尝试,如果它得到一个意外的字符....
    • ... 这应该使混合非常明显,因为尾随字符会显示出来,就好像我输入了它们一样。但是我还没有找到触发它的方法。可能我的环境和你的不一样。最后,可靠的解决方案将全部淘汰并用 ncurses 替换。它可能没有你想象的那么多工作:-)
    • 如何禁用用户输入?如描述中所述,我尝试了 CREAD、TCIOFF 但它们不起作用。事实上,如果我可以禁用用户输入,那么我可以在不使用 rl_event_hook 的情况下在回调中执行此操作。
    • 停止转发不等于禁用输入。当您停止转发时,用户输入仍会进入标准输入,可能与获取光标响应混合。我注意到获取光标响应不是原子的,因此标准输入可能包含类似“\033[12a;3R”的内容,其中“a”是用户输入,可以出现在任何地方。
    猜你喜欢
    • 2011-07-22
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-04-25
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多