简体   繁体   中英

How to get cursor position in C when another thread is reading stdin

I'm working on a console C/C++ program on linux (centOS7) where some info need to be displayed on top of the terminal screen. While the main thread processes stdin, another thread handles callbacks and displays status on stdout. To avoid clobbering, callback status is only displayed on top of the screen, but the cursor must be returned to the original position.

I tried ANSI save/restore cursor but it doesn't work as pointed out in the link. While this stackoverflow solution works in single-thread, it doesn't work in multi-thread as the two thread would both read stdin. I have tried several methods to disable stdin temporarily while getting current cursor positions, but they all failed:

  • disable CREAD in termios.c_cflag -- tcsetattr() returns error (invalid parameter)
  • tcflow(TCIOFF)
  • dup()

I know ncurses would work, but in my app there are too many stdio functions that I need to replace with ncurses wrappers. Does anyone know how to save/restore cursor position or get current position in multithread env where one thread is reading stdin?

I know ncurses would work, but in my app there are too many stdio functions that I need to replace with ncurses wrappers.

So, you are not interested in fixing the problem, only in papering over it. One approach you can try is

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

See man 3 flockfile() for details. The idea is to grab the C library internal lock for all three standard streams, so that any other thread doing I/O on them will block until we call funlockfile() on that stream.

This does not affect low-level I/O to STDIN_FILENO, STDOUT_FILENO, or STDERR_FILENO in any way.


A comment from rici made me realize that there is actually one approach that does not involve rewriting the original code.

Use a helper process (or thread) to handle all I/O to standard input, standard output, standard error, and the terminal.

Essentially, at the very start of your program, you construct three pipes and an Unix domain datagram socket pair, and create the helper.

(If you use a helper process, you can make it into an external executable, and write it using ncurses, without affecting the parent program.)

The helper is connected via the pipes and the socket pair to the parent process. Parent replaces STDIN_FILENO , STDOUT_FILENO , and STDERR_FILENO descriptors with the pipe ends (closing their respective original descriptors). That way, it can only read from the helper, and write to the helper, not directly to the original streams.

The Unix domain datagram socket pair allows the parent to query the current cursor location (and perhaps do other similar actions) from the helper.

The helper reads from two of the parent pipes and the original standard input, and writes to one of the parent pipe and the original standard output and error. I personally would make the helper pipe ends nonblocking, and use select() , so a single-threaded helper would suffice.

In a comment, you mention that

Existing code uses readline and ansi escape strings for nice terminal display...

You really should put that information into the question, since it is quite important.

The fact that your code base uses readline severely conditions your possibilities, since readline does not actually work with ncurses . In order to convert the program to use ncurses , you would need to recreate those features of readline which you rely upon. There may be additional libraries which can help with that, but I don't know of any.

On the other hand, ncurses is capable of dividing the screen into non-overlapping regions, and scrolling these regions independently. This is exactly what you need for an application which wants to keep status messages in a status line. Since version 5.7, released about a decade ago, ncurses has primitive threads support (on Linux, anyway), and it is possible to assign different "windows" (screen regions) to different threads. man curs_threads provides some information.

Ncurses also provides easy-to-use interfaces which can replace the use of console control sequences.

So that's probably the long-term solution, but it's going to be a fair amount of work. In the meantime, it is just barely possible to do what you want by using features built in to the readline library. Or at least I was able to write a proof-of-concept which successfully maintained a status line while accepting user input from readline. The import aspect of this solution is that readline is (almost) always active; that is, that there is a thread which is in a hard loop calling readline and passing the buffer read to a processing thread. (With my POC implementation, if the thread calling readline also processes input and the input processing takes a significant amount of time, then the status line will not be updated while input processing takes place.)

The key is the rl_event_hook function, which is called periodically by readline (about 10 times per second) while it is waiting for input. My implementation of rl_event_hook looks like this:

/* 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;
}

In order to get a status message to display, you need to lock the mutex and set status_line_ to a dynamically-allocated buffer containing the 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_);
}

Since readline does not preserve the status line when a newline character is read (and in certain other cases), and nothing will prevent the screen from scrolling when you send output to it, the above code keeps the current status line in saved_msg_ so that it can be redisplayed when necessary:

/* 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_);
}

That's a pretty messy solution, and about the best that can be said for it is that it mostly works, in an imaginary context which might or might not have anything to do with your actual use case. (It's not perfect. There's at least one race condition, although it doesn't actually get triggered in my test code because the only calls to reshow_status are performed in the thread which calls readline , and so the function is only called if there is no possibility for the event hook to run.

It might also be possible for user input to be interlaced with the console's status return, but I think this will be very rare. My implementation of get_cursor does attempt to deal with the possibility of user input characters arriving after the status request has been sent and before the status reply has been received:

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

I didn't test this thoroughly so about all I can say is that it seemed to work. ( rl_stuff_char inserts a character into a buffer to be used the next time that readline 's input loop runs.)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM