Selectel corporate blog
15 June 2011

Сага о том, как мы писали консоль

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

Извините фальстарт, это не я, это андроидный смартбук.

Когда мы только запускали облако, первой проблемой было «как нам получить консоль». Штатный механизм XCP поразумевает, что консоль рисуется с помощью VNCTerm, а желающий её увидеть должен сначала пойти в XenAPI, получить там session-id консоли, пойти на порт консоли, передать session-id, получить RFB, завёрнутый в HTTP, развернуть HTTP, вынуть RFB (он же VNC), отдать её локальному рендереру VNC (VNC-клиенту или java-апплету с тем же функционалом). При этом консоль закрывалась (сессия рвалась) при каждой перезагрузке виртуальной машины. Она рвалась даже при миграции виртуальной машины. Другими словами, это была технология, которая подразумевала «глянул одним глазком, починил ssh/iptables и забыл». Неудобно, медленно, сложно. Выкатывать такое в продакт совсем не хотелось.

И я залез в дебри serial-howto, console-howto и ещё несколько ужасных документов, рассказывающих о том, как правильно нужно конфигуриовать прерывания на ISA плате у мультикарт, а так же специфику настройки linux-2.2 для работы с оными. Параллельно изучалось устройство консоли в зене (внимательный читатель мог даже заметить, когда именно я более-менее разобрался в этом вопросе — я писал на хабре краткий обзор того, что происходит с консолью).

После этого пришла мысль: нужно писать своё, потому что готового чужого хорошего нет.

Сначала мы хотели взять хотя бы готовые компоненты и сделать из них своё. Я помню до сих пор ту замечательную схему, в которой мы планировали сохранять в БД вывод anyterm'а, делать двойное туннелирование последовательного порта с использованием UDP… Выглядело это, мягко скажем, неприглядно.

Потом пришла в голову мысль выпилить anyterm. Для этого нужно было посмотреть, как работают терминалки. Это было очень забавно и поучительно (желающие могут изучить исходный текст PuTTY). Главной проблемой в этом изучении было то, что они много рисуют на экран. Прямо в процессе обработки ввода. Отделить специфику DC от, собственно, того, что является консолью, было сложно.

Через некоторое время мы пришли к идее «нам нужен свой эмулятор терминала».
Задача казалась относительно простой, пока мы не прикоснулись к бездне, именуемой «escape-коды и типы терминалов...».

Пишущие машинки


Итак, в начале была пишущая машинка. В какой-то момент возникло желание совместить телеграф с пишущей машинкой. Так возник телетайп
Разумеется, инженерам, создававшим телетайп, не было никакого резона делать все с нуля. Они просто приделали коды к каждой клавише пишущей машинки. После некоторых боёв в стиле MS VS Netscape, был создан стандарт html5 на коды для оных машинок, то бишь телетайпов. Если мне память не изменяет, то это ASCII, где предусмотрены все комбинации клавиш, характерные для американской пишущей машинки. Включая код BELL, который, кстати, должен вовсе не делать BEEP, а делать «дзыньк», ибо у пишущих машинок был именно колокольчик, а не спикер.



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

Когда кто-то догадался приделать к мейнфрейму монитор и клавиатуру, то он естественным образом постарался обеспечить совместимость с существующими печатающими устройствами. Потом, когда производители начали делать новые модели терминалов, они старались обеспечить поддержку хотя бы базового набора функционала предыдущих версий… Цепочка эта тянулась очень долго vt52 (несовместимый с последующими) — и славная vt100, vt102, vt120, vt200, vt220, vt320, vt420, vt520…

Эмуляторы терминалов, эмулирующих телетайп, эмулирующего пишущую машинку


Потом, когда писали линукс, для возможности запускать программы (вроде vim'а, emacs'а или mc) не оставалось ничего, как реализовать поддерку эмулятора терминала… Тем же путём, кстати, пошёл UNIX и его клоны: solaris, FreeBSD, hp-ux и т.д.

