Как получить позицию курсора в C, когда другой поток читает stdin - PullRequest
0 голосов
/ 13 января 2019

Я работаю над консольной программой C / C ++ для linux (centOS7), где некоторая информация должна отображаться в верхней части экрана терминала. Пока основной поток обрабатывает стандартный ввод, другой обрабатывает обратные вызовы и отображает состояние стандартного вывода. Чтобы избежать дублирования, статус обратного вызова отображается только в верхней части экрана, но курсор должен быть возвращен в исходное положение.

Я пытался ANSI курсор для сохранения / восстановления , но он не работает, как указано в ссылке. Хотя это решение для стекового потока работает в однопоточном режиме, оно не работает в многопоточном, поскольку оба потока будут считывать stdin. Я пробовал несколько способов временно отключить stdin при получении текущих позиций курсора, но все они потерпели неудачу:

  • отключить CREAD в termios.c_cflag - tcsetattr () возвращает ошибку (неверный параметр)
  • tcflow (TCIOFF)
  • DUP ()

Я знаю, что ncurses будет работать, но в моем приложении слишком много функций stdio, которые мне нужно заменить на оболочки ncurses. Кто-нибудь знает, как сохранить / восстановить позицию курсора или получить текущую позицию в многопоточном env, где один поток читает stdin?

Ответы [ 2 ]

0 голосов
/ 14 января 2019

В комментарии вы упоминаете, что

Существующий код использует readline и escape-строки 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_);
}

Это довольно грязное решение, и самое лучшее, что можно сказать, это то, что оно в основном работает в воображаемом контексте, который может иметь или не иметь никакого отношения к вашему реальному варианту использования. (Это не идеально. Есть по крайней мере одно условие гонки, хотя на самом деле оно не запускается в моем тестовом коде, потому что в потоке, который вызывает readline, выполняются только вызовы reshow_status, поэтому функция вызывается только если нет возможности запустить обработчик события.

Возможно также, что пользовательский ввод будет чередоваться с возвратом состояния консоли, но я думаю, что это будет очень редко. Моя реализация get_cursor пытается справиться с возможностью ввода символов пользовательского ввода после того, как запрос о статусе и до получения ответа о статусе:

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

Я не проверил это полностью, поэтому все, что я могу сказать, - это то, что это сработало. (rl_stuff_char вставляет символ в буфер, который будет использоваться при следующем запуске цикла ввода readline.)

0 голосов
/ 13 января 2019

Я знаю, что ncurses будет работать, но в моем приложении слишком много функций stdio, которые мне нужно заменить на оболочки ncurses.

Итак, вы не заинтересованы в устранении проблемы, а только в том, чтобы ее исправить. Один из подходов, которые вы можете попробовать, это

    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 для всех трех стандартных потоков, чтобы любой другой поток, выполняющий операции ввода-вывода, блокировался до тех пор, пока мы не вызовем funlockfile() для этого потока.

Это никак не влияет на низкоуровневые операции ввода-вывода для STDIN_FILENO, STDOUT_FILENO или STDERR_FILENO.


Комментарий от rici заставил меня понять, что на самом деле существует один подход, который не предусматривает переписывание исходного кода.

Использование вспомогательного процесса (или потока) для обработки всех операций ввода-вывода со стандартным вводом, стандартным выводом, стандартной ошибкой и выводом.

По сути, в самом начале вашей программы вы создаете три канала и пару сокетов дейтаграммы домена Unix и создаете помощника.

(Если вы используете вспомогательный процесс, вы можете превратить его во внешний исполняемый файл и записать его с помощью ncurses, не затрагивая родительскую программу.)

Помощник подключен через каналы и пару сокетов к родительскому процессу. Родитель заменяет дескрипторы STDIN_FILENO, STDOUT_FILENO и STDERR_FILENO концами канала (закрывая их соответствующие оригинальные дескрипторы). Таким образом, он может только читать из помощника и записывать в помощника, а не напрямую в исходные потоки.

Пара сокетов дейтаграммы домена Unix позволяет родителю запрашивать текущее местоположение курсора (и, возможно, выполнять другие подобные действия) у помощника.

Помощник читает из двух родительских каналов и исходного стандартного ввода и записывает в один из родительских каналов и исходного стандартного вывода и ошибки. Я бы лично сделал концы вспомогательных каналов неблокирующими и использовал бы select(), так что однопоточного помощника было бы достаточно.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...