title: Рефакторинг проекта klish toc: yes categories: документация ...
Для хранения и поиска команд сейчас используется структура splay-tree. Профилирование показывает низкую скорость поиска. Требуется разработать или найти более эффективную структуру данных и алгоритм для этой задачи.
Новый принцип навигации. Уровень VIEW-а определяется не статически, а динамически. В памяти строится дерево текущего пути. Нулевым уровнем дерева считается пространство глобальных команд "__view_global". Первым уровнем считается начальный VIEW, указанный в STARTUP (или другим способом). Если пользователь входит во вложенные VIEW-ы, то они становятся уровнями 2, 3 и т.д. соответственно. Нельзя подняться по дереву выше уровня 1. Попытка подняться выше приводит к выходу из программы.
Режим совместимости не предусмотрен.
Атрибут "depth" для тега VIEW объявляется устаревшим. Атрибут тегов COMMAND и VIEW "restore=view" объявляется устаревшим. Атрибут "view" объявляется устаревшим.
Внутри тега COMMAND появляется новый атрибут "nav" - навигация. Новые команды навигации (атрибут "nav"):
down:<nested_view>
- зайти во вложенный VIEW с указанным именем. Уровень вложенности увеличивается на единицу.up[:<number>]
- выйти из вложенного VIEW. Уменьшает уровень вложенности на единицу. Если указан , то поднимается по дереву текущего пути на указанное количество уровней.
replace:<view>[@<level>]
- остаться на текущем уровне вложенности, заменив текущий VIEW на указанный в команде. Если указан уровень дерева , то заменяет не текущий VIEW, а VIEW, находящийся на указанном уровне. Уровень может быть недопустимым. Например быть выше текущего уровня или < 1. В таком случае выдается ошибка навигации.
exit
- выйти из программы.Сейчас схема и набор команд определяется с помощью XML-файлов. Необходимо разработать механизм, который позволял бы описать схему и команды другими способами. Каждый из способов может иметь свои сильные стороны и область применения. Возможные способы:
Механизм должен быть модульным и позволять добавление новых способов описания схемы и команд, помимо перечисленных.
Основное требование к механизму схем для klish - это возможность описания схемы с помощью XML. Другие способы описания могут расширять, улучшать, упрощать описание, но внутреннее представление схемы в klish должно покрываться, пусть неоптимально, с помощью XML.
На первом этапе достаточно реализовать вариант с XML. Реализация должна учитывать возможность реализации остальных вариантов в будущем.
Разделитель команд, как базовая функциональность, не требуется. При необходимости разделитель команд может быть реализован в клиентской части (см. описание архитектуры).
Может быть реализована возможность вводить несколько команд в одной строке. Команды разделяются символом ";". Такая возможность может быть полезна для случаев, когда источником команд является файл или командная строка shell.
В случае, если введена неверная команда, то cli должен показывать в каком месте команды обнаружена ошибка. С помощью указания номера символа и стрелочки "^". Одного номера символа недостаточно, т.к. тяжело считать. Одной стрелочки недостаточно т.к. существуют многострочные команды.
Усовершенствование Hot-keys механизма. В рамках общего рефакторинга внутренней структуры.
Автоматическое наследование NAMESPACE от VIEW более низких уровней. (VIEW с меньшим значением depth считаем более низким уровнем, т.е. корнем дерева).
VIEW-ы высоких уровней теперь не обязаны явно включать наследование команд из VIEW-ов более низкого уровня с помощью NAMESPACE. Наследование происходит автоматически. Необходимо задавать NAMESPACE только для служебных VIEW, которые используются для агрегации команд. Например, когда эти команды должны быть включены в несколько VIEW одновременно. Не рекомендуется использовать служебные VIEW как самостоятельные. Т.е. такие VIEW не должны попадать в дерево текущего пути.
Для тега VIEW появляются атрибуты, похожие на атрибуты NAMESPACE, но относящиеся к более низким уровням дерева текущего пути.
inherit="<true/false>"
- доступны ли в текущем VIEW команды из VIEW-ов более низких уровней дерева текущего пути. По-умолчанию inherit="true"
.completion="<true/false>"
- тоже, что и в NAMESPACE, но относительно более низких уровней.context_help="<true/false>"
- тоже, что и в NAMESPACE, но относительно более низких уровней.Поиск введенной команды происходит следующим образом. От более высокого уровня вложенности дерева пути к более низким уровням. На каждом уровне поиск производится в текущем VIEW и всех его NAMESPACE. Если команда не найдена, то поиск переходит на более низкий уровень дерева.
Когда команда найдена, то анализируется поле "restore" этой команды. Если поле не определено, то команда выполняется на текущем уровне вложенности. Если определено, то уровнем команды считается тот, на котором команда найдена. Восстанавливается этот уровень (либо он совпадает с текущим) и команда выполняется.
Сейчас типы PTYPE имеют только предопределенные методы проверки допустимости введенного аргумента. Требуется проверка PTYPE с помощью произвольного кода. Добавляется тег ACTION. Допустимость аргументов проверяется только с помощью ACTION. Встроенные проверки исключаются. Достигается единообразность. Бывшие встроенные проверки реализуются в plugin-е.
Сейчас все типы PTYPE - глобальные. Так как существует пространство глобальных команд, то технически существует и VIEW нулевого уровня, чтобы это пространство глобальных имен реализовать. Т.е. в общем случае структура VIEW содержит и набор команд, принадлежащих этой области видимости, и набор PTYPE, так как в глобальном VIEW они должны быть. Нет смысла делать глобальный VIEW (0 уровень) каким то особенным. Это только добавит сложности в код. Поэтому локальные PTYPE получаются практически автоматически. Более локальные PTYPE маскируют более глобальные PTYPE-ы с таким же именем (так же как и в языках программирования).
Переменные VAR должны иметь параметры PARAM, так же, как и команды.
VAR может быть не только глобальным, но и относиться к конкретному VIEW. Глобальные VAR находятся в глобальном VIEW. Переменные разрешаются от более локальных к более глобальным.
Сейчас одному формальному параметру PARAM соответствует ровно один фактически введенный параметр. Необходимо реализовать возможность ввести несколько фактических параметров на один формальный. Продумать как объявлять такие параметры. Возможные варианты:
<PARAM ... multi="true">
<PARAM ... number="1..4">
Цифры означают минимальное и максимальное количество фактических параметров данного типа, соответственно.Если есть поле, означающее возможное количество параметров, то optional="true'
означает multi="0..1"
. Как потом обращаться к мульти-параметрам? Нужен ли при этом args. Если нужен, то должен ли он быть типизированным?
Второй вариант реализации мульти-параметров:
Вводится тег MULTI
, который умеет повторять вложенные в него параметры. Преимущество этого способа в том, что можно повторять не один параметр, а целую последовательность. В том числе можно использовать вложенный SWITCH
. Недостаток этого подхода в том, что он более громоздкий. Количество возможных повторений задается полем number="1..4"
. Экземпляр тега может иметь опциональное имя name="my_multi"
. По переменной с таким именем можно узнать фактическое количество введенных повторений.
Вводится специальный режим работы klish, предназначенный для автоматизации управления. В этом режиме внешняя программа, общающаяся с klish, должна иметь возможность получить код возврата каждой выполняемой команды, а также ее вывод и ошибки. Это достигается тем, что протокол обмена между клиентом и ядром klish является двоичным. Для интерактивного режима используется один клиент, для режима автоматизации - другой. Протокол обмена подразумевает то, что клиенту будет отправлен код возврата при выполнении команды. Как клиент будет этот код использовать - его дело.
Дать возможность вводить команду, даже если она не проходит синтаксическую проверку.Для автоматического управления нужно принимать команду и говорить, что она неверна. А не блокировать ввод.
Дать возможность вводить пробел, даже если параметры не проходят синтаксическую проверку.
Сейчас в startup.xml есть хак, который заключается в реализации каталогов с настройками сессий /tmp/clish-session.. Сделано это, например, для pager off/on команды.
В новом klish должна быть возможность менять значения переменных. Этого достаточно, чтобы реализовать сессионные настройки. Клиент должен иметь возможность получить от ядра klish значения переменных.
Возможность пропускать вывод команд через '|'.
В klish, где набор команд предопределен и фиксирован, не имеет смысла делать возможность использовать произвольную команду справа от "|". Все команды, допустимые справа от "|" можно назвать фильтрами вывода. Это, например grep, head и т.д. Использование произвольной команды справа от "|" может привести к зависанию цепочки команд. Для фильтров вывода предусматривается новый тег FILTER. Этот тег полностью повторяет формат тега COMMAND. По внутреннему устройству, фильтр - это команда c дополнительным полем структуры filter=true;
. Команда не-фильтр не может появиться справа от "|", фильтр не может появиться до "|". Соответственно фильтры не попадают в автодополнение при вводе основной (первой) команды. А обычные команды не попадают в автодополнение после ввода "|".
У тега FILTER есть специальное поле auto="true"
. Такие фильтры запускаются автоматически для любой команды. Основное назначение такой команды - это пейджер. Если определено несколько фильтров с полем auto="true"
, то они будут запускаться в алфавитном порядке, по имени фильтра.
Пейджер является интерактивным, т.е. ждет ввода от пользователя. Интерактивные команды могут быть только последними в цепочке вызовов "|". Обычные команды тоже могут быть интерактивными. Например команда, запускающая текстовый редактор. Такие команды нельзя пропускать через пейджер. Вводится атрибут interactive="true/false"
для тега ACTION. Атрибут говорит о том, что после этой команды не должны вызываться никакие фильтры, иными словами, это команда является последней в цепочке. По умолчанию interactive="false"
.
Команда в klish может выполняться в контексте текущего процесса или в отдельном процессе (fork()). По-умолчанию все команды будут выполняться в отдельном процессе. В предыдущей версии задача порождения процессов была возложена на builtin команду (например код, который запускал shell-скрипт). Теперь же сам klish будет этим заниматься. Команды, выполняемые в текущем контексте не могут быть фильтрами. Т.к. невозможно построить цепочку функций, связанную через pipe, в однопоточном приложении. Для того, чтобы понять, какие команды нужно запускать в текущем контексте, а какие fork()-ать, в структуре clish_sym_t появляется поле fork=true/false
.
Должна быть команда, глобально отключающая пейджер, т.е. автоматические фильтры.
(?) Действительно ли это нужно? Нужно ли вводить новый тег FILTER?
Надо чтобы help к параметрам и командам можно было писать на произвольном языке. Язык вывода подсказок зависит от локали.
Продумать механизм встраивания документации в XML файл. Имеется в виду документация, описывающая команды, параметры. Например ввести тег DOC для каждого элемента, подлежащего описанию. На основе такой встроенной документации, можно автоматически генерировать описание всех команд. Генерировать выходной документ может отдельная утилита, а klish может игнорировать теги DOC.
(?) На каком языке должна быть эта документация? gettext? Не будет ли это слишком громоздко для PARAM, например?
Объект plugin должен содержать внутри себя ссылку на userdata.
Для тега ACTION поле shabang больше не требуется. Все будет задаваться полем builtin или его аналогом.
Название builtin
больше не отражает сущность атрибута. Вместо builtin
будет использоваться имя sym
. Атрибут является ссылкой на символ из plugin-а.
Механизм konfd больше не поддерживается. Рекомендуется аналогичную функциональность реализовать через другие механизмы. Возможно сделать обращение к хранилищу конфигурации просто частью ACTION
.
Сейчас существует поле access
для команд, но оно статическое, т.е. проверяется при запуске klish. Команды, отфильтрованные на этапе загрузки, вообще не попадают в список команд (в памяти программы). Для параметров PARAM существует поле test
, которое позволяет динамически скрывать отдельные параметры по заданному условию. Нужен аналогичный динамический механизм для команд. Только поле test
заменяется на тег <COND>
.
В текущей версии klish широко используются подстановки. Подстановками называются текстовые строки в состав которых входят ссылки на переменные klish. При использовании таких строк значения переменных klish разворачиваются прямо в текст на то место, где ранее стояла ссылка на переменную. Сейчас подстановки используются в следующих конструкциях:
Во многих случаях использование подстановок небезопасно, особенно в случае скрипта для выполнения shell интерпретатором. Кроме этого невозможно реализовать подстановку (т.е. вычисление значения строки) на произвольном языке. Нет возможности указать интерпретатор подстановки. В новом klish, подстановок, как базовой функциональности, не должно быть. В большинстве случаев подстановки используются в атрибутах тегов XML. Атрибуты заменяются на вложенные теги. В этих вложенных тегах может присутствовать тег ACTION. Таким образом, генерация строк, как в completion или проверка условия, как в test, могут быть написаны на любом поддерживаемом языке (см. тег ACTION).
Примеры замены атрибутов на вложенный тег:
<VIEW .... >
<PROMPT>
<ACTION>
echo "my_prompt"
</ACTION>
</PROMPT>
<PARAM ...>
<COND>
<ACTION>
test $env_var -eq 0
</ACTION>
</COND>
</PARAM>
В тех случаях, когда подстановки все-таки полезны, они могут быть реализованы внутри исполняемой функции, которая будет обрабатывать содержимое ACTION, т.е. скрипт.
Раздел касается случаев, когда необходимо ввести сложную строку (с пробелами и специальными символами) в командной строке. При этом желательно избежать лишнего эскейпинга, который делает строку нечитаемой. Для этого в новом klish в качестве кавычки должны использоваться 3 разных символа на выбор:
"
(двойная кавычка)'
(одинарная кавычка)Кроме этого в качестве открывающей и закрывающей кавычек могут использоваться повторяющиеся символы кавычек. Например последовательность символов """
может являться открывающей кавычкой. В этом случае закрывать кавычки должна такая же последовательность символов. Такой подход позволит использовать внутри строки такой же символ, что и открывающая/закрывающая кавычка, но с меньшим количеством последовательных символов. Например:
""This is a "long" string""
Тут вложенные кавычки вокруг слова long
являются частью строки. А открывающими и закрывающими кавычками является последовательность символов ""
. Количество символов в открывающей и закрывающей последовательности может быть разным, в зависимости от ситуации и содержания строки. Для определенности будем считать, что количество символов в последовательности не должно превышать трёх.
В настоящее время код, генерирующий варианты для автозаполнения, разделяет эти варианты пробелом. Иногда варианты внутри себя содержат пробелы. Тогда схема перестает работать. В новом klish код, генерирующий варианты для автозаполнения должен разделять их символом новой строки.
Для системы klish используется клиент - серверная модель. Сервером является ядро klish. Ядро запущено от имени определенного пользователя и все действия, производимые ядром, выполняются от имени того-же пользователя. Клиенты, запущенные от имени этого пользователя, устанавливают соединение с ядром с помощью сетевых средств (сокеты). Соответственно, в общем случае, к одному ядру могут подсоединиться сразу несколько клиентов. Для каждого клиента ядро создает собственную сессию.
Ядро при запуске обрабатывает файлы схемы, т.е. XML-файлы, определяющие множество доступных команд. Затем ядро ждет соединений от клиентов и отвечает на их запросы. Клиенты по сути реализуют пользовательский интерфейс и самостоятельно не выполняют никаких команд. Вместо этого они передают запрос на выполнение команд ядру, ядро выполняет команду и затем возвращает результат. Протокол обмена между ядром и клиентами стандартизован и позволяет передавать служебную информацию. На основе этого протокола можно реализовать как текстовый клиент, ориентированный на работу с конечным пользователем, текстовый клиент, ориентированный на автоматизированное управление, а также и графический клиент.
Поведение ядра и клиентов регулируется с помощью специальных конфигурационных файлов (не путать с файлами схемы).
Возможны, как минимум, две стандартные схемы использования klish.
Первая схема подразумевает, что для конкретного пользователя существует только один запущенный экземпляр ядра. Множество клиентов устанавливает соединения с этим ядром. В этой схеме может существовать две стратегии запуска ядра. Первая - ядро запускается один раз сторонними средствами и после этого постоянно присутствует в памяти. Выгружается также сторонними средствами. Вторая стратегия - ядро запускается автоматически при запуске первого клиента (локального). Выгружается когда последний клиент разрывает соединение. Таким образом ядро не занимает оперативную память, когда у него нет клиентов.
Вторая схема подразумевает, что для каждого клиента запускается свой экземпляр ядра. Эта схема соответствует ветке klish с номером 2, в которой нет разделения на клиент - сервер и пользовательский интерфейс объединен с исполняющим ядром.
Рассматривался вариант, когда в системе присутствует только один экземпляр ядра, расчитанный на работу со всеми пользователями сразу, а также вариант, когда клиенты могут быть удаленными. Для этих случаев понадобилась бы сложная аутентификация, встроенная в ядро. А также логика в ядре, выполняющая запросы разных клиентов от имени разных пользователей. А это в свою очередь повлекло бы за собой обязательный запуск ядра от имени root.
В данном случае исполняемыми функциями называются те функции, которые вызываются при выполнении klish-команд. Все исполняемые функции реализованы в plugin-ах. Plugin-ы могут быть как внешние, так и внутренними. Странное словосочетание "внутренний plugin" оправдано тем, что реализация структур для внутренних и внешних plugin-ов очень похожа. Нет смысла делать отдельные механизмы для стандартных функций, которые должны присутствовать в системе всегда, и для пользовательских функций.
Основная единица информации в plugin-е - это "символ". По аналогии с символами в разделяемых библиотеках. Символ представляет собой ссылку на исполняемую функцию. Кроме ссылки на функцию символ содержит дополнительную служебную информацию. Примером такой служебной информации может служить тип символа.
Символы (функции) могут быть двух основных видов:
Основное отличие синхронных символов от асинхронных в том, что синхронные функции исполняются в рамках ядра klish, в то время как асинхронные функции исполняются в порожденном (fork()) от ядра процессе. Ядро занимается своими делами, пока не получит сигнал о том, что порожденный процесс завершился. Таким образом достигается безопасное выполнение сторонних функций. Ошибки в асинхронных функциях не могут повлиять на ядро klish. А также длительное время выполнения асинхронных функции не повлияет на отзывчивость ядра.
Рекомендуется использовать асинхронные функции, а не синхронные. Использовать синхронные функции допустимо только тогда, когда длительность выполнения функции фиксирована и мала. Примером синхронной функции может служить функция формирования приглашения (prompt). Эта функция не занимает много времени и очень часто вызывается. Поэтому делать ее асинхронной - слишком ресурсоемко.
Из-за различий синхронных и асинхронных функций, их API и способ обращения из этих функций к переменным (VAR) klish - различны.
Ядро klish передает в синхронную функцию ссылку на внутреннее хранилище переменных, указатель на строку stdout и stderr.
С помощью ссылки на хранилище переменных и соответствующего API по обращению к этому хранилищу, функция может получать и устанавливать значения переменным klish. Важно, что синхронная функция может обращаться только к тем переменным, ACTION-ы для которых реализованы с помощью синхронных функций. Если ACTION реализован асинхронной функцией, то обращение к такой переменной вызовет ошибку. Обращаться из синхронной функции к асинхронной функции - нельзя.
Результатом работы синхронной функции является код возврата и сформированные строки stdout и stderr.
Код возврата может быть успешным (0) или неуспешным (любое другое число). Конкретное число, в случае неуспешного завершения, может указывать на тип ошибки.
С помощью указателей stdout, stderr функция может вернуть ядру строки для вывода в соответствующие потоки. Ядро, в свою очередь, переправит эти строки клиенту. В случае, когда функция выполняется, в качестве ACTION для переменной klish, то строка stdout будет являться значением переменной.
В асинхронную функцию передается идентификатор ядра klish. В данном случае под идентификатором ядра подразумевается объект, позволяющий связаться с ядром, так как при запуске асинхронной функции, она будет выполнена в порожденном процессе и процесс ядра является внешним для этого порожденного процесса. Объектом может быть сетевой сокет, пайп и т.д.
С помощью идентификатора ядра функция может отправить запрос ядру на получение или установку значения переменной klish. Для таких запросов должно использоваться специальное API, отличное от API для работы с переменными klish из синхронных функций. Для асинхронных функций нет ограничений на тип переменной, значение которой запрашивается у ядра. ACTION-ы переменных могут быть реализованы как асинхронными функциями, так и синхронными.
Асинхронная функция возвращает код возврата в таком же формате, как и синхронная функция.
Для интерактивного (или неинтерактивного) обмена строками с клиентом, перед порождением процесса для запуска асинхронной функции, ядро создает псевдотерминал для предоставления исполняемой функции в качестве stdin, stdout и пайп для предоставления stderr. Поток stderr отделен от stdout, чтобы ядро и клиент могли отличить эти два потока. После запуска функции, ядро читает свою сторону псевдотерминала и пайпа для пересылки данных клиенту. В случае, когда асинхронной функцией реализован ACTION переменной klish, весь вывод в stdout является значением этой переменной.
Асинхронные функции могут быть интерактивные и не-интерактивные. Интерактивным функциям предоставляется псевдотерминал для взаимодействия с пользователем. Таким образом, исполняемый код не увидит разницы между работой напрямую с пользователем и работой с пользователем через "удаленный" канал клиент-сервер системы klish. Не-интерактивным функциям предоставляется потоки ввода/вывода через pipe.
По возможности не использовать потоковые файловые операции (fopen(), fgets() и т.д.), так как при fork()-e и последующих операциях с файловыми объектами, в том числе просто atexit(), могут происходить fflush() на поток и в итоге lseek() на файловый дискриптор. Это все приводит к проблемам с позиционированием внутри файла.
В проекте принят стиль форматирования, похожий на стиль, используемый при разработке ядра Linux. Существует утилита indent
, которая приводит исходный текст программы к нужному стилю. В корне дереве исходных кодов проекта находится скрипт indent.sh
, который запускает утилиту indent
с нужными опциями. Набор используемых опций можно увидеть в исходном коде скрипта indent.sh
. Пример использования:
$ ./indent src/prog.c
Использование indent.sh
решает большую часть проблем с форматированием, но существуют и дополнительные правила оформления исходного кода.
Кроме утилиты indent
существует программа clang-format
, которая также занимается форматированием исходных кодов. Для этой программы в корне дерева исходных кодов размещен файл .clang-format
, который задает правила форматирования. Для использования clang-format
используется следующая команда:
$ clang-format -i filname.c
Где filename.c
имя файла, который необходимо обработать.
Чтобы отделить функции друг от друга используются две пустые строки.
Пустая строка отделяет объявление переменных от остальных команд функции или блока.
int fn1(void)
{
int i = 1;
int j = 0;
float b = 3.5;
i += 3;
return i;
}
int fn2(void)
{
int i = 1;
i += 4;
i = 5 + i * 9;
return i;
}
Переменные должны иметь понятные имена. Имена переменных могут содержать латинские буквы в нижнем регистре, цифры и символы подчеркивания _
.
Каждая переменная объявляется на отдельной строке.
Каждая переменная должна быть проинициализирована.
Указатели, после освобождения памяти, на которую они указывают, должны быть установлены в NULL.
Следует избегать использования глобальных переменных.
Присваивание значения каждой переменной производится на отдельной строке. Не следует присваивать значения сразу нескольких переменных одной строкой a = b = foo();
.
Предположим необходимо выделить память для хранения структуры. Обычно используются два различных стиля для определения размера выделяемой памяти.
Первый стиль:
struct mytype *a = NULL;
a = malloc(sizeof(struct mytype));
Второй стиль:
struct mytype *a = NULL;
a = malloc(sizeof(*a));
В данном проекте используется второй стиль. Этот стиль более безопасен, так как при изменении типа переменной, не требуется изменять строку выделения памяти.
Для более короткой и ясной записи, проверка указателя на ноль производится согласно следующему стилю:
if (ptr) ...
if (!ptr) ...
Проверка на нулевой байт, в таких случаях, как конец строки или отдельный нулевой символ, производится согласно следующему стилю:
if ('\0' == *ptr) ...
if ('\0' == c) ...
Обратный порядок операндов для сравнения объясняется защитой от случайной замены операции сравнения ==
на операцию присваивания '='. Такого рода ошибки трудны в отладке.
Проверка на ноль при обработке возвращаемого из функций значения (обычно тип int) производится следующим образом:
if (foo() == 0) ...
a = foo();
if (0 == a) ...
if (a < 0) ...
if (a != 0) ...
Функция bzero() более наглядна, но менее переносима. В современном стандарте POSIX bzero() помечена, как устаревшая (legacy). В программах следует использовать функцию memset(). Чтобы совместить достоинства двух этих функций, во вспомогательной библиотеке faux
имеется функция faux_bzero()
, которая имеет интерфейс функции bzero()
, а внутри использует более переносимую функцию memset()
.