С развитием веб-технологий в окне браузера появляется всё больше полезных сервисов, приложений, программ и даже игр. Пришло время и для терминала СУБД Caché.
Под катом вы найдете описание всех прелестей приложения и историю его разработки.
Функциональность
Веб-терминалу под силу следующее:
- Выполнение произвольного кода и команд Caché Object Script, терминальных утилит и программ
- Удобный SQL-режим для быстрого доступа к базе данных
- Автодополнение ключевых слов Caché Object Script: классов и их методов, свойств, параметров, глобалов и переменных в текущем пространстве имён
- Мониторинг изменений в глобалах и файлах (подобно tail -f)
- История команд
- Подсветка синтаксиса Caché Object Script
- Определение сокращений
- Многострочное редактирование
- Настройка поведения и тем оформления приложения
Все вышеперечисленное работает на любом сервере 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-форматирование, а именно символы «<», «>» и «&» соответствующими «<», «>» и «&». Потом – выполнить саму подсветку синтаксиса по ультра-регулярному выражению (которая, по сути, вставляет лишь теги в текст) и лишь потом вставить каретку, определив «настоящее» ее положение (без учёта тэгов и 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 – префикс к родительскому регулярному выражению, который будет рассмотрен при совпадении последнего. Алгоритм проверки получается достаточно простым. Если интересно, как именно устроен этот алгоритм, его можно найти внутри репозитория проекта.
Преимущества такого подхода очевидны – это и наглядная структура словаря всех синтаксических конструкций, и достаточно шустрый способ автодополнения, который при желании можно всячески расширять. Да, число, идущее как значение свойства “термина” – это предполагаемая его «частота употребления». Именно по этой цифре и будут сортироваться предлагаемые варианты.
Со стороны сервера весь словарь автодополнения классов и методов текущего пространства имён, в свою очередь, генерируется и сохраняется в JSON-файл, содержащий объект объектов, которые при необходимости будут подгружены и «слиты» с имеющимся на клиенте объектом словаря классов внутри основного объекта словаря. Вот так вот.
Сам сервер ранее был научен отправлять весь write сразу на клиент, с использованием этой штуки. Но для read, как оказалось, обойтись чем-то простым вида “+Т” не получится. Вся проблема в том, что когда пользователь пытался выполнить то ли терминальную утилиту, то ли сочинить сценарий с преподобным read — сервер, обрабатывая их в xecut’е просто зависал или портил вводимые данные.
Хорошо, допустим, поставим мы терминал в режим обработки стандартных терминаторов на входе (“+Т”), и будем их отправлять с клиента. Отлично, теперь read не зависает, но возникает другая ситуация — мы ведь читаем пакет, полученный от клиента, а не его тело. Сам пакет содержит немного “мусора” для нас — это первые несколько байтов, которые служат его заголовком. Их нужно было как-то отбросить.
Чтобы проще представить, что нужно и как это все должно происходить, рассмотрим предполагаемую последовательность выполнения команды “read a write a” на сервере.
- Получение команды от клиента
- Переход терминального приложения в режим выполнения — установка ввода/вывода «напрямую»
- Передача команды в xecute
- read a: читаем данные от клиента, заканчивающихся терминатором
- Достаём тело считанного пакета и помещаем в а
- write a: отправляем клиенту значение а
- Завершение режима выполнения
Но как же так же достать это тело, чтобы в a не попадали лишние байты (шапка пакета WebSocket)? Логично, нужно сделать какой-то свой обработчик read. Да и не простой, а на системном уровне: ведь терминальные утилиты тоже используют read.
К счастью, опытные ребята форума sql.ru мне подсказали чудесную недокументированная возможность Caché — I/O redirection. С её помощью можно было сделать именно то, что нужно — обработать все прилетающие/улетающие данные по-своему. Перенаправление ввода осуществляется написанием семи подпрограмм, которые будут брать на себя функции примитивных команд read и write при включенном ##class(%Device).ReDirectIO. Если интересна детальная реализация этой прелести, вам может пригодится этот тред.
Я надеюсь опыт, изложенный выше, кому-то обязательно пригодится и станет не менее полезным чем сам терминал. На пути от концепции к уже функциональному приложению появилось много новых идей, и это еще не предел — тут можно ограничиться лишь фантазией. Следите или участвуйте в развитии проекта на GitHub, предлагайте и обсуждайте идеи, будут полезны любые ваши отзывы. Приятного администрирования!