- PVSM.RU - https://www.pvsm.ru -

Терминальный доступ к СУБД Caché – теперь и в браузере

image

С развитием веб-технологий в окне браузера появляется всё больше полезных сервисов, приложений, программ и даже игр. Пришло время и для терминала СУБД 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-форматирование, а именно символы «<», «>» и «&» соответствующими «<», «&gt» и «&». Потом – выполнить саму подсветку синтаксиса по ультра-регулярному [1] выражению (которая, по сути, вставляет лишь теги в текст) и лишь потом вставить каретку, определив «настоящее» ее положение (без учёта тэгов и HTML-сущностей), для чего был написан еще один метод. Да, все вышеперечисленное выполняется только в этом порядке, иначе или сама каретка подсветится, или появится много битой HTML-разметки.

А вот с автодополнением было работать интересно. Я его трижды переписывал. А прогресс алгоритма был следующим:

  1. Просто получаем кусок строки от каретки до ближайшего левого разделителя или пробела и ищем совпадения с имеющимися вариантами в массиве всех вариантов.
  2. Тот же кусок строки, уже включая, возможно, символы «$», «%», «##» и прочие для определения типа дополнения ищем в специальном объекте, разбитом на «категории».
  3. Парсим всю левую от каретки часть по «маскам» – обратным регулярным выражениям, которые содержатся в специально структурированном объекте «терминального словаря».

Не знаю, знакомым ли кому покажется третий метод, к которому я постепенно подошел, но именно он показал самые шикарные и быстрые результаты.

Итак, как же он устроен? Очень просто. Всё что нужно, чтобы создать практически любой тип автодополнения – это грамотно составленные регулярные выражения в объекте «словаря». Вот как он может выглядеть:

Код

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” на сервере.

  • Получение команды от клиента
  • Переход терминального приложения в режим выполнения — установка ввода/вывода «напрямую»
  • Передача команды в xecute
  • 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/