Статья опубликована 2 августа 2018 года
Это вторая статья про командную строку Windows, где мы обсудим новую инфраструктуру и программные интерфейсы псевдоконсоли Windows, то есть Windows Pseudo Console (ConPTY): зачем мы её разработали, для чего она нужна, как работает, как её использовать и многое другое.
В прошлой статье «Тяжкое наследие прошлого. Проблемы командной строки Windows» мы рассказали о предпосылках появления терминала и эволюции командной строки в Windows, а также начали изучать внутреннее устройство Windows Console и инфраструктуры Windows Command-Line. Мы также обсудили многие преимущества и главные недостатки консоли Windows.
Один из недостатков заключается в том, что Windows пытается быть «полезной», но мешает разработчикам альтернативных и сторонних консолей, разработчикам служб и т.д. При создании консоли или службы разработчикам нужно иметь доступ к каналам связи, по которым их терминал/служба обменивается данными с приложениями командной строки, или предоставлять доступ к ним. В мире *NIX это не проблема, потому что *NIX предоставляет инфраструктуру «псевдотерминала» (PTY), которая позволяет легко создавать коммуникационные каналы для консоли или службы. Но в Windows такого не было…
… до настоящего времени!
Прежде чем подробно рассказать о нашей разработке, давайте кратко вернёмся к развитию терминалов.
Как обсуждалось в прошлой статье, в первые дни вычислений пользователи управляли компьютерами с помощью электромеханических телетайпов (TTY), подключённых к компьютеру через какой-то последовательный канал связи (обычно через токовую петлю 20 мА).
![](https://habrastorage.org/r/w780q1/getpro/habr/post_images/f7b/e47/7fa/f7be477fac4e46cc71e70df23b26bef6.jpg)
Кен Томпсон и Деннис Ричи (стоя) работают на DEC PDP-11 по телетайпу (сообщения без электронного дисплея)
На смену телетайпам пришли компьютеризированные терминалы с электронными дисплеями (обычно ЭЛТ-экранами). Как правило, терминалы — очень простые устройства (отсюда термин «тупой терминал»), содержащие только электронику и вычислительную мощность, необходимую для следующих задач:
Несмотря на простоту (а может, благодаря ей), терминалы быстро стали основным средством для управления миникомпьютерами, мейнфреймами и серверами: большинство операторов ввода данных, компьютерных операторов, системных администраторов, учёных, исследователей, разработчиков программного обеспечения и светил индустрии работали на терминалах DEC, IBM, Wyse и многих других.
![](https://habrastorage.org/r/w780q1/getpro/habr/post_images/929/e3f/fd0/929e3ffd0e281600c9ef115f61b8b23a.jpg)
Адмирал Грейс Хоппер в своём офисе с терминалом DEC VT220 на столе
Начиная с середины 1980-х годов вместо специализированных терминалов постепенно начали применяться компьютеры общего назначения, которые становились более доступными, популярными и мощными. Во многих ранних ПК и других компьютерах 80-х годов имелись терминальные приложения, которые открывали соединение по порту RS-232 на ПК и обменивались данными с кем угодно на другом конце соединения.
По мере того как компьютеры общего назначения становились всё более изощрёнными, появился графический пользовательский интерфейс (GUI) и целый новый мир одновременно работающих приложений, включая терминальные приложения.
Но возникла проблема: как терминальному приложению взаимодействовать с другим приложением командной строки, запущенным на той же машине? И как физически подключить последовательный кабель между двумя приложениями, работающими на одном компьютере?
В мире *NIX проблему решили введением псевдотерминала (PTY).
PTY эмулирует серийное телекоммуникационное оборудование в компьютере, выставляя ведущее и ведомое псевдоустройства (“master” и “slave”): терминальные приложения подключаются к ведущему псевдоустройству, а приложения командной строки (например, оболочки вроде cmd, PowerShell и bash) — к ведомому псевдоустройству. Когда терминальный клиент передаёт текст и/или команды управления (закодированные как текст) ведущему псевдоустройству, текст транслируется на связанное с ним ведомое. Текст от приложения направляется на ведомое псевдоустройство, затем обратно на ведущее и, таким образом, на терминал. Данные всегда отправляются/принимаются асинхронно.
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/a42/2ba/b58/a422bab5816acdd43dd7b1c562d65caf.png)
Приложение/оболочка псевдотерминала
Важно отметить, что «ведомое» псевдоустройство эмулирует поведение физического терминала и преобразует командные символы в сигналы POSIX. Например, если пользователь вводит в терминал CTRL+C, то значение ASCII для CTRL+C (0x03) отправляется через ведущее устройство. При получении на ведомом псевдоустройстве значение 0x03 удаляется из входного потока и генерируется сигнал SIGINT.
Такая инфраструктура PTY широко используется терминальными приложениями *NIX, менеджерами текстовых панелей (например, screen, tmux) и т.д. Данные приложения вызывают
Этот механизм позволяет терминальным приложениям «разговаривать» непосредственно с приложениями командной строки, запущенными локально, как терминал разговаривал бы с удалённым компьютером через последовательное/сетевое соединение.
Как мы обсуждали в предыдущей статье, в то время как консоль Windows концептуально похожа на традиционный терминал *NIX, она отличается несколькими ключевыми способами, особенно на самых низких уровнях, которые могут вызвать проблемы у разработчиков приложений командной строки Windows, сторонних терминалов/консолей и серверных приложений:
Многие, многие разработчики часто просили PTY-подобный механизм под Windows, особенно те, кто работает с инструментами ConEmu/Cmder, Console2/ConsoleZ, Hyper, VSCode, Visual Studio, WSL, Docker и OpenSSH.
Даже Питер Брайт — технологический редактор Ars Technica — попросил внедрить механизм PTY через несколько дней, как я начал работать в команде Console:
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/2dd/799/ebf/2dd799ebf0c14f418ae7d68d9a26acaa.png)
И недавно ещё раз:
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/4d1/c1e/c00/4d1c1ec00ffe5ad5e6f019d1a33198c8.png)
Что ж, мы наконец-то сделали это: мы создали псевдоконсоль для Windows:
С момента образования Console Team около четырёх лет назад группа занималась капитальным ремонтом консоли Windows и внутренних механизмов работы командной строки. При этом мы регулярно и тщательно рассматривали описанные выше вопросы и многие другие связанные вопросы и проблемы. Но инфраструктура и код были не готовы, чтобы сделать возможным выпуск псевдоконсоли… до настоящего момента!
Новая инфраструктура псевдоконсоли Windows (ConPTY), API и некоторые другие соответствующие изменения устранят/облегчат целый класс проблем… не ломая обратную совместимость с существующими приложениями командной строки!
Чтобы понять ConPTY, нужно изучить архитектуру консоли Windows, а точнее… ConHost!
Важно понимать, что хотя ConHost реализует всё, что вы видите и знаете как приложение Windows Console, но ConHost также содержит и реализует большую часть инфраструктуры командной строки Windows! Отныне же ConHost становится настоящим «консольным узлом», поддерживая все приложения командной строки и/или GUI-приложения, которые взаимодействуют с приложениями командной строки!
Как? Почему? Что? Давайте разберёмся подробнее.
Вот высокоуровневое представление внутренней архитектуры консоли/ConHost:
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/331/1bd/54d/3311bd54dbe80dc699ff243a4812b58a.png)
В сравнении с архитектурой из предыдущей статьи, ConHost теперь содержит несколько дополнительных модулей для обработки VT и новый модуль ConPTY, реализующий открытые API:
Хорошо, но что это на самом деле значит?
Чтобы лучше понять влияние новой инфраструктуры ConPTY, давайте рассмотрим, как до сих пор работали консольные приложения Windows и приложения командной строки.
Всякий раз, когда пользователь запускает приложение командной строки, такое как Cmd, PowerShell или ssh, Windows создаёт новый процесс Win32, в который загружает исполняемый двоичный файл приложения и любые зависимости (ресурсы или библиотеки).
Вновь созданный процесс обычно наследует дескрипторы stdin и stdout от своего родителя. Если родительский процесс был процессом Windows GUI, то дескрипторы stdin и stdout отсутствуют, поэтому Windows развернёт и присоединит новое приложение к новому экземпляру консоли. Связь между приложениями командной строки и их консолью передается через ConDrv.
Например, при запуске из экземпляра PowerShell без повышенных прав новый процесс приложения унаследует родительские дескрипторы stdin/stdout и, следовательно, получит входные данные и выдаст выходные данные в ту же консоль, что и родитель.
В конечном счёте, когда запускается приложение командной строки/оболочка, Windows подключает его к экземпляру консоли (ConHost.exe) через ConDrv:
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/fc3/4c8/d37/fc34c8d37f6f31c843422193bd5d01a6.png)
Всякий раз, когда выполняется приложение командной строки, Windows подключает приложение к новому или существующему экземпляру ConHost. Приложение и его экземпляр консоли подключаются через драйвер консоли режима ядра (ConDrv), который отправляет/получает сообщения IOCTL, содержащие сериализованные запросы вызовов API и/или текстовые данные.
Исторически, как указано в предыдущей статье, работа ConHost сегодня относительно проста:
Когда приложение командной строки вызывает Windows Console API, то вызовы API сериализуются в сообщения IOCTL и отправляются через драйвер ConDrv. Он затем доставляет сообщения IOCTL на присоединённую консоль, которая декодирует и выполняет запрошенный вызов API. Возвращаемые/выходные значения сериализуются обратно в сообщение IOCTL и отправляются обратно в приложение через ConDrv.
Microsoft старается по возможности поддерживать обратную совместимость с существующими приложениями и инструментами. Особенно для командной строки. На самом деле 32-битные версии Windows 10 ещё могут запускать многие/большинство 16-битных приложений и исполняемых файлов Win16!
Как упоминалось выше, одна из ключевых ролей ConHost заключается в предоставлении сервисов своим приложениям командной строки, особенно устаревшим приложениям, которые вызывают и полагаются на консольный API Win32. Теперь ConHost предлагает и новые сервисы:
Ниже приведен пример, как современное консольное приложение общается с приложением командной строки через ConPTY ConHost.
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/d72/7db/35e/d727db35e65b2e0eec45d0d535b4de83.png)
В этой новой модели:
Последний момент важен! Когда старое приложение командной строки использует вызовы к Console API вроде
В конечном счёте, даже традиционные приложения командной строки снаружи «говорят» текстом/VT без каких-либо изменений!
Используя новую инфраструктуру ConPTY, сторонние консоли теперь могут напрямую взаимодействовать с современными и традиционными приложениями командной строки и обмениваться со всеми ними данными в тексте/VT.
Описанный выше механизм отлично работает на одном компьютере, но также помогает при взаимодействии, например, с инстансом PowerShell на удалённом компьютере Windows или в контейнере.
При удалённом запуске приложения командной строки (т.е. на удалённых компьютерах, серверах или в контейнерах) имеется проблема. Дело в том, что приложения командной строки на удалённых машинах общаются с локальным инстансом ConHost, потому что сообщения IOCTL не предназначены для передачи по сети. Как же передать ввод из локальной консоли на удалённую машину и как получить выдачу из приложения, запущенного там? Более того, что делать с машинами Mac и Linux, где есть терминалы, но нет Windows-совместимых консолей?
Таким образом, чтобы удалённо управлять машиной Windows, нам нужен какой-то брокер связи, который сможет прозрачно сериализовать данные по сети, управлять временем жизни экземпляра приложения и т.д.
Возможно, что-то вроде ssh?
К счастью, OpenSSH недавно портировали на Windows и добавили как дополнительную опцию Windows 10. PowerShell Core также использует ssh в качестве одного из поддерживаемых протоколов удалённого взаимодействия PowerShell Core Remoting. А для тех, кто работал в Windows PowerShell, удалённое взаимодействие Windows PowerShell Remoting по-прежнему остаётся приемлемым вариантом.
Давайте посмотрим, как сейчас OpenSSH для Windows позволяет удалённо управлять оболочками и приложениями командной строки Windows:
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/30e/7fc/eeb/30e7fceebcbcea167c2d50d448c975a4.png)
В данный момент OpenSSH включает некоторые нежелательные усложнения:
Весело, правда? Вовсе нет! В такой ситуации многое может пойти наперекосяк, особенно в процессе имитации и отправки пользовательского ввода и очистки выходного буфера внеэкранной консоли. Это приводит к нестабильности, сбоям, повреждению данных, чрезмерному потреблению энергии и т.д. Кроме того, не все приложения выполняют работу по снятию не только самого текста, но и его свойств, из-за чего теряется форматирование и цвет!
Наверняка мы можем улучшить ситуацию? Да, конечно, можем — давайте сделаем несколько архитектурных изменений и применим наш новый ConPTY:
![](https://habrastorage.org/r/w1560/getpro/habr/post_images/5e8/677/f1d/5e8677f1da3a8abcf2951f4e539c6470.png)
Из диаграммы видно, что схема изменилась следующим образом:
Такой подход с использованием ConPTY явно чище и проще для службы sshd. Вызовы Windows Console API выполняются полностью в экземпляре ConHost приложения командной строки, который преобразует все видимые изменения в текст/VT. Кто бы не подключался к ConHost, ему необязательно знать, что приложение там вызывает Console API, а не генерирует текст/VT!
Согласитесь, что этот новый механизм удалённого взаимодействия ConPTY приводит к элегантной, последовательной и простой архитектуре. В сочетании с мощными функциями, встроенными в ConHost, поддержкой старых приложений и отображением изменений от приложений, вызывающих консольные Console API, в виде текста/VT, новая инфраструктура ConHost и ConPTY помогает нам перенести прошлое в будущее.
К настоящему времени я уверен, что вам не терпится увидеть какой-то код ;)
Взглянем на декларации API:
Вышеприведённый ConPTY по API по сути выставляет для использования три новых функции:
Это вторая статья про командную строку Windows, где мы обсудим новую инфраструктуру и программные интерфейсы псевдоконсоли Windows, то есть Windows Pseudo Console (ConPTY): зачем мы её разработали, для чего она нужна, как работает, как её использовать и многое другое.
В прошлой статье «Тяжкое наследие прошлого. Проблемы командной строки Windows» мы рассказали о предпосылках появления терминала и эволюции командной строки в Windows, а также начали изучать внутреннее устройство Windows Console и инфраструктуры Windows Command-Line. Мы также обсудили многие преимущества и главные недостатки консоли Windows.
Один из недостатков заключается в том, что Windows пытается быть «полезной», но мешает разработчикам альтернативных и сторонних консолей, разработчикам служб и т.д. При создании консоли или службы разработчикам нужно иметь доступ к каналам связи, по которым их терминал/служба обменивается данными с приложениями командной строки, или предоставлять доступ к ним. В мире *NIX это не проблема, потому что *NIX предоставляет инфраструктуру «псевдотерминала» (PTY), которая позволяет легко создавать коммуникационные каналы для консоли или службы. Но в Windows такого не было…
… до настоящего времени!
От TTY к PTY
Прежде чем подробно рассказать о нашей разработке, давайте кратко вернёмся к развитию терминалов.
Вначале был TTY
Как обсуждалось в прошлой статье, в первые дни вычислений пользователи управляли компьютерами с помощью электромеханических телетайпов (TTY), подключённых к компьютеру через какой-то последовательный канал связи (обычно через токовую петлю 20 мА).
![](https://habrastorage.org/getpro/habr/post_images/f7b/e47/7fa/f7be477fac4e46cc71e70df23b26bef6.jpg)
Кен Томпсон и Деннис Ричи (стоя) работают на DEC PDP-11 по телетайпу (сообщения без электронного дисплея)
Распространение терминалов
На смену телетайпам пришли компьютеризированные терминалы с электронными дисплеями (обычно ЭЛТ-экранами). Как правило, терминалы — очень простые устройства (отсюда термин «тупой терминал»), содержащие только электронику и вычислительную мощность, необходимую для следующих задач:
- Приём текстового ввода с клавиатуры.
- Буферизация введённого текста по одной строке (включая локальное редактирование перед отправкой).
- Отправка/получение текста по последовательному каналу (обычно через некогда повсеместный интерфейс RS-232).
- Отображение полученного текста на дисплее терминала.
Несмотря на простоту (а может, благодаря ей), терминалы быстро стали основным средством для управления миникомпьютерами, мейнфреймами и серверами: большинство операторов ввода данных, компьютерных операторов, системных администраторов, учёных, исследователей, разработчиков программного обеспечения и светил индустрии работали на терминалах DEC, IBM, Wyse и многих других.
![](https://habrastorage.org/getpro/habr/post_images/929/e3f/fd0/929e3ffd0e281600c9ef115f61b8b23a.jpg)
Адмирал Грейс Хоппер в своём офисе с терминалом DEC VT220 на столе
Распространение программных терминалов
Начиная с середины 1980-х годов вместо специализированных терминалов постепенно начали применяться компьютеры общего назначения, которые становились более доступными, популярными и мощными. Во многих ранних ПК и других компьютерах 80-х годов имелись терминальные приложения, которые открывали соединение по порту RS-232 на ПК и обменивались данными с кем угодно на другом конце соединения.
По мере того как компьютеры общего назначения становились всё более изощрёнными, появился графический пользовательский интерфейс (GUI) и целый новый мир одновременно работающих приложений, включая терминальные приложения.
Но возникла проблема: как терминальному приложению взаимодействовать с другим приложением командной строки, запущенным на той же машине? И как физически подключить последовательный кабель между двумя приложениями, работающими на одном компьютере?
Появление псевдотерминала (PTY)
В мире *NIX проблему решили введением псевдотерминала (PTY).
PTY эмулирует серийное телекоммуникационное оборудование в компьютере, выставляя ведущее и ведомое псевдоустройства (“master” и “slave”): терминальные приложения подключаются к ведущему псевдоустройству, а приложения командной строки (например, оболочки вроде cmd, PowerShell и bash) — к ведомому псевдоустройству. Когда терминальный клиент передаёт текст и/или команды управления (закодированные как текст) ведущему псевдоустройству, текст транслируется на связанное с ним ведомое. Текст от приложения направляется на ведомое псевдоустройство, затем обратно на ведущее и, таким образом, на терминал. Данные всегда отправляются/принимаются асинхронно.
![](https://habrastorage.org/getpro/habr/post_images/a42/2ba/b58/a422bab5816acdd43dd7b1c562d65caf.png)
Приложение/оболочка псевдотерминала
Важно отметить, что «ведомое» псевдоустройство эмулирует поведение физического терминала и преобразует командные символы в сигналы POSIX. Например, если пользователь вводит в терминал CTRL+C, то значение ASCII для CTRL+C (0x03) отправляется через ведущее устройство. При получении на ведомом псевдоустройстве значение 0x03 удаляется из входного потока и генерируется сигнал SIGINT.
Такая инфраструктура PTY широко используется терминальными приложениями *NIX, менеджерами текстовых панелей (например, screen, tmux) и т.д. Данные приложения вызывают
openpty()
, который возвращает пару файловых дескрипторов (fd) для ведущего и ведомого устройств PTY. Затем приложение может форкнуть/выполнить дочернее приложение командной строки (например, bash), которое использует свои ведомые fd для прослушивания и возврата текста на подключённый терминал.Этот механизм позволяет терминальным приложениям «разговаривать» непосредственно с приложениями командной строки, запущенными локально, как терминал разговаривал бы с удалённым компьютером через последовательное/сетевое соединение.
Что, нет псевдоконсоли Windows?
Как мы обсуждали в предыдущей статье, в то время как консоль Windows концептуально похожа на традиционный терминал *NIX, она отличается несколькими ключевыми способами, особенно на самых низких уровнях, которые могут вызвать проблемы у разработчиков приложений командной строки Windows, сторонних терминалов/консолей и серверных приложений:
- В Windows нет инфраструктуры PTY: когда пользователь запускает приложение командной строки (например, Cmd, PowerShell, wsl, ipconfig и т.д.), то Windows сама «подключает» новый или существующий экземпляр консоли к приложению.
- Windows мешает сторонним консолям и серверным приложениям: Windows (в настоящее время) не даёт терминалам способ предоставления каналов связи, через которые они хотят взаимодействовать с приложением командной строки. Сторонним терминалам приходится создавать консоли за пределами экрана, отправлять туда введённые пользователем данные и скрапить выдачу, перерисовывая её на собственном дисплее сторонней консоли!
- Только в Windows есть Console API: приложения командной строки Windows полагаются на Win32 Consol API, что снижает переносимость кода, поскольку все остальные платформы поддерживают текст/VT, а не API.
- Нестандартный удалённый доступ: зависимость приложений командной строки от Consol API существенно затрудняет взаимодействие и сценарии удалённого доступа.
Что делать?
Многие, многие разработчики часто просили PTY-подобный механизм под Windows, особенно те, кто работает с инструментами ConEmu/Cmder, Console2/ConsoleZ, Hyper, VSCode, Visual Studio, WSL, Docker и OpenSSH.
Даже Питер Брайт — технологический редактор Ars Technica — попросил внедрить механизм PTY через несколько дней, как я начал работать в команде Console:
![](https://habrastorage.org/getpro/habr/post_images/2dd/799/ebf/2dd799ebf0c14f418ae7d68d9a26acaa.png)
И недавно ещё раз:
![](https://habrastorage.org/getpro/habr/post_images/4d1/c1e/c00/4d1c1ec00ffe5ad5e6f019d1a33198c8.png)
Что ж, мы наконец-то сделали это: мы создали псевдоконсоль для Windows:
Добро пожаловать в псевдоконсоль Windows (ConPTY)
С момента образования Console Team около четырёх лет назад группа занималась капитальным ремонтом консоли Windows и внутренних механизмов работы командной строки. При этом мы регулярно и тщательно рассматривали описанные выше вопросы и многие другие связанные вопросы и проблемы. Но инфраструктура и код были не готовы, чтобы сделать возможным выпуск псевдоконсоли… до настоящего момента!
Новая инфраструктура псевдоконсоли Windows (ConPTY), API и некоторые другие соответствующие изменения устранят/облегчат целый класс проблем… не ломая обратную совместимость с существующими приложениями командной строки!
Новые Win32 ConPTY API (официальная документация будет скоро опубликована) теперь доступны в последних инсайдерских сборках Windows 10 и соответствующих Windows 10 Insider Preview SDK. Они появятся в следующем крупном релизе Windows 10 (где-то осенью/зимой 2018).
Архитектура консоли/ConHost
Чтобы понять ConPTY, нужно изучить архитектуру консоли Windows, а точнее… ConHost!
Важно понимать, что хотя ConHost реализует всё, что вы видите и знаете как приложение Windows Console, но ConHost также содержит и реализует большую часть инфраструктуры командной строки Windows! Отныне же ConHost становится настоящим «консольным узлом», поддерживая все приложения командной строки и/или GUI-приложения, которые взаимодействуют с приложениями командной строки!
Как? Почему? Что? Давайте разберёмся подробнее.
Вот высокоуровневое представление внутренней архитектуры консоли/ConHost:
![](https://habrastorage.org/getpro/habr/post_images/331/1bd/54d/3311bd54dbe80dc699ff243a4812b58a.png)
В сравнении с архитектурой из предыдущей статьи, ConHost теперь содержит несколько дополнительных модулей для обработки VT и новый модуль ConPTY, реализующий открытые API:
- ConPTY API: новые программные интерфейсы Win32 ConPTY API обеспечивают механизм, похожий на модель POSIX PTY, но в преломлении на Windows.
- VT Interactivity: получает входящий текст в кодировке UTF-8, преобразует каждый отображаемый текстовый символ в соответствующую запись
INPUT_RECORD
и сохраняет во входном буфере. Он также обрабатывает управляющие последовательности, такие как 0x03 (CTRL+C), преобразуя их вKEY_EVENT_RECORDS
, которые производят соответствующее управляющее действие. - VT Renderer: генерирует последовательности VT, необходимые для перемещения курсора и рендеринга текста и стиля в областях выходного буфера, которые изменились с предыдущего кадра.
Хорошо, но что это на самом деле значит?
Как работают приложения командной строки Windows?
Чтобы лучше понять влияние новой инфраструктуры ConPTY, давайте рассмотрим, как до сих пор работали консольные приложения Windows и приложения командной строки.
Всякий раз, когда пользователь запускает приложение командной строки, такое как Cmd, PowerShell или ssh, Windows создаёт новый процесс Win32, в который загружает исполняемый двоичный файл приложения и любые зависимости (ресурсы или библиотеки).
Вновь созданный процесс обычно наследует дескрипторы stdin и stdout от своего родителя. Если родительский процесс был процессом Windows GUI, то дескрипторы stdin и stdout отсутствуют, поэтому Windows развернёт и присоединит новое приложение к новому экземпляру консоли. Связь между приложениями командной строки и их консолью передается через ConDrv.
Например, при запуске из экземпляра PowerShell без повышенных прав новый процесс приложения унаследует родительские дескрипторы stdin/stdout и, следовательно, получит входные данные и выдаст выходные данные в ту же консоль, что и родитель.
Здесь нужно немного оговориться, потому что в некоторых случаях приложения командной строки запускаются прикреплёнными к новому экземпляру консоли, особенно по соображениям безопасности, но обычно верно описание выше.
В конечном счёте, когда запускается приложение командной строки/оболочка, Windows подключает его к экземпляру консоли (ConHost.exe) через ConDrv:
![](https://habrastorage.org/getpro/habr/post_images/fc3/4c8/d37/fc34c8d37f6f31c843422193bd5d01a6.png)
Как работает ConHost?
Всякий раз, когда выполняется приложение командной строки, Windows подключает приложение к новому или существующему экземпляру ConHost. Приложение и его экземпляр консоли подключаются через драйвер консоли режима ядра (ConDrv), который отправляет/получает сообщения IOCTL, содержащие сериализованные запросы вызовов API и/или текстовые данные.
Исторически, как указано в предыдущей статье, работа ConHost сегодня относительно проста:
- Пользователь генерирует входные данные с клавиатуры/мыши/ручки/тачпада, которые преобразуются в
KEY_EVENT_RECORD
илиMOUSE_EVENT_RECORD
и сохраняются во входном буфере. - Входной буфер опустошается по одной записи, выполняя запрошенные входные действия, такие как вывод текста на экран, перемещение курсора, копирование/вставка текста и т.д. Многие из этих действий приводят к изменению содержимого буфера вывода. Эти изменённые области записываются движком состояния ConHost.
- В каждом кадре консоль отображает на дисплее изменённые области буфера вывода.
Когда приложение командной строки вызывает Windows Console API, то вызовы API сериализуются в сообщения IOCTL и отправляются через драйвер ConDrv. Он затем доставляет сообщения IOCTL на присоединённую консоль, которая декодирует и выполняет запрошенный вызов API. Возвращаемые/выходные значения сериализуются обратно в сообщение IOCTL и отправляются обратно в приложение через ConDrv.
ConHost: вклад в прошлое ради будущего
Microsoft старается по возможности поддерживать обратную совместимость с существующими приложениями и инструментами. Особенно для командной строки. На самом деле 32-битные версии Windows 10 ещё могут запускать многие/большинство 16-битных приложений и исполняемых файлов Win16!
Как упоминалось выше, одна из ключевых ролей ConHost заключается в предоставлении сервисов своим приложениям командной строки, особенно устаревшим приложениям, которые вызывают и полагаются на консольный API Win32. Теперь ConHost предлагает и новые сервисы:
- Бесшовная PTY-подобная инфраструктура для коммуникации с современными консолями и терминалами
- Модернизация устаревших/традиционных приложений командной строки
- Получение и преобразование UTF-8 текста/VT во входные записи (как будто введённые пользователем)
- Вызовы консольного API для размещаемого приложения, соответствующим образом обновляя его буфер вывода
- Отображение изменённых областей буфера вывода в кодировке UTF-8, текст/VT
Ниже приведен пример, как современное консольное приложение общается с приложением командной строки через ConPTY ConHost.
![](https://habrastorage.org/getpro/habr/post_images/d72/7db/35e/d727db35e65b2e0eec45d0d535b4de83.png)
В этой новой модели:
- Консоль:
- Создаёт собственные каналы связи
- Вызывает ConPTY API для создания ConPTY, заставляя Windows запустить экземпляр ConHost, подключённый к другому концу каналов
- Создаёт экземпляр приложения командной строки (например, PowerShell), подключённый к ConHost, как обычно
- ConHost:
- Читает UTF-8 текст/VT на входе и преобразует его в записи
INPUT_RECORD
, которые отправляются в приложение командной строки - Выполняет вызовы API из приложения командной строки, которые могут изменять содержимое буфера вывода
- Отображает изменения в буфере вывода в кодировке UTF-8 (текст/VT) и отправляет полученный текст в свою консоль
- Читает UTF-8 текст/VT на входе и преобразует его в записи
- Приложение командной строки:
- Работает как обычно, читает входные данные и вызывает Console API, не имея никакого понятия, что его ConPTY ConHost переводит ввод и вывод из/в UTF-8!
Последний момент важен! Когда старое приложение командной строки использует вызовы к Console API вроде
WriteConsoleOutput(...)
, то указанный текст записывается в соответствующий выходной буфер ConHost. Периодически ConHost отображает изменённые области буфера вывода в виде текста/VT, который отправляется через stdout обратно в консоль.В конечном счёте, даже традиционные приложения командной строки снаружи «говорят» текстом/VT без каких-либо изменений!
Используя новую инфраструктуру ConPTY, сторонние консоли теперь могут напрямую взаимодействовать с современными и традиционными приложениями командной строки и обмениваться со всеми ними данными в тексте/VT.
Удалённое взаимодействие с приложениями командной строки Windows
Описанный выше механизм отлично работает на одном компьютере, но также помогает при взаимодействии, например, с инстансом PowerShell на удалённом компьютере Windows или в контейнере.
При удалённом запуске приложения командной строки (т.е. на удалённых компьютерах, серверах или в контейнерах) имеется проблема. Дело в том, что приложения командной строки на удалённых машинах общаются с локальным инстансом ConHost, потому что сообщения IOCTL не предназначены для передачи по сети. Как же передать ввод из локальной консоли на удалённую машину и как получить выдачу из приложения, запущенного там? Более того, что делать с машинами Mac и Linux, где есть терминалы, но нет Windows-совместимых консолей?
Таким образом, чтобы удалённо управлять машиной Windows, нам нужен какой-то брокер связи, который сможет прозрачно сериализовать данные по сети, управлять временем жизни экземпляра приложения и т.д.
Возможно, что-то вроде ssh?
К счастью, OpenSSH недавно портировали на Windows и добавили как дополнительную опцию Windows 10. PowerShell Core также использует ssh в качестве одного из поддерживаемых протоколов удалённого взаимодействия PowerShell Core Remoting. А для тех, кто работал в Windows PowerShell, удалённое взаимодействие Windows PowerShell Remoting по-прежнему остаётся приемлемым вариантом.
Давайте посмотрим, как сейчас OpenSSH для Windows позволяет удалённо управлять оболочками и приложениями командной строки Windows:
![](https://habrastorage.org/getpro/habr/post_images/30e/7fc/eeb/30e7fceebcbcea167c2d50d448c975a4.png)
В данный момент OpenSSH включает некоторые нежелательные усложнения:
- Пользователь:
- Запускает ssh-клиент, а Windows подключает экземпляр консоли как обычно
- Вводит текст в консоль, которая отправляет нажатия клавиш в ssh-клиент
- ssh-клиент:
- Считывает входные данные как байты текстовых данных
- Отправляет текстовые данные по сети в прослушивающую службу sshd
- Служба sshd проходит несколько этапов:
- Запускает оболочку по умолчанию (например, Cmd), которая заставляет Windows создать и подключить новый экземпляр консоли
- Находит и подключается к консоли экземпляра Cmd
- Перемещает консоль за пределы экрана (и/или скрывает её)
- Отправляет входные данные, полученные от клиента ssh, в консоль вне экрана в качестве входных данных
- Экземпляр cmd работает так, как всегда:
- Собирает входные от сервиса sshd
- Выполняет работу
- Вызывает Console API для выдачи/стилизации текста, перемещения курсора и т.д.
- Присоединённая [внеэкранная] консоль:
- Выполняет вызовы API, обновляя выходной буфер
- Служба sshd:
- Скрапит буфер вывода внеэкранной консоли, находит различия, кодирует их в текст/VT и отправляет обратно...
- Клиенту ssh, который отправляет текст...
- Консоли, которая выводит текст на экран
Весело, правда? Вовсе нет! В такой ситуации многое может пойти наперекосяк, особенно в процессе имитации и отправки пользовательского ввода и очистки выходного буфера внеэкранной консоли. Это приводит к нестабильности, сбоям, повреждению данных, чрезмерному потреблению энергии и т.д. Кроме того, не все приложения выполняют работу по снятию не только самого текста, но и его свойств, из-за чего теряется форматирование и цвет!
Удалённая работа с использованием современных ConHost и ConPTY
Наверняка мы можем улучшить ситуацию? Да, конечно, можем — давайте сделаем несколько архитектурных изменений и применим наш новый ConPTY:
![](https://habrastorage.org/getpro/habr/post_images/5e8/677/f1d/5e8677f1da3a8abcf2951f4e539c6470.png)
Из диаграммы видно, что схема изменилась следующим образом:
- Пользователь:
- Запускает ssh-клиент, а Windows подключает экземпляр консоли как обычно
- Вводит текст в консоль, которая отправляет нажатия клавиш в ssh-клиент
- ssh-клиент:
- Считывает входные данные как байты текстовых данных
- Отправляет текстовые данные по сети в прослушивающую службу sshd
- Служба sshd:
- Создаёт каналы stdin/stdout
- Вызывает ConPTY API для инициации ConPTY
- Запускает экземпляр Cmd, подключённый к другому концу ConPTY. Windows инициирует и подключает новый экземпляр ConHost
- Экземпляр cmd работает так, как всегда:
- Собирает входные от сервиса sshd
- Выполняет работу
- Вызывает Console API для выдачи/стилизации текста, перемещения курсора и т.д.
- Экземпляр ConPTY ConHost:
- Выполняет вызовы API, обновляя выходной буфер
- Отображает изменённые регионы выходного буфера как текст/VT в кодировке UTF-8, который отправляется обратно в консоль/терминал по ssh
Такой подход с использованием ConPTY явно чище и проще для службы sshd. Вызовы Windows Console API выполняются полностью в экземпляре ConHost приложения командной строки, который преобразует все видимые изменения в текст/VT. Кто бы не подключался к ConHost, ему необязательно знать, что приложение там вызывает Console API, а не генерирует текст/VT!
Согласитесь, что этот новый механизм удалённого взаимодействия ConPTY приводит к элегантной, последовательной и простой архитектуре. В сочетании с мощными функциями, встроенными в ConHost, поддержкой старых приложений и отображением изменений от приложений, вызывающих консольные Console API, в виде текста/VT, новая инфраструктура ConHost и ConPTY помогает нам перенести прошлое в будущее.
ConPTY API и как его использовать
ConPTY API доступен в текущей версии Windows 10 Insider Preview SDK.
К настоящему времени я уверен, что вам не терпится увидеть какой-то код ;)
Взглянем на декларации API:
// Creates a "Pseudo Console" (ConPTY).
HRESULT WINAPI CreatePseudoConsole(
_In_ COORD size, // ConPty Dimensions
_In_ HANDLE hInput, // ConPty Input
_In_ HANDLE hOutput, // ConPty Output
_In_ DWORD dwFlags, // ConPty Flags
_Out_ HPCON* phPC); // ConPty Reference
// Resizes the given ConPTY to the specified size, in characters.
HRESULT WINAPI ResizePseudoConsole(_In_ HPCON hPC, _In_ COORD size);
// Closes the ConPTY and all associated handles. Client applications attached
// to the ConPTY will also terminated.
VOID WINAPI ClosePseudoConsole(_In_ HPCON hPC);
Вышеприведённый ConPTY по API по сути выставляет для использования три новых функции:
CreatePseudoConsole(size, hInput, hOutput, dwFlags, phPC)
- Создаёт pty размерностью в
w
колонок иh
строк, используя каналы, созданные вызывающей стороной:
size
: ширина и высота (в символах) буфера ConPTYhInput
: для записи входных данных в PTY в виде последовательностей текст/VT в кодировке UTF-8hOutput
: для чтения выдачи из PTY в виде последовательностей текст/VT в кодировке UTF-8dwFlags
: Возможные значения:
PSEUDOCONSOLE_INHERIT_CURSOR
: созданный ConPTY попытается наследовать позицию курсора родительского приложения терминала
phPC
: дескриптор консоли для созданного ConPty
- Возвращает: успех/неудача. В случае успеха phPC содержит дескриптор нового ConPty
ResizePseudoConsole(hPC, size)
- Изменяет размер внутреннего буфера ConPTY для отображения определённой ширины и высоты
ClosePseudoConsole (hPC)
- Закрывает ConPTY и все связанные дескрипторы. Клиентские приложения, подключённые к ConPTY, также завершаются, как если бы они выполнялись в окне консоли, которое закрывается
Использование ConPTY API
Ниже приведён небольшой пример кода вызова ConPTY API для создания псевдоконсоли и присоединения приложения командной строки к созданному ConPTY.
Примечание: полная реализация будет опубликована в нашем репозитории GitHub
// Note: Most error checking removed for brevity. // ... // Initializes the specified startup info struct with the required properties and // updates its thread attribute list with the specified ConPTY handle HRESULT InitializeStartupInfoAttachedToConPTY(STARTUPINFOEX* siEx, HPCON hPC) { HRESULT hr = E_UNEXPECTED; size_t size; siEx->StartupInfo.cb = sizeof(STARTUPINFOEX); // Create the appropriately sized thread attribute list InitializeProcThreadAttributeList(NULL, 1, 0, &size); std::unique_ptr<BYTE[]> attrList = std::make_unique<BYTE[]>(size); // Set startup info's attribute list & initialize it siEx->lpAttributeList = reinterpret_cast<PPROC_THREAD_ATTRIBUTE_LIST>( attrList.get()); bool fSuccess = InitializeProcThreadAttributeList( siEx->lpAttributeList, 1, 0, (PSIZE_T)&size); if (fSuccess) { // Set thread attribute list's Pseudo Console to the specified ConPTY fSuccess = UpdateProcThreadAttribute( lpAttributeList, 0, PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE, hPC, sizeof(HPCON), NULL, NULL); return fSuccess ? S_OK : HRESULT_FROM_WIN32(GetLastError()); } else { hr = HRESULT_FROM_WIN32(GetLastError()); } return hr; } // ... HANDLE hOut, hIn; HANDLE outPipeOurSide, inPipeOurSide; HANDLE outPipePseudoConsoleSide, inPipePseudoConsoleSide; HPCON hPC = 0; // Create the in/out pipes: CreatePipe(&inPipePseudoConsoleSide, &inPipeOurSide, NULL, 0); CreatePipe(&outPipeOurSide, &outPipePseudoConsoleSide, NULL, 0); // Create the Pseudo Console, using the pipes CreatePseudoConsole( {80, 32}, inPipePseudoConsoleSide, outPipePseudoConsoleSide, 0, &hPC); // Prepare the StartupInfoEx structure attached to the ConPTY. STARTUPINFOEX siEx{}; InitializeStartupInfoAttachedToConPTY(&siEx, hPC); // Create the client application, using startup info containing ConPTY info wchar_t* commandline = L"c:\\windows\\system32\\cmd.exe"; PROCESS_INFORMATION piClient{}; fSuccess = CreateProcessW( nullptr, commandline, nullptr, nullptr, TRUE, EXTENDED_STARTUPINFO_PRESENT, nullptr, nullptr, &siEx->StartupInfo, &piClient); // ...
Теперь cmd.exe подключён к экземпляру ConPTY, созданномуCreatePseudoConsole()
. Вызывающий объект использует созданные им дескрипторы ConPTY для записи и чтения в/из экземпляра Cmd. Размер псевдоконсоли изменяется с помощьюResizePseudoConsole()
, а закрытие — по вызовуClosePseudoConsole()
.
Запись в псевдоконсоль
Запись входных данных в ConPTY осуществляется просто:
// Input "echo Hello, World!", press enter to have cmd process the command, // input an up arrow (to get the previous command), and enter again to execute. std::string helloWorld = "echo Hello, World!\n\x1b[A\n"; DWORD dwWritten; WriteFile(hIn, helloWorld.c_str(), (DWORD)helloWorld.length(), &dwWritten, nullptr);
Изменение размера псевдоконсоли
Следующий сценарий показывает, как изменить размер ConPTY:
// Suppose some other async callback triggered us to resize. // This call will update the Terminal with the size we received. HRESULT hr = ResizePseudoConsole(hPC, {120, 30});
Закрытие псевдоконсоли
Нет ничего проще закрытия ConPTY:
ClosePseudoConsole(hPC);
Примечание: закрытие ConPTY завершит связанный ConHost и любые присоединённые клиенты.
Призыв к действию!
Внедрение ConPTY API — пожалуй, одно из самых фундаментальных и раскрепощающих изменений, произошедших с командной строкой Windows за последние годы… если не десятилетия!
Мы уже портировали на ConPTY API некоторые инструменты Microsoft, а сейчас сотрудничаем с несколькими командами внутри Microsoft (подсистема Windows для Linux (WSL), команды Windows Containers, VSCode, Visual Studio и др.), а также с некоторыми независимыми разработчиками, включая @ConEmuMaximus5 — создателя потрясающей консоли ConEmu для Windows.
Но нам нужна ваша помощь, чтобы распространить информацию и начать использовать новые ConPTY API.
Разработчики приложений командной строки
Если у вас традиционное приложение командной строки, то вы свободны и можете ничего не делать: ConHost сделает всю работу за вас. Программа может продолжать работать как раньше и полагаться на вызовы Console API. Приложение продолжит работать как обычно, в то же время получив дополнительный бонус в виде улучшенного, более качественного удалённого взаимодействия.
Но если хотите, можно постепенно ввести новую поддержку VT, например, для новых функций — решать вам.
С другой стороны, если вы сейчас планируете новые приложения командной строки Windows, то мы настоятельно рекомендуем транслировать текст/VT в кодировке UTF-8 вместо обращения к Console API: такой «разговор на VT» даст доступ ко многим функциям, которые не будут доступны через Console API (например, поддержка 16M RGB True Color).
Разработчики сторонних консолей/сервисов
Если вы работаете над автономным приложением консоли/терминала или интегрируете консоль в приложение, то мы настоятельно призываем вас как можно скорее изучить и принять новые ConPTY API: использование новых программных интерфейсов вместо старого механизма внеэкранной консоли, скорее всего, ликвидирует несколько классов ошибок, одновременно повысив стабильность, надёжность и производительность.
В качестве примера команда VSCode в настоящее время решает вопрос (GitHub #45693) с несколькими проблемами, вызванными нынешним отсутствием псевдоконсоли Windows.
Обнаружение ConPTY API
Новые ConPTY API станут доступны в релизе Windows 10 осенью/зимой 2018 года.
Для поддержки более ранних версий Windows, вероятно, потребуется в рантайме проверять, поддерживает ли текущая версия ConPTY. Как и с большинством Win32 API, эффективным способом проверки наличия API является использование метода Runtime Dynamic Linking путём вызоваLoadLibrary()
иGetProcAddress()
.
Если текущая версия Windows поддерживает ConPTY, ваше приложение сможет найти и вызвать новые API ConPTY. Если нет, придётся вернуться к запутанным механизмам, используемым до сих пор.
Итак, на чём мы остановились?
Ещё одна длинная статья… это становится привычкой! Ещё раз, если вы смогли дочитать до этого места, СПАСИБО! :D
Из приведённой информации можно сделать многие выводы, но важно подчеркнуть, почему мы вносим такие улучшения, а также суть реализованных изменений. Наша цель — устранить целый класс проблем и ограничений для разработчиков консольных и серверных приложений, а также сделать разработку кода для инфраструктуры командной строки Windows более мощной, последовательной и увлекательной.
Будем рады отзывам через хаб обратной связи. О более сложных проблемах сообщайте в репозитории Windows Console на GitHub. А если есть вопросы, стучитесь ко мне в твиттер.- Создаёт pty размерностью в