Подключить терминал к процессу, работающему как демон (для запуска пользовательского интерфейса ncurses)

У меня есть (устаревшая) программа, которая действует как демон (в том смысле, что она работает вечно, ожидая запросов на обслуживание), но имеет пользовательский интерфейс на основе ncurses, который работает на хосте.

Я хотел бы изменить программу таким образом, чтобы при подключении к хосту через ssh я мог включить пользовательский интерфейс по требованию. Я знаю, что есть по крайней мере один способ использования псевдотерминалов, но я не совсем уверен, как этого добиться. Есть два поведения приложений, которые я считаю интересными:

Запускайте пользовательский интерфейс только в том случае, если приложение запущено на переднем плане терминала.

  1. Если приложение работает на переднем плане в терминале — отобразите пользовательский интерфейс
  2. Если приложение работает в фоновом режиме - не отображать пользовательский интерфейс
  3. Если приложение перемещено в фоновый режим - закройте пользовательский интерфейс
  4. Если приложение перемещено на передний план терминала - откройте пользовательский интерфейс

Создавайте новый пользовательский интерфейс по запросу, когда кто-то подключается к серверу

  1. Приложение работает в фоновом режиме
  2. Новый пользователь входит в систему
  3. Они запускают что-то, что заставляет экземпляр пользовательского интерфейса открываться в их терминале.
  4. Несколько пользователей могут иметь свои собственные экземпляры пользовательского интерфейса.

Примечания

Есть простой способ сделать это с помощью экрана. Так:

оригинал:

screen mydaemon etc...

новая сессия ssh:

screen -d     
screen -r

Это отсоединяет экран, оставляя его работающим в фоновом режиме, а затем снова подключает его к текущему терминалу. При закрытии терминала сеанс экрана отключается, так что это работает довольно хорошо.

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

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

В первом случае я предполагаю, что хочу, чтобы пользовательский интерфейс ncurses работал на ведомом терминале, на который и от которого передавалась бы основная сторона.

Главный процесс будет использовать что-то вроде isatty(), чтобы проверить, находится ли он в данный момент на переднем плане терминала, и активировать или деактивировать пользовательский интерфейс с помощью newterm() и endwin().

Я экспериментировал с этим, но у меня еще не получилось, так как есть некоторые аспекты терминалов и ncurses, с которыми я в лучшем случае еще не разобрался, а в худшем - фундаментальное недоразумение.

Псевдокод для этого:

openpty(masterfd,slavefd)
login_tty();  
fork();
ifslave 
  close(stdin)
  close(stdout)
  dup_a_new_stdin_from_slavefd();
  newterm(NULL, newinfd, newoutfd);  (
  printw("hello world");
  insert_uiloop_here();
  endwin();    
else ifmaster
  catchandforwardtoslave(SIGWINCH);
  while(noexit)
  {
     docommswithslave();         
     forward_output_as_appropriate();
  } 

Обычно я либо получаю segfault внутри fileno_unlocked() в newterm() или вывод на вызывающий терминал, а не на новый невидимый терминал.

Вопросы

  • Что не так с приведенным выше псевдокодом?
  • У меня есть главный и подчиненный концы правильно?
  • Что здесь делает login_tty?
  • Есть ли практическая разница между openpty() + login_tty() и posix_openpt() + grantpt()?
  • Должен ли быть постоянно работающий процесс, связанный с tty или подчиненным мастером?

Примечание. Это вопрос, отличный от ncurses-newterm-following-openpty, который описывает конкретную неправильную/неполную реализацию для этого варианта использования и спрашивает, что с ней не так.

У меня есть сопутствующие вопросы: *Есть ли способ подключить шелл к псевдотерминалу? * что-можно-сделать-с-пти

Bruce Adams 20.12.2020 01:34

Обратите внимание, что stackoverflow.com/a/1455417/1569204 описывает более безопасный способ определения статуса переднего плана/фона с помощью getpgrp() == tcgetpgrp(STDOUT_FILENO)

Bruce Adams 01.01.2021 03:46
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
2
1 284
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Это хороший вопрос и хороший пример того, почему у нас есть псевдотерминалы.


Чтобы демон мог использовать интерфейс ncurses, ему требуется псевдотерминал (подчиненная сторона пары псевдотерминалов), доступный с момента запуска демона непрерывно до его выхода.

Чтобы псевдотерминал существовал, должен существовать процесс с открытым дескриптором на стороне мастера пары псевдотерминалов. Кроме того, он должен потреблять весь вывод со стороны подчиненного псевдотерминала (видимые данные, выводимые ncurses). Обычно такая библиотека, как vterm, используется для интерпретации этого вывода, чтобы «отрисовать» фактический текстовый фреймбуфер в массив (ну, обычно два массива — один для широких символов, отображаемых в каждой ячейке (конкретная строка и столбец), а другой для атрибуты, такие как цвет).

Чтобы пара псевдотерминалов работала правильно, либо процесс на главном конце является родителем или предком процесса, выполняющего ncurses на подчиненном конце, либо эти два процесса совершенно не связаны. Процесс, выполняющий ncurses на ведомом конце, должен находиться в новом сеансе с псевдотерминалом в качестве управляющего терминала. Этого проще всего добиться, если мы используем небольшой псевдотерминальный «сервер», который запускает демон в дочернем процессе; и действительно, это шаблон, который обычно используется с псевдотерминалом.

Первый сценарий на самом деле невозможен, потому что нет родительского/главного процесса, поддерживающего псевдотерминал.

Мы можем обеспечить поведение первого сценария, добавив небольшой процесс «уборщика», предоставляющий псевдотерминал, чья задача состоит в том, чтобы поддерживать существование пары псевдотерминалов и потреблять любые выходные данные ncurses, сгенерированные процессом, работающим в паре псевдотерминалов.

Однако такое поведение также соответствует второму сценарию.

Другими словами, вот что будет работать:

  1. Вместо того, чтобы запускать демон напрямую, мы используем пользовательскую программу, скажем, «janitor», которая создает псевдотерминал и запускает демон внутри этого псевдотерминала.

  2. Дворник будет работать до тех пор, пока работает демон.

  3. Дворник предоставляет интерфейс для «подключения» других процессов к главной стороне пары псевдотерминалов.

    Это не обязательно означает проксирование данных 1:1. Обычно ввод (нажатие клавиш) для демона предоставляется без изменений, но то, как передается содержимое псевдотерминального «буфера кадра», содержимое виртуального окна на основе символов, действительно различается. Это полностью находится под нашим контролем.

  4. Для подключения к дворнику нам понадобится вторая программа-помощник.

    В случае «экрана» эти две программы на самом деле являются одним и тем же двоичным файлом; поведение просто контролируется параметрами командной строки, а нажатия клавиш «потребляются» самим «экраном» для управления поведением «экрана» и не передаются фактическому процессу на основе ncurses, работающему в псевдотерминале.

До сих пор мы могли просто исследовать источники tmux или screen, чтобы увидеть, как они делают вышеперечисленное; это очень простое терминальное мультиплексирование.

Однако здесь у нас есть очень интересный момент, который я раньше не рассматривал; этот небольшой фрагмент заставил меня понять довольно важную суть этого вопроса:

Несколько пользователей могут иметь свои собственные экземпляры пользовательского интерфейса.

У процесса может быть только один управляющий терминал. Это указывает на определенные отношения. Например, когда главная сторона управляющего терминала закрывается, пара псевдотерминалов исчезает, а дескрипторы, открытые для подчиненной стороны пары псевдотерминалов, становятся нефункциональными (все операции дают EIO, если я правильно помню); но более того, каждый процесс в группе процессов получает сигнал HUP.

Функция ncurses newterm() позволяет процессу подключаться к существующему терминалу или псевдотерминалу во время выполнения. Этот терминал не обязательно должен быть управляющим терминалом, и процесс, использующий ncurses, не должен принадлежать к этому сеансу. Важно понимать, что в этом случае стандартные потоки (стандартный ввод, вывод и ошибка) не перенаправляются на терминал.

Таким образом, если есть способ сообщить демону, что у него есть новый доступный псевдотерминал, и он должен открыть его, потому что есть пользователь, который хочет использовать интерфейс, предоставляемый демоном, мы можем сделать так, чтобы демон открывал и закрывал псевдотерминалы по требованию. !

Обратите внимание, однако, что это требует явного взаимодействия между демоном и процессами, которые используются для подключения к пользовательскому интерфейсу на основе ncurses, предоставляемому демоном. Не существует стандартного способа сделать это с произвольными процессами или демонами на основе ncurses. Например, насколько я знаю, nano и top такого интерфейса не предоставляют; они используют только псевдотерминал, связанный со стандартными потоками.

После публикации этого ответа — надеюсь, достаточно быстро, прежде чем вопрос будет закрыт, потому что другие не видят обоснованности вопроса и его полезности для других разработчиков POSIXy на стороне сервера — я создам примерную пару программ, чтобы проиллюстрировать вышеизложенное; вероятно, используя сокет домена Unix в качестве канала связи «новый пользовательский интерфейс для этого пользователя, пожалуйста», поскольку файловые дескрипторы могут передаваться в качестве вспомогательных данных с использованием сокетов домена Unix, и личность пользователя на любом конце сокета может быть проверена (учетные данные вспомогательные данные).

Однако пока вернемся к заданным вопросам.

Что не так с приведенным выше псевдокодом? [Обычно я либо получаю segfault внутри fileno_unlocked() в newterm(), либо вывожу на вызывающем терминале, а не на новом невидимом терминале.]

newinfd и newoutfd должны быть одинаковыми (или дубликатами) дескриптора конечного файла подчиненного псевдотерминала, slavefd.

Я думаю, что также должен быть явный set_term() с указателем SCREEN, возвращаемым newterm() в качестве параметра. (Возможно, он автоматически вызывается для самого первого терминала, предоставленного newterm(), но я бы предпочел вызвать его явно.)

newterm() подключается и подготавливает новый терминал. Оба дескриптора обычно относятся к одной и той же ведомой стороне пары псевдотерминалов; infd может быть другим дескриптором, откуда принимаются нажатия клавиш пользователем.

Одновременно в ncurses может быть активен только один терминал. Вам нужно использовать set_term(), чтобы выбрать, на какой из них будут влиять последующие printw() и т. д. вызовы. (Он возвращает терминал, который был ранее активен, так что можно выполнить обновление на другом терминале, а затем вернуться к исходному терминалу.)

(Это также означает, что если программа предоставляет несколько терминалов, она должна переключаться между ними, проверяя входные данные и обновляя каждый терминал с относительно высокой частотой, чтобы пользователи-люди чувствовали, что пользовательский интерфейс реагирует, а не «запаздывает». A Однако хитрый POSIX-программист может выбирать или опрашивать базовые дескрипторы и циклически проходить только через терминалы, ожидающие ввода.)

У меня есть главный и подчиненный концы правильно?

Да, я верю, что ты знаешь. Подчиненный конец — это тот, который видит терминал и может использовать ncurses. Главный конец — это тот, который обеспечивает нажатия клавиш и что-то делает с выводом ncurses (например, рисует их в текстовом фреймбуфере или проксирует на удаленный терминал).

Что здесь делает login_tty?

Есть два широко используемых псевдотерминальных интерфейса: UNIX98 (который стандартизирован в POSIX) и BSD.

С интерфейсом POSIX posix_openpt() создает новую пару псевдотерминалов и возвращает дескриптор на его главную сторону. Закрытие этого дескриптора (последнего открытого дубликата) уничтожает пару. В модели POSIX изначально ведомая сторона «заблокирована» и не может быть открыта. unlockpt() снимает эту блокировку, позволяя открыть ведомую сторону. grantpt() обновляет владельца и режим символьного устройства (соответствующего ведомой стороне пары псевдотерминалов), чтобы они соответствовали текущему реальному пользователю. unlockpt() и grantpt() можно вызывать в любом порядке, но имеет смысл сначала вызвать grantpt(); таким образом, подчиненная сторона не может быть открыта «случайно» другими процессами, пока ее право собственности и режим доступа не будут установлены должным образом. POSIX предоставляет путь к символьному устройству, соответствующему ведомой стороне пары псевдотерминалов, через ptsname(), но Linux предоставляет TIOCGPTPEER ioctl (в ядрах 4.13 и более поздних версиях), который позволяет открыть ведомый конец, даже если узел символьного устройства не отображается в текущем пространстве имен монтирования.

Как правило, grantpt(), unlockpt() и открытие подчиненной стороны пары псевдотерминалов выполняются в дочернем процессе (который все еще имеет доступ к дескриптору на стороне мастера), который начал новый сеанс с помощью setsid(). Дочерний процесс перенаправляет стандартные потоки (стандартный ввод, вывод и ошибки) на подчиненную сторону псевдотерминала, закрывает свою копию дескриптора на стороне мастера и убеждается, что псевдотерминал является его управляющим терминалом. Обычно за этим следует выполнение бинарного файла, который будет использовать псевдотерминал (обычно через ncurses) для пользовательского интерфейса.

С интерфейсом BSD openpty() создает пару псевдотерминалов, предоставляя дескрипторы открытых файлов обеим сторонам, и, при необходимости, устанавливает параметры псевдотерминала и размер окна. Это примерно соответствует POSIX posix_openpt() + grantpt() + unlockpt() + открытие ведомой стороны пары псевдотерминалов + опциональная установка настроек termios и размера окна терминала.

В интерфейсе BSD login_tty запускается в дочернем процессе. Он запускается setsid() для создания нового сеанса, делает подчиненную сторону управляющим терминалом, перенаправляет стандартные потоки на подчиненную сторону управляющего терминала и закрывает копию дескриптора главной стороны.

В интерфейсе BSD forkpty() объединяет openpty(), fork() и login_tty(). Он возвращается дважды; один раз в родительском (возвращая PID дочернего процесса) и один раз в дочернем (возвращающем ноль). Дочерний процесс выполняется в новом сеансе с подчиненной стороной псевдотерминала в качестве управляющего терминала, уже перенаправленного на стандартные потоки.

Есть ли практическая разница между openpty() + login_tty() и posix_openpt() + grantpt() [+ unlockpt() + открытие ведомой стороны]?

Нет, не совсем.

Как Linux, так и большинство BSD, как правило, предоставляют и то, и другое. (В Linux при использовании интерфейса BSD вам необходимо подключить библиотеку libutil (опция -lutil gcc), но она предоставляется тем же пакетом, который предоставляет стандартную библиотеку C, и можно предположить, что она всегда доступна.)

Я склонен предпочитать интерфейс POSIX, даже несмотря на то, что он намного более многословен, но кроме того, что я предпочитаю интерфейсы POSIX интерфейсам BSD, я даже не знаю, почему я предпочитаю его интерфейсу BSD. BSD forkpty() делает практически все для наиболее распространенных вариантов использования за один вызов!

Кроме того, вместо того, чтобы полагаться на ptsname() (или расширение GNU ptsname_r()), я обычно сначала пробую специфичный для Linux ioctl, если он выглядит доступным, и возвращаюсь к ptsname(), если он недоступен. Так что, во всяком случае, я, вероятно, должен предпочесть интерфейс BSD ... но libutil немного раздражает меня, я думаю, поэтому я не делаю.

Я определенно не возражаю против того, чтобы другие предпочитали интерфейс BSD. Во всяком случае, я немного озадачен тем, как вообще существуют мои предпочтения; обычно я предпочитаю более простые и надежные интерфейсы многословным и сложным.

Должен ли быть постоянно работающий процесс, связанный с tty или подчиненным мастером?

Должен быть процесс с открытой главной стороной псевдотерминала. Когда последний дубликат дескриптора закрывается, ядро ​​уничтожает пару.

Кроме того, если процесс, имеющий дескриптор главной стороны, не читает из него, процесс, работающий в псевдотерминале, неожиданно заблокируется в каком-то вызове ncurses. Обычно вызовы не блокируются (или блокируются только на очень короткие промежутки времени, короче, чем замечают люди). Если процесс просто читает, но отбрасывает ввод, то мы на самом деле не знаем содержимого терминала ncurses!

Таким образом, мы можем сказать, что абсолютно необходим процесс, который читает данные с главной стороны пары псевдотерминалов, сохраняя дескриптор открытым для главной стороны.

(Подчиненная сторона отличается; поскольку узел символьного устройства обычно виден, процесс может временно закрыть свое соединение с псевдотерминалом и просто открыть его позже. В Linux, когда ни у одного процесса нет открытого дескриптора для подчиненной стороны, процесс чтение или запись на главную сторону приведет к ошибкам EIO (read() и write() возвращают -1 с errno==EIO).Я не совсем уверен, что это гарантированное поведение; до сих пор никогда полагался на это и только недавно заметил это (при реализации примера) сам.

Вы можете проголосовать за вопрос, если считаете его действительным. Это хороший способ избежать его закрытия.

Bruce Adams 21.12.2020 11:22

Удалось создать пример? Я все еще борюсь с этим.

Bruce Adams 23.12.2020 03:56

Вот пример приложения ncurses, которое анимирует прыгающий X на каждом терминале, указанном в качестве параметра:

// SPDX-License-Identifier: CC0-1.0
#define  _POSIX_C_SOURCE  200809L
#include <stdlib.h>
#include <sys/ioctl.h>
#include <locale.h>
#include <curses.h>
#include <time.h>
#include <string.h>
#include <signal.h>
#include <stdio.h>
#include <errno.h>

#ifndef   FRAMES_PER_SECOND
#define   FRAMES_PER_SECOND  25
#endif

#define   FRAME_DURATION (1.0 / (double)(FRAMES_PER_SECOND))

/* Because the terminals are not the controlling terminal for this process,
 * this process may not receive the SIGWINCH signal whenever a screen size
 * changes.  Therefore, we call this function to update it whenever we switch
 * between terminals.
*/
extern void _nc_update_screensize(SCREEN *);

/*
 * Signal handler to notice if this program - all its terminals -- should exit.
*/

static volatile sig_atomic_t  done = 0;

static void handle_done(int signum)
{
    done = signum;
}

static int install_done(int signum)
{
    struct sigaction  act;
    memset(&act, 0, sizeof act);
    sigemptyset(&act.sa_mask);
    act.sa_handler = handle_done;
    act.sa_flags = 0;
    return sigaction(signum, &act, NULL);
}

/* Difference in seconds between to timespec structures.
*/
static inline double difftimespec(const struct timespec after, const struct timespec before)
{
    return (double)(after.tv_sec - before.tv_sec)
         + (double)(after.tv_nsec - before.tv_nsec) / 1000000000.0;
}

/* Sleep the specified number of seconds using nanosleep().
*/
static inline double nsleep(const double seconds)
{
    if (seconds <= 0.0)
        return 0.0;

    const long  sec = (long)seconds;
    long       nsec = (long)(1000000000.0 * (seconds - (double)sec));
    if (nsec < 0)
        nsec = 0;
    if (nsec > 999999999)
        nsec = 999999999;

    if (sec == 0 && nsec < 1)
        return 0.0;

    struct timespec  req = { .tv_sec = (time_t)sec, .tv_nsec = nsec };
    struct timespec  rem = { .tv_sec = 0,           .tv_nsec = 0    };

    if (nanosleep(&req, &rem) == -1 && errno == EINTR)
        return (double)(rem.tv_sec) + (double)(rem.tv_nsec) / 1000000000.0;

    return 0.0;
}

/*
 * Structure describing each client (terminal) state.
*/
struct client {
    SCREEN  *term;
    FILE    *in;
    FILE    *out;
    int      col;     /* Ball column */
    int      row;     /* Ball row */
    int      dcol;    /* Ball direction in column axis */
    int      drow;    /* Ball direction in row axis */
};

static size_t          clients_max = 0;
static size_t          clients_num = 0;
static struct client  *clients = NULL;

/* Add a new terminal, based on device path, and optionally terminal type.
*/
static int add_client(const char *ttypath, const char *term)
{
    if (!ttypath || !*ttypath)
        return errno = EINVAL;

    if (clients_num >= clients_max) {
        const size_t   temps_max = (clients_num | 15) + 13;
        struct client *temps;

        temps = realloc(clients, temps_max * sizeof clients[0]);
        if (!temps)
            return errno = ENOMEM;

        clients_max = temps_max;
        clients     = temps;
    }

    clients[clients_num].term = NULL;
    clients[clients_num].in   = NULL;
    clients[clients_num].out  = NULL;
    clients[clients_num].col  = 0;
    clients[clients_num].row  = 0;
    clients[clients_num].dcol = +1;
    clients[clients_num].drow = +1;

    clients[clients_num].in = fopen(ttypath, "r+");
    if (!clients[clients_num].in)
        return errno;

    clients[clients_num].out = fopen(ttypath, "r+");
    if (!clients[clients_num].out) {
        const int  saved_errno = errno;
        fclose(clients[clients_num].in);
        return errno = saved_errno;
    }

    clients[clients_num].term = newterm(term, clients[clients_num].in,
                                              clients[clients_num].out);
    if (!clients[clients_num].term) {
        fclose(clients[clients_num].out);
        fclose(clients[clients_num].in);
        return errno = ENOMEM;
    }

    set_term(clients[clients_num].term);
    start_color();
    cbreak();
    noecho();
    nodelay(stdscr, TRUE);
    keypad(stdscr, TRUE);
    scrollok(stdscr, FALSE);
    curs_set(0);
    clear();
    refresh();

    clients_num++;

    return 0;
}

static void close_all_clients(void)
{
    while (clients_num > 0) {
        clients_num--;

        if (clients[clients_num].term) {
            set_term(clients[clients_num].term);
            endwin();
            delscreen(clients[clients_num].term);
            clients[clients_num].term = NULL;
        }

        if (clients[clients_num].in) {
            fclose(clients[clients_num].in);
            clients[clients_num].in = NULL;
        }

        if (clients[clients_num].out) {
            fclose(clients[clients_num].out);
            clients[clients_num].out = NULL;
        }
    }
}

int main(int argc, char *argv[])
{
    struct timespec  curr, prev;
    int              arg;

    if (argc < 2 || !strcmp(argv[1], "-h") || !strcmp(argv[1], "--help")) {
        const char *arg0 = (argc > 0 && argv && argv[0] && argv[0][0]) ? argv[0] : "(this)";
        fprintf(stderr, "\n");
        fprintf(stderr, "Usage: %s [ -h | --help ]\n", arg0);
        fprintf(stderr, "       %s TERMINAL [ TERMINAL ... ]\n", arg0);
        fprintf(stderr, "\n");
        fprintf(stderr, "This program displays a bouncing ball animation in each terminal.\n");
        fprintf(stderr, "Press Q or . in any terminal, or send this process an INT, HUP,\n");
        fprintf(stderr, "QUIT, or TERM signal to quit.\n");
        fprintf(stderr, "\n");
        return EXIT_SUCCESS;
    }

    setlocale(LC_ALL, "");

    for (arg = 1; arg < argc; arg++) {
        if (add_client(argv[arg], NULL)) {
            fprintf(stderr, "%s: %s.\n", argv[arg], strerror(errno));
            close_all_clients();
            return EXIT_FAILURE;
        }
    }

    if (install_done(SIGINT) == -1 ||
        install_done(SIGHUP) == -1 ||
        install_done(SIGQUIT) == -1 ||
        install_done(SIGTERM) == -1) {
        fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
        close_all_clients();
        return EXIT_FAILURE;
    }

    clock_gettime(CLOCK_MONOTONIC, &curr);
    while (!done && clients_num > 0) {
        size_t  n;

        /* Wait until it is time for the next frame. */
        prev = curr;
        clock_gettime(CLOCK_MONOTONIC, &curr);
        nsleep(FRAME_DURATION - difftimespec(curr, prev));

        /* Update each terminal. */
        n = 0;
        while (n < clients_num) {
            int  close_this_terminal = 0;
            int  ch, rows, cols;

            set_term(clients[n].term);

            /* Because the terminal is not our controlling terminal,
               we may miss SIGWINCH window size change signals.
               To work around that, we explicitly check it here. */
            _nc_update_screensize(clients[n].term);

            /* Process inputs - if we get any */
            while ((ch = getch()) != ERR)
                if (ch == 'x' || ch == 'X' || ch == 'h' || ch == 'H')
                    clients[n].dcol = -clients[n].dcol;
                else
                if (ch == 'y' || ch == 'Y' || ch == 'v' || ch == 'V')
                    clients[n].drow = -clients[n].drow;
                else
                if (ch == '.' || ch == 'q' || ch == 'Q')
                    close_this_terminal = 1;

            if (close_this_terminal) {
                endwin();
                delscreen(clients[n].term);
                fclose(clients[n].in);
                fclose(clients[n].out);
                /* Remove from array. */
                clients_num--;
                clients[n] = clients[clients_num];
                clients[clients_num].term = NULL;
                clients[clients_num].in   = NULL;
                clients[clients_num].out  = NULL;
                continue;
            }

            /* Obtain current terminal size. */
            getmaxyx(stdscr, rows, cols);

            /* Leave a trace of dots. */
            if (clients[n].row >= 0 && clients[n].row < rows &&
                clients[n].col >= 0 && clients[n].col < cols)
                mvaddch(clients[n].row, clients[n].col, '.');

            /* Top edge bounce. */
            if (clients[n].row <= 0) {
                clients[n].row  = 0;
                clients[n].drow = +1;
            }

            /* Left edge bounce. */
            if (clients[n].col <= 0) {
                clients[n].col  = 0;
                clients[n].dcol = +1;
            }

            /* Bottom edge bounce. */
            if (clients[n].row >= rows - 1) {
                clients[n].row  = rows - 1;
                clients[n].drow = -1;
            }

            /* Right edge bounce. */
            if (clients[n].col >= cols - 1) {
                clients[n].col  = cols - 1;
                clients[n].dcol = -1;
            }

            clients[n].row += clients[n].drow;
            clients[n].col += clients[n].dcol;
            mvaddch(clients[n].row, clients[n].col, 'X');
            refresh();

            /* Next terminal. */
            n++;
        }
    }

    close_all_clients();
    return EXIT_SUCCESS;
}

Он не содержит псевдотерминалов, и единственной реальной особенностью является использование _nc_update_screensize() для определения того, изменился ли какой-либо из терминалов. (Поскольку они не являются нашим управляющим терминалом, мы не получаем сигнал SIGWINCH, и поэтому ncurses пропускает смену окна.)

Я рекомендую скомпилировать это с помощью gcc -Wall -Wextra -O2 bounce.c -lncurses -o bounce.

Откройте пару окон терминалов и запустите tty, чтобы увидеть путь к их управляющим терминалам (обычно ведомые концы псевдотерминалов, /dev/pts/N). Запустите ./bounce с одним или несколькими из этих путей в качестве параметров, и пусть начнется подпрыгивание.

Если вы не хотите, чтобы оболочка в окне использовала ваш ввод, и хотите, чтобы указанная выше программа увидела его, запустите, например. sleep 6000 в окнах терминала перед выполнением вышеуказанной команды.

Эта программа просто открывает два потока для каждого терминала и позволяет ncurses управлять ими; в основном, это пример мультитерминального приложения ncurses и того, как жонглировать ими, используя newterm(), set_term() и так далее.

Если вы указываете один и тот же терминал более одного раза, нажатие Q закрывает их в случайном порядке, поэтому ncurses может неправильно вернуть терминал в исходное состояние. (Возможно, вам придется ввести reset вслепую, чтобы сбросить терминал в рабочее состояние; это команда-компаньон для clear, которая просто очищает терминал. Они больше ничего не делают, только работу с терминалом.)

Вместо предоставления путей к терминальным устройствам в качестве параметра командной строки программа могла бы работать все время, но прослушивать входящие дейтаграммы домена Unix со вспомогательными данными типа SCM_RIGHTS уровня SOL_SOCKET, которые можно использовать для дублировать файловые дескрипторы между несвязанными процессами.

Однако, если кто-то отказывается от управления таким терминалом (либо открывая терминал, либо передавая дескриптор файла терминала другому процессу), проблема заключается в том, что отменить этот доступ невозможно. Мы можем избежать этого, используя псевдотерминал между ними и проксируя данные между псевдотерминалом и нашим реальным терминалом. Чтобы разорвать соединение, мы просто останавливаем проксирование данных, уничтожаем пару псевдотерминалов и возвращаем наш терминал в исходное состояние.

Исследуя вышеприведенную программу, мы видим, что процедура в псевдокоде для управления новым терминалом выглядит так:

  1. Получите два дескриптора потока FILE на терминал.

    Вышеприведенная программа использует fopen(), чтобы открывать их как обычно. Другие программы могут использовать dup() для дублирования одного дескриптора и fdopen() для преобразования их в дескрипторы потока stdio FILE.

  2. Позвоните SCREEN *term = newterm(NULL, in, out), чтобы сообщить ncurses об этом новом терминале.

    in и out — два дескриптора потока FILE. Первый параметр — это строка типа терминала; если NULL, вместо этого используется переменная среды TERM. Типичное значение сегодня — xterm-256color, но ncurses также поддерживает множество других типов терминалов.

  3. Позвоните set_term(term), чтобы сделать новый терминал активным в данный момент.

    На этом этапе мы можем выполнять обычные действия по настройке ncurses, такие как cbreak(); noecho(); и так далее.

Сбросить управление терминалом также просто:

  1. Позвоните set_term(term), чтобы сделать этот терминал активным в данный момент.

  2. Звоните endwin() и delscreen(term).

  3. Закройте два потока FILE на терминал.

Для обновления содержимого терминала требуется цикл, при котором каждая итерация обрабатывает один терминал, начиная с вызова set_term(term) (за которым следует вызов _nc_update_screensize(term), если мы хотим реагировать на изменения размера окна в этих терминалах).

В приведенном выше примере программы используется режим nodelay(), так что getch() вернет либо нажатие клавиши, либо ERR, если с текущего терминала нет ожидающих ввода. (По крайней мере, в Linux мы будем получать KEY_RESIZE всякий раз, когда изменяется размер окна, если либо терминал является нашим управляющим терминалом, либо мы вызываем _nc_update_screensize().)

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

1. Вы такой же гларбо, как и в другом ответе? ваша репутация отличается? 2. Ваш ответ кажется не связанным с вопросом. Он использует ncurses, но не псевдотерминальные пары.

Bruce Adams 24.12.2020 03:07

На самом деле я могу сказать, что вы тот же Гларбо, потому что у вас такой же идентификатор.

Bruce Adams 24.12.2020 12:45

Я думаю, вы предполагаете, что это лучше, чем использование псевдотерминалов напрямую. Я предполагаю, что это имеет смысл как ответ на этот вопрос, не улучшая мое понимание псевдотерминалов, как это требуется для другого вопроса.

Bruce Adams 24.12.2020 12:50

@BruceAdams: Да, то же самое (не хочу регистрироваться!). Нет, это не лучше, это просто то, как служба может предоставить несколько одновременных пользовательских интерфейсов ncurses пользователям, которые хотят один. (Конкретный пример пункта 4 во втором наборе целей.) Лучшим вариантом является клиентская программа, которую пользователь-человек использует для подключения к службе, чтобы пользовательский интерфейс использовал псевдотерминал, поскольку тогда клиентская программа может прервать соединение (и легко разорвать псевдотерминальную пару). Я создаю пример этого и опубликую его здесь, просто праздники означают, что я сейчас занят в реальной жизни. Терпение.

Glärbo 24.12.2020 21:58

С Рождеством. У меня та же проблема. Я мог бы уже все это решить, если бы дети не лазили по мне 24 часа в сутки.

Bruce Adams 25.12.2020 10:36

Другие вопросы по теме