Для графического режима были написаны свои эмуляторы терминалов — самые совершенные из них — это xterm и rxvt (совершенный — это значит, поддерживающий всё, а не самый удобный/красивый). Кстати, самые распространённые — gnome-terminal и konsole — поддерживают далеко не всё и далеко не всё, что поддерживают, обрабатывают правильно (желающие могут сами запустить vttest на своих любимых терминалах и посмотреть на это безобразие).

Все эти эмуляторы терминалов делали одно и то же — они эмулировали работу терминала, который эмулирует терминалы предыдущих поколений, которые эмулируют телетайп, которые эмулируют пишущую машинку.

Пишущие машинки never die!


Живые примеры:

Например, все уважающие себя терминалы имеют настройку издавать BELL за 8 символов до конца строки. Эта функция бесполезна на терминале — но очень полезна для пишущей машинки.

Почему мы имеем раздельно коды «Возврат каретки» (догадайтесь, о каретке какой именно машинки идёт речь) и «перевод строки»? Разумеется, потому что на пишущих машинках это были раздельные действия (на наиболее совершенных машинках был рычаг, совмещающий в себе CR/LF).

Более тонкий и неочевидный пример. Почему символ Backspace не удаляет символ слева, а только сдвигает курсор, а delete символ справа удаляет, да ещё и со сдвигом всей строки?

Очевидно: на пишущих машинках была кнопка backspace. И, поскольку пишущая машинка может только двигать каретку туда-сюда, а удалять символы не может, то и backspace символ не удаляет. А delete на пишущих машинках не было — и причин делать обратную совместимость не было.

Матричные принтеры тоже оставили свой неизгладимый оттиск в линуксе. Программа pager умеет эмулировать матричный принтер. Когда при выводе на матричный принтер хотели сделать текст жирным или подчеркнуть, то каретку сдвигали влево и печатали текст снова (или печатали знаки подчёркивания поверх существующего текста). Если в программу pager (pg) скормить такой текст, то она покажет его соответствующе (сэмулирует матричный принтер). Впрочем, я отвёкся от консоли.

Ещё одной вещью, которая сильно повлияла на современную консоль, было то, как выводилась картинка на экран. Это повлияло на текстовый режим CGA (который, в свою очередь, немало повлиял на линукс). Чтобы понять как это происходило, нужно вспомнить, как выводили текстовое изображение примитивные текстовые экраны:

В памяти хранились две области — атрибутов и символов (в некоторых реализациях через один, в некоторых — как две раздельные области памяти). Цветовые атрибуты использовались как бинарные ключи для подачи напряжения со стороны R, G, B каналов. Последовательность битов (результат сканирования линии битов из знака шрифта, выбранного по индексу (коду) выводимого символа) коммутировала между двумя вариантами сигнала — цветом фона и цветом тона.

Появившийся позже атрибут «яркость» всего лишь накидывал к итоговому сигналу дополнительное напряжение. Это же объясняет, почему «ярко чёрный» для консоли, это не deep black, а всего лишь серый. Ну и совсем неожиданным является то, что ярко чёрный по ярко чёрному — вполне различим, он выглядит как тёмно-серный по почти чёрному.

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

Внутреннее устройство терминала определило и множество других интересных моментов. Например, куда мы должны пойти, если нам передают номера строк и колонок за пределами допустимых значений? Очевидно — в последнее разрешенное значение. А если очень большое? Разумеется, у нас координаты в int8/int16, так что: делать кольцевое переполнение — вполне естественно.

Ещё одна замечательная вещь, которая пришла в XXI век из века XIX — это табстопы. Когда пишущие машинки стали совершенствоваться, возникла проблема ввода текста в несколько колонок. Решение было изящным — сзади пишущей машинки был длинный ряд штифтов, каждый рамером примерно в ширину символа. Штифт мог быть в нормальном состоянии или вдавленном. Для вдавливания штифта в текущей позиции использовался специальный рычаг. Другой рычаг отпускал каретку (которую тянула пружина) так, чтобы каретка остановилась на заданном табстопе. Ввод текста в этом случае выглядел так «ввести текст, нажать табуляцию», ввести следующий текст.Самые продвинутые пишущие машинки, вроде Башкирия-М, имели кнопки «2Т», «3Т», которые позволяли пропустить несколько табуляций. Как это было реализовано в механике — не знаю. Помимо установки табстопов (их, очевидно, можно было сделать практически на каждой колонке), был рычак «сброса табуляций», который просто «отжимал» все штифты. И разумеется, все терминалы, начиная с vt102 и заканчивая linux-3.0rc2 поддерживают esc-код для «отжатия штифтов табуляции пишущих машинок».

