Как изучать исходные тексты

    Бувально в тот момент, когда я (не очень успешно) вычитывал ошибки и опечатки в предыдущем посте, bobry предложил обсудить, как сделать в консоли историю (которая, Shift-PgUp).

    Очевидным методом сделать что-то связанное с терминалами — посмотреть, как сделано у других и сделать так же. В процессе изучения этого мы обратили внимание на интересную особенность: некоторые программы, показывая содержимое, восстанавливают экран до запуска приложения (mc, vim, nano, less и т.д.). Кроме того, при их запуске исчезает (в xterm/gnome-terminal) скролл-бар.

    Для изучения «каким образом» было решено остановиться на MC, как самом старинном (и не зависящем от ncurses) приложении.

    Далее идёт роматичная история о том, как mc делает toggle_panel() с большим количеством цитат из исходного кода.

    Заодно, читатель сможет посмотреть, как выглядит процесс «посмотри в исходниках».

    Итак, исходный текст MC. Известно, что экран «сзади» можно увидеть по Ctrl-O. Комбинация кнопок появилась ещё в старинном Norton Commander, откуда её позаимствовали все последующие консольные клоны NC: Dos Navigator, Volkov Commander, Far Navigator и т.д.

    Скачали сырцы (apt-get source mc), глянули во внутрь.

    Довольно быстро мы нашли файл keybind.c, где легко увидели, что комбинации Ctrl-буква кодируются как «C-L» (в нашем случае «C-o»). Простой массив enum'ов (или это define'ы? Не важно), искомая строчка:

        { XCTRL ('o'),  CK_ShowCommandLine,               "C-o" },
    

    Дальше (мы знать не знаем, архитектуры MC, и знать не особо хотим), мы просто используем утилиту grep для того, чтобы найти, где эта функция есть: grep CK_ShowCommandLine -A3 -B3 -r *

    Искомый файл — main.c

    main.c:    case CK_ShowCommandLine:
    main.c-        view_other_cmd ();
    main.c-        break;
    

    и рядом же:

    viewer/actions_cmd.c:    case CK_ShowCommandLine:
    viewer/actions_cmd.c-        view_other_cmd ();
    viewer/actions_cmd.c-        break;
    

    И вьюер. и сам mc испольузют одну и ту же функцию: view_other_cmd. Аналогичный греп, находится файл cmd.c с искомой функцией:

    void
    view_other_cmd (void)
    {
        static int message_flag = TRUE;
    
        if (!xterm_flag && !console_flag && !use_subshell && !output_starts_shell) {
            if (message_flag)
                message (D_ERROR, MSG_ERROR,
                         _(" Not an xterm or Linux console; \n"
                           " the panels cannot be toggled. "));
            message_flag = FALSE;
        } else {
            toggle_panels ();
        }
    }
    

    Глаз немного режет старинный стиль форматирования текста, но…

    Итак,
    1. Искомая функция toggle_pannels(). На самом деле, мы тут же ринулись смотреть её, но сейчас будем более последовательны и внимательно прочтём случай ошибки (обратите внимание на изящную эмуляцию замыканий в Си — использование static у переменной для того, чтобы выводить сообщение об ошибке только один раз за всё время работы программы).
    2. Ошибка выводится если… если у нас НЕ xterm и НЕ консоль. Часть связанная с subshell нас не интересует, а вот насчёт отдельной проверки xterm'а поставим галочку.

    Ищем toggle_pannels(). Файл execute.c. Функция большая, процитирую интересное место.

        tty_reset_screen ();
        do_exit_ca_mode ();
        tty_raw_mode ();
        if (console_flag)
            handle_console (CONSOLE_RESTORE);
    
    1. tty_reset_screen — допустим
    2. do_exit_ca_mode — о, интересно
    3. tty_raw_mode — это просто переключение режимов ввода символов
    4. Интересно, если у нас консоль, то делается handle_console...


    Что такое ca_mode? Мелкая заминка (файл не в src, а в lib), tty/win.c. рядом же обратная ей do_enter_ca_mode:
    void
    do_enter_ca_mode (void)
    {
        if (xterm_flag) {
            fprintf (stdout, /* ESC_STR ")0" */ ESC_STR "7" ESC_STR "[?47h");
            fflush (stdout);
        }
    }
    
    void
    do_exit_ca_mode (void)
    {
        if (xterm_flag) {
            fprintf (stdout, ESC_STR "[?47l" ESC_STR "8" ESC_STR "[m");
            fflush (stdout);
        }
    }
    

    Итого — переключение странного режима, которого мы не знаем. Ассоциированный с xterm. Быстрогугль подсказывает, что это «Use Alternate Screen Buffer»:

    Xterm maintains two screen buffers. The normal screen buffer allows you to scroll back to view saved lines of output up to the maximum set by the saveLines resource. The alternate screen buffer is exactly as large as the display, contains no additional saved lines. When the alternate screen buffer is active, you cannot scroll back to view saved lines. Xterm provides control sequences and menu entries for switching between the two.


    Ок. Понятно. У xterm'а есть специальный esc-код.

    А у linux? Идём в консоль linux (настоящую, Ctrl-Alt-F1), пробуем vim — при выходе мы видим предыдущее содержимое на экране. less… видим. nano… видим. То есть linux (согласно console_codes) этого не поддерживает. Ага, ясно. А mc? А mc работает, засранец! Как? Почему? Идём в исходный текст mc обратно, смотрим… А что делает handle_console?

    Ищем… (cons.handler.c):
    
    void
    handle_console (unsigned char action)
    {
        (void) action;
    
        if (look_for_rxvt_extensions ())
            return;
    
    #ifdef __linux__
        handle_console_linux (action);
    #elif defined (__FreeBSD__)
        handle_console_freebsd (action);
    #endif
    

    Хм… Оказывается, в linux и в FreeBSD это обрабатывается по-разному. (А как же solaris? Нету больше вашего solaris, Oracle на пульт валенок бросил.)

    Смотрим handle_console_linux, через него console_save. Что мы видим? MC форкает отдельный процесс с названием cons.saver, который делает ioctl, чтение и запись в странные устройства /dev/vcsa*. Что за устройства?

    О, новый, дивный мир


    Оказывается, в linux есть псевдоустройства, которые соответствуют памяти CGA (VGA) адаптера в текстовом режиме, где данные хранятся в виде массива

    struct {
    char Char;
    char Attrib;
    }

    Да-да, тот старый давно забытый DOS, с прямым доступом в видео-память. Проверяем — действительно, hexdump файлов вполне показывает нам содержимое экрана. Заметим, для доступа к устройству нужно иметь права на это. Обычно это делается так: на файл устройства выставляют группу, на файл выставляют owner'а, входящего в эту группу и выставляют ему sgid-бит.

    В Debian cons.saver имеет sgid бит для группы tty (общая группа для доступа к псевдотерминалам), а в centos — для специальной группы vcsa. В принципе, соглашусь с подходом RHEL/CentOS, это более безопасно.

    Итак, специальный файл… То есть MC с помощью хитрого хака читает содержимое экрана, а когда нужно, пишет его обратно…

    Но у виртуальной машины консоль не на VGA-адаптере! Это чистой воды последовательный порт. И простая проверка подверждает подозрения — на всех виртуалках vcsa девственно чистый, потому что у нас нет VGA-адаптера. То бишь, видеокарты.

    Что делать? Ну, для начала проверить, что будет делать mc в ситуации с последовательным портом. Как мы знаем, нет никакой разницы между псевдотерминалом UNIX98 и последовательным портом. Идём обратно в настоящий linux (консоль), логинимся по ssh на любой сервер, запускам mc, и, вуаля — not supported.

    Таким образом, мы, делая консоль на последовательном порту, в принципе не можем предоставить аналог vcsa — и хак в mc насчёт ctrl-o не может работать. В принципе.

    А как же xterm?


    Реализовать ESC-код для xterm'а было бы просто. Но… Мы не имеем доступа к содержимому виртуальных машин, мы не можем менять среду окружения пользователям — а значит, по-умолчанию, linux ожидает, что на последовательном порту у него тип терминала linux (TERM=linux) и реализовывать функции xterm/xrvt нет смысла — их всё равно не будут использовать.

    На всякий случай проверяем, каким образом mc узнаёт про существование xterm-расширений…

        const char *termvalue;
    
        termvalue = getenv ("TERM");
    


    Увы — переменная среды окружения (а я надеялся на query string). Ну точно нельзя. Увы.

    Кстати, ровно тот же хак (но с поправкой на особенности архитектуры) используется и в FreeBSD. Увы.

    Мораль?


    У нас не будет полноценно работать Ctrl-O в нашей замечательной консоли в линуксе. Увы, никогда. А в остальном было интересно.

    … Кстати, если у вас есть живой сервер, и он жив, вы можете удалённо посмотреть, что у него там на консолях происходит — cat /dev/vcsa*. А особые фанаты могут сделать watch и наблюдать, как на консолях работают другие.
    Selectel
    IT-инфраструктура для бизнеса

    Комментарии 47

      0
      Я не совсем понял, о чем речь.

      >>Некоторые программы, показывая содержимое, восстанавливают
      >>экран до запуска приложения
      Oк.

      >> Идём в консоль linux (настоящую, Ctrl-Alt-F1), пробуем vim —
      >>при выходе мы видим предыдущее содержимое на экране.
      >>less… видим. nano… видим.
      Ok

      >>То есть linux (согласно console_codes) этого не поддерживает.
      вот тут я запутался. Какой именнно эффект не поддерживается?

      >>Простая проверка подверждает подозрения —
      >>на всех виртуалках vcsa девственно чистый,
      >>потому что у нас нет VGA-адаптера.

      Проверил в виртуалке на KVM: cat /dev/vcsa*
      получил семь консолей с приглашением login.
      Проверил на xen:
      cat: /dev/vcsa*: No such file or directory

      >>Идём обратно в настоящий linux (консоль),
      >>логинимся по ssh на любой сервер, запускам mc,
      >>и, вуаля — not supported.
      Опять не понял, что именно «not supported». Можно дать подробное описание эффекта, которого необходимо достичь? Я из статьи понял, что вам нужно восстановление экрана до запуска приложения? Но, если всё так, как вы описываете, почему в putty все отлично работает?

      Спасибо.

        +1
        console_codes (точнее, консоль линукса) не поддерживает secondary display. Как увидеть разницу: пойти в консоль, запустить mc, нажать Ctrl-O. Выйти из mc, из той же консоли подключиться по ssh к чему угодно, запустить mc, нажать ctrl-o, заметить разницу.

        В putty работает, потому что путти реализует не linux, а xterm.
          +1
          Как увидеть разницу: пойти в консоль, запустить mc, нажать Ctrl-O. Выйти из mc, из той же консоли подключиться по ssh к чему угодно, запустить mc, нажать ctrl-o, заметить разницу.

          Эмм, никакой разницы. На удалённом хосте показывает TERM=linux, как и ожидалось, но ctrl-o работает. Debian Sid, mc 4.7.0.9-1
            +2
            Пожалуйста, закройте ваш любимый X-server, и выйдите в консоль (которая Ctrl-Alt-F1). gnome-console, putty, xterm, rxvt, konsole и т.д. не годятся — нужно именно консоль линукса.
              0
              Я именно так и делал.
                0
                Либо вы после этого забыли подключиться по ssh (или, ещё вариант, запустили ssh не выйдя из mc).

                Я выше цитировал код — на удалённой машине mc просто физически не сможет получить доступ к файлам /dev/vcsa* на локальной машине.
                  0
                  Я ничего не забыл и ещё раз проверил. Я понимаю ваше объяснение и цитаты из кода и именно поэтому написал о том, что я наблюдаю другой эффект.
                    +1
                    Ну, значит у вас произошло мистическое чудо и программа работает не так, как написано в её исходных текстах.

                    К сожалению, на чудесах я не специализируюсь.
                      0
                      у меня тоже работает.
                        0
                        а, вижу, понял о чём речь.
                    0
                    Да, и ещё я не вижу чтобы запускался cons.saver (хотя сам бинарник в системе есть).
                      0
                      906 if (!xterm_flag && !console_flag && !use_subshell && !output_starts_shell) {
                      (gdb) p use_subshell
                      $4 = 1


                      Вот поэтому у меня мессадж и не выводится. Другой вопрос как же всё-таки панели прячутся…
                        +1
                        Панели-то прячутся, но содержимое того, что было под панелями на экран не выводится по нажатию на ctrl-o, только чёрный экран.
                          0
                          Действительно! Теперь всё понятно, спасибо.
                0
                Ага, вот теперь понятно, еще раз спасибо.
              0
              У вас же ядра всё-равно не абы какие, что мешает малость пропатчить и добавить эти устройства?
                +2
                У нас эти устройства есть. Но тут нужно понимать, что зен не эмулирует vga-адаптер, он предоставляет виртуальный последовательный порт.

                Отсюда, кстати, и заморочки с необходимостью рендеринга esc-последовательностей за пределами машины. Если бы был VGA-адаптер, то мы бы мирно-тихо читали/писали картинку, рендерингом которой бы занимался линукс.

                Т.к. последовательный порт, то говорить про «устройство с копией видеопамяти» в принципе не имеет смысла.
                  0
                  Так и не нужен VGA-адаптер. От устройства требуется предоставлять матрицу nxm, где лежат символы. Почему так проблемно прокинуть этот интерфейс до софтины в Dom0?
                    0
                    написать свое устройство для зена сложно. как минимум, это два модуля ядра.

                    Далее, передача этих данных между доменами тоже не слишком просто.

                    и главное — даже если я создам такое устройство, то его не признает mc. А единственный метод его обмануть — хакнуть major от vga. мягко говоря, не очень мудрое решение.
                0
                > suid-бит.

                sgid?
                  0
                  да, спвсибо, сейчас поправлю.
                  0
                  А почему для эмуляции терминала не использовать screen? Не получалось забрать «отрендерённый» вывод с браузером? Или ещё что-то?
                    0
                    screen нужно ещё декодить, раз (сам screen тоже рисует esc-кодами, так что это шило на мыло), во-вторых у него нет нормальной сериализации, в третьих проблема с подсовыванием/забиранием ввода-вывода, в четвёртых screen имеет свой собственный TERM, который пользователям бы пришлось настраивать вручную. Плюс проблема выключенных машин.

                    Геморроя не меньше, а гибкости существенно меньше.
                    0
                    Раз в шарите, расскажите как работает мышь в MC? Щелкнул — выделился файл.

                    Ну вообще как события мыши обрабатывается в локальной консоли, удаленной. Вы по-любому знаете.
                      –1
                      Вообще проблема мыши в консолях и терминалах (особенно в TUI) до сих пор не имеет готового стандартного решения (причем ни в никсах ни даже в винде).
                      Поддержка мыши изначально есть в GUI-терминалах, и если МС запущен в окне графического терминала, то, на мышь он корректно реагирует.
                      Если я не ошибаюсь, то в GUI-терминалах можно даже эмулировать МУЛЬТИТАЧ.
                      Однако вменяемой поддержки мыши (а тем более мультитача) в TUI-терминалах я к сожалению не встречал.
                      Поэтому у меня даже была мысль написать свой собственный TUI-терминал с поддержкой мыши (в том числе мультитача) и спрайт-анимации.
                      • НЛО прилетело и опубликовало эту надпись здесь
                          0
                          Есть набор ESC-кодов, MC может послать терминалу сообщение «давай сюда мышь», в этом случае нажатия кнопок мыши передаются как ESC-коды.

                          Мы не делали, потому что тогда возможность копирования текста сломается.
                            –1
                            Во-первых, в MC встроенное копирование текста там, где это нужно. Т.е. в редакторе эти кнопки-и-положения-мышки-как-ESC-коды как раз очень даже нужны.

                            Во-вторых, во всех известных мне терминалках это поведение «эскейпится» — если человеку нужно срочно именно покопировать какой-то текст с экрана из приложения, которое «захватило мышку» — можно зажать Shift и тогда мышка начинаешь работать традиционно — выделять кусок текста на экране и копировать его в primary selection.
                          0
                          > откуда её позаимствовали все последующие консольные клоны NC: Dos Navigator, Volkov Commander, Far Navigator Manager и т.д.
                            +2
                            поправил
                            0
                            А в чем драматичность проблемы-то? Я так понимаю, что у вас один эмулятор терминала, для него вполне можно прописать банально, что он работает только с TERM=xterm и всё. Для чего может быть нужна поддержка TERM=linux — и, самое главное, чем вы тут хуже всех остальных эмуляторов терминалов, которые толком тоже с ней не работают?
                              +1
                              >Я так понимаю, что у вас один эмулятор терминала, для него вполне можно прописать банально, что он работает только с TERM=xterm и всё
                              Это нужно будет объяснять каждому пользователю. Каждый пользователь должен будет вручную указывать $TERM.
                                0
                                Вообще у пользователя в теории может быть миллион других значений TERM — не смущает? Вот люди из-под BSD будут ходить с какими-нибудь TERM=con80x25 и точно так же огребать проблем, пока не настроят TERM.

                                TERM=linux — это вообще очень странная по нынешним временам штука. Подавляющее большинство ходят либо из TERM={xterm,rxvt,rxvt-unicode} (либо юниксовые терминалки, либо PuTTY), либо TERM={screen,screen.linux,screen.bsd} (который почти тот же xterm).
                                  +1
                                  Эм… Этот терминал висит на виртуальном RS232 в виртуальном линуксе. Там точно TERM=linux.
                                  >либо PuTTY
                                  И рисуется в браузере. Зачем — можешь прочитать предыдущие посты.
                                    0
                                    Ребят, я, возможно, тупой, но я не понимаю, как из того, что терминал проходит свой транспортный путь через «RS232 в виртуальном линуксе» следует то, что TERM=linux. У меня вот AIX есть, который через Serial-over-IPMI (тоже своего рода виртуальный RS232) отдает свой терминал — и там TERM будет такой, каким его выставлю _я_, прийдя туда каким-нибудь эмулятором терминала. Собственно, TERM и задает обычно сам эмулятор терминала — запуская под себя на удаленном конце первое приложение, с которым будет общаться (login или shell ли), неким образом передается, какие переменные среды можно начиная с этого приложения выставить.
                                      +1
                                      >запуская под себя на удаленном конце первое приложение
                                      Тут ничего не запускается. Терминал работает, начиная с загрузки.
                                        0
                                        Там login есть? Думаю, что есть. Вот в нем и выставить, нет?
                                          +1
                                          Кхм. login запускается инитом/*getty, не терминалом. И не такой уж факт, что он есть.
                                            0
                                            Если речь идет об init'е, то там будет вот такая строчка в inittab, так?

                                            T0:23:respawn:/sbin/getty -L ttyS0 115200
                                            

                                            И, внезапно, именно сюда, в getty и можно прописать getty -L ttyS0 115200 xterm, чтобы в этом терминале был именно TERM=xterm с самого начала.
                                              +2
                                              >И, внезапно, именно сюда, в getty и можно прописать getty -L ttyS0 115200 xterm, чтобы в этом терминале был именно TERM=xterm с самого начала.
                                              Влезть в *чужую* виртуальную машину, чтобы прописать это?
                                                0
                                                В нее так или иначе влезать, чтобы прописать эту строчку. По умолчанию оно практически во всех известных мне современных дистрибутивах отключено. Да и ставятся там скорее всего не дистрибутивы с нуля, а все-таки клонируются система из каких-то заранее заготовленных образов — в них и прописывают обычно такое.
                                                  0
                                                  Системы ставятся полностьюс нуля
                                                    0
                                                    Как ни странно, этот топик я читал, только вот назвать это «чистой установкой с нуля», извините, я не могу. Там так или иначе для каждого дистрибутива делается свой набор управляющих воздействий (которые, кстати, в этой статье и описаны — для preseed, для kickstart и для YaST). И в любом случае есть какая-то стадия донастройки системы заранее заготовленными скриптами.
                                                      +2
                                                      А с существующими системами что делать?
                                                        0
                                                        А сейчас вы с ними что делаете? Каким образом добавляется вывод на сериальную консоль?
                                                          +1
                                                          Штатное при установке. Инсталлеры тоже, кстати, на последовательный порт рисуют.
                                                            0
                                                            «Штатное при установке» — оно у всех немножко разное, и, боюсь, что если уж его брать — то там как раз у большинства будет стоять что-то вроде TERM=vt100, а не TERM=linux.

                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                            Самое читаемое