- PVSM.RU - https://www.pvsm.ru -
С развитием веб-технологий в окне браузера появляется всё больше полезных сервисов, приложений, программ и даже игр. Пришло время и для терминала СУБД Caché.
Под катом вы найдете описание всех прелестей приложения и историю его разработки.
Веб-терминалу под силу следующее:
Все вышеперечисленное работает на любом сервере Caché, где присутствует поддержка WebSockets. Достаточно лишь пройти по ссылке на веб-приложение и начать работу.
Естественно, главной целью любого веб-приложения должна быть его безопасность. Поскольку общение с сервером происходит через открытый порт, последний нуждается в защите. Первое, что потребует от вас терминальный порт на вход при установке подключения – ключ произвольной длины. Сервер, получая неверный ключ, тут же разрывает соединение. Сейчас используется GUID, который каждый раз генерируется при обращении к основной CSP странице или в случае неудачного присоединения к открытому порту. То есть каждый раз, когда случилась попытка подключиться к сокету с неверным ключом, будет сгенерирован новый, и так раз за разом, что делает невозможным его подбор. То же самое происходит и при вызове CSP страницы – только теперь ключ отдается клиенту и используется для установки соединения с сокетом.
Проще говоря, остается только установить авторизацию на CSP страницу, тем самым не дав возможности получить ключ нежеланным посетителям открытого порта.
Если посмотреть на терминал со стороны, кому-то может показаться что его механика достаточно проста. Поначалу мне тоже так казалось. Разбираясь в поведении такого рода приложений, наряду со знакомством с Caché возникало немало интересных моментов и трудностей, о которых пойдёт речь дальше. Основной задачей оставалось сделать терминал привычным для гиков и одновременно дружественным для простых пользователей, предоставив новые возможности и наконец-то приукрасить черно-белый графический интерфейс обычного терминала, ведь за окном уже 2013-й!
Дальше я буду описывать в основном расправы с граблями, на которые мне приходилось натыкаться и те самые, по которым походили все знакомые с вебом или с Caché дизайнеры-программисты. Может, представленные ниже способы и решения можно сделать еще интереснее, и если вы знаете как — будет очень любопытно услышать.
Начиная создавать новый программный продукт нужно продумать, а как же все это должно работать. Поскольку я только знакомился с Caché, поначалу посещали мысли, что терминал функционирует достаточно просто – нужно лишь выполнять команды на сервере и читать с него ответ. За фронтэнд я совсем не переживал, так как с ним мне приходилось ранее работать. Но только лишь я стал все больше углубляться в разработку, выяснилось, что нужно не просто выполнять команды, а и всяческие терминальные утилиты, настроить чтение информации с клиента (например, при обработке команды read), добиться потокового ввода/вывода и решить еще ряд всяких мелких задачек.
Немного поразмыслив с коллегами, стало абсолютно понятно, что для передачи данных будет использоваться протокол WebSocket – относительно новый, стабильный и надёжный способ передавать данные через веб. А на стороне клиента, понятное дело, все прелести HTML5. Это и CSS-анимации, и чистый-прозрачный JavaScript.
Первым делом сев за клиентскую часть, мне не хотелось использовать уже готовые решения или применять какие-либо фреймворки, так как терминал в идеале – это лёгкое приложение, которому не нужен продвинутый доступ к DOM’у как у JQuery и разные волшебные JavaScript-анимации. Нет, на самом деле анимации может и были бы кстати, но в формате всего лишь пачки CSS3-свойств, не более. Приложение обещало выйти лёгким, как для браузера, так и для пользователя.
С отображением информации я долго не возился – это моноширинный шрифт, лёгкая блочная верстка и знакомые стили. А вот над полем для ввода информации пришлось подумать: нужно было реализовать подсветку синтаксиса. Множество решений с плагинами было отброшено, поэтому последний, как казалось, вариант – это лаконичный editable div в HTML5. Но и там нашлись свои прелести – контент нужно было переводить с HTML в plaintext и наоборот, устанавливать и получать позицию каретки, копировать в буфер оригинальный, не разукрашенный текст – эти задачки решаются далеко не в несколько строчек. В придачу нужно же еще вставлять в текст свою, мигающую терминальную каретку и реализовать обычный системный Ctrl-C, Ctrl-V, Right click + Paste, Insert, …
Вариант реализации “подсветки и каретки” в поле для ввода нашелся очень хитрый и простой. По сути, нам нужно только лишь подсветить текст, ну и каретку вставить. Предположим, что текст у нас есть обычный и подсвеченный. Первый содержится в самом типичном textarea, а подсвеченный – в блоке точно такого же размера. Улавливаете? Вот так просто с использованием нескольких CSS-трюков делаем поле для ввода прозрачным с прозрачным шрифтом, под которым располагаем блок с подсвеченным текстом. В результате получаем видимое выделение текста и полную свободу его редактирования. Тут только Internet Explorer, как всегда, отличился – каретка в нём всё равно остается видимой, когда в других браузерах она прозрачна. Потому от обычной мигающей каретки терминала в нём пришлось отказаться – пускай остаётся со своей, родной.
Интересный момент так же возник с обработкой нажатия клавиш – нужно было подсветить вводимые данные, а значит, по нажатию обработать содержимое поля ввода. Да, но получить содержание этого поля именно в момент нажатия не получится (точнее получится, но без последнего «нажатого» знака) – DOM не успевает обновится до выполнения событий keydown, keypress, а по keyup обновлять видимую часть ввода совсем не интересно, хотя это тоже еще один выход. Второй выход – добавлять символ в строку вручную. Но последний сразу отпадает в случае Ctrl+V. Сделаем третьим методом – вызовем функцию-обработчик нажатия через 1мс после самого нажатия. Да, теперь мы получили ввод, но пропала возможность управлять передаваемым в обработчик event’ом, например, чтобы запретить действие клавиши по умолчанию. Выходом стало разбивка нажатия на два события – по нажатию сама обработка события и сочетаний, а через 1мс – обновление введённого текста.
Парсинг ввода, подсветка синтаксиса и вставка туда каретки в виде было реализовать несложно – сперва нужно заменить то, что может испортить HTML-форматирование, а именно символы «<», «>» и «&» соответствующими «<», «>» и «&». Потом – выполнить саму подсветку синтаксиса по ультра-регулярному [1] выражению (которая, по сути, вставляет лишь теги в текст) и лишь потом вставить каретку, определив «настоящее» ее положение (без учёта тэгов и HTML-сущностей), для чего был написан еще один метод. Да, все вышеперечисленное выполняется только в этом порядке, иначе или сама каретка подсветится, или появится много битой HTML-разметки.
А вот с автодополнением было работать интересно. Я его трижды переписывал. А прогресс алгоритма был следующим:
Не знаю, знакомым ли кому покажется третий метод, к которому я постепенно подошел, но именно он показал самые шикарные и быстрые результаты.
Итак, как же он устроен? Очень просто. Всё что нужно, чтобы создать практически любой тип автодополнения – это грамотно составленные регулярные выражения в объекте «словаря». Вот как он может выглядеть:
language = {
"client": {
"!autocomplete": {
reversedRegExp: new RegExp("([a-z]*/)+")
},
"/help": 1,
"/clear": 1,
…
},
"commands": {
"!autocomplete": {
reversedRegExp: new RegExp("([a-zA-Z]+)\s.*")
},
"SET": 0,
"KILL": 0,
"WRITE": 0,
…
},
"staticMethods": {
"!autocomplete": {
reversedRegExp: new RegExp("([a-zA-Z]*)##\s.*")
},
"class": 0,
…
},
"class": {
"!autocomplete": {
reversedRegExp: new RegExp("(([a-zA-Z\.]*[a-zA-Z])?%?)\(ssalc##\s.*"),
separator: ".",
child: {
reversedRegExp: new RegExp("([a-zA-Z]*)\.\)")
}
},
"EXAMPLE": {
"Method": 0,
"Property": 0,
"Parameter": 0
},
…
}
}
Каждый объект внутри language может иметь специальное свойство-объект «!autocomplete». Если оно присутствует, парсер автодополнения будет обращать на этот объект внимание, а именно читать его свойства reversedRegExp и child.
Как уже можно было догадаться, reversedRegExp составлен специальным образом, и именно он определяет, уместно ли использовать свойства текущего «словарного» объекта (далее – просто «словаря») для автодополнения. Запоминающие скобки в регулярном выражении служат для выделения части искомой строки, которая будет сверяться с именами свойств словаря («терминами»). Это позволяет найти в любой синтаксической структуре ключ, по которому и будет производится выбор доступных вариантов.
С классами же немного иная задачка – нужно получить имя класса и подсказать соответствующие ему свойства. Это решилось путём дополнения свойства-объекта «!autocomplete» подобным ему свойством-объектом «child», который тоже содержит reversedRegExp – префикс к родительскому регулярному выражению, который будет рассмотрен при совпадении последнего. Алгоритм проверки получается достаточно простым. Если интересно, как именно устроен этот алгоритм, его можно найти внутри [2] репозитория проекта.
Преимущества такого подхода очевидны – это и наглядная структура словаря всех синтаксических конструкций, и достаточно шустрый способ автодополнения, который при желании можно всячески расширять. Да, число, идущее как значение свойства “термина” – это предполагаемая его «частота употребления». Именно по этой цифре и будут сортироваться предлагаемые варианты.
Со стороны сервера весь словарь автодополнения классов и методов текущего пространства имён, в свою очередь, генерируется и сохраняется в JSON-файл, содержащий объект объектов, которые при необходимости будут подгружены и «слиты» с имеющимся на клиенте объектом словаря классов внутри основного объекта словаря. Вот так вот.
Сам сервер ранее был научен отправлять весь write сразу на клиент, с использованием этой [3] штуки. Но для read, как оказалось, обойтись чем-то простым вида “+Т” не получится. Вся проблема в том, что когда пользователь пытался выполнить то ли терминальную утилиту, то ли сочинить сценарий с преподобным read — сервер, обрабатывая их в xecut’е просто зависал или портил вводимые данные.
Хорошо, допустим, поставим мы терминал в режим обработки стандартных терминаторов на входе (“+Т”), и будем их отправлять с клиента. Отлично, теперь read не зависает, но возникает другая ситуация — мы ведь читаем пакет, полученный от клиента, а не его тело. Сам пакет содержит немного “мусора” для нас — это первые несколько байтов, которые служат его заголовком. Их нужно было как-то отбросить.
Чтобы проще представить, что нужно и как это все должно происходить, рассмотрим предполагаемую последовательность выполнения команды “read a write a” на сервере.
Но как же так же достать это тело, чтобы в a не попадали лишние байты (шапка пакета WebSocket)? Логично, нужно сделать какой-то свой обработчик read. Да и не простой, а на системном уровне: ведь терминальные утилиты тоже используют read.
К счастью, опытные ребята форума sql.ru [4] мне подсказали чудесную недокументированная возможность Caché — I/O redirection. С её помощью можно было сделать именно то, что нужно — обработать все прилетающие/улетающие данные по-своему. Перенаправление ввода осуществляется написанием семи подпрограмм, которые будут брать на себя функции примитивных команд read и write при включенном ##class(%Device).ReDirectIO. Если интересна детальная реализация этой прелести, вам может пригодится этот [5] тред.
Я надеюсь опыт, изложенный выше, кому-то обязательно пригодится и станет не менее полезным чем сам терминал. На пути от концепции к уже функциональному приложению появилось много новых идей, и это еще не предел — тут можно ограничиться лишь фантазией. Следите [6] или участвуйте в развитии проекта на GitHub [7], предлагайте и обсуждайте идеи, будут полезны любые ваши отзывы. Приятного администрирования!
Автор: ZitRo
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/42401
Ссылки в тексте:
[1] ультра-регулярному: https://gist.github.com/ZitRos/6310513
[2] внутри: https://github.com/intersystems-ru/webterminal/blob/master/csp/webTerminal/js/parser.js.xml#L217
[3] этой: http://docs.intersystems.com/cache20131/csp/docbook/DocBook.UI.Page.cls?KEY=GIOD_tcp#GIOD_tcp_sendimmediate
[4] sql.ru: http://www.sql.ru/forum/cache
[5] этот: http://www.sql.ru/forum/1037610/websocket-i-read
[6] Следите: http://intersystems-ru.github.io/webterminal/#history
[7] GitHub: https://github.com/intersystems-ru/webterminal
[8] Источник: http://habrahabr.ru/post/192242/
Нажмите здесь для печати.