Наша работа


Когда мы писали терминал много было непонятно, например, какими становятся табстопы после сброса? (Читатели, воспитанные в msdos неправильно ответят «8»). Ответ нужно смотреть в пишущих машинках. Какие табстопы остаются после отжима всех штифтов? Разумеется, никакие. Пока руками их не сделаешь, ничего не появится. Именно потому команда reset, первые копирайты в которой были поставлены ещё до моего рождения, в ходе сброса терминала посылает код «сброс табстопов», а потом проходится по экрану порциями по 8 пробелов и расставляет столь желанные табстопы через каждые 8 символов. Хотя разные типы терминалов могут воображать себе разный размер «табов по умолчанию».

В ходе нашей работы над эмулятором терминала, именно эта «железная логика», которая становилась понятна только при воспоминаниях о пишущих машинках, терминалах с 8кб набортной памяти, аппаратным схемам развёртки сигнала и т.д. доставляля наибольшие мучения для программистов. Собственно, для программиста — bobry — автор нашей библиотеки эмуляции терминала.

Многие поколения терминалов, их эмуляторов и т.д. привели к нескольким стандартам разной степени размытости и железному правилу писателя софтового эмулятора терминала — mimic legacy. Полноценной документации по коду терминалок нет ни у кого (даже мануал по vt102, огромный талмуд, всё же имеет довольно много мутных мест).

Ситуацию с документацией по терминалам можно описать примерно так: у нас есть отличная документация по терминалам и их кодам, большая её часть написана на Си и может противоречить друг другу.

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

Между первым работающим прототипом и релизом прошло примерно два месяца — и эти два месяца нам всё время казалось, что большей частью оно работает, остались мелочи.

Собственно, главный виновник торжества, основной и главный автор библиотеки — это bobry, мужественно сражавшийся с бездной легаси, си и longjump'ов в коде xterm'а (под мой аккомпанимент троллинга на тему compability с пишущими машинками и amazing stories про природу табстопов — бабушкина «Баширия-3М» всё-таки пригодилась, не зря я с ней возился в голодные и холодные 90ые в районе 90-93 годов...). Вполне серьёзно могу сказать, что он на настоящий момент — один из немногих программистов в стране (особенно — молодых программистов, не заставших PL/I в продакте), который хорошо знает про то, как устроена консоль линукса, знает esc-коды и прочие ужасы тайной кухни эмуляции терминалов… Кстати, название ncurses — библиотеки для рендеринга изображения с использованием esc-кодов, вообще говоря, идёт от английского to curse — высказывать благодарность за удобную и понятную вещь.

Впрочем, похвалю нас ещё раз:

В настоящий момент наша библиотека — одна из лучших, и уж точно лучшая среди тех, которые не рисуют сразу же на экран. Кстати, это главная особенность нашей библиотеки — она позволяет оперировать с изображением терминала в отрыве от того, как оно рисуется. Именно это и позволило нам, например, легко и непринуждённо писать на терминал сообщение о том, что виртуальная машина была завершена принудительно (если пользователь убивает машину принудительно). Сама виртуальная машина к этому моменту уже остановлена — и мы, хитрой сапой просто дописываем в неё сообщение о завершении работы. Обычные игры с vncterm такого сделать не позволят.

В следующей части я расскажу подробнее об этой библиотеке, кроме того, там будет ещё одна драма современного человечества — браузеры и их понимание того, что такое «нажатая кнопка» и как мы сделали работающим paste в браузерах.

+119
18.8k 75
Comments 91
Top of the day