Fixed force parameter. Added new worker ipc, some pydantic response and requests schemas and server api description.
This commit is contained in:
parent
91690f73e2
commit
c0a552a1cf
70 changed files with 5730 additions and 2066 deletions
|
@ -42,11 +42,11 @@ class Command:
|
|||
def __init__(self, command_id: str = '',
|
||||
category: str = '',
|
||||
title: str = '',
|
||||
command: str = '',
|
||||
script: Union[Callable, Script, None] = None,
|
||||
args: Union[tuple, list] = tuple(),
|
||||
parameters: Union[List[BaseParameter], None] = None,
|
||||
namespace: Union[str, NamespaceNode, None] = None,
|
||||
command: str = '',
|
||||
gui: bool = False,
|
||||
icon: Union[str, List[str]] = '',
|
||||
setvars: dict = {},
|
||||
|
@ -176,7 +176,7 @@ class CommandRunner:
|
|||
def __init__(self, command: Command,
|
||||
datavars: Union[Datavars, NamespaceNode],
|
||||
output: IOModule):
|
||||
'''Класс инкапулирующий все данные о команде, а также необходимые для
|
||||
'''Класс инкапcулирующий все данные о команде, а также необходимые для
|
||||
нее параметры, модуль ввода-вывода, переменные и прочее. Предназначен
|
||||
для конфигурации команды и запуска ее в воркере.'''
|
||||
self._command = command
|
||||
|
|
|
@ -232,7 +232,7 @@ class Table(ParameterType):
|
|||
def process_value(self, value: TableValue, parameter):
|
||||
if isinstance(value, TableValue):
|
||||
# Если родитель не установлен -- значит параметр был только что
|
||||
# инициализирован.его, и проводим полную проверку
|
||||
# инициализирован. Устанавливаем его, и проводим полную проверку
|
||||
# инициализированных значений таблицы.
|
||||
if value._parent is None:
|
||||
value._parent = parameter
|
||||
|
@ -270,7 +270,7 @@ class Password(ParameterType):
|
|||
|
||||
class Description:
|
||||
'''Класс, содержащий описание параметра и способ его отображения.'''
|
||||
# словарь со способами отображения параметра в графическом интерфейсе.
|
||||
# Словарь со способами отображения параметра в графическом интерфейсе.
|
||||
representations = {}
|
||||
|
||||
def __init__(self, short='', full='', usage=''):
|
||||
|
@ -434,7 +434,7 @@ class BaseParameter:
|
|||
else:
|
||||
return None, None
|
||||
|
||||
def to_set(self, *variables):
|
||||
def set_to(self, *variables):
|
||||
'''Метод для добавления переменных, которым будет установлено значение
|
||||
параметра.'''
|
||||
self._subscribers = variables
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
#
|
||||
import re
|
||||
import inspect
|
||||
from typing import Callable, Any, Union, List, Generator
|
||||
from typing import Callable, Any, Union, List, Generator, Optional
|
||||
from calculate.templates.template_processor import DirectoryProcessor
|
||||
from calculate.variables.datavars import (
|
||||
DependenceAPI,
|
||||
|
@ -46,18 +46,18 @@ class Script:
|
|||
в себе. Создает экземпляры лаунчеров.'''
|
||||
def __init__(self, script_id: str,
|
||||
args: List[Any] = [],
|
||||
success_message: Union[str, None] = None,
|
||||
failed_message: Union[str, None] = None,
|
||||
interrupt_message: Union[str, None] = None):
|
||||
success_message: Optional[str] = None,
|
||||
failed_message: Optional[str] = None,
|
||||
interrupt_message: Optional[str] = None):
|
||||
self._id: str = script_id
|
||||
self.__items: List[Union['Task', 'Block', 'Handler', 'Run']] = None
|
||||
self.__tasks_is_set: bool = False
|
||||
|
||||
self.__args: list = args
|
||||
|
||||
self.__success_message: Union[str, None] = success_message
|
||||
self.__failed_message: Union[str, None] = failed_message
|
||||
self.__interrupt_message: Union[str, None] = interrupt_message
|
||||
self.__success_message: Optional[str] = success_message
|
||||
self.__failed_message: Optional[str] = failed_message
|
||||
self.__interrupt_message: Optional[str] = interrupt_message
|
||||
|
||||
@property
|
||||
def id(self) -> str:
|
||||
|
@ -198,7 +198,7 @@ class ScriptLauncher:
|
|||
|
||||
return scripts
|
||||
|
||||
def __call__(self, *args):
|
||||
def __call__(self, *args) -> None:
|
||||
'''Метод для запуска скрипта с аргументами.'''
|
||||
if len(args) < len(self._script.args):
|
||||
raise ScriptError(
|
||||
|
@ -219,7 +219,7 @@ class ScriptLauncher:
|
|||
|
||||
def _get_args_values(self, args: List[Any],
|
||||
datavars: Union[Datavars, NamespaceNode],
|
||||
namespace: NamespaceNode):
|
||||
namespace: NamespaceNode) -> list:
|
||||
'''Метод для получения значений аргументов.'''
|
||||
args_values = []
|
||||
for argument in args:
|
||||
|
@ -235,7 +235,7 @@ class ScriptLauncher:
|
|||
return args_values
|
||||
|
||||
|
||||
class RunTemplate:
|
||||
class RunTemplates:
|
||||
'''Класс запускателя наложения шаблонов.'''
|
||||
def __init__(self, id: str = '',
|
||||
action: str = '',
|
||||
|
|
830
calculate/server/interfaces.txt
Normal file
830
calculate/server/interfaces.txt
Normal file
|
@ -0,0 +1,830 @@
|
|||
Вопросы
|
||||
-------
|
||||
1. >> Что является ресурсом?
|
||||
* commands -- информация о командах, их список, группы, описания и тд.
|
||||
* workers(?) -- воркеры, исполняющие команды. Сомнительно.
|
||||
Возможно есть смысл скрыть реализацию, т.е. воркеров, под некоторыми
|
||||
другими сущностями. Например, execution и configuration.
|
||||
workers -- мб оставить как дополнительный низкоуровневый интерфейс.
|
||||
<< Ресурсы: commands, configuration, execution, workers (low_level)
|
||||
|
||||
2. >> Что делать с демонами-воркерами, обнаруженными при инциализации сервера,
|
||||
но не зарегистрированными в БД?
|
||||
<< Убиваем их и удаляем PID-файлы.
|
||||
|
||||
3. >> Что делать с демонами-воркерами, для которых найдены PID-файлы, по PID
|
||||
есть информация в БД, но WID из имени PID-файла не совпадает с WID в БД.
|
||||
<< Убиваем воркера, удаляем PID-файл и запись в БД.
|
||||
|
||||
4. >> Стоит ли убрать разделение команд и их id?
|
||||
<< Не стоит.
|
||||
|
||||
5. >> Нужна ли возможность сброса текущих параметров конфигурации?
|
||||
<< Да.
|
||||
|
||||
|
||||
Взаимодействие с воркером с точки зрения сервера
|
||||
------------------------------------------------
|
||||
1. Сервер получает от клиента запрос, POST /configs/{command_id}. Далее:
|
||||
|
||||
* Сервер создает объект воркера команды и запускает с его помощью
|
||||
демона-рабочего, оставляя его в режиме ожидания конфигурационных данных.
|
||||
|
||||
* После создания объекта воркера в БД сохраняется основная информация
|
||||
необходимая для воссоздания объекта воркера в новом процессе сервера. Такая
|
||||
ситуация может возникнуть при переходе на новую версию сервера:
|
||||
|
||||
- int wid -- идентификатор воркера;
|
||||
|
||||
- str socket -- путь к сокету для чтения данных воркера;
|
||||
|
||||
- int daemon_pid -- PID демона-воркера для взаимодействия с ним.
|
||||
|
||||
* В ответ на запрос сервер получает список доступных ресурсов, включающих
|
||||
в себя в том числе действия воркера, а также URI этих ресурсов и
|
||||
доступные для этого URI http-методы. На данном этапе:
|
||||
|
||||
- configure (patch) -- изменить некоторые параметры конфигурации;
|
||||
|
||||
- cancel (delete) -- закончить конфигурацию без продолжения выполнения.
|
||||
По сути удаляет данную конфигурацию как ресурс;
|
||||
|
||||
- execute (post) -- запустить выполнение команды с дефолтной
|
||||
конфигурацией. По сути удаляет конфигурацию как ресурс и создает
|
||||
новый ресурс -- выполнение.
|
||||
|
||||
2. Сервер получает от клиента запрос PATCH /configs/{WID}/parameters,
|
||||
содержащий в теле конфигурационные данные. Далее:
|
||||
|
||||
* Формируется конфигурационное сообщение (1a), содержащее в себе список
|
||||
объектов, описывающих один или несколько параметров команды, которое
|
||||
затем отправляется воркеру.
|
||||
|
||||
* Oжидается ответ от воркера (1a) с информацией о результатах обработки
|
||||
параметров, найденных ошибках и т.д, который пересылается клиенту.
|
||||
|
||||
* Конфигурационный цикл повторяется.
|
||||
|
||||
Возможно стоит добавить на стороне сервера таймаут при получении ответа от
|
||||
воркера.
|
||||
|
||||
3. Сервер получает от клиента запрос POST /execs/{WID}, указывающий
|
||||
закончить конфигурацию команды и начать выполнение.
|
||||
Далее:
|
||||
|
||||
* Формируется сообщение воркеру (3a), сообщающее уже ему об окончании цикла
|
||||
конфигурации.
|
||||
|
||||
* Если есть неисправленные ошибки, воркер отвечает на него сообщением (4b),
|
||||
содержащим оповещение о том, что он заканчивает работу;
|
||||
Если ошибок нет, то сообщением (4a), оповещающим об успешном запуске
|
||||
скрипта команды.
|
||||
|
||||
* В случае, когда обработка параметров прошла с ошибками -- сразу после
|
||||
выдачи сообщения об ошибках конфигурации, воркер отправляет сообщение
|
||||
об окончании своей работы, содержащее список ошибок (9b) и (?) ждет
|
||||
подтверждение от сервера (?).
|
||||
|
||||
4. Сервер получает от клиента запрос GET /execs/{WID}/output на
|
||||
получение имеющихся выходных данных воркера. Далее:
|
||||
|
||||
* Cервер читает все, что есть на сокете воркера. Сообщения (5a, 5b или 6a)
|
||||
на сокете разделены нулевым символом '\0'. Собирается список полных
|
||||
сообщений на сокете, формируется сообщение, содержащее этот список,
|
||||
которое отправляется клиенту.
|
||||
|
||||
5. Сервер получает от клиента запрос PATCH /execs/{WID}/input на передачу
|
||||
воркеру ввода, который был запрошен последним в одном из сообщений ранее
|
||||
переданных клиенту через сервер. Далее:
|
||||
|
||||
* Сервер формирует сообщение (7a) воркеру с данными ввода и отправляет его
|
||||
воркеру. Если данных нет, что означает, что ввод был сброшен
|
||||
пользователем, воркер отправляет серверу сообщение (7b) об некорректном
|
||||
вводе, а метод input объекта ввода/вывода кидает в скрипте специальное
|
||||
исключение.
|
||||
|
||||
Возможно, необходимо убеждаться в том, что данные успешно получены воркером.
|
||||
Воркер должен ответить сообщением (8a) об успешном получении данных.
|
||||
|
||||
- Если данное сообщение не получено в течение некоторого времени
|
||||
(таймаута) -- выполняем некоторые действия по диагностике и устранению
|
||||
последствий ошибки, затем отправляем клиенту сообщение, сообщающее о том,
|
||||
что воркер закончил работу, и содержащее последние сообщения, полученные
|
||||
от воркера, если таковые имеются.
|
||||
|
||||
- Если сообщение об успешном вводе в воркер получено -- отправляем
|
||||
соответствующее сообщение клиенту.
|
||||
|
||||
6. Работа воркера закончилась в результате успешного выполнения команды,
|
||||
или было прервано клиентом командой stop, или по причине ошибок, возникших
|
||||
при выполнении скриптов команды. Далее:
|
||||
|
||||
* Воркер отправляет на сервер одно из двух специальных сообщений,
|
||||
оповещающих об окончании работы:
|
||||
|
||||
- Содержащее также информацию об ошибках (9b), если таковые
|
||||
были, неотправленных по каким-то причинам сообщениях (?) или о факте
|
||||
выполнения команды stop.
|
||||
|
||||
- Содержащие только объявление об окончании выполнения команды, то есть
|
||||
состояния run.
|
||||
|
||||
В зависимости от того, возможно ли чтение из сокета, когда процесс воркера
|
||||
уже закрыт сервер будет или не будет отправлять сообщение разрешающее
|
||||
воркеру прекратить работу, но клиент его видеть не должен. По факту
|
||||
получения конечного сообщения и, возможно, данных вместе с ним, клиенту
|
||||
будет присылаться специальное сообщение.
|
||||
|
||||
|
||||
Контекст воркера
|
||||
----------------
|
||||
worker_state -- состояние воркера;
|
||||
socket_context -- контекст чтения сокета воркера;
|
||||
worker_output -- вывод воркера перед его выключением сервера.
|
||||
|
||||
|
||||
Что делать
|
||||
----------
|
||||
[*] Проверить возможность чтения сервером данных из сокета уже закрытого
|
||||
воркера. Если такой возможности нет, предусмотреть обработку завершения и
|
||||
успешной, и некорректной работы воркера с передачей всех необходимых
|
||||
сообщений. Сделать это внутри воркера.
|
||||
Итог: чтение из сокета убитого демона возможно.
|
||||
|
||||
[ ] Добавить в воркер цикл конфигурации или его тестовый вариант;
|
||||
|
||||
[*] Убрать на стороне интерфейса клиента лишний шаг создания объекта воркера;
|
||||
|
||||
[*] Убрать self._runner_process и self._pid_file, они в таком случае не нужны;
|
||||
|
||||
[*] PID-файл демона удаляем сразу после чтения;
|
||||
|
||||
[ ] Реализовать описанный ниже протокол взаимодействия сервера с воркером;
|
||||
|
||||
[ ] Добавить в метод kill воркера чтение всего содержимого сокета перед его
|
||||
удалением.
|
||||
|
||||
[ ] Разработать HAL-формат сообщений об ошибках вместо HTTPException.
|
||||
|
||||
[*] Сделать метод read воркера асинхронным.
|
||||
|
||||
[ ] На случай если воркер убит через SIGKILL -- поскольку обработку
|
||||
такого завершения работы воркера в самом воркере организовать невозможно,
|
||||
добавить дополнительную проверку is_alive воркера и предусмотреть обработку
|
||||
этой ситуации.
|
||||
|
||||
[*] Добавить главный блок try-catch-finally в воркер. В раздел finally добавить
|
||||
отправку сообщения об окончании работы воркера. В зависимости от
|
||||
возможности читать данный из сокета законченного процесса воркера, ждать
|
||||
или не ждать ответ сервера по поводу окончания работы воркера;
|
||||
|
||||
[*] Добавить предварительно деление воркеров по состояниям, вероятно добавить
|
||||
WorkersManager;
|
||||
|
||||
[ ] Добавить состояния воркера:
|
||||
- config -- воркер в состоянии конфигурации, Т.е. ждёт значения
|
||||
параметров или собщение об окончании конфигурации;
|
||||
- exec -- воркер в процессе выполнения команды;
|
||||
- input -- воркер в процессе ожидания ввода пользователя;
|
||||
- finish -- воркер в состоянии завершения некоторого состояния.
|
||||
|
||||
[ ] Реализовать предварительно REST API в соответствии с наработками;
|
||||
|
||||
[*] Решить, какое значение должны иметь заголовки Content-Type и Accept:
|
||||
- application/json;
|
||||
- application/hal+json;
|
||||
- application/vnd.cphl+json;
|
||||
- application/vnd.api+json.
|
||||
Подумать над версионированием API;
|
||||
|
||||
[ ] Добавить в ответы сервера обозначения о кэшируемости и некэшируемости,
|
||||
проработать кэширование в целом, определить, что должно кэшироваться, а что
|
||||
нет;
|
||||
|
||||
[ ] Проработать механизм остановки взаимодействия сервера с клиентом и
|
||||
возвращения к нему после переподключения клиента, перезагрузки сервера, а
|
||||
также при этих обоих событиях;
|
||||
|
||||
[ ] Добавить проверку во время инициализации сервера наличия PID-файлов
|
||||
демонов-воркеров:
|
||||
- Если такие есть -- проверить, что это за процессы;
|
||||
- Если такие процессы не существуют или не являются воркерами -- удалить
|
||||
эти PID-файлы;
|
||||
- Если процессы существуют, являются воркерами и есть информация о них
|
||||
в БД -- проверяем соответствие PID и WID.
|
||||
-- Если все совпадает -- удаляем PID-файлы и далее создаем объекты
|
||||
воркеров по информации в БД;
|
||||
-- [Q#3] Если есть несовпадения -- убиваем воркер, PID-файл и
|
||||
записи в БД;
|
||||
- [Q#2] Если процессы существуют, являются воркерами, но в БД о них нет
|
||||
информации -- наверное, удаляем PID-файлы и пытаемся убить этих
|
||||
воркеров с помощью их PID.
|
||||
|
||||
|
||||
Интерфейс сервера
|
||||
-----------------
|
||||
REST-интерфейс (вроде). Представляем состояния воркеров в виде ресурсов или
|
||||
объектов. В соответствии с HATEAOS присылаем URI на ресурсы и действия воркера,
|
||||
которые доступны в тот или иной момент. Для этого применяем формат
|
||||
json-сообщений напоминающий HAL или CPHL (не очень распространенный формат).
|
||||
Различие от HAL в наличии атрибута "methods" в "_links".
|
||||
|
||||
* root -- корневой ресурс, являющийся точкой входа для всего API. Возможно,
|
||||
стоит создать алиас /index или заменить на него.
|
||||
|
||||
- GET / -- получить доступные корневые ресурсы;
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response:
|
||||
Media-Type: application/hal+json
|
||||
Cache-Control: public
|
||||
|
||||
{
|
||||
"data": {
|
||||
// Some information about server.
|
||||
},
|
||||
"_links": {
|
||||
"commands": {
|
||||
"href": "/commands/"
|
||||
},
|
||||
"workers": {
|
||||
"href": "/workers/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"meta": {
|
||||
// Server info
|
||||
},
|
||||
"links": {
|
||||
}
|
||||
}
|
||||
|
||||
* сommands -- совокупность данных о командах, доступных для запуска на
|
||||
сервере.
|
||||
|
||||
- GET /commands/ -- получить список команд;
|
||||
* request:
|
||||
Media-Type: application/json
|
||||
|
||||
{
|
||||
"gui": {true/false}
|
||||
}
|
||||
|
||||
* response:
|
||||
Media-Type: application/hal+json
|
||||
Cache-Control: private
|
||||
|
||||
{
|
||||
"{command_id}": {
|
||||
"data": {
|
||||
// Some command info.
|
||||
},
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "/commands/{CID}"
|
||||
},
|
||||
"parameters": {
|
||||
"href": "/commands/{CID}/parameters"
|
||||
},
|
||||
"cofigure": {
|
||||
"href": "/configs/{CID}"
|
||||
}
|
||||
}
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
- GET /commands/{command}/ -- получить информацию об указанной команде, a
|
||||
также ее параметры. Запрос используемый
|
||||
консольным клиентом;
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response:
|
||||
Media-Type: application/hal+json
|
||||
Cache-Control: private
|
||||
|
||||
{
|
||||
"data": {
|
||||
// Some command info.
|
||||
},
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "/commands/{CID}"
|
||||
},
|
||||
"parameters": {
|
||||
"href": "/commands/{CID}/parameters"
|
||||
}
|
||||
"cofigure": {
|
||||
"href": "/configuration/{CID}"
|
||||
}
|
||||
},
|
||||
"_embedded": {
|
||||
"parameters": {
|
||||
"data": [
|
||||
{
|
||||
"group_id": "{group_id}",
|
||||
"parameters": [...]
|
||||
},
|
||||
...
|
||||
],
|
||||
"_links":
|
||||
"self": {
|
||||
"href": "/commands/{CID}/parameters"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- GET /commands/{CID}/parameters -- получить параметры указанной команды;
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response:
|
||||
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"group_id": "{group_id}",
|
||||
"parameters": [...]
|
||||
},
|
||||
...
|
||||
],
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "/commands/{CID}/parameters"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
* configs -- совокупность объектов воркеров, находящихся в состоянии
|
||||
конфигурации.
|
||||
|
||||
- POST /configs/{command} -- создать конфигурацию;
|
||||
201 -- конфигурация уже создана;
|
||||
404 -- команды с указанным id нет;
|
||||
400 -- неправильный формат запроса.
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response:
|
||||
|
||||
{
|
||||
"_links": {
|
||||
"configure": {
|
||||
"href": "/configs/{WID}/parameters"
|
||||
},
|
||||
"execute": {
|
||||
"href": "/executions/{WID}"
|
||||
},
|
||||
"cancel": {
|
||||
"href": "/configs/{WID}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- PATCH /configs/{WID}/parameters -- изменить конфигурацию
|
||||
указанными в теле значениями;
|
||||
404 -- конфигурации с указанным
|
||||
WID нет;
|
||||
400 -- неправильный формат
|
||||
запроса.
|
||||
* request:
|
||||
|
||||
[
|
||||
{"id": "{param_id}", "value": "{param_value}"},
|
||||
...
|
||||
]
|
||||
|
||||
* response:
|
||||
|
||||
{
|
||||
"data": {
|
||||
"{param_id}": "{error_message}",
|
||||
...
|
||||
},
|
||||
"_links": {
|
||||
"configure": {
|
||||
"href": "/configs/{WID}/parameters"
|
||||
},
|
||||
"cancel": {
|
||||
"href": "/configs/{WID}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OR
|
||||
|
||||
{
|
||||
"_links": {
|
||||
"configure": {
|
||||
"href": "/configs/{WID}/parameters"
|
||||
},
|
||||
"execute": {
|
||||
"href": "/executions/{WID}"
|
||||
},
|
||||
"cancel": {
|
||||
"href": "/configs/{WID}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- PUT /configs/{WID}/parameters -- заменить конфигурацию с
|
||||
использованием указанных в теле
|
||||
запроса значений. Пустой набор
|
||||
значений позволяет сбросить
|
||||
конфигурацию;
|
||||
* request:
|
||||
|
||||
[
|
||||
{"id": "{param_id}", "value": "{param_value}"},
|
||||
...
|
||||
]
|
||||
|
||||
* response:
|
||||
|
||||
{
|
||||
"data": {
|
||||
"{param_id}": "{error_message}",
|
||||
...
|
||||
},
|
||||
"_links": {
|
||||
"configure": {
|
||||
"href": "/configs/{WID}/parameters"
|
||||
},
|
||||
"cancel": {
|
||||
"href": "/configs/{WID}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OR
|
||||
|
||||
{
|
||||
"_links": {
|
||||
"configure": {
|
||||
"href": "/configs/{WID}/parameters"
|
||||
},
|
||||
"execute": {
|
||||
"href": "/executions/{WID}"
|
||||
},
|
||||
"cancel": {
|
||||
"href": "/configs/{WID}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- DELETE /configs/{WID} -- закончить конфигурации без выполнения
|
||||
команды.
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response:
|
||||
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"type": "output",
|
||||
"logging": {logging_level/null},
|
||||
"text": "{message_text}",
|
||||
"source": "{source_script}",
|
||||
}
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
* executions -- совокупность объектов воркеров, находящихся в состоянии
|
||||
исполнения.
|
||||
|
||||
- POST /executions/{WID} -- создать новое исполнение из конфигурации;
|
||||
201 -- исполнение создано;
|
||||
409 -- исполнение с этим WID уже есть;
|
||||
400 -- неправильный формат запроса.
|
||||
404 -- конфигурация не найдена.
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response:
|
||||
|
||||
- Response Code: 201
|
||||
|
||||
{
|
||||
"_links": {
|
||||
"output": {
|
||||
"href": "/executions/{WID}/output"
|
||||
},
|
||||
"stop": {
|
||||
"href": "/executions/{WID}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- DELETE /executions/{WID} -- удалить исполнение, происходит путем
|
||||
отправки в скрипт SIGINT;
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response (возвращает список непереданных сообщений, если они есть):
|
||||
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"type": "output",
|
||||
"logging": {logging_level/null},
|
||||
"text": "{message_text}",
|
||||
"source": "{source_script}",
|
||||
}
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
- GET /executions/{WID}/output -- получить список имеющихся сообщений от
|
||||
команды;
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response (необходимость остановить выполнение определяется по
|
||||
Response Code):
|
||||
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"type": "output",
|
||||
"logging": {logging_level/null},
|
||||
"text": "{message_text}",
|
||||
"source": "{source_script}",
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"type": "input",
|
||||
"text": "{input_message}"
|
||||
"source": "{source_script}"
|
||||
},
|
||||
"_links": {
|
||||
"input": {
|
||||
"href": "/executions/{WID}/input",
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
- PATCH /executions/{WID}/input -- отправить в воркер некоторый ввод;
|
||||
* request (если null, значит ввод прерывается):
|
||||
|
||||
{
|
||||
"data": {input_data/null}
|
||||
}
|
||||
|
||||
* response:
|
||||
- Response Code: 200
|
||||
|
||||
null
|
||||
|
||||
- Response Code: 404
|
||||
|
||||
{
|
||||
"error": "Execution {WID} is not found"
|
||||
}
|
||||
|
||||
- Response Code: 400
|
||||
|
||||
{
|
||||
"error": "Input message is not correct"
|
||||
}
|
||||
|
||||
* workers -- вся совокупность воркеров.
|
||||
|
||||
- GET /workers/ -- получить список демонов-воркеров и информацию о них;
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response:
|
||||
|
||||
{
|
||||
"{WID}": {
|
||||
"data": {
|
||||
"socket": "{socket_path}",
|
||||
"command": "{worker_command}",
|
||||
"pid": "{daemon_pid}",
|
||||
}
|
||||
"_links": {
|
||||
"kill": {
|
||||
"href": "/workers/{WID}",
|
||||
"methods": ["delete"]
|
||||
}
|
||||
}
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
- DELETE /workers/{WID} -- отправить сигнал SIGKILL демону-воркеру;
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response (сообщения полученные от убитого воркера):
|
||||
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"type": "output",
|
||||
"logging": {logging_level/null},
|
||||
"text": "{message_text}",
|
||||
"source": "{source_script}",
|
||||
}
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
|
||||
Протокол взаимодействия воркера и сервера
|
||||
-----------------------------------------
|
||||
Все сообщения представляют собой json-структуры, включающие в себя 3 поля:
|
||||
- state -- состояние воркера:
|
||||
* Если сообщение пришло воркеру от сервера, тогда это поле указывает
|
||||
воркеру, какое состояние он должен принять;
|
||||
* Если сообщение пришло серверу от воркера, тогда это поле указывает на
|
||||
текущее состояние воркера.
|
||||
|
||||
- status -- статус текущего состояния:
|
||||
0 -- состояние корректно;
|
||||
1 -- состояние некорректно.
|
||||
|
||||
- data -- некоторые данные, структура которых может быть любой.
|
||||
|
||||
1. Данные конфигурации от клиента.
|
||||
a. Продолжить конфигурацию указанными значениями:
|
||||
|
||||
{
|
||||
"state": "config",
|
||||
"status": 0,
|
||||
"data": [
|
||||
{"id": "{param_id}", "value": "{parameter_value}"},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
b. Сбросить конфигурацию и продолжить указанными значениями:
|
||||
|
||||
{
|
||||
"state": "config",
|
||||
"status": 1,
|
||||
"data": [
|
||||
{"id": "{param_id}", "value": "{parameter_value}"},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
2. Ответ воркера по данным конфигурации.
|
||||
a. Все параметры конфигурации корректны:
|
||||
|
||||
{
|
||||
"state": "config",
|
||||
"status": 0,
|
||||
"data": null
|
||||
}
|
||||
|
||||
b. Некоторые параметры конфигурации некорректны:
|
||||
|
||||
{
|
||||
"state": "config",
|
||||
"status": 1,
|
||||
"data": {
|
||||
"{param_id}": "{error_message}",
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
3. Запрос на окончание конфигурации от клиента.
|
||||
a. Закончить конфигурацию, начать выполнение команды:
|
||||
|
||||
{
|
||||
"state": "finish",
|
||||
"status": 0,
|
||||
"data": null
|
||||
}
|
||||
|
||||
b. Закончить конфигурацию и работу воркера:
|
||||
|
||||
{
|
||||
"state": "finish",
|
||||
"status": 1,
|
||||
"data": null
|
||||
}
|
||||
|
||||
4. Ответ воркера на запрос об окончании конфигурации:
|
||||
a. Закончить конфигурацию, начать выполнение команды:
|
||||
|
||||
{
|
||||
"state": "finish",
|
||||
"status": 0,
|
||||
"data": null
|
||||
}
|
||||
|
||||
b. Закончить конфигурацию и работу воркера:
|
||||
|
||||
{
|
||||
"state": "finish",
|
||||
"status": 1,
|
||||
"data": {
|
||||
"{param_id}": "{error_message}",
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
5. Сообщение с выводом воркера.
|
||||
a. Сообщение из скрипта при его "штатной" работe:
|
||||
|
||||
{
|
||||
"state": "exec",
|
||||
"status": 0,
|
||||
"data": {
|
||||
"type": "message",
|
||||
"logging": {logging_level/null},
|
||||
"text": "{message_text}",
|
||||
"source": "{source_script}"
|
||||
}
|
||||
}
|
||||
|
||||
b. Сообщение об ошибке приводящей к завершению выполнения скрипта и
|
||||
команды:
|
||||
|
||||
{
|
||||
"state": "exec",
|
||||
"status": 1,
|
||||
"data": {
|
||||
"error": "{error_message}",
|
||||
"source": "{source_script}"
|
||||
}
|
||||
}
|
||||
|
||||
6. Сообщение с запросом на ввод.
|
||||
a. Состояние "input" -- воркер в состоянии ожидания ввода:
|
||||
{
|
||||
"state": "input",
|
||||
"status": 0,
|
||||
"data": {
|
||||
"text": "{message_text}",
|
||||
"source": "{source_script}"
|
||||
}
|
||||
}
|
||||
|
||||
7. Сообщение от клиента с данными.
|
||||
a. Ввод успешно выполнен и передается:
|
||||
|
||||
{
|
||||
"state": "input",
|
||||
"status": 0,
|
||||
"data": "{client_data}"
|
||||
}
|
||||
|
||||
b. Ввод данных прерван, данных не будет:
|
||||
|
||||
{
|
||||
"state": "input",
|
||||
"status": 1,
|
||||
"data": null
|
||||
}
|
||||
|
||||
8. Сообщение от воркера о получении ввода.
|
||||
a. Ввод успешно получен, о чем говорит переход в состояние выполнения со
|
||||
статусом 0, т.е. без ошибок:
|
||||
|
||||
{
|
||||
"state": "exec",
|
||||
"status": 0,
|
||||
"data": null
|
||||
}
|
||||
|
||||
9. Сообщение воркера об окончании работы:
|
||||
a. Успешное окончание работы:
|
||||
|
||||
{
|
||||
"state": "finish",
|
||||
"status": 0,
|
||||
"data": null
|
||||
}
|
||||
|
||||
b. Работа воркера была прервана или закончилась с ошибками:
|
||||
|
||||
{
|
||||
"state": "finish",
|
||||
"status": 1,
|
||||
"data": [
|
||||
"{error_message}",
|
||||
...
|
||||
]
|
||||
}
|
165
calculate/server/ipc_draft/async_client.py
Executable file
165
calculate/server/ipc_draft/async_client.py
Executable file
|
@ -0,0 +1,165 @@
|
|||
#! env/bin/python3
|
||||
import sys
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import logging
|
||||
|
||||
from typing import List, Tuple, Any
|
||||
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
|
||||
SERVER_DOMAIN = "http://127.0.0.1:2007"
|
||||
|
||||
|
||||
LOG_LEVELS = {logging.ERROR: "ERROR",
|
||||
logging.INFO: "INFO",
|
||||
logging.WARNING: "WARNING",
|
||||
logging.DEBUG: "DEBUG",
|
||||
logging.CRITICAL: "CRITICAL",
|
||||
}
|
||||
|
||||
|
||||
async def check_server(client: aiohttp.ClientSession):
|
||||
try:
|
||||
async with client.get(f"{SERVER_DOMAIN}/") as response:
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert data == {"status": "active"}
|
||||
|
||||
print(f"Connected to the Calculate Server on {SERVER_DOMAIN}")
|
||||
return True
|
||||
except Exception:
|
||||
print("Can not connect to the calculate server.")
|
||||
return False
|
||||
|
||||
|
||||
async def get_commands(client: aiohttp.ClientSession):
|
||||
async with client.get(f"{SERVER_DOMAIN}/commands") as response:
|
||||
assert response.status == 200
|
||||
data = await response.json()
|
||||
return data
|
||||
|
||||
|
||||
async def create_worker(client: aiohttp.ClientSession, command: str,
|
||||
arguments: List[str]):
|
||||
worker_args = {"arguments": arguments}
|
||||
async with client.post(f"{SERVER_DOMAIN}/workers/{command}",
|
||||
json=worker_args) as response:
|
||||
assert response.status == 200
|
||||
data = await response.json()
|
||||
return data["wid"]
|
||||
|
||||
|
||||
async def start_worker(client: aiohttp.ClientSession, wid: int):
|
||||
async with client.post(f"{SERVER_DOMAIN}/workers/{wid}/start") as response:
|
||||
assert response.status == 200
|
||||
data = await response.json()
|
||||
return data
|
||||
|
||||
|
||||
async def get_worker_messages(client: aiohttp.ClientSession, wid: int):
|
||||
async with client.get(f"{SERVER_DOMAIN}/workers/{wid}/messages"
|
||||
) as response:
|
||||
assert response.status == 200
|
||||
data = await response.json()
|
||||
return data["data"]
|
||||
|
||||
|
||||
async def send_data_to_worker(client: aiohttp.ClientSession, wid: int,
|
||||
data: Any):
|
||||
first_try = True
|
||||
while True:
|
||||
answer = {"data": data}
|
||||
try:
|
||||
async with client.post(f"{SERVER_DOMAIN}/workers/{wid}/send",
|
||||
json=answer) as response:
|
||||
assert response.status == 200
|
||||
data = await response.json()
|
||||
return
|
||||
except aiohttp.client_exceptions.ClientOSError:
|
||||
if first_try:
|
||||
first_try = False
|
||||
continue
|
||||
else:
|
||||
raise
|
||||
# return data
|
||||
|
||||
|
||||
async def finish_worker(client: aiohttp.ClientSession, wid: int):
|
||||
async with client.post(f"{SERVER_DOMAIN}/workers/{wid}/finish"
|
||||
) as response:
|
||||
assert response.status == 200
|
||||
data = await response.json()
|
||||
return data
|
||||
|
||||
|
||||
async def main():
|
||||
print("Calculate Console Client 0.0.1")
|
||||
async with aiohttp.ClientSession(
|
||||
connector=aiohttp.TCPConnector(force_close=False)) as client:
|
||||
if not await check_server(client):
|
||||
return
|
||||
|
||||
commands = await get_commands(client)
|
||||
command, command_args = get_console_command()
|
||||
message = check_command(command, commands)
|
||||
if message:
|
||||
print(message)
|
||||
return
|
||||
|
||||
wid = await create_worker(client, command, command_args)
|
||||
try:
|
||||
print("CREATED WORKER ID:", wid)
|
||||
status_data = await start_worker(client, wid)
|
||||
if status_data.get("status", None) == "error":
|
||||
print("ERROR:", status_data.get("description", "NONE"))
|
||||
return
|
||||
|
||||
finished = False
|
||||
while not finished:
|
||||
messages = await get_worker_messages(client, wid)
|
||||
for message in messages:
|
||||
if message["type"] == "output":
|
||||
print(f"{LOG_LEVELS[message['level']]}:"
|
||||
f" {message['msg']}")
|
||||
elif message["type"] == "input":
|
||||
print(message["msg"])
|
||||
input_data = input(">> ")
|
||||
await send_data_to_worker(client, wid, input_data)
|
||||
elif (message["type"] == "control"
|
||||
and message["action"] == "finish"):
|
||||
finished = True
|
||||
finally:
|
||||
await finish_worker(client, wid)
|
||||
|
||||
|
||||
def get_console_command() -> Tuple[str, List[str]]:
|
||||
offset = 1 if sys.argv[0].endswith("client.py") else 0
|
||||
|
||||
if len(sys.argv) < 1 + offset:
|
||||
command = None
|
||||
command_args = []
|
||||
else:
|
||||
command = sys.argv[offset]
|
||||
command_args = sys.argv[1 + offset:]
|
||||
|
||||
return command, command_args
|
||||
|
||||
|
||||
def check_command(command: str, available_commands: List[str]) -> str:
|
||||
if command is None:
|
||||
return "Command is not set."
|
||||
if command not in available_commands:
|
||||
return "Command is not available."
|
||||
return ''
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
loop.run_until_complete(main())
|
||||
except KeyboardInterrupt:
|
||||
print("\r<< Keyboard interrupt.")
|
||||
loop.close()
|
1
calculate/server/ipc_draft/cl-command-0
Symbolic link
1
calculate/server/ipc_draft/cl-command-0
Symbolic link
|
@ -0,0 +1 @@
|
|||
client.py
|
1
calculate/server/ipc_draft/cl-command-1
Symbolic link
1
calculate/server/ipc_draft/cl-command-1
Symbolic link
|
@ -0,0 +1 @@
|
|||
client.py
|
1
calculate/server/ipc_draft/cl-command-2
Symbolic link
1
calculate/server/ipc_draft/cl-command-2
Symbolic link
|
@ -0,0 +1 @@
|
|||
client.py
|
1
calculate/server/ipc_draft/cl-command-3
Symbolic link
1
calculate/server/ipc_draft/cl-command-3
Symbolic link
|
@ -0,0 +1 @@
|
|||
client.py
|
183
calculate/server/ipc_draft/client.py
Executable file
183
calculate/server/ipc_draft/client.py
Executable file
|
@ -0,0 +1,183 @@
|
|||
#! env/bin/python3
|
||||
import sys
|
||||
import requests
|
||||
import logging
|
||||
|
||||
from typing import Tuple, List, Union, Dict
|
||||
|
||||
from pprint import pprint
|
||||
|
||||
|
||||
LOG_LEVELS = {logging.ERROR: "ERROR",
|
||||
logging.INFO: "INFO",
|
||||
logging.WARNING: "WARNING",
|
||||
logging.DEBUG: "DEBUG",
|
||||
logging.CRITICAL: "CRITICAL",
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
# Обращаемся к корню сервера для получения списка доступных ресурсов.
|
||||
data, links = get_root_data("http://127.0.0.1:2007")
|
||||
if data is None:
|
||||
return
|
||||
print(f"Connected to {data['name']}")
|
||||
|
||||
print("\nRoot links:")
|
||||
pprint(links)
|
||||
|
||||
# Получаем данные о текущей команде.
|
||||
cl_command, cl_parameters = get_console_command()
|
||||
print_console_parameters(cl_parameters)
|
||||
data, links, parameters = find_command(cl_command, links)
|
||||
if data is None:
|
||||
return
|
||||
print_command_info(data)
|
||||
print_parameters(parameters)
|
||||
|
||||
links = start_command_configuration(links)
|
||||
|
||||
print("\nConfig links:")
|
||||
pprint(links)
|
||||
|
||||
print("\nSetting Parameters")
|
||||
print("\nLinks:")
|
||||
data, links = set_parameters(links, cl_parameters)
|
||||
pprint(links)
|
||||
|
||||
if data is not None:
|
||||
print_config_errors(data)
|
||||
|
||||
print("\nCancel configuration:")
|
||||
result = cancel_configuration(links)
|
||||
pprint(result)
|
||||
|
||||
|
||||
def get_root_data(base_url: str) -> Tuple[Union[dict, None],
|
||||
Union[dict, None]]:
|
||||
try:
|
||||
response = requests.get(f"{base_url}/")
|
||||
if response.status_code == 200:
|
||||
data = response.json()["data"]
|
||||
links = response.json()["_links"]
|
||||
|
||||
return data, links
|
||||
else:
|
||||
detail = response.json()['detail']
|
||||
print(f"{response.status_code}: {detail}")
|
||||
except Exception as error:
|
||||
print("ERROR:", str(error))
|
||||
return None, None
|
||||
|
||||
|
||||
def find_command(console_command: str, links: dict) -> dict:
|
||||
find_uri = links["find_command"]["href"].format(
|
||||
console_command=console_command,
|
||||
is_gui=0)
|
||||
response = requests.get(find_uri)
|
||||
|
||||
if response.status_code == 200:
|
||||
json = response.json()
|
||||
data = json["data"]
|
||||
links = json["_links"]
|
||||
parameters = json["_embedded"]["parameters"]
|
||||
return data, links, parameters
|
||||
else:
|
||||
detail = response.json()['detail']
|
||||
print(f"{response.status_code}: {detail}")
|
||||
return None, None, None
|
||||
|
||||
|
||||
def get_parameters(links: dict) -> List[dict]:
|
||||
params_uri = links["parameters"]["href"]
|
||||
response = requests.get(params_uri)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
print(f"Response Code: {response.status_code}, {response.text}")
|
||||
return None
|
||||
|
||||
|
||||
def start_command_configuration(links: dict) -> Union[dict, None]:
|
||||
configure_uri = links["configure"]["href"]
|
||||
response = requests.post(configure_uri)
|
||||
if response.status_code == 200:
|
||||
json = response.json()
|
||||
return json["_links"]
|
||||
else:
|
||||
print(f"Response Code: {response.status_code}, {response.text}")
|
||||
return None
|
||||
|
||||
|
||||
def set_parameters(links: dict, parameters: List[dict]) -> Dict[str, dict]:
|
||||
configure_uri = links["configure"]["href"]
|
||||
response = requests.patch(configure_uri, json=parameters)
|
||||
if response.status_code == 200:
|
||||
json = response.json()
|
||||
data = json.get("data", None)
|
||||
links = json["_links"]
|
||||
return data, links
|
||||
else:
|
||||
print(f"Response Code: {response.status_code}, {response.text}")
|
||||
return None, None
|
||||
|
||||
|
||||
def cancel_configuration(links: dict) -> List[dict]:
|
||||
configure_uri = links["cancel"]["href"]
|
||||
response = requests.delete(configure_uri)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
print(f"Response Code: {response.status_code}, {response.text}")
|
||||
return None
|
||||
|
||||
|
||||
def get_console_command() -> Tuple[str, List[str]]:
|
||||
offset = 1 if sys.argv[0].endswith("client.py") else 0
|
||||
command_args = []
|
||||
|
||||
if len(sys.argv) < 1 + offset:
|
||||
command = None
|
||||
else:
|
||||
command = sys.argv[offset].split("/")[-1]
|
||||
args = sys.argv[1 + offset:]
|
||||
if len(args) >= 2 and not (len(args) % 2):
|
||||
for index in range(0, len(args) - len(args) % 2, 2):
|
||||
id, value = args[index: index + 2]
|
||||
command_args.append({"id": id.strip("-"),
|
||||
"value": value})
|
||||
|
||||
return command, command_args
|
||||
|
||||
|
||||
def print_console_parameters(parameters: dict):
|
||||
print("\nConsole parameters:")
|
||||
for parameter in parameters:
|
||||
print(f" {parameter['id']} = {parameter['value']}")
|
||||
|
||||
|
||||
def print_command_info(data: dict) -> None:
|
||||
print(f"\nCurrent command: {data['id']}\n"
|
||||
f" category: {data['category']}\n"
|
||||
f" title: {data['title']}")
|
||||
|
||||
|
||||
def print_parameters(parameters_data: list) -> None:
|
||||
parameters = parameters_data["data"]
|
||||
print("\nParameters:")
|
||||
for group in parameters:
|
||||
print(f" {group['group_id']}")
|
||||
group_parameters = group["parameters"]
|
||||
for parameter in group_parameters:
|
||||
print(f" parameter:\t{parameter['id']}\n"
|
||||
f" default:\t{parameter['default']}\n")
|
||||
|
||||
|
||||
def print_config_errors(errors: Dict[str, str]) -> None:
|
||||
print("\nConfiguration errors:")
|
||||
for parameter_id, error in errors.items():
|
||||
print(f" {parameter_id}: {error}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
77
calculate/server/ipc_draft/commands.py
Normal file
77
calculate/server/ipc_draft/commands.py
Normal file
|
@ -0,0 +1,77 @@
|
|||
from typing import List, Callable, Dict
|
||||
from scripts import script_0, script_1, script_2
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
class Command:
|
||||
'''Тестовый класс команд'''
|
||||
def __init__(self,
|
||||
command_id: str,
|
||||
command: str,
|
||||
category: str,
|
||||
title: str,
|
||||
scripts: List[Callable],
|
||||
parameters: Dict[str, dict]):
|
||||
self.id = command_id
|
||||
self.command = command
|
||||
self.category = category
|
||||
self.command = command
|
||||
self.title = title
|
||||
self.scripts = scripts
|
||||
self.parameters = parameters
|
||||
|
||||
|
||||
command_0 = Command(command_id="command_0",
|
||||
category="Test",
|
||||
title="Test command number 0",
|
||||
command="cl-command-0",
|
||||
scripts=OrderedDict({"script_0": script_0,
|
||||
"script_1": script_1}),
|
||||
parameters=[{"group_id": "group_0",
|
||||
"parameters": [{"id": "value_0",
|
||||
"default": 253},
|
||||
{"id": "value_1",
|
||||
"default": 413}]
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
command_1 = Command(command_id="command_1",
|
||||
category="Test",
|
||||
title="Test command number 1",
|
||||
command="cl-command-1",
|
||||
scripts=OrderedDict({"script_0": script_0,
|
||||
"script_1": script_1,
|
||||
"script_2": script_2}),
|
||||
parameters=[{"group_id": "group_1",
|
||||
"parameters": [{"id": "value_0",
|
||||
"default": 35},
|
||||
{"id": "value_1",
|
||||
"default": 41}]
|
||||
},
|
||||
{"group_id": "group_2",
|
||||
"parameters": [{"id": "value_2",
|
||||
"default": 11}]
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
command_2 = Command(command_id="command_2",
|
||||
category="Test",
|
||||
title="Test command number 2",
|
||||
command="cl-command-2",
|
||||
scripts=OrderedDict({"script_0": script_0,
|
||||
"script_2": script_2}),
|
||||
parameters=[{"group_id": "group_0",
|
||||
"parameters": [{"id": "value_0",
|
||||
"default": 253}]
|
||||
},
|
||||
{"group_id": "group_1",
|
||||
"parameters": [{"id": "value_2",
|
||||
"default": 413},
|
||||
{"id": "value_5",
|
||||
"default": 23}]
|
||||
}]
|
||||
)
|
121
calculate/server/ipc_draft/daemon.py
Normal file
121
calculate/server/ipc_draft/daemon.py
Normal file
|
@ -0,0 +1,121 @@
|
|||
# import time
|
||||
from typing import Union, List, Optional, Dict
|
||||
from multiprocessing.connection import Listener
|
||||
|
||||
from commands import Command
|
||||
from io_module import IOModule
|
||||
|
||||
|
||||
def daemon(listener: Listener, wid: int, base_dir: str, command: Command):
|
||||
"""Daemon main function."""
|
||||
with IOModule(listener, command.id) as io:
|
||||
io.script = "worker"
|
||||
try:
|
||||
# int("lolkek")
|
||||
parameters = configure(io,
|
||||
get_default_parameters(command.parameters))
|
||||
except KeyboardInterrupt:
|
||||
# Остановлено пользователем.
|
||||
send_finish(io)
|
||||
return
|
||||
except Exception as error:
|
||||
# Остановлено из-за ошибки.
|
||||
send_finish(io, errors=[str(error)])
|
||||
return
|
||||
|
||||
try:
|
||||
for name, script in command.scripts.items():
|
||||
script_func = script[0]
|
||||
|
||||
parameters = configure(io, dict())
|
||||
|
||||
io.set_info(f"Running script '{name}'")
|
||||
io.script = name
|
||||
|
||||
script_func(io, parameters)
|
||||
|
||||
io.script = "worker"
|
||||
|
||||
for num in range(5):
|
||||
io.set_error(f"test_error_{num}")
|
||||
raise Exception("critical error LOL")
|
||||
|
||||
io.set_error("there is no life after CRITICAL error")
|
||||
|
||||
except Exception as error:
|
||||
io.set_error(str(error))
|
||||
finally:
|
||||
io.set_info("Command is done.")
|
||||
io.send({"type": "finish", "msg": "finishing."})
|
||||
for num in range(5):
|
||||
io.set_info(f"After finish message_{num}.")
|
||||
|
||||
|
||||
def configure(io: IOModule, parameters: dict) -> Union[None, dict]:
|
||||
# Копируем чтобы уже для тестов реализовать возможность сброса.
|
||||
processing_parameters = parameters.copy()
|
||||
|
||||
while True:
|
||||
errors = []
|
||||
server_message = io.receive()
|
||||
|
||||
state = server_message["state"]
|
||||
status = server_message["status"]
|
||||
|
||||
if state == "config":
|
||||
if status == 1:
|
||||
# Сброс параметров.
|
||||
processing_parameters = parameters.copy()
|
||||
values: list = server_message.get("data", None)
|
||||
if values is not None:
|
||||
# Модифицируем параметры.
|
||||
errors = modify_parameters(processing_parameters, values)
|
||||
elif state == "finish":
|
||||
if status == 0:
|
||||
# Заканчиваем конфигурацию и возвращаем полученные параметры
|
||||
# если статус 1.
|
||||
return parameters
|
||||
# Останавливаем конфигурацию и работу воркера если статус 1.
|
||||
raise KeyboardInterrupt()
|
||||
|
||||
if errors:
|
||||
send_config_status(io, errors=errors)
|
||||
else:
|
||||
send_config_status(io)
|
||||
|
||||
|
||||
def modify_parameters(current: dict, new: list) -> None:
|
||||
errors = {}
|
||||
for parameter in new:
|
||||
parameter_id = parameter["id"]
|
||||
parameter_value = parameter["value"]
|
||||
if parameter_id in current:
|
||||
try:
|
||||
parameter_value = int(parameter_value)
|
||||
current[parameter_id] = parameter_value
|
||||
except ValueError as error:
|
||||
errors[parameter_id] = str(error)
|
||||
else:
|
||||
errors[parameter_id] = f'Parameter "{parameter_id}" is not found.'
|
||||
return errors
|
||||
|
||||
|
||||
def send_finish(io: IOModule, errors: Optional[List[str]] = []) -> None:
|
||||
io.send({"state": "finish",
|
||||
"status": int(bool(errors)),
|
||||
"data": errors})
|
||||
|
||||
|
||||
def send_config_status(io: IOModule,
|
||||
errors: Optional[Dict[str, str]] = {}) -> None:
|
||||
io.send({"state": "config",
|
||||
"status": int(bool(errors)),
|
||||
"data": errors})
|
||||
|
||||
|
||||
def get_default_parameters(parameters_description):
|
||||
parameters = {}
|
||||
for group in parameters_description:
|
||||
for parameter in group["parameters"]:
|
||||
parameters[parameter["id"]] = parameter["default"]
|
||||
return parameters
|
143
calculate/server/ipc_draft/example/ipc.py
Normal file
143
calculate/server/ipc_draft/example/ipc.py
Normal file
|
@ -0,0 +1,143 @@
|
|||
# Copyright 2017 Dan Krause
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import socketserver
|
||||
import socket
|
||||
import struct
|
||||
import json
|
||||
|
||||
|
||||
class IPCError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnknownMessageClass(IPCError):
|
||||
pass
|
||||
|
||||
|
||||
class InvalidSerialization(IPCError):
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionClosed(IPCError):
|
||||
pass
|
||||
|
||||
|
||||
def _read_objects(sock):
|
||||
header = sock.recv(4)
|
||||
if len(header) == 0:
|
||||
raise ConnectionClosed()
|
||||
size = struct.unpack('!i', header)[0]
|
||||
data = sock.recv(size - 4)
|
||||
if len(data) == 0:
|
||||
raise ConnectionClosed()
|
||||
return Message.deserialize(json.loads(data))
|
||||
|
||||
|
||||
def _write_objects(sock, objects):
|
||||
data = json.dumps([o.serialize() for o in objects])
|
||||
sock.sendall(struct.pack('!i', len(data) + 4))
|
||||
sock.sendall(data)
|
||||
|
||||
|
||||
def _recursive_subclasses(cls):
|
||||
classmap = {}
|
||||
for subcls in cls.__subclasses__():
|
||||
classmap[subcls.__name__] = subcls
|
||||
classmap.update(_recursive_subclasses(subcls))
|
||||
return classmap
|
||||
|
||||
|
||||
class Message(object):
|
||||
@classmethod
|
||||
def deserialize(cls, objects):
|
||||
classmap = _recursive_subclasses(cls)
|
||||
serialized = []
|
||||
for obj in objects:
|
||||
if isinstance(obj, Message):
|
||||
serialized.append(obj)
|
||||
else:
|
||||
try:
|
||||
serialized.append(classmap[obj['class']](*obj['args'],
|
||||
**obj['kwargs']))
|
||||
except KeyError as e:
|
||||
raise UnknownMessageClass(e)
|
||||
except TypeError as e:
|
||||
raise InvalidSerialization(e)
|
||||
return serialized
|
||||
|
||||
def serialize(self):
|
||||
args, kwargs = self._get_args()
|
||||
return {'class': type(self).__name__, 'args': args, 'kwargs': kwargs}
|
||||
|
||||
def _get_args(self):
|
||||
return [], {}
|
||||
|
||||
def __repr__(self):
|
||||
r = self.serialize()
|
||||
args = ', '.join([repr(arg) for arg in r['args']])
|
||||
kwargs = ''.join([', {}={}'.format(k, repr(v))
|
||||
for k, v in r['kwargs'].items()])
|
||||
name = r['class']
|
||||
return '{}({}{})'.format(name, args, kwargs)
|
||||
|
||||
|
||||
class Client(object):
|
||||
def __init__(self, server_address):
|
||||
self.addr = server_address
|
||||
if isinstance(self.addr, str):
|
||||
address_family = socket.AF_UNIX
|
||||
else:
|
||||
address_family = socket.AF_INET
|
||||
self.sock = socket.socket(address_family, socket.SOCK_STREAM)
|
||||
|
||||
def connect(self):
|
||||
self.sock.connect(self.addr)
|
||||
|
||||
def close(self):
|
||||
self.sock.close()
|
||||
|
||||
def __enter__(self):
|
||||
self.connect()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
|
||||
def send(self, objects):
|
||||
_write_objects(self.sock, objects)
|
||||
return _read_objects(self.sock)
|
||||
|
||||
|
||||
class Server(socketserver.ThreadingUnixStreamServer):
|
||||
def __init__(self, server_address, callback, bind_and_activate=True):
|
||||
if not callable(callback):
|
||||
callback = lambda x: []
|
||||
|
||||
class IPCHandler(socketserver.BaseRequestHandler):
|
||||
def handle(self):
|
||||
while True:
|
||||
try:
|
||||
results = _read_objects(self.request)
|
||||
except ConnectionClosed:
|
||||
return
|
||||
_write_objects(self.request, callback(results))
|
||||
|
||||
if isinstance(server_address, str):
|
||||
self.address_family = socket.AF_UNIX
|
||||
else:
|
||||
self.address_family = socket.AF_INET
|
||||
|
||||
socketserver.TCPServer.__init__(self, server_address, IPCHandler,
|
||||
bind_and_activate)
|
830
calculate/server/ipc_draft/interfaces.txt
Normal file
830
calculate/server/ipc_draft/interfaces.txt
Normal file
|
@ -0,0 +1,830 @@
|
|||
Вопросы
|
||||
-------
|
||||
1. >> Что является ресурсом?
|
||||
* commands -- информация о командах, их список, группы, описания и тд.
|
||||
* workers(?) -- воркеры, исполняющие команды. Сомнительно.
|
||||
Возможно есть смысл скрыть реализацию, т.е. воркеров, под некоторыми
|
||||
другими сущностями. Например, execution и configuration.
|
||||
workers -- мб оставить как дополнительный низкоуровневый интерфейс.
|
||||
<< Ресурсы: commands, configuration, execution, workers (low_level)
|
||||
|
||||
2. >> Что делать с демонами-воркерами, обнаруженными при инциализации сервера,
|
||||
но не зарегистрированными в БД?
|
||||
<< Убиваем их и удаляем PID-файлы.
|
||||
|
||||
3. >> Что делать с демонами-воркерами, для которых найдены PID-файлы, по PID
|
||||
есть информация в БД, но WID из имени PID-файла не совпадает с WID в БД.
|
||||
<< Убиваем воркера, удаляем PID-файл и запись в БД.
|
||||
|
||||
4. >> Стоит ли убрать разделение команд и их id?
|
||||
<< Не стоит.
|
||||
|
||||
5. >> Нужна ли возможность сброса текущих параметров конфигурации?
|
||||
<< Да.
|
||||
|
||||
|
||||
Взаимодействие с воркером с точки зрения сервера
|
||||
------------------------------------------------
|
||||
1. Сервер получает от клиента запрос, POST /configuration/{command_id}. Далее:
|
||||
|
||||
* Сервер создает объект воркера команды и запускает с его помощью
|
||||
демона-рабочего, оставляя его в режиме ожидания конфигурационных данных.
|
||||
|
||||
* После создания объекта воркера в БД сохраняется основная информация
|
||||
необходимая для воссоздания объекта воркера в новом процессе сервера. Такая
|
||||
ситуация может возникнуть при переходе на новую версию сервера:
|
||||
|
||||
- int wid -- идентификатор воркера;
|
||||
|
||||
- str socket -- путь к сокету для чтения данных воркера;
|
||||
|
||||
- int daemon_pid -- PID демона-воркера для взаимодействия с ним.
|
||||
|
||||
* В ответ на запрос сервер получает список доступных ресурсов, включающих
|
||||
в себя в том числе действия воркера, а также URI этих ресурсов и
|
||||
доступные для этого URI http-методы. На данном этапе:
|
||||
|
||||
- configure (patch) -- изменить некоторые параметры конфигурации;
|
||||
|
||||
- cancel (delete) -- закончить конфигурацию без продолжения выполнения.
|
||||
По сути удаляет данную конфигурацию как ресурс;
|
||||
|
||||
- execute (post) -- запустить выполнение команды с дефолтной
|
||||
конфигурацией. По сути удаляет конфигурацию как ресурс и создает
|
||||
новый ресурс -- выполнение.
|
||||
|
||||
2. Сервер получает от клиента запрос PATCH /configuration/{WID}/parameters,
|
||||
содержащий в теле конфигурационные данные. Далее:
|
||||
|
||||
* Формируется конфигурационное сообщение (1a), содержащее в себе список
|
||||
объектов, описывающих один или несколько параметров команды, которое
|
||||
затем отправляется воркеру.
|
||||
|
||||
* Oжидается ответ от воркера (1a) с информацией о результатах обработки
|
||||
параметров, найденных ошибках и т.д, который пересылается клиенту.
|
||||
|
||||
* Конфигурационный цикл повторяется.
|
||||
|
||||
Возможно стоит добавить на стороне сервера таймаут при получении ответа от
|
||||
воркера.
|
||||
|
||||
3. Сервер получает от клиента запрос POST /executions/{WID}, указывающий
|
||||
закончить конфигурацию команды и начать выполнение.
|
||||
Далее:
|
||||
|
||||
* Формируется сообщение воркеру (3a), сообщающее уже ему об окончании цикла
|
||||
конфигурации.
|
||||
|
||||
* Если есть неисправленные ошибки, воркер отвечает на него сообщением (4b),
|
||||
содержащим оповещение о том, что он заканчивает работу;
|
||||
Если ошибок нет, то сообщением (4a), оповещающим об успешном запуске
|
||||
скрипта команды.
|
||||
|
||||
* В случае, когда обработка параметров прошла с ошибками -- сразу после
|
||||
выдачи сообщения об ошибках конфигурации, воркер отправляет сообщение
|
||||
об окончании своей работы, содержащее список ошибок (9b) и (?) ждет
|
||||
подтверждение от сервера (?).
|
||||
|
||||
4. Сервер получает от клиента запрос GET /executions/{WID}/output на
|
||||
получение имеющихся выходных данных воркера. Далее:
|
||||
|
||||
* Cервер читает все, что есть на сокете воркера. Сообщения (5a, 5b или 6a)
|
||||
на сокете разделены нулевым символом '\0'. Собирается список полных
|
||||
сообщений на сокете, формируется сообщение, содержащее этот список,
|
||||
которое отправляется клиенту.
|
||||
|
||||
5. Сервер получает от клиента запрос PATCH /executions/{WID}/input на передачу
|
||||
воркеру ввода, который был запрошен последним в одном из сообщений ранее
|
||||
переданных клиенту через сервер. Далее:
|
||||
|
||||
* Сервер формирует сообщение (7a) воркеру с данными ввода и отправляет его
|
||||
воркеру. Если данных нет, что означает, что ввод был сброшен
|
||||
пользователем, воркер отправляет серверу сообщение (7b) об некорректном
|
||||
вводе, а метод input объекта ввода/вывода кидает в скрипте специальное
|
||||
исключение.
|
||||
|
||||
Возможно, необходимо убеждаться в том, что данные успешно получены воркером.
|
||||
Воркер должен ответить сообщением (8a) об успешном получении данных.
|
||||
|
||||
- Если данное сообщение не получено в течение некоторого времени
|
||||
(таймаута) -- выполняем некоторые действия по диагностике и устранению
|
||||
последствий ошибки, затем отправляем клиенту сообщение, сообщающее о том,
|
||||
что воркер закончил работу, и содержащее последние сообщения, полученные
|
||||
от воркера, если таковые имеются.
|
||||
|
||||
- Если сообщение об успешном вводе в воркер получено -- отправляем
|
||||
соответствующее сообщение клиенту.
|
||||
|
||||
6. Работа воркера закончилась в результате успешного выполнения команды,
|
||||
или было прервано клиентом командой stop, или по причине ошибок, возникших
|
||||
при выполнении скриптов команды. Далее:
|
||||
|
||||
* Воркер отправляет на сервер одно из двух специальных сообщений,
|
||||
оповещающих об окончании работы:
|
||||
|
||||
- Содержащее также информацию об ошибках (9b), если таковые
|
||||
были, неотправленных по каким-то причинам сообщениях (?) или о факте
|
||||
выполнения команды stop.
|
||||
|
||||
- Содержащие только объявление об окончании выполнения команды, то есть
|
||||
состояния run.
|
||||
|
||||
В зависимости от того, возможно ли чтение из сокета, когда процесс воркера
|
||||
уже закрыт сервер будет или не будет отправлять сообщение разрешающее
|
||||
воркеру прекратить работу, но клиент его видеть не должен. По факту
|
||||
получения конечного сообщения и, возможно, данных вместе с ним, клиенту
|
||||
будет присылаться специальное сообщение.
|
||||
|
||||
|
||||
Контекст воркера
|
||||
----------------
|
||||
worker_state -- состояние воркера;
|
||||
socket_context -- контекст чтения сокета воркера;
|
||||
worker_output -- вывод воркера перед его выключением сервера.
|
||||
|
||||
|
||||
Что делать
|
||||
----------
|
||||
[*] Проверить возможность чтения сервером данных из сокета уже закрытого
|
||||
воркера. Если такой возможности нет, предусмотреть обработку завершения и
|
||||
успешной, и некорректной работы воркера с передачей всех необходимых
|
||||
сообщений. Сделать это внутри воркера.
|
||||
Итог: чтение из сокета убитого демона возможно.
|
||||
|
||||
[ ] Добавить в воркер цикл конфигурации или его тестовый вариант;
|
||||
|
||||
[*] Убрать на стороне интерфейса клиента лишний шаг создания объекта воркера;
|
||||
|
||||
[*] Убрать self._runner_process и self._pid_file, они в таком случае не нужны;
|
||||
|
||||
[*] PID-файл демона удаляем сразу после чтения;
|
||||
|
||||
[ ] Реализовать описанный ниже протокол взаимодействия сервера с воркером;
|
||||
|
||||
[ ] Добавить в метод kill воркера чтение всего содержимого сокета перед его
|
||||
удалением.
|
||||
|
||||
[ ] Разработать HAL-формат сообщений об ошибках вместо HTTPException.
|
||||
|
||||
[*] Сделать метод read воркера асинхронным.
|
||||
|
||||
[ ] На случай если воркер убит через SIGKILL -- поскольку обработку
|
||||
такого завершения работы воркера в самом воркере организовать невозможно,
|
||||
добавить дополнительную проверку is_alive воркера и предусмотреть обработку
|
||||
этой ситуации.
|
||||
|
||||
[ ] Добавить главный блок try-catch-finally в воркер. В раздел finally добавить
|
||||
отправку сообщения об окончании работы воркера. В зависимости от
|
||||
возможности читать данный из сокета законченного процесса воркера, ждать
|
||||
или не ждать ответ сервера по поводу окончания работы воркера;
|
||||
|
||||
[ ] Добавить предварительно деление воркеров по состояниям, вероятно добавить
|
||||
WorkersPool;
|
||||
|
||||
[ ] Добавить состояния воркера:
|
||||
- config -- воркер в состоянии конфигурации, Т.е. ждёт значения
|
||||
параметров или собщение об окончании конфигурации;
|
||||
- exec -- воркер в процессе выполнения команды;
|
||||
- input -- воркер в процессе ожидания ввода пользователя;
|
||||
- finish -- воркер в состоянии завершения некоторого состояния.
|
||||
|
||||
[ ] Реализовать предварительно REST API в соответствии с наработками;
|
||||
|
||||
[ ] Решить, какое значение должны иметь заголовки Content-Type и Accept:
|
||||
- application/json;
|
||||
- application/hal+json;
|
||||
- application/vnd.cphl+json;
|
||||
- application/vnd.api+json.
|
||||
Подумать над версионированием API;
|
||||
|
||||
[ ] Добавить в ответы сервера обозначения о кэшируемости и некэшируемости,
|
||||
проработать кэширование в целом, определить, что должно кэшироваться, а что
|
||||
нет;
|
||||
|
||||
[ ] Проработать механизм остановки взаимодействия сервера с клиентом и
|
||||
возвращения к нему после переподключения клиента, перезагрузки сервера, а
|
||||
также при этих обоих событиях;
|
||||
|
||||
[ ] Добавить проверку во время инициализации сервера наличия PID-файлов
|
||||
демонов-воркеров:
|
||||
- Если такие есть -- проверить, что это за процессы;
|
||||
- Если такие процессы не существуют или не являются воркерами -- удалить
|
||||
эти PID-файлы;
|
||||
- Если процессы существуют, являются воркерами и есть информация о них
|
||||
в БД -- проверяем соответствие PID и WID.
|
||||
-- Если все совпадает -- удаляем PID-файлы и далее создаем объекты
|
||||
воркеров по информации в БД;
|
||||
-- [Q#3] Если есть несовпадения -- убиваем воркер, PID-файл и
|
||||
записи в БД;
|
||||
- [Q#2] Если процессы существуют, являются воркерами, но в БД о них нет
|
||||
информации -- наверное, удаляем PID-файлы и пытаемся убить этих
|
||||
воркеров с помощью их PID.
|
||||
|
||||
|
||||
Интерфейс сервера
|
||||
-----------------
|
||||
REST-интерфейс (вроде). Представляем состояния воркеров в виде ресурсов или
|
||||
объектов. В соответствии с HATEAOS присылаем URI на ресурсы и действия воркера,
|
||||
которые доступны в тот или иной момент. Для этого применяем формат
|
||||
json-сообщений напоминающий HAL или CPHL (не очень распространенный формат).
|
||||
Различие от HAL в наличии атрибута "methods" в "_links".
|
||||
|
||||
* root -- корневой ресурс, являющийся точкой входа для всего API. Возможно,
|
||||
стоит создать алиас /index или заменить на него.
|
||||
|
||||
- GET / -- получить доступные корневые ресурсы;
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response:
|
||||
Media-Type: application/hal+json
|
||||
Cache-Control: public
|
||||
|
||||
{
|
||||
"data": {
|
||||
// Some information about server.
|
||||
},
|
||||
"_links": {
|
||||
"commands": {
|
||||
"href": "/commands/"
|
||||
},
|
||||
"workers": {
|
||||
"href": "/workers/"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
"meta": {
|
||||
// Server info
|
||||
},
|
||||
"links": {
|
||||
}
|
||||
}
|
||||
|
||||
* сommands -- совокупность данных о командах, доступных для запуска на
|
||||
сервере.
|
||||
|
||||
- GET /commands/ -- получить список команд;
|
||||
* request:
|
||||
Media-Type: application/json
|
||||
|
||||
{
|
||||
"gui": {true/false}
|
||||
}
|
||||
|
||||
* response:
|
||||
Media-Type: application/hal+json
|
||||
Cache-Control: private
|
||||
|
||||
{
|
||||
"{command_id}": {
|
||||
"data": {
|
||||
// Some command info.
|
||||
},
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "/commands/{CID}"
|
||||
},
|
||||
"parameters": {
|
||||
"href": "/commands/{CID}/parameters"
|
||||
},
|
||||
"cofigure": {
|
||||
"href": "/configs/{CID}"
|
||||
}
|
||||
}
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
- GET /commands/{command}/ -- получить информацию об указанной команде, a
|
||||
также ее параметры. Запрос используемый
|
||||
консольным клиентом;
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response:
|
||||
Media-Type: application/hal+json
|
||||
Cache-Control: private
|
||||
|
||||
{
|
||||
"data": {
|
||||
// Some command info.
|
||||
},
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "/commands/{CID}"
|
||||
},
|
||||
"parameters": {
|
||||
"href": "/commands/{CID}/parameters"
|
||||
}
|
||||
"cofigure": {
|
||||
"href": "/configuration/{CID}"
|
||||
}
|
||||
},
|
||||
"_embedded": {
|
||||
"parameters": {
|
||||
"data": [
|
||||
{
|
||||
"group_id": "{group_id}",
|
||||
"parameters": [...]
|
||||
},
|
||||
...
|
||||
],
|
||||
"_links":
|
||||
"self": {
|
||||
"href": "/commands/{CID}/parameters"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- GET /commands/{CID}/parameters -- получить параметры указанной команды;
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response:
|
||||
|
||||
{
|
||||
"data": [
|
||||
{
|
||||
"group_id": "{group_id}",
|
||||
"parameters": [...]
|
||||
},
|
||||
...
|
||||
],
|
||||
"_links": {
|
||||
"self": {
|
||||
"href": "/commands/{CID}/parameters"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
* configs -- совокупность объектов воркеров, находящихся в состоянии
|
||||
конфигурации.
|
||||
|
||||
- POST /configs/{command} -- создать конфигурацию;
|
||||
201 -- конфигурация уже создана;
|
||||
404 -- команды с указанным id нет;
|
||||
400 -- неправильный формат запроса.
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response:
|
||||
|
||||
{
|
||||
"_links": {
|
||||
"configure": {
|
||||
"href": "/configs/{WID}/parameters"
|
||||
},
|
||||
"execute": {
|
||||
"href": "/executions/{WID}"
|
||||
},
|
||||
"cancel": {
|
||||
"href": "/configs/{WID}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- PATCH /configs/{WID}/parameters -- изменить конфигурацию
|
||||
указанными в теле значениями;
|
||||
404 -- конфигурации с указанным
|
||||
WID нет;
|
||||
400 -- неправильный формат
|
||||
запроса.
|
||||
* request:
|
||||
|
||||
[
|
||||
{"id": "{param_id}", "value": "{param_value}"},
|
||||
...
|
||||
]
|
||||
|
||||
* response:
|
||||
|
||||
{
|
||||
"data": {
|
||||
"{param_id}": "{error_message}",
|
||||
...
|
||||
},
|
||||
"_links": {
|
||||
"configure": {
|
||||
"href": "/configs/{WID}/parameters"
|
||||
},
|
||||
"cancel": {
|
||||
"href": "/configs/{WID}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OR
|
||||
|
||||
{
|
||||
"_links": {
|
||||
"configure": {
|
||||
"href": "/configs/{WID}/parameters"
|
||||
},
|
||||
"execute": {
|
||||
"href": "/executions/{WID}"
|
||||
},
|
||||
"cancel": {
|
||||
"href": "/configs/{WID}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- PUT /configs/{WID}/parameters -- заменить конфигурацию с
|
||||
использованием указанных в теле
|
||||
запроса значений. Пустой набор
|
||||
значений позволяет сбросить
|
||||
конфигурацию;
|
||||
* request:
|
||||
|
||||
[
|
||||
{"id": "{param_id}", "value": "{param_value}"},
|
||||
...
|
||||
]
|
||||
|
||||
* response:
|
||||
|
||||
{
|
||||
"data": {
|
||||
"{param_id}": "{error_message}",
|
||||
...
|
||||
},
|
||||
"_links": {
|
||||
"configure": {
|
||||
"href": "/configs/{WID}/parameters"
|
||||
},
|
||||
"cancel": {
|
||||
"href": "/configs/{WID}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OR
|
||||
|
||||
{
|
||||
"_links": {
|
||||
"configure": {
|
||||
"href": "/configs/{WID}/parameters"
|
||||
},
|
||||
"execute": {
|
||||
"href": "/executions/{WID}"
|
||||
},
|
||||
"cancel": {
|
||||
"href": "/configs/{WID}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- DELETE /configs/{WID} -- закончить конфигурации без выполнения
|
||||
команды.
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response:
|
||||
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"type": "output",
|
||||
"logging": {logging_level/null},
|
||||
"text": "{message_text}",
|
||||
"source": "{source_script}",
|
||||
}
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
* executions -- совокупность объектов воркеров, находящихся в состоянии
|
||||
исполнения.
|
||||
|
||||
- POST /executions/{WID} -- создать новое исполнение из конфигурации;
|
||||
201 -- исполнение создано;
|
||||
409 -- исполнение с этим WID уже есть;
|
||||
400 -- неправильный формат запроса.
|
||||
404 -- конфигурация не найдена.
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response:
|
||||
|
||||
- Response Code: 201
|
||||
|
||||
{
|
||||
"_links": {
|
||||
"output": {
|
||||
"href": "/executions/{WID}/output"
|
||||
},
|
||||
"stop": {
|
||||
"href": "/executions/{WID}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- DELETE /executions/{WID} -- удалить исполнение, происходит путем
|
||||
отправки в скрипт SIGINT;
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response (возвращает список непереданных сообщений, если они есть):
|
||||
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"type": "output",
|
||||
"logging": {logging_level/null},
|
||||
"text": "{message_text}",
|
||||
"source": "{source_script}",
|
||||
}
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
- GET /executions/{WID}/output -- получить список имеющихся сообщений от
|
||||
команды;
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response (необходимость остановить выполнение определяется по
|
||||
Response Code):
|
||||
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"type": "output",
|
||||
"logging": {logging_level/null},
|
||||
"text": "{message_text}",
|
||||
"source": "{source_script}",
|
||||
}
|
||||
},
|
||||
{
|
||||
"data": {
|
||||
"type": "input",
|
||||
"text": "{input_message}"
|
||||
"source": "{source_script}"
|
||||
},
|
||||
"_links": {
|
||||
"input": {
|
||||
"href": "/executions/{WID}/input",
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
- PATCH /executions/{WID}/input -- отправить в воркер некоторый ввод;
|
||||
* request (если null, значит ввод прерывается):
|
||||
|
||||
{
|
||||
"data": {input_data/null}
|
||||
}
|
||||
|
||||
* response:
|
||||
- Response Code: 200
|
||||
|
||||
null
|
||||
|
||||
- Response Code: 404
|
||||
|
||||
{
|
||||
"error": "Execution {WID} is not found"
|
||||
}
|
||||
|
||||
- Response Code: 400
|
||||
|
||||
{
|
||||
"error": "Input message is not correct"
|
||||
}
|
||||
|
||||
* workers -- вся совокупность воркеров.
|
||||
|
||||
- GET /workers/ -- получить список демонов-воркеров и информацию о них;
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response:
|
||||
|
||||
{
|
||||
"{WID}": {
|
||||
"data": {
|
||||
"socket": "{socket_path}",
|
||||
"command": "{worker_command}",
|
||||
"pid": "{daemon_pid}",
|
||||
}
|
||||
"_links": {
|
||||
"kill": {
|
||||
"href": "/workers/{WID}",
|
||||
"methods": ["delete"]
|
||||
}
|
||||
}
|
||||
},
|
||||
...
|
||||
}
|
||||
|
||||
- DELETE /workers/{WID} -- отправить сигнал SIGKILL демону-воркеру;
|
||||
* request:
|
||||
|
||||
null
|
||||
|
||||
* response (сообщения полученные от убитого воркера):
|
||||
|
||||
[
|
||||
{
|
||||
"data": {
|
||||
"type": "output",
|
||||
"logging": {logging_level/null},
|
||||
"text": "{message_text}",
|
||||
"source": "{source_script}",
|
||||
}
|
||||
},
|
||||
...
|
||||
]
|
||||
|
||||
|
||||
Протокол взаимодействия воркера и сервера
|
||||
-----------------------------------------
|
||||
Все сообщения представляют собой json-структуры, включающие в себя 3 поля:
|
||||
- state -- состояние воркера:
|
||||
* Если сообщение пришло воркеру от сервера, тогда это поле указывает
|
||||
воркеру, какое состояние он должен принять;
|
||||
* Если сообщение пришло серверу от воркера, тогда это поле указывает на
|
||||
текущее состояние воркера.
|
||||
|
||||
- status -- статус текущего состояния:
|
||||
0 -- состояние корректно;
|
||||
1 -- состояние некорректно.
|
||||
|
||||
- data -- некоторые данные, структура которых может быть любой.
|
||||
|
||||
1. Данные конфигурации от клиента.
|
||||
a. Продолжить конфигурацию указанными значениями:
|
||||
|
||||
{
|
||||
"state": "config",
|
||||
"status": 0,
|
||||
"data": [
|
||||
{"id": "{param_id}", "value": "{parameter_value}"},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
b. Сбросить конфигурацию и продолжить указанными значениями:
|
||||
|
||||
{
|
||||
"state": "config",
|
||||
"status": 1,
|
||||
"data": [
|
||||
{"id": "{param_id}", "value": "{parameter_value}"},
|
||||
...
|
||||
]
|
||||
}
|
||||
|
||||
2. Ответ воркера по данным конфигурации.
|
||||
a. Все параметры конфигурации корректны:
|
||||
|
||||
{
|
||||
"state": "config",
|
||||
"status": 0,
|
||||
"data": null
|
||||
}
|
||||
|
||||
b. Некоторые параметры конфигурации некорректны:
|
||||
|
||||
{
|
||||
"state": "config",
|
||||
"status": 1,
|
||||
"data": {
|
||||
"{param_id}": "{error_message}",
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
3. Запрос на окончание конфигурации от клиента.
|
||||
a. Закончить конфигурацию, начать выполнение команды:
|
||||
|
||||
{
|
||||
"state": "finish",
|
||||
"status": 0,
|
||||
"data": null
|
||||
}
|
||||
|
||||
b. Закончить конфигурацию и работу воркера:
|
||||
|
||||
{
|
||||
"state": "finish",
|
||||
"status": 1,
|
||||
"data": null
|
||||
}
|
||||
|
||||
4. Ответ воркера на запрос об окончании конфигурации:
|
||||
a. Закончить конфигурацию, начать выполнение команды:
|
||||
|
||||
{
|
||||
"state": "finish",
|
||||
"status": 0,
|
||||
"data": null
|
||||
}
|
||||
|
||||
b. Закончить конфигурацию и работу воркера:
|
||||
|
||||
{
|
||||
"state": "finish",
|
||||
"status": 1,
|
||||
"data": {
|
||||
"{param_id}": "{error_message}",
|
||||
...
|
||||
}
|
||||
}
|
||||
|
||||
5. Сообщение с выводом воркера.
|
||||
a. Сообщение из скрипта при его "штатной" работe:
|
||||
|
||||
{
|
||||
"state": "exec",
|
||||
"status": 0,
|
||||
"data": {
|
||||
"type": "message",
|
||||
"logging": {logging_level/null},
|
||||
"text": "{message_text}",
|
||||
"source": "{source_script}"
|
||||
}
|
||||
}
|
||||
|
||||
b. Сообщение об ошибке приводящей к завершению выполнения скрипта и
|
||||
команды:
|
||||
|
||||
{
|
||||
"state": "exec",
|
||||
"status": 1,
|
||||
"data": {
|
||||
"error": "{error_message}",
|
||||
"source": "{source_script}"
|
||||
}
|
||||
}
|
||||
|
||||
6. Сообщение с запросом на ввод.
|
||||
a. Состояние "input" -- воркер в состоянии ожидания ввода:
|
||||
{
|
||||
"state": "input",
|
||||
"status": 0,
|
||||
"data": {
|
||||
"text": "{message_text}",
|
||||
"source": "{source_script}"
|
||||
}
|
||||
}
|
||||
|
||||
7. Сообщение от клиента с данными.
|
||||
a. Ввод успешно выполнен и передается:
|
||||
|
||||
{
|
||||
"state": "input",
|
||||
"status": 0,
|
||||
"data": "{client_data}"
|
||||
}
|
||||
|
||||
b. Ввод данных прерван, данных не будет:
|
||||
|
||||
{
|
||||
"state": "input",
|
||||
"status": 1,
|
||||
"data": null
|
||||
}
|
||||
|
||||
8. Сообщение от воркера о получении ввода.
|
||||
a. Ввод успешно получен, о чем говорит переход в состояние выполнения со
|
||||
статусом 0, т.е. без ошибок:
|
||||
|
||||
{
|
||||
"state": "exec",
|
||||
"status": 0,
|
||||
"data": null
|
||||
}
|
||||
|
||||
9. Сообщение воркера об окончании работы:
|
||||
a. Успешное окончание работы:
|
||||
|
||||
{
|
||||
"state": "finish",
|
||||
"status": 0,
|
||||
"data": null
|
||||
}
|
||||
|
||||
b. Работа воркера была прервана или закончилась с ошибками:
|
||||
|
||||
{
|
||||
"state": "finish",
|
||||
"status": 1,
|
||||
"data": [
|
||||
"{error_message}",
|
||||
...
|
||||
]
|
||||
}
|
100
calculate/server/ipc_draft/io_module.py
Normal file
100
calculate/server/ipc_draft/io_module.py
Normal file
|
@ -0,0 +1,100 @@
|
|||
import json
|
||||
import logging
|
||||
from multiprocessing.connection import Listener, Connection
|
||||
from typing import Union
|
||||
|
||||
|
||||
class WorkerIOError(KeyboardInterrupt):
|
||||
pass
|
||||
|
||||
|
||||
class IOModule:
|
||||
'''Класс модуля ввода/вывода для воркеров.'''
|
||||
def __init__(self, listener: Listener, command_name: str):
|
||||
self._listener: Listener = listener
|
||||
self._connection: Union[Connection, None] = None
|
||||
|
||||
self._command: str = command_name
|
||||
self._script: str = ""
|
||||
|
||||
@property
|
||||
def command(self) -> str:
|
||||
return self._command
|
||||
|
||||
@property
|
||||
def script(self) -> str:
|
||||
return self._script
|
||||
|
||||
@script.setter
|
||||
def script(self, script: str) -> None:
|
||||
self._script = script
|
||||
|
||||
def set_debug(self, msg: str) -> None:
|
||||
self.output(msg, level=logging.DEBUG)
|
||||
|
||||
def set_info(self, msg: str) -> None:
|
||||
self.output(msg, level=logging.INFO)
|
||||
|
||||
def set_warning(self, msg: str) -> None:
|
||||
self.output(msg, level=logging.WARNING)
|
||||
|
||||
def set_error(self, msg: str) -> None:
|
||||
self.output(msg, level=logging.ERROR)
|
||||
|
||||
def set_critical(self, msg: str) -> None:
|
||||
self.output(msg, level=logging.CRITICAL)
|
||||
|
||||
def output(self, text: str, level: int = logging.INFO) -> None:
|
||||
'''Метод для отправки серверу вывода с указанием уровня.'''
|
||||
output_request = {"state": "exec",
|
||||
"status": 0,
|
||||
"data": {
|
||||
"type": "message",
|
||||
"logging": level,
|
||||
"text": text,
|
||||
"source": self._script
|
||||
}
|
||||
}
|
||||
self.send(output_request)
|
||||
|
||||
def input(self, msg: str) -> str:
|
||||
'''Метод через который возможен ввод данных в скрипт.'''
|
||||
input_request = {"state": "input",
|
||||
"status": 0,
|
||||
"data": {"text": msg,
|
||||
"source": f"{self.command}:{self.script}"}}
|
||||
|
||||
answer = None
|
||||
while answer is None:
|
||||
self.send(input_request)
|
||||
answer = self.receive()
|
||||
|
||||
return answer['data']
|
||||
|
||||
def send(self, data: dict) -> None:
|
||||
'''Метод для отправки данных серверу.'''
|
||||
if self._connection is None:
|
||||
raise WorkerIOError("No one is connected now.")
|
||||
data = json.dumps(data).encode()
|
||||
self._connection.send_bytes(data)
|
||||
|
||||
def receive(self) -> dict:
|
||||
'''Метод для получения данных от сервера.'''
|
||||
if self._connection is None:
|
||||
raise WorkerIOError("No one is connected now.")
|
||||
data: bytes = self._connection.recv_bytes()
|
||||
return json.loads(data.decode())
|
||||
|
||||
def __enter__(self):
|
||||
self._connection = self._listener.accept()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||
if exc_type:
|
||||
print("exc_type =", exc_type)
|
||||
print("exc_value =", exc_value)
|
||||
print("exc_traceback =", exc_traceback)
|
||||
if exc_type is KeyboardInterrupt:
|
||||
print("correctly")
|
||||
if not self._connection.closed:
|
||||
self._connection.close()
|
202
calculate/server/ipc_draft/schemas.py
Normal file
202
calculate/server/ipc_draft/schemas.py
Normal file
|
@ -0,0 +1,202 @@
|
|||
from typing import Any, List, Dict, Optional, Literal, Union
|
||||
from pydantic import BaseModel, Field, HttpUrl
|
||||
# from utils import ResponseStructure
|
||||
# from pprint import pprint
|
||||
# import json
|
||||
# from fastapi import HTTPException, status
|
||||
|
||||
|
||||
class NotTemplatedLinkData(BaseModel):
|
||||
href: HttpUrl = "URI"
|
||||
|
||||
class Config:
|
||||
extra = "forbid"
|
||||
|
||||
|
||||
class TemplatedLinkData(NotTemplatedLinkData):
|
||||
templated: bool = False
|
||||
|
||||
|
||||
LinkData = Union[NotTemplatedLinkData, TemplatedLinkData]
|
||||
|
||||
|
||||
# GET /
|
||||
class ServerInfo(BaseModel):
|
||||
name: str = "Server name"
|
||||
version: str = "Server version"
|
||||
|
||||
|
||||
class GetRootResponse(BaseModel):
|
||||
data: ServerInfo
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links",
|
||||
description="Links to resources")
|
||||
|
||||
|
||||
# GET /commands/{CID}/parameters
|
||||
class ParameterInfo(BaseModel):
|
||||
id: str
|
||||
default: Any
|
||||
|
||||
|
||||
class ParametersGroup(BaseModel):
|
||||
group_id: str
|
||||
parameters: List[ParameterInfo]
|
||||
|
||||
|
||||
class GetCommandParametersResponse(BaseModel):
|
||||
data: List[ParametersGroup]
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
|
||||
# GET /commands/{CID}
|
||||
class CommandData(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
category: str
|
||||
command: str
|
||||
|
||||
|
||||
class CommandInfo(BaseModel):
|
||||
data: CommandData
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
|
||||
class FoundCommandInfo(CommandInfo):
|
||||
embedded: Dict[str,
|
||||
GetCommandParametersResponse] = Field(...,
|
||||
alias="_embedded")
|
||||
|
||||
|
||||
# GET /commands/
|
||||
class GetCommandsResponse(BaseModel):
|
||||
__root__: Dict[str, CommandInfo]
|
||||
|
||||
# class Config:
|
||||
# @staticmethod
|
||||
# def schema_extra(schema, model):
|
||||
# example = {}
|
||||
# for num in range(2):
|
||||
# resp = ResponseStructure()
|
||||
# resp.add_data(id=f"command_{num}",
|
||||
# command=f"cl-command-{num}",
|
||||
# title=f"Command {num}",
|
||||
# category="cool-category"
|
||||
# )
|
||||
# resp.add_link("parameters",
|
||||
# f"/commands/command_{num}/parameters")
|
||||
# resp.add_link("configure", f"/configs/command_{num}")
|
||||
|
||||
# example[f"command_{num}"] = resp.get_dict()
|
||||
# schema["example"] = example
|
||||
|
||||
|
||||
# POST /configs/{command}
|
||||
class CreateConfigResponse(BaseModel):
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
|
||||
# PATCH /configs/{WID}/parameters
|
||||
class ParameterValue(BaseModel):
|
||||
id: str
|
||||
value: Any
|
||||
|
||||
|
||||
class ModifyConfigRequest(BaseModel):
|
||||
__root__: List[ParameterValue]
|
||||
|
||||
|
||||
class ModifyConfigCorrectResponse(BaseModel):
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
class Config:
|
||||
extra = "forbid"
|
||||
|
||||
|
||||
class ModifyConfigUncorrectResponse(ModifyConfigCorrectResponse):
|
||||
data: Optional[Dict[str, str]]
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
|
||||
ModifyConfigResponse = Union[ModifyConfigCorrectResponse,
|
||||
ModifyConfigUncorrectResponse]
|
||||
|
||||
|
||||
# PUT /configs/{WID}/parameters
|
||||
class ResetConfigRequest(BaseModel):
|
||||
__root__: List[ParameterValue]
|
||||
|
||||
|
||||
class ResetConfigResponse(BaseModel):
|
||||
data: Optional[Dict[str, str]]
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
|
||||
# POST /executions/{WID}
|
||||
class CreateExecutionResponse(BaseModel):
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
|
||||
# DELETE /executions/{WID}
|
||||
class OutputData(BaseModel):
|
||||
type: Literal["output"]
|
||||
logging: Union[int, None]
|
||||
text: str
|
||||
source: str
|
||||
|
||||
|
||||
class OutputMessage(BaseModel):
|
||||
data: OutputData
|
||||
|
||||
|
||||
class StopExecutionsResponse(BaseModel):
|
||||
__root__: List[OutputMessage]
|
||||
|
||||
|
||||
# DELETE /configs/{WID}
|
||||
class StopConfigResponse(BaseModel):
|
||||
__root__: List[OutputMessage]
|
||||
|
||||
|
||||
# GET /executions/{WID}/output
|
||||
class InputRequestData(BaseModel):
|
||||
type: Literal["input"]
|
||||
text: str
|
||||
source: str
|
||||
|
||||
|
||||
class InputMessage(BaseModel):
|
||||
data: InputRequestData
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
|
||||
class GetExecutionOutputResponse(BaseModel):
|
||||
__root__: List[Union[OutputMessage, InputMessage]]
|
||||
|
||||
|
||||
# PATCH /executions/{WID}/input
|
||||
class WriteToExecutionRequest(BaseModel):
|
||||
data: Any
|
||||
|
||||
|
||||
# GET /workers/
|
||||
class WorkerData(BaseModel):
|
||||
socket: str
|
||||
command: str
|
||||
pid: int
|
||||
|
||||
|
||||
class WorkerInfo(BaseModel):
|
||||
data: WorkerData
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
|
||||
class GetWorkersResponse(BaseModel):
|
||||
__root__: Dict[int, WorkerInfo]
|
||||
|
||||
# DELETE /workers/{WID}
|
||||
|
||||
|
||||
# Сообщения протокола взаимодействия сервера и воркеров.
|
||||
class WorkerMessageBase(BaseModel):
|
||||
state: str
|
||||
status: int
|
55
calculate/server/ipc_draft/scripts.py
Normal file
55
calculate/server/ipc_draft/scripts.py
Normal file
|
@ -0,0 +1,55 @@
|
|||
from io_module import IOModule
|
||||
import time
|
||||
|
||||
|
||||
def script_0(io: IOModule, parameters: dict) -> None:
|
||||
try:
|
||||
io.set_info(f"PARAMETERS: {parameters}")
|
||||
value_0 = get_integer(0, parameters, io)
|
||||
value_1 = get_integer(1, parameters, io)
|
||||
|
||||
time.sleep(2)
|
||||
result = value_0 + value_1
|
||||
|
||||
io.set_info(f"Script executed with result = {result}")
|
||||
except Exception as error:
|
||||
io.set_error(f"{str(error)}")
|
||||
|
||||
|
||||
def script_1(io: IOModule, parameters: dict) -> None:
|
||||
try:
|
||||
io.set_info(f"PARAMETERS: {parameters}")
|
||||
value_0 = get_integer(0, parameters, io)
|
||||
value_1 = get_integer(1, parameters, io)
|
||||
|
||||
time.sleep(3)
|
||||
result = value_0 * value_1
|
||||
|
||||
io.set_info(f"Script executed with result = {result}")
|
||||
except Exception as error:
|
||||
io.set_error(f"{str(error)}")
|
||||
|
||||
|
||||
def script_2(io: IOModule, parameters: dict) -> None:
|
||||
try:
|
||||
io.set_info(f"PARAMETERS: {parameters}")
|
||||
value_0 = get_integer(0, parameters, io)
|
||||
value_1 = get_integer(1, parameters, io)
|
||||
|
||||
time.sleep(4)
|
||||
result = value_0 ** value_1
|
||||
|
||||
io.set_info(f"Script executed with result = {result}")
|
||||
except Exception as error:
|
||||
io.set_error(f"{str(error)}")
|
||||
|
||||
|
||||
def get_integer(index: int, parameters: dict, io: IOModule) -> int:
|
||||
io.set_info(f"Looking for 'value_{index}'")
|
||||
value = parameters.get(f"value_{index}", None)
|
||||
if value is None:
|
||||
io.set_warning(f"'value_{index}' not found")
|
||||
value = io.input(f"Enter 'value_{index}'")
|
||||
else:
|
||||
io.set_info(f"value_{index} = {value}")
|
||||
return int(value)
|
282
calculate/server/ipc_draft/server.py
Normal file
282
calculate/server/ipc_draft/server.py
Normal file
|
@ -0,0 +1,282 @@
|
|||
import asyncio
|
||||
|
||||
# import logging
|
||||
import uvicorn
|
||||
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.requests import Request
|
||||
|
||||
from fastapi import FastAPI, status, HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
from pydantic.main import ModelMetaclass
|
||||
from pydantic import parse_obj_as
|
||||
|
||||
from typing import _GenericAlias, Union, Optional
|
||||
|
||||
from commands import command_0, command_1, command_2
|
||||
from utils import ResponseStructure, Commands
|
||||
from worker import (
|
||||
WorkersManager,
|
||||
UnexpectedWorkerFinish,
|
||||
UnexpectedWorkerState,
|
||||
DaemonIsDead
|
||||
)
|
||||
|
||||
from schemas import (
|
||||
GetRootResponse,
|
||||
GetCommandsResponse,
|
||||
FoundCommandInfo,
|
||||
GetCommandParametersResponse,
|
||||
CreateConfigResponse,
|
||||
ModifyConfigRequest,
|
||||
ModifyConfigResponse,
|
||||
ResetConfigRequest,
|
||||
ResetConfigResponse,
|
||||
StopConfigResponse,
|
||||
CreateExecutionResponse
|
||||
)
|
||||
|
||||
from pprint import pprint
|
||||
|
||||
|
||||
api = FastAPI(title="Calculate Server")
|
||||
workers = WorkersManager(loop=asyncio.get_event_loop())
|
||||
|
||||
|
||||
commands = Commands(command_0,
|
||||
command_1,
|
||||
command_2)
|
||||
|
||||
|
||||
@api.on_event("startup")
|
||||
async def startup_event():
|
||||
check_pid_files()
|
||||
|
||||
|
||||
@api.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
workers.clean()
|
||||
|
||||
|
||||
@api.get("/", response_model=GetRootResponse, tags=["Home"])
|
||||
async def get_primary_commands(request: Request):
|
||||
resp = ResponseStructure(get_base_url(request))
|
||||
resp.add_data(name="Calculate Server", version="0.1")
|
||||
resp.add_link("commands", "/commands?gui={is_gui}", templated=True)
|
||||
resp.add_link("find_command", "/commands/{console_command}?gui={is_gui}",
|
||||
templated=True)
|
||||
resp.add_link("workers", "/workers")
|
||||
return validate_response(resp.get_dict(), GetRootResponse,
|
||||
media_type="application/hal+json")
|
||||
|
||||
|
||||
@api.get("/commands", response_model=GetCommandsResponse, tags=["Commands"])
|
||||
async def get_available_commands(request: Request,
|
||||
gui: Optional[bool] = False):
|
||||
response_data = commands.get_commands(get_base_url(request))
|
||||
return validate_response(response_data, GetCommandsResponse,
|
||||
media_type="application/hal+json")
|
||||
|
||||
|
||||
@api.get("/commands/{command}",
|
||||
response_model=FoundCommandInfo,
|
||||
tags=["Commands"])
|
||||
async def find_command_data(command: str, request: Request,
|
||||
gui: Optional[bool] = False,
|
||||
by_id: Optional[bool] = False):
|
||||
base_url = get_base_url(request)
|
||||
|
||||
if by_id:
|
||||
command_data = commands.get_by_id(command, base_url)
|
||||
if command_data is None:
|
||||
raise get_command_not_found(command)
|
||||
else:
|
||||
command_data = commands.find_command(command, base_url)
|
||||
if command_data is None:
|
||||
raise get_cl_command_not_found(command)
|
||||
|
||||
return validate_response(command_data, FoundCommandInfo,
|
||||
media_type="application/hal+json")
|
||||
|
||||
|
||||
@api.get("/commands/{command_id}/parameters",
|
||||
response_model=GetCommandParametersResponse,
|
||||
tags=["Commands"])
|
||||
async def get_command_parameters(command_id: str, request: Request):
|
||||
parameters_data = commands.get_parameters(command_id,
|
||||
get_base_url(request))
|
||||
if parameters_data is None:
|
||||
raise get_command_not_found(command_id)
|
||||
return parameters_data
|
||||
|
||||
|
||||
@api.post("/configs/{command_id}", response_model=CreateConfigResponse,
|
||||
tags=["Configurations"])
|
||||
async def create_configuration(command_id: str, request: Request):
|
||||
command = commands.get_command_object(command_id)
|
||||
if command is None:
|
||||
raise get_command_not_found(command_id)
|
||||
|
||||
print("Making worker")
|
||||
worker_id, error = await workers.make_worker(command)
|
||||
if error is not None:
|
||||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=error)
|
||||
|
||||
resp = ResponseStructure(get_base_url(request))
|
||||
resp.add_link("configure", f"/configs/{worker_id}/parameters")
|
||||
resp.add_link("execute", f"/execs/{worker_id}")
|
||||
resp.add_link("cancel", f"/configs/{worker_id}")
|
||||
|
||||
return validate_response(resp.get_dict(), CreateConfigResponse)
|
||||
|
||||
|
||||
@api.patch("/configs/{wid}/parameters", tags=["Configurations"])
|
||||
async def modify_configuration_parameters(wid: int,
|
||||
parameters: ModifyConfigRequest,
|
||||
request: Request):
|
||||
response = await change_config(get_base_url(request), wid, parameters)
|
||||
return validate_response(response, ModifyConfigResponse)
|
||||
|
||||
|
||||
@api.put("/configs/{wid}/parameters", tags=["Configurations"])
|
||||
async def reset_configuration_parameters(wid: int,
|
||||
parameters: ResetConfigRequest,
|
||||
request: Request):
|
||||
response = await change_config(get_base_url(request), wid, parameters,
|
||||
reset=True)
|
||||
return validate_response(response, ResetConfigResponse)
|
||||
|
||||
|
||||
async def change_config(base_url: str, wid: int,
|
||||
parameters: ModifyConfigRequest,
|
||||
reset: bool = False) -> dict:
|
||||
parameters = parameters.dict()["__root__"]
|
||||
try:
|
||||
result = await workers.configure_worker(wid, parameters, reset=reset)
|
||||
except UnexpectedWorkerFinish as error:
|
||||
return HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=("Unexpected configuration finish. Reason:"
|
||||
f" {error.reason[0]}"))
|
||||
except UnexpectedWorkerState as error:
|
||||
print("Unexpected worker state.")
|
||||
return HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail=('Unexpected configuration state:'
|
||||
f' "{error.state}". Expected:'
|
||||
f' "{error.expected}"'))
|
||||
except DaemonIsDead:
|
||||
print("DAEMON IS DEAD")
|
||||
return HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Daemon is dead")
|
||||
print("RESULT:")
|
||||
pprint(result)
|
||||
if result is None:
|
||||
raise get_configuration_not_found(wid)
|
||||
|
||||
resp = ResponseStructure(base_url)
|
||||
resp.add_link("configure", f"configs/{wid}/parameters")
|
||||
resp.add_link("reset", f"configs/{wid}/parameters")
|
||||
resp.add_link("cancel", f"configs/{wid}")
|
||||
|
||||
if not result:
|
||||
resp.add_link("execute", f"/execs/{wid}")
|
||||
else:
|
||||
resp.add_data(result)
|
||||
|
||||
return resp.get_dict()
|
||||
|
||||
|
||||
@api.delete("/configs/{wid}", response_model=StopConfigResponse,
|
||||
tags=["Configurations"])
|
||||
async def stop_configuration(wid: int):
|
||||
output = await workers.cancel_worker_configuration(wid)
|
||||
if output is None:
|
||||
raise get_configuration_not_found(wid)
|
||||
return []
|
||||
|
||||
|
||||
@api.post("/execs/{wid}", tags=["Executions"])
|
||||
async def start_command_execution(wid: int, request: Request):
|
||||
result = workers.run_execution(wid)
|
||||
if result > 0:
|
||||
raise get_configuration_not_found(wid)
|
||||
elif result < 0:
|
||||
raise HTTPException(status_code=status.HTTP_409_CONFLICT,
|
||||
detail=f"Execution id = {wid} already exists.")
|
||||
|
||||
resp = ResponseStructure(get_base_url(request))
|
||||
resp.add_link("configure", f"/configs/{wid}/parameters")
|
||||
resp.add_link("reset", f"/configs/{wid}/parameters")
|
||||
resp.add_link("cancel", f"/configs/{wid}")
|
||||
|
||||
return validate_response(resp.get_dict(), CreateExecutionResponse)
|
||||
|
||||
|
||||
@api.delete("/execs/{wid}", tags=["Executions"])
|
||||
async def stop_command_execution(wid: int):
|
||||
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="'stop execution' is not implemented")
|
||||
|
||||
|
||||
@api.get("/execs/{wid}/output", tags=["Executions"])
|
||||
async def read_execution_output(wid: int):
|
||||
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="'stop execution' is not implemented")
|
||||
|
||||
|
||||
@api.patch("/execs/{wid}/input", tags=["Executions"])
|
||||
async def write_to_execution(wid: int):
|
||||
raise HTTPException(status_code=status.HTTP_501_NOT_IMPLEMENTED,
|
||||
detail="'write to execution' is not implemented")
|
||||
|
||||
|
||||
def check_pid_files(pid_files_dir: str = "./") -> None:
|
||||
pass
|
||||
|
||||
|
||||
def get_base_url(request: Request):
|
||||
base_url = f"{request.base_url.scheme}://{request.base_url.netloc}"
|
||||
return base_url
|
||||
|
||||
|
||||
def get_command_not_found(command_id: str) -> HTTPException:
|
||||
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=(f'Command with id "{command_id}"'
|
||||
' is not found.'))
|
||||
|
||||
|
||||
def get_cl_command_not_found(console_command: str) -> HTTPException:
|
||||
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=(f'Console command "{console_command}" is not'
|
||||
' found.'))
|
||||
|
||||
|
||||
def get_configuration_not_found(wid: int) -> HTTPException:
|
||||
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Configuration with id={wid} is not found.")
|
||||
|
||||
|
||||
def validate_response(data: Union[dict, list],
|
||||
schema: Union[ModelMetaclass, _GenericAlias, type],
|
||||
media_type: Optional[str] = None,
|
||||
status_code: int = 200
|
||||
) -> JSONResponse:
|
||||
"""Функция для валидации данных ответа сервера по указанной схеме с учетом
|
||||
наличия псевдонимов полей и возможности того, что схема задана обычным
|
||||
типом, типом из typing или BaseModel с __root__. Возвращает объект ответа.
|
||||
"""
|
||||
if isinstance(schema, ModelMetaclass):
|
||||
if "__root__" in schema.__annotations__:
|
||||
validated = schema(__root__=data)
|
||||
else:
|
||||
validated = schema(**data)
|
||||
elif isinstance(schema, (_GenericAlias, type)):
|
||||
validated = parse_obj_as(schema, data)
|
||||
|
||||
return JSONResponse(content=jsonable_encoder(validated, by_alias=True),
|
||||
media_type=media_type, status_code=status_code)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run("server:api", host="127.0.0.1", port=2007, reload=True)
|
199
calculate/server/ipc_draft/utils.py
Normal file
199
calculate/server/ipc_draft/utils.py
Normal file
|
@ -0,0 +1,199 @@
|
|||
from fastapi import status, HTTPException
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.requests import Request
|
||||
|
||||
from pydantic.main import ModelMetaclass
|
||||
from pydantic import parse_obj_as
|
||||
|
||||
from typing import Dict, Union, List, _GenericAlias, Optional
|
||||
|
||||
from commands import Command
|
||||
# from pprint import pprint
|
||||
|
||||
|
||||
class ResponseStructure:
|
||||
'''Класс структуры данных, для получения HATEAOS ответа на запросы
|
||||
клиента.'''
|
||||
def __init__(self, base_url: str):
|
||||
self._base_url: str = base_url.strip("/")
|
||||
|
||||
self._data: dict = dict()
|
||||
self._links: Dict[str, Dict[str, str]] = dict()
|
||||
self._embedded: Dict[str, dict] = dict()
|
||||
|
||||
def get_dict(self) -> dict:
|
||||
dictionary = dict()
|
||||
if self._data:
|
||||
dictionary["data"] = self._data
|
||||
if self._links:
|
||||
dictionary["_links"] = self._links
|
||||
if self._embedded:
|
||||
dictionary["_embedded"] = self._embedded
|
||||
return dictionary
|
||||
|
||||
def add_data(self, *args, **kwargs) -> "ResponseStructure":
|
||||
if args:
|
||||
self._data = args[0]
|
||||
if kwargs:
|
||||
self._data.update(kwargs)
|
||||
return self
|
||||
|
||||
def embed(self, resource: str,
|
||||
content: Union[dict, list]) -> "ResponseStructure":
|
||||
self._embedded[resource] = content
|
||||
return self
|
||||
|
||||
def add_link(self, action: str, uri: str,
|
||||
templated: bool = False) -> "ResponseStructure":
|
||||
uri = self._get_uri(uri)
|
||||
|
||||
if action in self._links:
|
||||
link_section = self._links[action]
|
||||
if link_section["href"] != uri:
|
||||
link_section["href"] = uri
|
||||
else:
|
||||
link_section = {"href": uri}
|
||||
self._links[action] = link_section
|
||||
|
||||
if templated:
|
||||
print("TEMPLATED:", action)
|
||||
link_section["templated"] = templated
|
||||
|
||||
return self
|
||||
|
||||
def _get_uri(self, uri: str) -> str:
|
||||
if uri.startswith(self._base_url):
|
||||
return uri
|
||||
return f"{self._base_url}/{uri.strip('/')}"
|
||||
|
||||
|
||||
class Commands:
|
||||
'''Предварительная реализация контейнера описаний команд.'''
|
||||
def __init__(self, *commands: List[Command]):
|
||||
self._by_id: dict = dict()
|
||||
self._by_command: dict = dict()
|
||||
for command in commands:
|
||||
self._by_id[command.id] = command
|
||||
self._by_command[command.command] = command
|
||||
|
||||
def get_commands(self, base_url: str) -> Dict[str, dict]:
|
||||
response = dict()
|
||||
|
||||
for command in self._by_id.values():
|
||||
data = ResponseStructure(base_url)
|
||||
data.add_data(id=command.id,
|
||||
title=command.title,
|
||||
category=command.category,
|
||||
command=command.command)
|
||||
data.add_link("self", f"/commands/{command.id}?by_id=true")
|
||||
data.add_link("parameters", f"/commands/{command.id}/parameters")
|
||||
data.add_link("configure", f"/configs/{command.id}")
|
||||
response[command.id] = data.get_dict()
|
||||
|
||||
return response
|
||||
|
||||
def get_by_id(self, command_id: str, base_url: str) -> Dict[str, Dict]:
|
||||
if command_id in self._by_id:
|
||||
command = self._by_id[command_id]
|
||||
data = ResponseStructure(base_url)
|
||||
data.add_data(id=command.id,
|
||||
title=command.title,
|
||||
category=command.category,
|
||||
command=command.command)
|
||||
data.add_link("self", f"/commands/{command.id}?by_id=1")
|
||||
data.add_link("parameters", f"/commands/{command.id}/parameters")
|
||||
data.add_link("configure", f"/configs/{command.id}")
|
||||
parameters_data = self._get_parameters_data(command.parameters,
|
||||
command.id, base_url)
|
||||
data.embed("parameters", parameters_data)
|
||||
return data.get_dict()
|
||||
|
||||
return None
|
||||
|
||||
def find_command(self, console_command: str,
|
||||
base_url: str) -> Union[dict, None]:
|
||||
if console_command in self._by_command:
|
||||
command = self._by_command[console_command]
|
||||
|
||||
data = ResponseStructure(base_url)
|
||||
data.add_data(id=command.id,
|
||||
title=command.title,
|
||||
category=command.category,
|
||||
command=command.command)
|
||||
data.add_link("self", f"/commands/{command.id}?by_id=1")
|
||||
data.add_link("parameters", f"/commands/{command.id}/parameters")
|
||||
data.add_link("configure", f"/configs/{command.id}")
|
||||
parameters_data = self._get_parameters_data(command.parameters,
|
||||
command.id, base_url)
|
||||
data.embed("parameters", parameters_data)
|
||||
|
||||
return data.get_dict()
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_parameters(self, command_id: str, base_url: str
|
||||
) -> Union[dict, None]:
|
||||
if command_id in self._by_id:
|
||||
command = self._by_id[command_id]
|
||||
parameters_data = self._get_parameters_data(command.parameters,
|
||||
command.id, base_url)
|
||||
return parameters_data
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_command_object(self, command_id: str) -> Command:
|
||||
if command_id in self._by_id:
|
||||
return self._by_id[command_id]
|
||||
return None
|
||||
|
||||
def _get_parameters_data(self, parameters: list, command_id: str,
|
||||
base_url: str) -> List[dict]:
|
||||
data = ResponseStructure(base_url)
|
||||
data.add_data(parameters)
|
||||
data.add_link("self", f"/commands/{command_id}/parameters")
|
||||
return data.get_dict()
|
||||
|
||||
|
||||
def get_base_url(request: Request):
|
||||
base_url = f"{request.base_url.scheme}://{request.base_url.netloc}"
|
||||
return base_url
|
||||
|
||||
|
||||
def get_command_not_found(command_id: str) -> HTTPException:
|
||||
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=(f'Command with id "{command_id}"'
|
||||
' is not found.'))
|
||||
|
||||
|
||||
def get_cl_command_not_found(console_command: str) -> HTTPException:
|
||||
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=(f'Console command "{console_command}" is not'
|
||||
' found.'))
|
||||
|
||||
|
||||
def get_configuration_not_found(wid: int) -> HTTPException:
|
||||
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Configuration with id={wid} is not found.")
|
||||
|
||||
|
||||
def validate_response(data: Union[dict, list],
|
||||
schema: Union[ModelMetaclass, _GenericAlias, type],
|
||||
media_type: Optional[str] = None,
|
||||
status_code: int = 200
|
||||
) -> JSONResponse:
|
||||
"""Функция для валидации данных ответа сервера по указанной схеме с учетом
|
||||
наличия псевдонимов полей и возможности того, что схема задана обычным
|
||||
типом, типом из typing или BaseModel с __root__. Возвращает объект ответа.
|
||||
"""
|
||||
if isinstance(schema, ModelMetaclass):
|
||||
if "__root__" in schema.__annotations__:
|
||||
validated = schema(__root__=data)
|
||||
else:
|
||||
validated = schema(**data)
|
||||
elif isinstance(schema, (_GenericAlias, type)):
|
||||
validated = parse_obj_as(schema, data)
|
||||
|
||||
return JSONResponse(content=jsonable_encoder(validated, by_alias=True),
|
||||
media_type=media_type, status_code=status_code)
|
413
calculate/server/ipc_draft/worker.py
Normal file
413
calculate/server/ipc_draft/worker.py
Normal file
|
@ -0,0 +1,413 @@
|
|||
import os
|
||||
import sys
|
||||
import json
|
||||
import struct
|
||||
import signal
|
||||
import asyncio
|
||||
import logging
|
||||
from multiprocessing import Process
|
||||
from multiprocessing.connection import Listener
|
||||
# import logging
|
||||
# import time
|
||||
from daemon import daemon
|
||||
from commands import Command
|
||||
|
||||
from typing import Dict, Optional, List, Union, Tuple, Literal
|
||||
|
||||
from asyncio.unix_events import _UnixSelectorEventLoop
|
||||
from asyncio import get_event_loop
|
||||
|
||||
|
||||
LOG_LEVELS = {logging.ERROR: "ERROR",
|
||||
logging.INFO: "INFO",
|
||||
logging.WARNING: "WARNING",
|
||||
logging.DEBUG: "DEBUG",
|
||||
logging.CRITICAL: "CRITICAL",
|
||||
}
|
||||
|
||||
|
||||
class WorkerException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnexpectedWorkerFinish(WorkerException):
|
||||
def __init__(self, reason: List[str], output: Optional[List[str]] = []):
|
||||
"""Исключение кидаемое в случае, если воркер неожиданно умер."""
|
||||
self.reason: List[str] = reason
|
||||
self.output: List[dict] = output
|
||||
|
||||
|
||||
class UnexpectedWorkerState(WorkerException):
|
||||
"""Исключение кидаемое в случае, если состояние воркера не соответствует
|
||||
ожидаемому."""
|
||||
def __init__(self, state: str, expected: str):
|
||||
self.state: str = state
|
||||
self.expected: str = expected
|
||||
|
||||
|
||||
class DaemonIsDead(WorkerException):
|
||||
"""Исключение кидаемое в случае, если демон-воркер неожиданно оказался
|
||||
мертв."""
|
||||
def __init__(self, info: str, output: Optional[List[str]] = []):
|
||||
self.info: str = info
|
||||
self.output: List[dict] = output
|
||||
|
||||
|
||||
class IncorrectConfiguration(WorkerException):
|
||||
"""Исключение кидаемое при попытке запустить выполнение конфигурации, в
|
||||
которой есть ошибки."""
|
||||
def __init__(self, errors: Dict[str, str]):
|
||||
self._errors = errors
|
||||
|
||||
|
||||
class WorkersManager:
|
||||
def __init__(self, loop: Optional[_UnixSelectorEventLoop] = None):
|
||||
if loop is None:
|
||||
self._loop = get_event_loop()
|
||||
else:
|
||||
self._loop = loop
|
||||
|
||||
self._configs: Dict[int, WorkerProtocol] = {}
|
||||
self._execs: Dict[int, WorkerProtocol] = {}
|
||||
self._wids: List[int] = []
|
||||
|
||||
async def make_worker(self, command: Command) -> Tuple[Union[int, None],
|
||||
Union[str, None]]:
|
||||
"""Метод для создания воркеров."""
|
||||
wid = self._get_wid()
|
||||
try:
|
||||
# Создаем воркера и получаем интерфейс для взаимодействия с ним.
|
||||
worker_api = await self._create_worker(wid, command)
|
||||
self._configs[wid] = worker_api
|
||||
except Exception as error:
|
||||
if wid in self._configs:
|
||||
self._configs.pop(wid)
|
||||
return None, str(error)
|
||||
return wid, None
|
||||
|
||||
async def configure_worker(self, wid: int, parameters: List[dict],
|
||||
reset: bool = False) -> Union[dict, None]:
|
||||
"""Метод для модификации или сброса параметров конфигурации указанными
|
||||
значениями.
|
||||
"""
|
||||
if not parameters:
|
||||
return {}
|
||||
if wid not in self._configs:
|
||||
return None
|
||||
|
||||
worker_api: WorkerProtocol = self._configs[wid]
|
||||
try:
|
||||
result = await worker_api.configure(parameters)
|
||||
except (UnexpectedWorkerFinish, DaemonIsDead):
|
||||
worker_api.kill()
|
||||
self._configs.pop(wid)
|
||||
raise
|
||||
|
||||
return result
|
||||
|
||||
async def cancel_worker_configuration(self, wid: int):
|
||||
"""Метод для завершения конфигурации воркера и завершения его работы.
|
||||
"""
|
||||
if wid not in self._configs:
|
||||
return None
|
||||
|
||||
worker_api: WorkerProtocol = self._configs[wid]
|
||||
output = await worker_api.cancel_configuration()
|
||||
worker_api.delete_socket()
|
||||
self._configs.pop(wid)
|
||||
|
||||
return output
|
||||
|
||||
def clean(self):
|
||||
"""Метод для убийства всех работающих на данный момент демонов."""
|
||||
# TODO доработать его логику так, чтобы при перезапуске сервера этот
|
||||
# метод никого не убивал.
|
||||
for wid, worker in {**self._configs, **self._execs}.items():
|
||||
result = False
|
||||
try:
|
||||
result = worker.kill()
|
||||
finally:
|
||||
if result:
|
||||
print(f"[*] Worker {wid} is killed.")
|
||||
else:
|
||||
print(f"[x] Worker {wid} is not killed.")
|
||||
continue
|
||||
|
||||
def get_config(self, wid: int):
|
||||
if wid in self._configs:
|
||||
return self._configs[wid]
|
||||
return None
|
||||
|
||||
def get_execution(self, wid: int):
|
||||
if wid in self._execs:
|
||||
return self._execs[wid]
|
||||
return None
|
||||
|
||||
def run_execution(self, wid: int) -> Tuple[int, Union[dict, None]]:
|
||||
'''Метод для запуска исполнения команды, используя указанную
|
||||
конфигурацию.
|
||||
Вывод:
|
||||
0, None -- исполнение успешно запущено;
|
||||
1, None -- конфигурация с данным wid не найдена;
|
||||
2, errors -- конфигурация с данным wid содержит ошибки;
|
||||
3, None -- исполнение с данным wid уже существует;
|
||||
'''
|
||||
if wid not in self._configs:
|
||||
return 1, None
|
||||
if self._configs[wid].status > 0:
|
||||
# Берем ошибки, как-то.
|
||||
errors = {}
|
||||
return 2, errors
|
||||
if wid in self._execs:
|
||||
return 3, None
|
||||
|
||||
# worker = self._configs.pop(wid)
|
||||
return 0, None
|
||||
|
||||
async def _create_worker(self, wid: int, command: Command
|
||||
) -> "WorkerProtocol":
|
||||
# Путь к временному файлу, в котором будет лежать pid демона-рабочего.
|
||||
pid_file_path: str = os.path.join(os.getcwd(), f"pid_file_{wid}")
|
||||
socket_path: str = os.path.join(os.getcwd(),
|
||||
f"worker_{wid}.sock")
|
||||
if os.path.exists(socket_path):
|
||||
os.remove(socket_path)
|
||||
|
||||
process_runner = Process(target=daemonize_worker,
|
||||
args=(socket_path, wid, pid_file_path,
|
||||
command)
|
||||
)
|
||||
process_runner.start()
|
||||
process_runner.join()
|
||||
|
||||
daemon_pid = await self._get_daemon_pid(pid_file_path)
|
||||
self._delete_pid_file(pid_file_path)
|
||||
|
||||
transport, worker_interface = await self._loop.create_unix_connection(
|
||||
lambda: WorkerProtocol(daemon_pid,
|
||||
socket_path),
|
||||
socket_path)
|
||||
return worker_interface
|
||||
|
||||
def _get_wid(self):
|
||||
if not self._wids:
|
||||
worker_id = 0
|
||||
else:
|
||||
worker_id = self._wids[-1] + 1
|
||||
self._wids.append(worker_id)
|
||||
return worker_id
|
||||
|
||||
async def _get_daemon_pid(self, pid_file_path: str) -> int:
|
||||
while True:
|
||||
if not os.path.exists(pid_file_path):
|
||||
# Если файла нет -- уступаем работу другим воркерам.
|
||||
await asyncio.sleep(0)
|
||||
with open(pid_file_path, "r") as pid_file:
|
||||
daemon_pid = int(pid_file.readline())
|
||||
return daemon_pid
|
||||
|
||||
def _delete_pid_file(self, pid_file_path: str) -> None:
|
||||
try:
|
||||
os.remove(pid_file_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
configs = ["Configurations:"]
|
||||
configs.extend([f"wid={wid}" for wid in self._configs.keys()])
|
||||
configs_repr = "\n\t".join(configs)
|
||||
|
||||
execs = ["Executions:"]
|
||||
execs.extend([f"wid={wid}" for wid in self._execs.keys()])
|
||||
execs_repr = "\n\t".join(execs)
|
||||
|
||||
return f"{configs_repr}\n{execs_repr}"
|
||||
|
||||
|
||||
def daemonize_worker(socket_path: str, wid: int, pid_file: str,
|
||||
command: Command) -> None:
|
||||
'''Функция запускаемая в отдельном процессе и порождающая демона при помощи
|
||||
магии двойного форка.'''
|
||||
base_dir = os.getcwd()
|
||||
|
||||
new_fork()
|
||||
|
||||
os.chdir("/")
|
||||
os.setsid()
|
||||
os.umask(0)
|
||||
|
||||
new_fork()
|
||||
|
||||
# Создаем файл, с помощью которого будем передавать pid демона-рабочего.
|
||||
daemon_pid = str(os.getpid())
|
||||
with open(pid_file, "w") as pid_file:
|
||||
pid_file.write(daemon_pid)
|
||||
|
||||
with Listener(socket_path) as listener:
|
||||
daemon(listener, wid, base_dir, command)
|
||||
|
||||
|
||||
def new_fork() -> None:
|
||||
'''Функция для создания форка текущего процесса.'''
|
||||
try:
|
||||
middle_pid = os.fork()
|
||||
if middle_pid > 0:
|
||||
sys.exit(0)
|
||||
except OSError as error:
|
||||
print(f"FORK ERROR: {error.errno} - {str(error)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class WorkerProtocol(asyncio.Protocol):
|
||||
"""Класс протокола управления воркером."""
|
||||
def __init__(self, pid: int, socket_path: str):
|
||||
self.loop = asyncio.get_running_loop()
|
||||
|
||||
self._pid: int = pid
|
||||
self._socket_path: str = socket_path
|
||||
|
||||
# Объект Event, предназначенный для остановки записи при переполнении
|
||||
# буффера записи в трaнспорте.
|
||||
self._can_write = asyncio.Event()
|
||||
self._can_write.clear()
|
||||
|
||||
self._msg_queue = asyncio.Queue()
|
||||
|
||||
# Контекст чтения данных воркера.
|
||||
self._buffer = bytearray()
|
||||
self._msg_size: int = 0
|
||||
|
||||
self._connection_closed: bool = True
|
||||
|
||||
async def configure(self, parameters: List[dict],
|
||||
reset: bool = False) -> list:
|
||||
"""Метод для конфигурации воркера указанными значениями параметров.
|
||||
Возвращает список ошибок, если таковые есть, или пустой список, если
|
||||
никаких ошибок нет.
|
||||
"""
|
||||
message = self._make_worker_message("config", int(reset), parameters)
|
||||
await self._write(message)
|
||||
response = await self._read()
|
||||
response = json.loads(response)
|
||||
|
||||
return response["data"]
|
||||
|
||||
async def cancel_configuration(self) -> List[dict]:
|
||||
"""Метод для остановки конфигурации воркера и его работы в целом."""
|
||||
stop_message: bytes = self._make_worker_message("finish", 1, None)
|
||||
await self._write(stop_message)
|
||||
response = await self._read()
|
||||
response = json.loads(response)
|
||||
|
||||
return response["data"]
|
||||
|
||||
def connection_made(self, transport: asyncio.Transport) -> None:
|
||||
self.transport = transport
|
||||
self._can_write.set()
|
||||
self._connection_closed: bool = False
|
||||
print("Connection made.")
|
||||
|
||||
def connection_lost(self, exc: BaseException) -> None:
|
||||
if exc:
|
||||
print("Connection lost. Error:", str(exc))
|
||||
else:
|
||||
print("Connection lost.")
|
||||
self._connection_closed: bool = True
|
||||
|
||||
def data_received(self, data: bytes) -> None:
|
||||
self._buffer.extend(data)
|
||||
|
||||
while True:
|
||||
if not self._msg_size:
|
||||
if len(self._buffer) < 4:
|
||||
return
|
||||
size_length = struct.calcsize(">i")
|
||||
self._msg_size, = struct.unpack(">i",
|
||||
self._buffer[:size_length])
|
||||
self._buffer = self._buffer[size_length:]
|
||||
|
||||
if len(self._buffer) >= self._msg_size:
|
||||
data = self._buffer[:self._msg_size]
|
||||
self._buffer = self._buffer[self._msg_size:]
|
||||
self._msg_size = 0
|
||||
|
||||
self._msg_queue.put_nowait(data.decode())
|
||||
else:
|
||||
break
|
||||
|
||||
def pause_writing(self) -> None:
|
||||
self._can_write.clear()
|
||||
|
||||
def resume_writing(self) -> None:
|
||||
self._can_write.set()
|
||||
|
||||
async def _read(self) -> str:
|
||||
if self._connection_closed and self._msg_queue.empty():
|
||||
return None
|
||||
return await self._msg_queue.get()
|
||||
|
||||
async def read_all(self) -> List[str]:
|
||||
if self._msg_queue.empty():
|
||||
if self._connection_closed:
|
||||
return None
|
||||
else:
|
||||
return await self._msg_queue.get()
|
||||
output = []
|
||||
while not self._msg_queue.empty():
|
||||
output.append(self._msg_queue.get_nowait())
|
||||
return output
|
||||
|
||||
async def _write(self, data: Union[str, bytes]) -> None:
|
||||
await self._can_write.wait()
|
||||
if isinstance(data, str):
|
||||
data = data.encode()
|
||||
self.transport.write(struct.pack(">i", len(data)))
|
||||
self.transport.write(data)
|
||||
|
||||
def _make_worker_message(
|
||||
self,
|
||||
state: Literal["config", "finish", "exec", "input"],
|
||||
status: int, data: Union[list, dict]) -> dict:
|
||||
return json.dumps({"state": state,
|
||||
"status": status,
|
||||
"data": data}).encode()
|
||||
|
||||
def delete_socket(self) -> None:
|
||||
"""Метод для удаления сокета."""
|
||||
try:
|
||||
os.remove(self._socket_path)
|
||||
self.transport.close()
|
||||
self._connection_closed = True
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def kill(self) -> bool:
|
||||
"""Метод для убийства демона."""
|
||||
print("GONNA KILLA DAEMON:", self._pid)
|
||||
if self.is_daemon_alive():
|
||||
try:
|
||||
os.kill(self._pid, signal.SIGKILL)
|
||||
except OSError:
|
||||
return False
|
||||
else:
|
||||
print("ALREADY DEAD")
|
||||
return True
|
||||
|
||||
def stop(self) -> bool:
|
||||
print("STOP DAEMON:", self._daemon_pid)
|
||||
if self.is_daemon_alive():
|
||||
try:
|
||||
# Для остановки демона посылаем в него KeyboardInterrupt.
|
||||
os.kill(self._pid, signal.SIGINT)
|
||||
except OSError as error:
|
||||
print(f"ERROR: Failed stopping daemon PID={self._pid}. "
|
||||
f"Reason: {str(error)}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_daemon_alive(self) -> bool:
|
||||
try:
|
||||
os.kill(self._pid, 0)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
428
calculate/server/ipc_draft/worker_old.py
Normal file
428
calculate/server/ipc_draft/worker_old.py
Normal file
|
@ -0,0 +1,428 @@
|
|||
import os
|
||||
import sys
|
||||
import json
|
||||
import socket
|
||||
import signal
|
||||
import logging
|
||||
from multiprocessing import Process
|
||||
# from asyncio import sleep
|
||||
# import logging
|
||||
# import time
|
||||
from io_module import IOModule
|
||||
from daemon import daemon
|
||||
from commands import Command
|
||||
|
||||
from typing import Dict, Optional, List, Union, Tuple, Literal
|
||||
|
||||
from asyncio.unix_events import _UnixSelectorEventLoop
|
||||
from asyncio import get_event_loop, sleep
|
||||
|
||||
from pprint import pprint
|
||||
|
||||
|
||||
LOG_LEVELS = {logging.ERROR: "ERROR",
|
||||
logging.INFO: "INFO",
|
||||
logging.WARNING: "WARNING",
|
||||
logging.DEBUG: "DEBUG",
|
||||
logging.CRITICAL: "CRITICAL",
|
||||
}
|
||||
|
||||
|
||||
class WorkerException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnexpectedWorkerFinish(WorkerException):
|
||||
def __init__(self, reason: List[str], output: Optional[List[str]] = []):
|
||||
"""Исключение кидаемое в случае, если воркер неожиданно умер."""
|
||||
self.reason: List[str] = reason
|
||||
self.output: List[dict] = output
|
||||
|
||||
|
||||
class UnexpectedWorkerState(WorkerException):
|
||||
"""Исключение кидаемое в случае, если состояние воркера не соответствует
|
||||
ожидаемому."""
|
||||
def __init__(self, state: str, expected: str):
|
||||
self.state: str = state
|
||||
self.expected: str = expected
|
||||
|
||||
|
||||
class DaemonIsDead(WorkerException):
|
||||
"""Исключение кидаемое в случае, если демон-воркер неожиданно оказался
|
||||
мертв."""
|
||||
def __init__(self, info: str, output: Optional[List[str]] = []):
|
||||
self.info: str = info
|
||||
self.output: List[dict] = output
|
||||
|
||||
|
||||
class IncorrectConfiguration(WorkerException):
|
||||
"""Исключение кидаемое при попытке запустить выполнение конфигурации, в
|
||||
которой есть ошибки."""
|
||||
def __init__(self, errors: Dict[str, str]):
|
||||
self._errors = errors
|
||||
|
||||
|
||||
class WorkersManager:
|
||||
def __init__(self, loop: Optional[_UnixSelectorEventLoop] = None):
|
||||
if loop is None:
|
||||
self._loop = get_event_loop()
|
||||
else:
|
||||
self._loop = loop
|
||||
|
||||
self._configs: Dict[int, Worker] = {}
|
||||
self._execs: Dict[int, Worker] = {}
|
||||
self._wids: List[int] = []
|
||||
|
||||
async def make_worker(self, command: Command) -> Tuple[Union[int, None],
|
||||
Union[str, None]]:
|
||||
wid = self._get_wid()
|
||||
try:
|
||||
worker = Worker(wid, loop=self._loop)
|
||||
await worker.create_daemon(command)
|
||||
self._configs[wid] = worker
|
||||
except Exception as error:
|
||||
if wid in self._configs:
|
||||
self._configs.pop(wid)
|
||||
return None, str(error)
|
||||
return wid, None
|
||||
|
||||
async def configure_worker(self, wid: int, parameters: List[dict],
|
||||
reset: bool = False) -> Union[dict, None]:
|
||||
"""Метод для модификации или сброса параметров конфигурации указанными
|
||||
значениями."""
|
||||
if not parameters:
|
||||
return {}
|
||||
if wid not in self._configs:
|
||||
return None
|
||||
|
||||
worker: Worker = self._configs[wid]
|
||||
try:
|
||||
result = await worker.configure(parameters)
|
||||
except (UnexpectedWorkerFinish, DaemonIsDead):
|
||||
worker.kill()
|
||||
self._configs.pop(wid)
|
||||
raise
|
||||
|
||||
return result
|
||||
|
||||
async def cancel_worker_configuration(self, wid: int):
|
||||
if wid not in self._configs:
|
||||
return None
|
||||
|
||||
worker: Worker = self._configs[wid]
|
||||
output = await worker.cancel_configuration()
|
||||
self._configs.pop(wid)
|
||||
|
||||
return output
|
||||
|
||||
def clean(self):
|
||||
"""Метод для убийства всех работающих на данный момент демонов."""
|
||||
# TODO доработать его логику так, чтобы при перезапуске сервера этот
|
||||
# метод никого не убивал.
|
||||
print("KILLING WORKERS:")
|
||||
for wid, worker in {**self._configs, **self._execs}.items():
|
||||
result = False
|
||||
try:
|
||||
result = worker.kill()
|
||||
finally:
|
||||
if result:
|
||||
print(f"[*] Worker {wid} is killed.")
|
||||
else:
|
||||
print(f"[x] Worker {wid} is not killed.")
|
||||
continue
|
||||
|
||||
def get_config(self, wid: int):
|
||||
if wid in self._configs:
|
||||
return self._configs[wid]
|
||||
return None
|
||||
|
||||
def get_execution(self, wid: int):
|
||||
if wid in self._execs:
|
||||
return self._execs[wid]
|
||||
return None
|
||||
|
||||
def run_execution(self, wid: int) -> Tuple[int, Union[dict, None]]:
|
||||
'''Метод для запуска исполнения команды, используя указанную
|
||||
конфигурацию.
|
||||
Вывод:
|
||||
0, None -- исполнение успешно запущено;
|
||||
1, None -- конфигурация с данным wid не найдена;
|
||||
2, errors -- конфигурация с данным wid содержит ошибки;
|
||||
3, None -- исполнение с данным wid уже существует;
|
||||
'''
|
||||
if wid not in self._configs:
|
||||
return 1, None
|
||||
if self._configs[wid].status > 0:
|
||||
# Берем ошибки, как-то.
|
||||
errors = {}
|
||||
return 2, errors
|
||||
if wid in self._execs:
|
||||
return 3, None
|
||||
|
||||
worker = self._configs.pop(wid)
|
||||
return 0, None
|
||||
|
||||
def _get_wid(self):
|
||||
if not self._wids:
|
||||
worker_id = 0
|
||||
else:
|
||||
worker_id = self._wids[-1] + 1
|
||||
self._wids.append(worker_id)
|
||||
return worker_id
|
||||
|
||||
def __repr__(self):
|
||||
configs = ["Configurations:"]
|
||||
configs.extend([f"wid={wid}" for wid in self._configs.keys()])
|
||||
configs_repr = "\n\t".join(configs)
|
||||
|
||||
execs = ["Executions:"]
|
||||
execs.extend([f"wid={wid}" for wid in self._execs.keys()])
|
||||
execs_repr = "\n\t".join(execs)
|
||||
|
||||
return f"{configs_repr}\n{execs_repr}"
|
||||
|
||||
|
||||
class Worker:
|
||||
def __init__(self, wid: int,
|
||||
loop: Optional[_UnixSelectorEventLoop] = None):
|
||||
if loop is None:
|
||||
self._loop = get_event_loop()
|
||||
else:
|
||||
self._loop = loop
|
||||
|
||||
self._wid: int = wid
|
||||
self._socket_path: str = os.path.join(os.getcwd(),
|
||||
f"worker_{self._wid}.sock")
|
||||
if os.path.exists(self._socket_path):
|
||||
os.remove(self._socket_path)
|
||||
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
self._daemon_pid = None
|
||||
|
||||
# Контекст для чтения данных из сокета.
|
||||
self._context: str = ""
|
||||
|
||||
self._state: str = ""
|
||||
self._status: int = 0
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
return self._state
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._status
|
||||
|
||||
async def configure(self, parameters: List[dict],
|
||||
reset: bool = False) -> Union[dict, list]:
|
||||
'''Метод для отправки в демона-воркера занчений параметров во время
|
||||
конфигурации.'''
|
||||
if self._state != "config":
|
||||
raise UnexpectedWorkerState(self._state, "config")
|
||||
|
||||
message = self._make_worker_message("config", int(reset), parameters)
|
||||
await self.send(message)
|
||||
|
||||
answers = await self.read()
|
||||
|
||||
print("WORKER'S ANSWERS")
|
||||
pprint(answers)
|
||||
|
||||
config_answer = answers[0]
|
||||
if config_answer["state"] == "finish":
|
||||
raise UnexpectedWorkerFinish(config_answer["data"],
|
||||
output=answers[1:])
|
||||
return config_answer["data"]
|
||||
|
||||
async def cancel_configuration(self) -> List[dict]:
|
||||
if self._state != "config":
|
||||
raise UnexpectedWorkerState(self._state, "config")
|
||||
|
||||
stop_message = self._make_worker_message("finish", 1, None)
|
||||
await self.send(stop_message)
|
||||
|
||||
output = await self.read()
|
||||
|
||||
print("OUTPUT")
|
||||
pprint(output)
|
||||
if output:
|
||||
return output
|
||||
return []
|
||||
|
||||
async def send(self, data: dict):
|
||||
await self._loop.run_in_executor(None, self._send,
|
||||
json.dumps(data) + '\0')
|
||||
|
||||
async def read(self):
|
||||
messages = await self._loop.run_in_executor(None, self._read)
|
||||
return messages
|
||||
|
||||
async def create_daemon(self, command: Command) -> None:
|
||||
# Путь к временному файлу, в котором будет лежать pid демона-рабочего.
|
||||
pid_file: str = os.path.join(os.getcwd(), f"PID_file_{self._wid}")
|
||||
|
||||
io = IOModule(self._socket_path, command.id)
|
||||
|
||||
runner_process = Process(target=daemonize_worker,
|
||||
args=(io, self._wid, pid_file,
|
||||
command)
|
||||
)
|
||||
runner_process.start()
|
||||
self.sock.connect(self._socket_path)
|
||||
runner_process.join()
|
||||
|
||||
self._daemon_pid = 0
|
||||
while not self._daemon_pid:
|
||||
await sleep(0.5)
|
||||
self._daemon_pid = self._get_daemon_pid(pid_file)
|
||||
self._delete_pid_file(pid_file)
|
||||
|
||||
print("DAEMON PID =", self._daemon_pid)
|
||||
print("IS_ALIVE:", self.is_daemon_alive())
|
||||
self._state = "config"
|
||||
|
||||
def kill(self) -> bool:
|
||||
"""Метод для убийства демона."""
|
||||
print("GONNA KILLA DAEMON:", self._daemon_pid)
|
||||
if self.is_daemon_alive():
|
||||
try:
|
||||
os.kill(self._daemon_pid, signal.SIGKILL)
|
||||
except OSError:
|
||||
return False
|
||||
else:
|
||||
print("ALREADY DEAD")
|
||||
# self._delete_socket()
|
||||
self._daemon_pid = None
|
||||
return True
|
||||
|
||||
def _stop_daemon(self) -> bool:
|
||||
print("STOP DAEMON:", self._daemon_pid)
|
||||
if self.is_daemon_alive():
|
||||
try:
|
||||
# Для остановки демона посылаем в него KeyboardInterrupt.
|
||||
os.kill(self._daemon_pid, signal.SIGINT)
|
||||
except OSError as error:
|
||||
print(f"ERROR: Failed stopping daemon PID={self._daemon_pid}. "
|
||||
f"Reason: {str(error)}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_daemon_alive(self) -> bool:
|
||||
try:
|
||||
os.kill(self._daemon_pid, 0)
|
||||
except OSError:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
|
||||
def _send(self, data: str) -> bool:
|
||||
try:
|
||||
self.sock.sendall(data.encode())
|
||||
except socket.error as error:
|
||||
print(f"ERROR: can not send data to worker {self._wid}."
|
||||
f" Reason: {str(error)}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def _read(self) -> Union[dict, None]:
|
||||
input_queue = []
|
||||
while True:
|
||||
try:
|
||||
data = self.sock.recv(1024)
|
||||
except Exception as error:
|
||||
print(f"Connection is broken. Error: {str(error)}")
|
||||
return None
|
||||
raise DaemonIsDead(f"Connection is broken. "
|
||||
f"Error: {str(error)}")
|
||||
if data:
|
||||
# Если данные получены делим их по нулевому байту.
|
||||
data = data.decode().split('\0')
|
||||
break
|
||||
# Иначе считаем, что соединение нарушилось.
|
||||
print("Connection is broken. Empty data.")
|
||||
return None
|
||||
raise DaemonIsDead("Connection is broken.")
|
||||
|
||||
if self._context:
|
||||
self._context += data.pop(0)
|
||||
# Если после взятия первого значения еще что-то есть в списке
|
||||
# данных -- значит среди них был нулевой байт. И из значения
|
||||
# контекста можно получить прочитанные данные.
|
||||
if data:
|
||||
request = json.loads(self._context)
|
||||
input_queue.append(request)
|
||||
self._context = ""
|
||||
|
||||
if data:
|
||||
# Если данные еще есть в списке -- значит последнее значение в
|
||||
# списке, даже если это пустая строка, является котекстом.
|
||||
self._context = data.pop()
|
||||
if data:
|
||||
# Остальное добавляем в очередь.
|
||||
for request in data:
|
||||
input_queue.append(json.loads(request))
|
||||
|
||||
return input_queue
|
||||
|
||||
def _make_worker_message(self, state: Literal["config", "finish",
|
||||
"exec", "input"],
|
||||
status: int, data: Union[list, dict]) -> dict:
|
||||
return {"state": state,
|
||||
"status": status,
|
||||
"data": data}
|
||||
|
||||
def _get_daemon_pid(self, pid_file_path: str) -> int:
|
||||
if not os.path.exists(pid_file_path):
|
||||
return 0
|
||||
try:
|
||||
with open(pid_file_path, "r") as pid_file:
|
||||
daemon_pid = int(pid_file.readline())
|
||||
return daemon_pid
|
||||
except Exception:
|
||||
return 0
|
||||
|
||||
def _delete_socket(self) -> None:
|
||||
"""Метод для удаления сокета."""
|
||||
try:
|
||||
self.sock.close()
|
||||
os.remove(self._socket_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def _delete_pid_file(self, pid_file_path: str) -> None:
|
||||
try:
|
||||
os.remove(pid_file_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
def daemonize_worker(io: IOModule, wid: int, pid_file: str,
|
||||
command: Command) -> None:
|
||||
'''Функция запускаемая в отдельном процессе и порождающая демона при помощи
|
||||
магии двойного форка.'''
|
||||
base_dir = os.getcwd()
|
||||
|
||||
new_fork()
|
||||
|
||||
os.chdir("/")
|
||||
os.setsid()
|
||||
os.umask(0)
|
||||
|
||||
new_fork()
|
||||
|
||||
# Создаем файл, с помощью которого будем передавать pid демона-рабочего.
|
||||
daemon_pid = str(os.getpid())
|
||||
with open(pid_file, "w") as pid_file:
|
||||
pid_file.write(daemon_pid)
|
||||
|
||||
daemon(io, wid, base_dir, command)
|
||||
|
||||
|
||||
def new_fork() -> None:
|
||||
'''Функция для создания форка текущего процесса.'''
|
||||
try:
|
||||
middle_pid = os.fork()
|
||||
if middle_pid > 0:
|
||||
sys.exit(0)
|
||||
except OSError as error:
|
||||
print(f"FORK ERROR: {error.errno} - {str(error)}")
|
||||
sys.exit(1)
|
|
@ -1,56 +1,66 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from starlette.requests import Request
|
||||
from ..utils.responses import (
|
||||
ResponseStructure,
|
||||
validate_response,
|
||||
get_base_url,
|
||||
get_command_not_found,
|
||||
get_cl_command_not_found,
|
||||
)
|
||||
from typing import Optional
|
||||
|
||||
from ..utils.dependencies import right_checkers
|
||||
|
||||
from ..utils.dependencies import get_current_user
|
||||
from ..utils.users import check_user_rights
|
||||
from ..server_data import ServerData
|
||||
from ..schemas.users import User
|
||||
|
||||
from ..schemas.responses import (
|
||||
GetRootResponse,
|
||||
GetCommandsResponse,
|
||||
FoundCommandInfo,
|
||||
GetCommandParametersResponse,
|
||||
)
|
||||
|
||||
|
||||
data = ServerData()
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/commands", tags=["Commands management"],
|
||||
dependencies=[Depends(right_checkers["read"])])
|
||||
async def get_commands() -> dict:
|
||||
'''Обработчик, отвечающий на запросы списка команд.'''
|
||||
response = {}
|
||||
for command_id, command_object in data.commands.items():
|
||||
response.update({command_id: {"title": command_object.title,
|
||||
"category": command_object.category,
|
||||
"icon": command_object.icon,
|
||||
"command": command_object.command}})
|
||||
return response
|
||||
@router.get("/commands", response_model=GetCommandsResponse, tags=["Commands"])
|
||||
async def get_available_commands(request: Request,
|
||||
gui: Optional[bool] = False):
|
||||
response_data = commands.get_commands(get_base_url(request))
|
||||
return validate_response(response_data, GetCommandsResponse,
|
||||
media_type="application/hal+json")
|
||||
|
||||
|
||||
@router.get("/commands/{cid}", tags=["Commands management"],
|
||||
dependencies=[Depends(right_checkers["read"])])
|
||||
async def get_command(cid: int) -> dict:
|
||||
'''Обработчик запросов списка команд.'''
|
||||
if cid not in data.commands_instances:
|
||||
# TODO добавить какую-то обработку ошибки.
|
||||
pass
|
||||
return {'id': cid,
|
||||
'name': f'command_{cid}'}
|
||||
@router.get("/commands/{command}",
|
||||
response_model=FoundCommandInfo,
|
||||
tags=["Commands"])
|
||||
async def find_command_data(command: str, request: Request,
|
||||
gui: Optional[bool] = False,
|
||||
by_id: Optional[bool] = False):
|
||||
base_url = get_base_url(request)
|
||||
|
||||
if by_id:
|
||||
command_data = commands.get_by_id(command, base_url)
|
||||
if command_data is None:
|
||||
raise get_command_not_found(command)
|
||||
else:
|
||||
command_data = commands.find_command(command, base_url)
|
||||
if command_data is None:
|
||||
raise get_cl_command_not_found(command)
|
||||
|
||||
return validate_response(command_data, FoundCommandInfo,
|
||||
media_type="application/hal+json")
|
||||
|
||||
|
||||
@router.get("/commands/{cid}/groups", tags=["Commands management"],
|
||||
dependencies=[Depends(right_checkers["read"])])
|
||||
async def get_command_parameters_groups(cid: int) -> dict:
|
||||
'''Обработчик запросов на получение групп параметров указанной команды.'''
|
||||
pass
|
||||
|
||||
|
||||
@router.get("/commands/{cid}/parameters", tags=["Commands management"],
|
||||
dependencies=[Depends(right_checkers["read"])])
|
||||
async def get_command_parameters(cid: int) -> dict:
|
||||
'''Обработчик запросов на получение параметров указанной команды.'''
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/commands/{command_id}", tags=["Commands management"],
|
||||
dependencies=[Depends(right_checkers["write"])])
|
||||
async def post_command(command_id: str) -> int:
|
||||
if command_id not in data.commands:
|
||||
# TODO добавить какую-то обработку ошибки.
|
||||
pass
|
||||
return
|
||||
@router.get("/commands/{command_id}/parameters",
|
||||
response_model=GetCommandParametersResponse,
|
||||
tags=["Commands"])
|
||||
async def get_command_parameters(command_id: str, request: Request):
|
||||
parameters_data = commands.get_parameters(command_id,
|
||||
get_base_url(request))
|
||||
if parameters_data is None:
|
||||
raise get_command_not_found(command_id)
|
||||
return parameters_data
|
||||
|
|
22
calculate/server/schemas/requests.py
Normal file
22
calculate/server/schemas/requests.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
from typing import Any, List
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
# PATCH /configs/{WID}/parameters
|
||||
class ParameterValue(BaseModel):
|
||||
id: str
|
||||
value: Any
|
||||
|
||||
|
||||
class ModifyConfigRequest(BaseModel):
|
||||
__root__: List[ParameterValue]
|
||||
|
||||
|
||||
# PUT /configs/{WID}/parameters
|
||||
class ResetConfigRequest(BaseModel):
|
||||
__root__: List[ParameterValue]
|
||||
|
||||
|
||||
# PATCH /executions/{WID}/input
|
||||
class WriteToExecutionRequest(BaseModel):
|
||||
data: Any
|
180
calculate/server/schemas/responses.py
Normal file
180
calculate/server/schemas/responses.py
Normal file
|
@ -0,0 +1,180 @@
|
|||
from typing import Any, List, Dict, Optional, Literal, Union
|
||||
from pydantic import BaseModel, Field, HttpUrl
|
||||
|
||||
|
||||
class NotTemplatedLinkData(BaseModel):
|
||||
href: HttpUrl = "URI"
|
||||
|
||||
class Config:
|
||||
extra = "forbid"
|
||||
|
||||
|
||||
class TemplatedLinkData(NotTemplatedLinkData):
|
||||
templated: bool = False
|
||||
|
||||
|
||||
LinkData = Union[NotTemplatedLinkData, TemplatedLinkData]
|
||||
|
||||
|
||||
# GET /
|
||||
class ServerInfo(BaseModel):
|
||||
name: str = "Server name"
|
||||
version: str = "Server version"
|
||||
|
||||
|
||||
class GetRootResponse(BaseModel):
|
||||
data: ServerInfo
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links",
|
||||
description="Links to resources")
|
||||
|
||||
|
||||
# GET /commands/{CID}/parameters
|
||||
class ParameterInfo(BaseModel):
|
||||
id: str
|
||||
default: Any
|
||||
|
||||
|
||||
class ParametersGroup(BaseModel):
|
||||
group_id: str
|
||||
parameters: List[ParameterInfo]
|
||||
|
||||
|
||||
class GetCommandParametersResponse(BaseModel):
|
||||
data: List[ParametersGroup]
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
|
||||
# GET /commands/{CID}
|
||||
class CommandData(BaseModel):
|
||||
id: str
|
||||
title: str
|
||||
category: str
|
||||
command: str
|
||||
|
||||
|
||||
class CommandInfo(BaseModel):
|
||||
data: CommandData
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
|
||||
class FoundCommandInfo(CommandInfo):
|
||||
embedded: Dict[str,
|
||||
GetCommandParametersResponse] = Field(...,
|
||||
alias="_embedded")
|
||||
|
||||
|
||||
# GET /commands/
|
||||
class GetCommandsResponse(BaseModel):
|
||||
__root__: Dict[str, CommandInfo]
|
||||
|
||||
# class Config:
|
||||
# @staticmethod
|
||||
# def schema_extra(schema, model):
|
||||
# example = {}
|
||||
# for num in range(2):
|
||||
# resp = ResponseStructure()
|
||||
# resp.add_data(id=f"command_{num}",
|
||||
# command=f"cl-command-{num}",
|
||||
# title=f"Command {num}",
|
||||
# category="cool-category"
|
||||
# )
|
||||
# resp.add_link("parameters",
|
||||
# f"/commands/command_{num}/parameters")
|
||||
# resp.add_link("configure", f"/configs/command_{num}")
|
||||
|
||||
# example[f"command_{num}"] = resp.get_dict()
|
||||
# schema["example"] = example
|
||||
|
||||
|
||||
# POST /configs/{command}
|
||||
class CreateConfigResponse(BaseModel):
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
|
||||
# PATCH /configs/{WID}/parameters
|
||||
class ModifyConfigCorrectResponse(BaseModel):
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
class Config:
|
||||
extra = "forbid"
|
||||
|
||||
|
||||
class ModifyConfigUncorrectResponse(ModifyConfigCorrectResponse):
|
||||
data: Optional[Dict[str, str]]
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
|
||||
ModifyConfigResponse = Union[ModifyConfigCorrectResponse,
|
||||
ModifyConfigUncorrectResponse]
|
||||
|
||||
|
||||
# PUT /configs/{WID}/parameters
|
||||
class ResetConfigResponse(BaseModel):
|
||||
data: Optional[Dict[str, str]]
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
|
||||
# POST /executions/{WID}
|
||||
class CreateExecutionResponse(BaseModel):
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
|
||||
# DELETE /executions/{WID}
|
||||
class OutputData(BaseModel):
|
||||
type: Literal["output"]
|
||||
logging: Union[int, None]
|
||||
text: str
|
||||
source: str
|
||||
|
||||
|
||||
class OutputMessage(BaseModel):
|
||||
data: OutputData
|
||||
|
||||
|
||||
class StopExecutionsResponse(BaseModel):
|
||||
__root__: List[OutputMessage]
|
||||
|
||||
|
||||
# DELETE /configs/{WID}
|
||||
class StopConfigResponse(BaseModel):
|
||||
__root__: List[OutputMessage]
|
||||
|
||||
|
||||
# GET /executions/{WID}/output
|
||||
class InputRequestData(BaseModel):
|
||||
type: Literal["input"]
|
||||
text: str
|
||||
source: str
|
||||
|
||||
|
||||
class InputMessage(BaseModel):
|
||||
data: InputRequestData
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
|
||||
class GetExecutionOutputResponse(BaseModel):
|
||||
__root__: List[Union[OutputMessage, InputMessage]]
|
||||
|
||||
|
||||
# GET /workers/
|
||||
class WorkerData(BaseModel):
|
||||
socket: str
|
||||
command: str
|
||||
pid: int
|
||||
|
||||
|
||||
class WorkerInfo(BaseModel):
|
||||
data: WorkerData
|
||||
links: Dict[str, LinkData] = Field(..., alias="_links")
|
||||
|
||||
|
||||
class GetWorkersResponse(BaseModel):
|
||||
__root__: Dict[int, WorkerInfo]
|
||||
|
||||
# DELETE /workers/{WID}
|
||||
|
||||
|
||||
# Сообщения протокола взаимодействия сервера и воркеров.
|
||||
# class WorkerMessageBase(BaseModel):
|
||||
# state: str
|
||||
# status: int
|
|
@ -1,15 +1,33 @@
|
|||
from fastapi import FastAPI, Depends
|
||||
from starlette.requests import Request
|
||||
|
||||
from .server_data import ServerData
|
||||
from .utils.dependencies import right_checkers
|
||||
from .models.database import database
|
||||
from .database.db import database
|
||||
|
||||
# from .utils.dependencies import right_checkers, get_current_user
|
||||
from .utils.dependencies import get_current_user
|
||||
from .utils.responses import (
|
||||
ResponseStructure,
|
||||
get_base_url,
|
||||
validate_response,
|
||||
)
|
||||
from .utils.users import check_user_rights
|
||||
|
||||
from .schemas.responses import GetRootResponse
|
||||
from .schemas.users import User
|
||||
|
||||
from .routers.users import router as users_router
|
||||
from .routers.commands import router as commands_router
|
||||
|
||||
|
||||
# TODO
|
||||
# 1. Разобраться с описанием команд как ресурсов и со всем, что от них зависит.
|
||||
# 2. Разобраться с объектами воркеров. И способом их функционирования.
|
||||
# 1. Добавить список отклоняемых токенов и их обработку.
|
||||
# 2. Добавить предварительный вариант генерации access_tokens.
|
||||
# 3. Продумать, наконец, концепцию cid и wid, в ключе их применения при
|
||||
# настройке команд и запуске воркеров.
|
||||
# 4. Решить сколько команд может запускать пользователь единовременно.
|
||||
# 5. Добавить способ указания принадлежности воркеров и команд пользователям.
|
||||
# 6. Разобраться с местом раcположения базы данных.
|
||||
|
||||
|
||||
data = ServerData()
|
||||
|
@ -26,30 +44,39 @@ async def shutdown():
|
|||
await database.disconnect()
|
||||
|
||||
|
||||
@app.get("/", tags=["Root"],
|
||||
dependencies=[Depends(right_checkers["read"])])
|
||||
async def get_root() -> dict:
|
||||
'''Обработчик корневых запросов.'''
|
||||
return {'msg': 'root msg'}
|
||||
@app.get("/", response_model=GetRootResponse, tags=["Home"])
|
||||
async def get_primary_commands(request: Request,
|
||||
user: User = Depends(get_current_user)):
|
||||
check_user_rights(user, "read")
|
||||
|
||||
resp = ResponseStructure(get_base_url(request))
|
||||
resp.add_data(name="Calculate Server", version="0.1")
|
||||
resp.add_link("commands", "/commands?gui={is_gui}", templated=True)
|
||||
resp.add_link("find_command", "/commands/{console_command}?gui={is_gui}",
|
||||
templated=True)
|
||||
resp.add_link("workers", "/workers")
|
||||
|
||||
return validate_response(resp.get_dict(), GetRootResponse,
|
||||
media_type="application/hal+json")
|
||||
|
||||
|
||||
@app.get("/workers/{wid}", tags=["Workers management"],
|
||||
dependencies=[Depends(right_checkers["write"])])
|
||||
async def get_worker(wid: int):
|
||||
'''Тестовый обработчик.'''
|
||||
worker = data.get_worker_object(wid=wid)
|
||||
worker.run()
|
||||
print(f"worker: {type(worker)} object {worker}")
|
||||
# @app.get("/workers/{wid}", tags=["Workers management"],
|
||||
# dependencies=[Depends(right_checkers["write"])])
|
||||
# async def get_worker(wid: int):
|
||||
# '''Тестовый обработчик.'''
|
||||
# worker = data.get_worker_object(wid=wid)
|
||||
# worker.run()
|
||||
# print(f"worker: {type(worker)} object {worker}")
|
||||
|
||||
await worker.send({"text": "INFO"})
|
||||
worker_data = await worker.get()
|
||||
# await worker.send({"text": "INFO"})
|
||||
# worker_data = await worker.get()
|
||||
|
||||
if worker_data['type'] == 'log':
|
||||
data.log_message[data['level']](data['msg'])
|
||||
return worker_data
|
||||
# if worker_data['type'] == 'log':
|
||||
# data.log_message[data['level']](data['msg'])
|
||||
# return worker_data
|
||||
|
||||
# Authentification and users management.
|
||||
app.include_router(users_router)
|
||||
|
||||
# Commands creation and management.
|
||||
# Commands running and management.
|
||||
app.include_router(commands_router)
|
||||
|
|
|
@ -1,20 +1,25 @@
|
|||
import os
|
||||
import asyncio
|
||||
import importlib
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict, Optional, List, Union
|
||||
|
||||
from logging.config import dictConfig
|
||||
from logging import getLogger, Logger
|
||||
|
||||
from ..variables.loader import Datavars
|
||||
from ..commands.commands import Command, CommandRunner
|
||||
from .utils.workers import Worker
|
||||
from .utils.responses import ResponseStructure
|
||||
# from .utils.workers import Worker
|
||||
|
||||
from .schemas.config import ConfigSchema
|
||||
|
||||
from calculate.utils.tools import Singleton
|
||||
|
||||
|
||||
# Получаем конфигурацию сервера.
|
||||
TESTING = bool(os.environ.get("TESTING", False))
|
||||
|
||||
|
||||
if not TESTING:
|
||||
from .config import config
|
||||
server_config = ConfigSchema(**config)
|
||||
|
@ -23,7 +28,7 @@ else:
|
|||
server_config = ConfigSchema(**config)
|
||||
|
||||
|
||||
class ServerData:
|
||||
class ServerData(metaclass=Singleton):
|
||||
def __init__(self, config: ConfigSchema = server_config):
|
||||
self.event_loop = asyncio.get_event_loop()
|
||||
|
||||
|
@ -48,7 +53,8 @@ class ServerData:
|
|||
self.commands_runners: Dict[str, CommandRunner] = {}
|
||||
|
||||
# Словарь WID и экземпляров процессов-воркеров, передаваемых воркерам.
|
||||
self.workers: Dict[int, Worker] = {}
|
||||
# TODO добавить менеджера воркеров.
|
||||
# self.workers: Dict[int, Worker] = {}
|
||||
|
||||
@property
|
||||
def datavars(self):
|
||||
|
@ -87,18 +93,105 @@ class ServerData:
|
|||
|
||||
def make_command(self, command_id: str, ) -> int:
|
||||
'''Метод для создания команды по ее описанию.'''
|
||||
command_description = self.commands[command_id]
|
||||
# command_description = self.commands[command_id]
|
||||
|
||||
def _get_worker_object(self, wid: Optional[int] = None) -> Worker:
|
||||
'''Метод для получения воркера для команды.'''
|
||||
if wid is not None:
|
||||
worker = Worker(wid, self._event_loop, self._datavars)
|
||||
self._workers[wid] = worker
|
||||
elif not self._workers:
|
||||
worker = Worker(0, self._event_loop, self._datavars)
|
||||
self._workers[0] = worker
|
||||
# def _get_worker_object(self, wid: Optional[int] = None) -> Worker:
|
||||
# '''Метод для получения воркера для команды.'''
|
||||
# if wid is not None:
|
||||
# worker = Worker(wid, self._event_loop, self._datavars)
|
||||
# self._workers[wid] = worker
|
||||
# elif not self._workers:
|
||||
# worker = Worker(0, self._event_loop, self._datavars)
|
||||
# self._workers[0] = worker
|
||||
# else:
|
||||
# wid = max(self._workers.keys()) + 1
|
||||
# worker = Worker(wid, self._event_loop, self._datavars)
|
||||
# self._workers[wid] = worker
|
||||
# return worker
|
||||
|
||||
|
||||
class Commands:
|
||||
'''Предварительная реализация контейнера описаний команд.'''
|
||||
def __init__(self, *commands: List[Command]):
|
||||
self._by_id: dict = dict()
|
||||
self._by_command: dict = dict()
|
||||
for command in commands:
|
||||
self._by_id[command.id] = command
|
||||
self._by_command[command.command] = command
|
||||
|
||||
def get_commands(self, base_url: str) -> Dict[str, dict]:
|
||||
response = dict()
|
||||
|
||||
for command in self._by_id.values():
|
||||
data = ResponseStructure(base_url)
|
||||
data.add_data(id=command.id,
|
||||
title=command.title,
|
||||
category=command.category,
|
||||
command=command.command)
|
||||
data.add_link("self", f"/commands/{command.id}?by_id=true")
|
||||
data.add_link("parameters", f"/commands/{command.id}/parameters")
|
||||
data.add_link("configure", f"/configs/{command.id}")
|
||||
response[command.id] = data.get_dict()
|
||||
|
||||
return response
|
||||
|
||||
def get_by_id(self, command_id: str, base_url: str) -> Dict[str, Dict]:
|
||||
if command_id in self._by_id:
|
||||
command = self._by_id[command_id]
|
||||
data = ResponseStructure(base_url)
|
||||
data.add_data(id=command.id,
|
||||
title=command.title,
|
||||
category=command.category,
|
||||
command=command.command)
|
||||
data.add_link("self", f"/commands/{command.id}?by_id=1")
|
||||
data.add_link("parameters", f"/commands/{command.id}/parameters")
|
||||
data.add_link("configure", f"/configs/{command.id}")
|
||||
parameters_data = self._get_parameters_data(command.parameters,
|
||||
command.id, base_url)
|
||||
data.embed("parameters", parameters_data)
|
||||
return data.get_dict()
|
||||
|
||||
return None
|
||||
|
||||
def find_command(self, console_command: str,
|
||||
base_url: str) -> Union[dict, None]:
|
||||
if console_command in self._by_command:
|
||||
command = self._by_command[console_command]
|
||||
|
||||
data = ResponseStructure(base_url)
|
||||
data.add_data(id=command.id,
|
||||
title=command.title,
|
||||
category=command.category,
|
||||
command=command.command)
|
||||
data.add_link("self", f"/commands/{command.id}?by_id=1")
|
||||
data.add_link("parameters", f"/commands/{command.id}/parameters")
|
||||
data.add_link("configure", f"/configs/{command.id}")
|
||||
parameters_data = self._get_parameters_data(command.parameters,
|
||||
command.id, base_url)
|
||||
data.embed("parameters", parameters_data)
|
||||
|
||||
return data.get_dict()
|
||||
else:
|
||||
wid = max(self._workers.keys()) + 1
|
||||
worker = Worker(wid, self._event_loop, self._datavars)
|
||||
self._workers[wid] = worker
|
||||
return worker
|
||||
return None
|
||||
|
||||
def get_parameters(self, command_id: str, base_url: str
|
||||
) -> Union[dict, None]:
|
||||
if command_id in self._by_id:
|
||||
command = self._by_id[command_id]
|
||||
parameters_data = self._get_parameters_data(command.parameters,
|
||||
command.id, base_url)
|
||||
return parameters_data
|
||||
else:
|
||||
return None
|
||||
|
||||
def get_command_object(self, command_id: str) -> Command:
|
||||
if command_id in self._by_id:
|
||||
return self._by_id[command_id]
|
||||
return None
|
||||
|
||||
def _get_parameters_data(self, parameters: list, command_id: str,
|
||||
base_url: str) -> List[dict]:
|
||||
data = ResponseStructure(base_url)
|
||||
data.add_data(parameters)
|
||||
data.add_link("self", f"/commands/{command_id}/parameters")
|
||||
return data.get_dict()
|
||||
|
|
|
@ -14,7 +14,7 @@ from .users import get_user_by_username
|
|||
from calculate.utils.files import Process
|
||||
|
||||
|
||||
def make_secret_key():
|
||||
def make_secret_key() -> str:
|
||||
openssl_process = Process("/usr/bin/openssl", "rand", "-hex", "32")
|
||||
secret_key = openssl_process.read()
|
||||
return secret_key.strip()
|
||||
|
|
5
calculate/server/utils/daemon.py
Normal file
5
calculate/server/utils/daemon.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
from multiprocessing.connection import Listener
|
||||
|
||||
|
||||
def daemon(listener: Listener, wid: int, base_dir: str, command):
|
||||
pass
|
122
calculate/server/utils/responses.py
Normal file
122
calculate/server/utils/responses.py
Normal file
|
@ -0,0 +1,122 @@
|
|||
from fastapi import HTTPException, status
|
||||
from fastapi.encoders import jsonable_encoder
|
||||
|
||||
from starlette.responses import JSONResponse
|
||||
from starlette.requests import Request
|
||||
|
||||
from pydantic.main import ModelMetaclass
|
||||
from pydantic import parse_obj_as
|
||||
|
||||
from typing import Dict, Union, _GenericAlias, Optional
|
||||
|
||||
|
||||
class ResponseStructure:
|
||||
'''Класс структуры данных, для получения HATEOAS ответа на запросы
|
||||
клиента.'''
|
||||
def __init__(self, base_url: str):
|
||||
self._base_url: str = base_url.strip("/")
|
||||
|
||||
self._data: dict = dict()
|
||||
self._links: Dict[str, Dict[str, str]] = dict()
|
||||
self._embedded: Dict[str, dict] = dict()
|
||||
|
||||
def get_dict(self) -> dict:
|
||||
dictionary = dict()
|
||||
if self._data:
|
||||
dictionary["data"] = self._data
|
||||
if self._links:
|
||||
dictionary["_links"] = self._links
|
||||
if self._embedded:
|
||||
dictionary["_embedded"] = self._embedded
|
||||
return dictionary
|
||||
|
||||
def add_data(self, *args, **kwargs) -> "ResponseStructure":
|
||||
if args:
|
||||
if isinstance(self._data, dict):
|
||||
if len(args) > 1:
|
||||
self._data = list(args)
|
||||
else:
|
||||
self._data = args[0]
|
||||
elif isinstance(self._data, list):
|
||||
if len(args) > 1:
|
||||
self._data.extend(args)
|
||||
else:
|
||||
self._data.append(args[0])
|
||||
else:
|
||||
self._data = [self._data, *args]
|
||||
elif kwargs:
|
||||
if not isinstance(self._data, dict):
|
||||
self._data = {}
|
||||
self._data.update(kwargs)
|
||||
return self
|
||||
|
||||
def embed(self, resource: str,
|
||||
content: Union[dict, list]) -> "ResponseStructure":
|
||||
self._embedded[resource] = content
|
||||
return self
|
||||
|
||||
def add_link(self, action: str, uri: str,
|
||||
templated: bool = False) -> "ResponseStructure":
|
||||
uri = self._get_uri(uri)
|
||||
|
||||
if action in self._links:
|
||||
link_section = self._links[action]
|
||||
if link_section["href"] != uri:
|
||||
link_section["href"] = uri
|
||||
else:
|
||||
link_section = {"href": uri}
|
||||
self._links[action] = link_section
|
||||
|
||||
if templated:
|
||||
print("TEMPLATED:", action)
|
||||
link_section["templated"] = templated
|
||||
|
||||
return self
|
||||
|
||||
def _get_uri(self, uri: str) -> str:
|
||||
if uri.startswith(self._base_url):
|
||||
return uri
|
||||
return f"{self._base_url}/{uri.strip('/')}"
|
||||
|
||||
|
||||
def get_base_url(request: Request) -> str:
|
||||
base_url = f"{request.base_url.scheme}://{request.base_url.netloc}"
|
||||
return base_url
|
||||
|
||||
|
||||
def get_command_not_found(command_id: str) -> HTTPException:
|
||||
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=(f'Command with id "{command_id}"'
|
||||
' is not found.'))
|
||||
|
||||
|
||||
def get_cl_command_not_found(console_command: str) -> HTTPException:
|
||||
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=(f'Console command "{console_command}" is not'
|
||||
' found.'))
|
||||
|
||||
|
||||
def get_configuration_not_found(wid: int) -> HTTPException:
|
||||
return HTTPException(status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail=f"Configuration with id={wid} is not found.")
|
||||
|
||||
|
||||
def validate_response(data: Union[dict, list],
|
||||
schema: Union[ModelMetaclass, _GenericAlias, type],
|
||||
media_type: Optional[str] = None,
|
||||
status_code: int = 200
|
||||
) -> JSONResponse:
|
||||
"""Функция для валидации данных ответа сервера по указанной схеме с учетом
|
||||
наличия псевдонимов полей и возможности того, что схема задана обычным
|
||||
типом, типом из typing или BaseModel с __root__. Возвращает объект ответа.
|
||||
"""
|
||||
if isinstance(schema, ModelMetaclass):
|
||||
if "__root__" in schema.__annotations__:
|
||||
validated = schema(__root__=data)
|
||||
else:
|
||||
validated = schema(**data)
|
||||
elif isinstance(schema, (_GenericAlias, type)):
|
||||
validated = parse_obj_as(schema, data)
|
||||
|
||||
return JSONResponse(content=jsonable_encoder(validated, by_alias=True),
|
||||
media_type=media_type, status_code=status_code)
|
|
@ -1,30 +1,31 @@
|
|||
from fastapi import HTTPException, status
|
||||
from sqlalchemy import func, and_
|
||||
from typing import List
|
||||
|
||||
from ..models.database import database
|
||||
from ..models.users import users_table, users_rights, rights_table
|
||||
from ..database.db import database
|
||||
from ..database.users import users_table as ut, users_rights, rights_table
|
||||
|
||||
from ..schemas.users import UserCreate
|
||||
from ..schemas.users import UserCreate, User
|
||||
|
||||
|
||||
async def get_user_by_username(username: str):
|
||||
'''Метод для получения строки с данными пользователя из базы данных по
|
||||
username.'''
|
||||
query = users_table.select().where(users_table.c.login == username)
|
||||
query = ut.select().where(ut.c.login == username)
|
||||
user_data = await database.fetch_one(query)
|
||||
query = (users_table.
|
||||
query = (ut.
|
||||
join(users_rights).
|
||||
join(rights_table).
|
||||
select().
|
||||
where(and_(users_table.c.id == users_rights.c.user_id,
|
||||
users_table.c.login == username,
|
||||
where(and_(ut.c.id == users_rights.c.user_id,
|
||||
ut.c.login == username,
|
||||
rights_table.c.id == users_rights.c.right_id)).
|
||||
with_only_columns([users_table.c.id,
|
||||
users_table.c.login,
|
||||
users_table.c.password,
|
||||
with_only_columns([ut.c.id,
|
||||
ut.c.login,
|
||||
ut.c.password,
|
||||
func.group_concat(rights_table.c.name,
|
||||
' ').label("rights")]).
|
||||
group_by(users_table.c.id))
|
||||
group_by(ut.c.id))
|
||||
response = await database.fetch_one(query)
|
||||
|
||||
user_data = dict(response)
|
||||
|
@ -33,6 +34,16 @@ async def get_user_by_username(username: str):
|
|||
|
||||
|
||||
async def create_user(username: str, hashed_password: str, rights: List[str]):
|
||||
user = UserCreate(login=username,
|
||||
password=hashed_password,
|
||||
rights=rights)
|
||||
UserCreate(login=username,
|
||||
password=hashed_password,
|
||||
rights=rights)
|
||||
|
||||
|
||||
def check_user_rights(user_data: User, *rights: List[str]) -> None:
|
||||
user_rights = user_data.rights
|
||||
for right in user_rights:
|
||||
if right not in user_rights:
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail='Not enough permissions',
|
||||
headers={"WWW-Authenticate": "Bearer"}
|
||||
)
|
||||
|
|
|
@ -1,12 +1,415 @@
|
|||
import os
|
||||
import sys
|
||||
import json
|
||||
import socket
|
||||
import struct
|
||||
import signal
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import Union
|
||||
from calculate.variables.loader import Datavars
|
||||
from multiprocessing import Queue, Process
|
||||
from calculate.commands.commands import CommandRunner, Command
|
||||
# from time import sleep
|
||||
from multiprocessing import Process
|
||||
from multiprocessing.connection import Listener, Connection
|
||||
# import logging
|
||||
# import time
|
||||
from .daemon import daemon
|
||||
# from commands import Command
|
||||
|
||||
from typing import Dict, Optional, List, Union, Tuple, Literal
|
||||
|
||||
from asyncio.unix_events import _UnixSelectorEventLoop
|
||||
from asyncio import get_event_loop
|
||||
|
||||
|
||||
LOG_LEVELS = {logging.ERROR: "ERROR",
|
||||
logging.INFO: "INFO",
|
||||
logging.WARNING: "WARNING",
|
||||
logging.DEBUG: "DEBUG",
|
||||
logging.CRITICAL: "CRITICAL",
|
||||
}
|
||||
|
||||
|
||||
class WorkerException(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class UnexpectedWorkerFinish(WorkerException):
|
||||
def __init__(self, reason: List[str], output: Optional[List[str]] = []):
|
||||
"""Исключение кидаемое в случае, если воркер неожиданно умер."""
|
||||
self.reason: List[str] = reason
|
||||
self.output: List[dict] = output
|
||||
|
||||
|
||||
class UnexpectedWorkerState(WorkerException):
|
||||
"""Исключение кидаемое в случае, если состояние воркера не соответствует
|
||||
ожидаемому."""
|
||||
def __init__(self, state: str, expected: str):
|
||||
self.state: str = state
|
||||
self.expected: str = expected
|
||||
|
||||
|
||||
class DaemonIsDead(WorkerException):
|
||||
"""Исключение кидаемое в случае, если демон-воркер неожиданно оказался
|
||||
мертв."""
|
||||
def __init__(self, info: str, output: Optional[List[str]] = []):
|
||||
self.info: str = info
|
||||
self.output: List[dict] = output
|
||||
|
||||
|
||||
class IncorrectConfiguration(WorkerException):
|
||||
"""Исключение кидаемое при попытке запустить выполнение конфигурации, в
|
||||
которой есть ошибки."""
|
||||
def __init__(self, errors: Dict[str, str]):
|
||||
self._errors = errors
|
||||
|
||||
|
||||
class WorkersManager:
|
||||
def __init__(self, loop: Optional[_UnixSelectorEventLoop] = None):
|
||||
if loop is None:
|
||||
self._loop = get_event_loop()
|
||||
else:
|
||||
self._loop = loop
|
||||
|
||||
self._configs: Dict[int, WorkerProtocol] = {}
|
||||
self._execs: Dict[int, WorkerProtocol] = {}
|
||||
self._wids: List[int] = []
|
||||
|
||||
async def make_worker(self, command) -> Tuple[Union[int, None],
|
||||
Union[str, None]]:
|
||||
"""Метод для создания воркеров."""
|
||||
wid = self._get_wid()
|
||||
try:
|
||||
# Создаем воркера и получаем интерфейс для взаимодействия с ним.
|
||||
worker_api = await self._create_worker(wid, command)
|
||||
self._configs[wid] = worker_api
|
||||
except Exception as error:
|
||||
if wid in self._configs:
|
||||
self._configs.pop(wid)
|
||||
return None, str(error)
|
||||
return wid, None
|
||||
|
||||
async def configure_worker(self, wid: int, parameters: List[dict],
|
||||
reset: bool = False) -> Union[dict, None]:
|
||||
"""Метод для модификации или сброса параметров конфигурации указанными
|
||||
значениями.
|
||||
"""
|
||||
if not parameters:
|
||||
return {}
|
||||
if wid not in self._configs:
|
||||
return None
|
||||
|
||||
worker_api: WorkerProtocol = self._configs[wid]
|
||||
try:
|
||||
result = await worker_api.configure(parameters)
|
||||
except (UnexpectedWorkerFinish, DaemonIsDead):
|
||||
worker_api.kill()
|
||||
self._configs.pop(wid)
|
||||
raise
|
||||
|
||||
return result
|
||||
|
||||
async def cancel_worker_configuration(self, wid: int):
|
||||
"""Метод для завершения конфигурации воркера и завершения его работы.
|
||||
"""
|
||||
if wid not in self._configs:
|
||||
return None
|
||||
|
||||
worker_api: WorkerProtocol = self._configs[wid]
|
||||
output = await worker_api.cancel_configuration()
|
||||
worker_api.delete_socket()
|
||||
self._configs.pop(wid)
|
||||
|
||||
return output
|
||||
|
||||
def clean(self):
|
||||
"""Метод для убийства всех работающих на данный момент демонов."""
|
||||
# TODO доработать его логику так, чтобы при перезапуске сервера этот
|
||||
# метод никого не убивал.
|
||||
for wid, worker in {**self._configs, **self._execs}.items():
|
||||
result = False
|
||||
try:
|
||||
result = worker.kill()
|
||||
finally:
|
||||
if result:
|
||||
print(f"[*] Worker {wid} is killed.")
|
||||
else:
|
||||
print(f"[x] Worker {wid} is not killed.")
|
||||
continue
|
||||
|
||||
def get_config(self, wid: int):
|
||||
if wid in self._configs:
|
||||
return self._configs[wid]
|
||||
return None
|
||||
|
||||
def get_execution(self, wid: int):
|
||||
if wid in self._execs:
|
||||
return self._execs[wid]
|
||||
return None
|
||||
|
||||
def run_execution(self, wid: int) -> Tuple[int, Union[dict, None]]:
|
||||
'''Метод для запуска исполнения команды, используя указанную
|
||||
конфигурацию.
|
||||
Вывод:
|
||||
0, None -- исполнение успешно запущено;
|
||||
1, None -- конфигурация с данным wid не найдена;
|
||||
2, errors -- конфигурация с данным wid содержит ошибки;
|
||||
3, None -- исполнение с данным wid уже существует;
|
||||
'''
|
||||
if wid not in self._configs:
|
||||
return 1, None
|
||||
if self._configs[wid].status > 0:
|
||||
# Берем ошибки, как-то.
|
||||
errors = {}
|
||||
return 2, errors
|
||||
if wid in self._execs:
|
||||
return 3, None
|
||||
|
||||
# worker = self._configs.pop(wid)
|
||||
return 0, None
|
||||
|
||||
async def _create_worker(self, wid: int, command) -> "WorkerProtocol":
|
||||
# Путь к временному файлу, в котором будет лежать pid демона-рабочего.
|
||||
pid_file_path: str = os.path.join(os.getcwd(), f"pid_file_{wid}")
|
||||
socket_path: str = os.path.join(os.getcwd(),
|
||||
f"worker_{wid}.sock")
|
||||
if os.path.exists(socket_path):
|
||||
os.remove(socket_path)
|
||||
|
||||
process_runner = Process(target=daemonize_worker,
|
||||
args=(socket_path, wid, pid_file_path,
|
||||
command)
|
||||
)
|
||||
process_runner.start()
|
||||
process_runner.join()
|
||||
|
||||
daemon_pid = await self._get_daemon_pid(pid_file_path)
|
||||
self._delete_pid_file(pid_file_path)
|
||||
|
||||
transport, worker_interface = await self._loop.create_unix_connection(
|
||||
lambda: WorkerProtocol(daemon_pid,
|
||||
socket_path),
|
||||
socket_path)
|
||||
return worker_interface
|
||||
|
||||
def _get_wid(self):
|
||||
if not self._wids:
|
||||
worker_id = 0
|
||||
else:
|
||||
worker_id = self._wids[-1] + 1
|
||||
self._wids.append(worker_id)
|
||||
return worker_id
|
||||
|
||||
async def _get_daemon_pid(self, pid_file_path: str) -> int:
|
||||
while True:
|
||||
if not os.path.exists(pid_file_path):
|
||||
# Если файла нет -- уступаем работу другим воркерам.
|
||||
await asyncio.sleep(0)
|
||||
with open(pid_file_path, "r") as pid_file:
|
||||
daemon_pid = int(pid_file.readline())
|
||||
return daemon_pid
|
||||
|
||||
def _delete_pid_file(self, pid_file_path: str) -> None:
|
||||
try:
|
||||
os.remove(pid_file_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
configs = ["Configurations:"]
|
||||
configs.extend([f"wid={wid}" for wid in self._configs.keys()])
|
||||
configs_repr = "\n\t".join(configs)
|
||||
|
||||
execs = ["Executions:"]
|
||||
execs.extend([f"wid={wid}" for wid in self._execs.keys()])
|
||||
execs_repr = "\n\t".join(execs)
|
||||
|
||||
return f"{configs_repr}\n{execs_repr}"
|
||||
|
||||
|
||||
def daemonize_worker(socket_path: str, wid: int, pid_file: str,
|
||||
command) -> None:
|
||||
'''Функция запускаемая в отдельном процессе и порождающая демона при помощи
|
||||
магии двойного форка.'''
|
||||
base_dir = os.getcwd()
|
||||
|
||||
new_fork()
|
||||
|
||||
os.chdir("/")
|
||||
os.setsid()
|
||||
os.umask(0)
|
||||
|
||||
new_fork()
|
||||
|
||||
# Создаем файл, с помощью которого будем передавать pid демона-рабочего.
|
||||
daemon_pid = str(os.getpid())
|
||||
with open(pid_file, "w") as pid_file:
|
||||
pid_file.write(daemon_pid)
|
||||
|
||||
with Listener(socket_path) as listener:
|
||||
daemon(listener, wid, base_dir, command)
|
||||
|
||||
|
||||
def new_fork() -> None:
|
||||
'''Функция для создания форка текущего процесса.'''
|
||||
try:
|
||||
middle_pid = os.fork()
|
||||
if middle_pid > 0:
|
||||
sys.exit(0)
|
||||
except OSError as error:
|
||||
print(f"FORK ERROR: {error.errno} - {str(error)}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
class WorkerProtocol(asyncio.Protocol):
|
||||
"""Класс протокола управления воркером."""
|
||||
def __init__(self, pid: int, socket_path: str):
|
||||
self.loop = asyncio.get_running_loop()
|
||||
|
||||
self._pid: int = pid
|
||||
self._socket_path: str = socket_path
|
||||
|
||||
# Объект Event, предназначенный для остановки записи при переполнении
|
||||
# буффера записи в трaнспорте.
|
||||
self._can_write = asyncio.Event()
|
||||
self._can_write.clear()
|
||||
|
||||
self._msg_queue = asyncio.Queue()
|
||||
|
||||
# Контекст чтения данных воркера.
|
||||
self._buffer = bytearray()
|
||||
self._msg_size: int = 0
|
||||
|
||||
self._connection_closed: bool = True
|
||||
|
||||
async def configure(self, parameters: List[dict],
|
||||
reset: bool = False) -> list:
|
||||
"""Метод для конфигурации воркера указанными значениями параметров.
|
||||
Возвращает список ошибок, если таковые есть, или пустой список, если
|
||||
никаких ошибок нет.
|
||||
"""
|
||||
message = self._make_worker_message("config", int(reset), parameters)
|
||||
await self._write(message)
|
||||
response = await self._read()
|
||||
response = json.loads(response)
|
||||
|
||||
return response["data"]
|
||||
|
||||
async def cancel_configuration(self) -> List[dict]:
|
||||
"""Метод для остановки конфигурации воркера и его работы в целом."""
|
||||
stop_message: bytes = self._make_worker_message("finish", 1, None)
|
||||
await self._write(stop_message)
|
||||
response = await self._read()
|
||||
response = json.loads(response)
|
||||
|
||||
return response["data"]
|
||||
|
||||
def connection_made(self, transport: asyncio.Transport) -> None:
|
||||
self.transport = transport
|
||||
self._can_write.set()
|
||||
self._connection_closed: bool = False
|
||||
print("Connection made.")
|
||||
|
||||
def connection_lost(self, exc: BaseException) -> None:
|
||||
if exc:
|
||||
print("Connection lost. Error:", str(exc))
|
||||
else:
|
||||
print("Connection lost.")
|
||||
self._connection_closed: bool = True
|
||||
|
||||
def data_received(self, data: bytes) -> None:
|
||||
self._buffer.extend(data)
|
||||
|
||||
while True:
|
||||
if not self._msg_size:
|
||||
if len(self._buffer) < 4:
|
||||
return
|
||||
size_length = struct.calcsize(">i")
|
||||
self._msg_size, = struct.unpack(">i",
|
||||
self._buffer[:size_length])
|
||||
self._buffer = self._buffer[size_length:]
|
||||
|
||||
if len(self._buffer) >= self._msg_size:
|
||||
data = self._buffer[:self._msg_size]
|
||||
self._buffer = self._buffer[self._msg_size:]
|
||||
self._msg_size = 0
|
||||
|
||||
self._msg_queue.put_nowait(data.decode())
|
||||
else:
|
||||
break
|
||||
|
||||
def pause_writing(self) -> None:
|
||||
self._can_write.clear()
|
||||
|
||||
def resume_writing(self) -> None:
|
||||
self._can_write.set()
|
||||
|
||||
async def _read(self) -> str:
|
||||
if self._connection_closed and self._msg_queue.empty():
|
||||
return None
|
||||
return await self._msg_queue.get()
|
||||
|
||||
async def read_all(self) -> List[str]:
|
||||
if self._msg_queue.empty():
|
||||
if self._connection_closed:
|
||||
return None
|
||||
else:
|
||||
return await self._msg_queue.get()
|
||||
output = []
|
||||
while not self._msg_queue.empty():
|
||||
output.append(self._msg_queue.get_nowait())
|
||||
return output
|
||||
|
||||
async def _write(self, data: Union[str, bytes]) -> None:
|
||||
await self._can_write.wait()
|
||||
if isinstance(data, str):
|
||||
data = data.encode()
|
||||
self.transport.write(struct.pack(">i", len(data)))
|
||||
self.transport.write(data)
|
||||
|
||||
def _make_worker_message(
|
||||
self,
|
||||
state: Literal["config", "finish", "exec", "input"],
|
||||
status: int, data: Union[list, dict]) -> dict:
|
||||
return json.dumps({"state": state,
|
||||
"status": status,
|
||||
"data": data}).encode()
|
||||
|
||||
def delete_socket(self) -> None:
|
||||
"""Метод для удаления сокета."""
|
||||
try:
|
||||
os.remove(self._socket_path)
|
||||
self.transport.close()
|
||||
self._connection_closed = True
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def kill(self) -> bool:
|
||||
"""Метод для убийства демона."""
|
||||
print("GONNA KILLA DAEMON:", self._pid)
|
||||
if self.is_daemon_alive():
|
||||
try:
|
||||
os.kill(self._pid, signal.SIGKILL)
|
||||
except OSError:
|
||||
return False
|
||||
else:
|
||||
print("ALREADY DEAD")
|
||||
return True
|
||||
|
||||
def stop(self) -> bool:
|
||||
print("STOP DAEMON:", self._daemon_pid)
|
||||
if self.is_daemon_alive():
|
||||
try:
|
||||
# Для остановки демона посылаем в него KeyboardInterrupt.
|
||||
os.kill(self._pid, signal.SIGINT)
|
||||
except OSError as error:
|
||||
print(f"ERROR: Failed stopping daemon PID={self._pid}. "
|
||||
f"Reason: {str(error)}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_daemon_alive(self) -> bool:
|
||||
try:
|
||||
os.kill(self._pid, 0)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
class WorkerIOError(KeyboardInterrupt):
|
||||
|
@ -15,24 +418,24 @@ class WorkerIOError(KeyboardInterrupt):
|
|||
|
||||
class IOModule:
|
||||
'''Класс модуля ввода/вывода для воркеров.'''
|
||||
def __init__(self, socket_path: str):
|
||||
self._sock = socket.socket(socket.AF_UNIX,
|
||||
socket.SOCK_STREAM)
|
||||
self._sock.bind(socket_path)
|
||||
self._sock.listen()
|
||||
self._connection = None
|
||||
def __init__(self, listener: Listener, command_name: str):
|
||||
self._listener: Listener = listener
|
||||
self._connection: Union[Connection, None] = None
|
||||
|
||||
def input(self, msg: str) -> str:
|
||||
'''Метод через который возможен ввод данных в скрипт.'''
|
||||
input_request = json.dumps({"type": "input",
|
||||
"msg": msg}) + '\0'
|
||||
answer = None
|
||||
while answer is None:
|
||||
if not self._check_connection(self._connection):
|
||||
self._connection = self._make_connection()
|
||||
self._connection.sendall(input_request.encode())
|
||||
answer = self._get()
|
||||
return answer['data']
|
||||
self._command: str = command_name
|
||||
self._script: str = ""
|
||||
|
||||
@property
|
||||
def command(self) -> str:
|
||||
return self._command
|
||||
|
||||
@property
|
||||
def script(self) -> str:
|
||||
return self._script
|
||||
|
||||
@script.setter
|
||||
def script(self, script: str) -> None:
|
||||
self._script = script
|
||||
|
||||
def set_debug(self, msg: str) -> None:
|
||||
self.output(msg, level=logging.DEBUG)
|
||||
|
@ -49,142 +452,57 @@ class IOModule:
|
|||
def set_critical(self, msg: str) -> None:
|
||||
self.output(msg, level=logging.CRITICAL)
|
||||
|
||||
def output(self, msg: str, level: int = logging.INFO) -> None:
|
||||
def output(self, text: str, level: int = logging.INFO) -> None:
|
||||
'''Метод для отправки серверу вывода с указанием уровня.'''
|
||||
if not self._check_connection(self._connection):
|
||||
self._connection = self._make_connection()
|
||||
output_request = json.dumps({"type": "output",
|
||||
"level": level,
|
||||
"msg": msg}) + '\0'
|
||||
self._connection.sendall(output_request.encode())
|
||||
output_request = {"state": "exec",
|
||||
"status": 0,
|
||||
"data": {
|
||||
"type": "message",
|
||||
"logging": level,
|
||||
"text": text,
|
||||
"source": self._script
|
||||
}
|
||||
}
|
||||
self.send(output_request)
|
||||
|
||||
def input(self, msg: str) -> str:
|
||||
'''Метод через который возможен ввод данных в скрипт.'''
|
||||
input_request = {"state": "input",
|
||||
"status": 0,
|
||||
"data": {"text": msg,
|
||||
"source": f"{self.command}:{self.script}"}}
|
||||
|
||||
answer = None
|
||||
while answer is None:
|
||||
self.send(input_request)
|
||||
answer = self.receive()
|
||||
|
||||
return answer['data']
|
||||
|
||||
def send(self, data: dict) -> None:
|
||||
'''Метод для отправки данных серверу.'''
|
||||
if not self._check_connection(self._connection):
|
||||
self._connection = self._make_connection()
|
||||
data = json.dumps(data) + '\0'
|
||||
self._connection.sendall(data.encode())
|
||||
if self._connection is None:
|
||||
raise WorkerIOError("No one is connected now.")
|
||||
data = json.dumps(data).encode()
|
||||
self._connection.send_bytes(data)
|
||||
|
||||
def receive(self) -> dict:
|
||||
'''Метод для получения данных от сервера.'''
|
||||
data = None
|
||||
while data is None:
|
||||
if not self._check_connection(self._connection):
|
||||
self._connection = self._make_connection()
|
||||
data = self._get()
|
||||
return data
|
||||
if self._connection is None:
|
||||
raise WorkerIOError("No one is connected now.")
|
||||
data: bytes = self._connection.recv_bytes()
|
||||
return json.loads(data.decode())
|
||||
|
||||
def _get(self) -> Union[None, dict]:
|
||||
'''Метод для считывания данных, возможно, поделенных на кадры.'''
|
||||
try:
|
||||
data = b''
|
||||
while True:
|
||||
chunk = self._connection.recv(1024)
|
||||
if not chunk:
|
||||
return None
|
||||
def __enter__(self):
|
||||
self._connection = self._listener.accept()
|
||||
return self
|
||||
|
||||
data += chunk
|
||||
if not data.endswith(b'\0'):
|
||||
if b'\0' in data:
|
||||
# Если после символа конца сообщения есть еще какие-то
|
||||
# данные -- считаем их наличие ошибкой.
|
||||
raise WorkerIOError("Unexpected message.")
|
||||
continue
|
||||
return json.loads(data[:-1].decode())
|
||||
except ConnectionResetError:
|
||||
return None
|
||||
|
||||
def _make_connection(self) -> socket.socket:
|
||||
'''Метод для создания подключения.'''
|
||||
connection, parent_address = self._sock.accept()
|
||||
return connection
|
||||
|
||||
def _check_connection(self, connection: socket.socket) -> bool:
|
||||
'''Метод для проверки соединения путем отправки на сокет пустого
|
||||
сообщения.'''
|
||||
if connection is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
connection.sendall(b'')
|
||||
return True
|
||||
except BrokenPipeError:
|
||||
return False
|
||||
|
||||
def __del__(self) -> None:
|
||||
if self._connection is not None:
|
||||
def __exit__(self, exc_type, exc_value, exc_traceback):
|
||||
if exc_type:
|
||||
print("exc_type =", exc_type)
|
||||
print("exc_value =", exc_value)
|
||||
print("exc_traceback =", exc_traceback)
|
||||
if exc_type is KeyboardInterrupt:
|
||||
print("correctly")
|
||||
if not self._connection.closed:
|
||||
self._connection.close()
|
||||
self._sock.close()
|
||||
|
||||
|
||||
class Worker:
|
||||
def __init__(self, wid: int, command: Command, datavars: Datavars):
|
||||
self._wid: int = wid
|
||||
self._datavars: Datavars = datavars
|
||||
|
||||
self._socket_path: str = f"./worker_{self._wid}.sock"
|
||||
if os.path.exists(self._socket_path):
|
||||
os.remove(self._socket_path)
|
||||
self._input_queue: list = []
|
||||
|
||||
def run(self):
|
||||
io = IOModule(self._socket_path)
|
||||
title = io.receive()
|
||||
print(title['msg'])
|
||||
while True:
|
||||
msg = "What is your desire?"
|
||||
print(">", msg)
|
||||
answer = io.input(msg)
|
||||
print(f'> {answer}')
|
||||
if answer == "stop":
|
||||
break
|
||||
elif answer == "output":
|
||||
msg = "What kind of output you wanna see?"
|
||||
print(">", msg)
|
||||
answer = io.input(msg)
|
||||
io.set_info(answer)
|
||||
else:
|
||||
msg = "I am sorry, but I could not help you("
|
||||
print(">", msg)
|
||||
io.output(msg)
|
||||
print('STOPPED')
|
||||
|
||||
def initialize(self, io: IOModule):
|
||||
pass
|
||||
|
||||
def run_command(self):
|
||||
pass
|
||||
|
||||
|
||||
class DeprecatedWorker:
|
||||
def __init__(self, wid, loop, sockets_path: str = 'calculate/server/'):
|
||||
self._wid = wid
|
||||
self._event_loop = loop
|
||||
|
||||
# Создаем сокет для взаимодействия воркера и сервера.
|
||||
socket_path = os.path.join(sockets_path, f'worker_{self._wid}')
|
||||
if os.path.exists(socket_path):
|
||||
os.remove(socket_path)
|
||||
self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
||||
self._socket.bind(socket_path)
|
||||
|
||||
@staticmethod
|
||||
def _get_output(output_queue: Queue) -> dict:
|
||||
return output_queue.get()
|
||||
|
||||
@staticmethod
|
||||
def _main_loop(command, in_queue: Queue, out_queue: Queue):
|
||||
data = in_queue.get()
|
||||
print('\nworker for command:', command)
|
||||
output = {"type": "log",
|
||||
"level": "INFO",
|
||||
"msg": f"recieved message {data['text']}"}
|
||||
out_queue.put(output)
|
||||
|
||||
def run(self, command):
|
||||
'''Метод для запуска процесса воркера с заданным '''
|
||||
worker_process = Process(target=self._main_loop,
|
||||
args=(command,
|
||||
self._in_queue,
|
||||
self._output_queue))
|
||||
worker_process.start()
|
||||
|
|
|
@ -6,7 +6,7 @@ from ...utils.images import ImageMagick, ImageMagickError
|
|||
from ...variables.loader import Datavars
|
||||
from .base_format import Format, FormatError
|
||||
|
||||
from typing import Union, List, Tuple, NoReturn
|
||||
from typing import Union, List, Tuple
|
||||
import hashlib
|
||||
import re
|
||||
import os
|
||||
|
@ -296,7 +296,7 @@ class BackgroundsFormat(Format):
|
|||
return md5_object.hexdigest()
|
||||
|
||||
def _create_md5sum_file(self, target_path: str, action_md5sum: str
|
||||
) -> NoReturn:
|
||||
) -> None:
|
||||
"""Метод для создания файла с md5-суммой действия, выполненного
|
||||
данным шаблоном."""
|
||||
if not self._empty_name:
|
||||
|
|
|
@ -23,6 +23,8 @@ class Format:
|
|||
CALCULATE_VERSION: Union[str, None] = None
|
||||
SHEBANG_PATTERN: str = r"^(?P<shebang>#!\s*[\w\d\/]+\n)"
|
||||
|
||||
comment_symbol = None
|
||||
|
||||
def __init__(self, processing_methods: List[Callable]):
|
||||
self._processing_methods: List[Callable] = processing_methods
|
||||
self._document_dictionary: OrderedDict = OrderedDict()
|
||||
|
|
|
@ -1,13 +1,27 @@
|
|||
# vim: fileencoding=utf-8
|
||||
#
|
||||
# ToDo: добавить проверку того, полностью ли парсился документ. Если отпарсился
|
||||
# TODO: добавить проверку того, полностью ли парсился документ. Если отпарсился
|
||||
# не весь файл -- выдаем ошибку.
|
||||
#
|
||||
from .base_format import Format
|
||||
from collections import OrderedDict
|
||||
from pyparsing import originalTextFor, OneOrMore, Word, alphanums, Literal,\
|
||||
ZeroOrMore, Forward, Optional, Group, restOfLine,\
|
||||
cppStyleComment, Keyword, printables, nums, SkipTo
|
||||
from pyparsing import (
|
||||
originalTextFor,
|
||||
OneOrMore,
|
||||
Word,
|
||||
alphanums,
|
||||
Literal,
|
||||
ZeroOrMore,
|
||||
Forward,
|
||||
Optional,
|
||||
Group,
|
||||
restOfLine,
|
||||
cppStyleComment,
|
||||
Keyword,
|
||||
printables,
|
||||
nums,
|
||||
SkipTo
|
||||
)
|
||||
|
||||
|
||||
class BINDFormat(Format):
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# vim: fileencoding=utf-8
|
||||
#
|
||||
# ToDo: написать счетчик скобок для финальной оценки корректности
|
||||
# TODO: написать счетчик скобок для финальной оценки корректности
|
||||
# документа.
|
||||
#
|
||||
from .base_format import Format
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
# vim: fileencoding=utf-8
|
||||
#
|
||||
from .base_format import Format
|
||||
from ..template_engine import ParametersContainer
|
||||
from collections import OrderedDict
|
||||
from pyparsing import originalTextFor, Literal, ZeroOrMore, Word, printables,\
|
||||
OneOrMore, alphanums, ParseException, restOfLine,\
|
||||
Group, Optional, Regex
|
||||
from pyparsing import (
|
||||
originalTextFor,
|
||||
Literal,
|
||||
ZeroOrMore,
|
||||
Word,
|
||||
printables,
|
||||
OneOrMore,
|
||||
alphanums,
|
||||
ParseException,
|
||||
restOfLine,
|
||||
Group,
|
||||
Optional
|
||||
)
|
||||
|
||||
|
||||
class KDEFormat(Format):
|
||||
|
|
|
@ -1,11 +1,18 @@
|
|||
# vim: fileencoding=utf-8
|
||||
#
|
||||
from .base_format import Format
|
||||
from ..template_engine import ParametersContainer
|
||||
from collections import OrderedDict
|
||||
from pyparsing import Word, Literal, alphanums, originalTextFor,\
|
||||
OneOrMore, ParseException, restOfLine, Group, Optional,\
|
||||
Regex
|
||||
from pyparsing import (
|
||||
Word,
|
||||
Literal,
|
||||
alphanums,
|
||||
originalTextFor,
|
||||
ParseException,
|
||||
restOfLine,
|
||||
Group,
|
||||
Optional,
|
||||
Regex
|
||||
)
|
||||
|
||||
|
||||
class KernelFormat(Format):
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
#
|
||||
from .base_format import Format, FormatError
|
||||
from calculate.utils.files import Process
|
||||
from typing import NoReturn
|
||||
from os import path
|
||||
|
||||
|
||||
|
@ -25,7 +24,7 @@ class PatchFormat(Format):
|
|||
# Предупреждения.
|
||||
self._warnings: list = []
|
||||
|
||||
def _init_command(self) -> NoReturn:
|
||||
def _init_command(self) -> None:
|
||||
self._patch_cmd = "/usr/bin/patch"
|
||||
if not path.exists(self._patch_cmd):
|
||||
self._patch_cmd = None
|
||||
|
|
|
@ -2,9 +2,19 @@
|
|||
#
|
||||
from .base_format import Format
|
||||
from collections import OrderedDict
|
||||
from pyparsing import Word, Literal, alphanums, printables, originalTextFor,\
|
||||
OneOrMore, ParseException, restOfLine, Group, Optional,\
|
||||
Regex
|
||||
from pyparsing import (
|
||||
Word,
|
||||
Literal,
|
||||
alphanums,
|
||||
printables,
|
||||
originalTextFor,
|
||||
OneOrMore,
|
||||
ParseException,
|
||||
restOfLine,
|
||||
Group,
|
||||
Optional,
|
||||
Regex
|
||||
)
|
||||
|
||||
|
||||
class ProcmailFormat(Format):
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# vim: fileencoding=utf-8
|
||||
#
|
||||
from .base_format import Format
|
||||
from ..template_engine import ParametersContainer
|
||||
from collections import OrderedDict
|
||||
from pyparsing import originalTextFor, Literal, Word, printables, OneOrMore,\
|
||||
alphanums, ParseException, Optional, Group, restOfLine
|
||||
|
@ -26,7 +25,6 @@ class SambaFormat(Format):
|
|||
join_before=False,
|
||||
add_header=False,
|
||||
already_changed=False,
|
||||
parameters=ParametersContainer(),
|
||||
**kwargs):
|
||||
processing_methods = [self._parse_comment_line,
|
||||
self._parse_section_line,
|
||||
|
|
|
@ -4,17 +4,19 @@ from jinja2.ext import Extension
|
|||
from jinja2.lexer import Token
|
||||
from jinja2.parser import Parser
|
||||
from jinja2 import (
|
||||
nodes,
|
||||
Template,
|
||||
Environment,
|
||||
contextfunction,
|
||||
FileSystemLoader,
|
||||
TemplateSyntaxError,
|
||||
nodes,
|
||||
contextfunction,
|
||||
Template,
|
||||
)
|
||||
from jinja2.utils import missing
|
||||
from jinja2.runtime import Context, Undefined
|
||||
|
||||
from collections.abc import MutableMapping
|
||||
from collections import OrderedDict
|
||||
|
||||
from importlib import import_module
|
||||
from pprint import pprint
|
||||
import copy
|
||||
|
@ -22,37 +24,38 @@ import re
|
|||
import os
|
||||
import stat
|
||||
from typing import (
|
||||
Union,
|
||||
Any,
|
||||
List,
|
||||
Tuple,
|
||||
NoReturn,
|
||||
Optional,
|
||||
Union,
|
||||
List,
|
||||
Any,
|
||||
Dict,
|
||||
Tuple,
|
||||
Iterator,
|
||||
)
|
||||
|
||||
from ..utils.package import (
|
||||
PackageAtomName,
|
||||
PackageAtomParser,
|
||||
PackageAtomError,
|
||||
Package,
|
||||
PackageAtomName,
|
||||
NOTEXIST,
|
||||
Version
|
||||
Package,
|
||||
Version,
|
||||
)
|
||||
from ..utils.files import (
|
||||
join_paths,
|
||||
check_directory_link,
|
||||
check_command,
|
||||
FilesError
|
||||
join_paths,
|
||||
FilesError,
|
||||
)
|
||||
from calculate.variables.datavars import (
|
||||
VariableNotFoundError,
|
||||
HashType,
|
||||
NamespaceNode,
|
||||
VariableNode,
|
||||
IniType,
|
||||
IntegerType,
|
||||
FloatType,
|
||||
ListType
|
||||
ListType,
|
||||
HashType,
|
||||
IniType,
|
||||
)
|
||||
from calculate.utils.fs import readFile
|
||||
from calculate.variables.loader import Datavars
|
||||
|
@ -116,10 +119,10 @@ class Variables(MutableMapping):
|
|||
def __getitem__(self, name: str) -> Any:
|
||||
return self.__attrs[name]
|
||||
|
||||
def __setitem__(self, name: str, value: Any) -> NoReturn:
|
||||
def __setitem__(self, name: str, value: Any) -> None:
|
||||
self.__attrs[name] = value
|
||||
|
||||
def __delitem__(self, name: str) -> NoReturn:
|
||||
def __delitem__(self, name: str) -> None:
|
||||
del self.__attrs[name]
|
||||
|
||||
def __iter__(self) -> Iterator:
|
||||
|
@ -254,7 +257,7 @@ class ParametersProcessor:
|
|||
|
||||
def set_parameters_container(self,
|
||||
parameters_container: "ParametersContainer"
|
||||
) -> NoReturn:
|
||||
) -> None:
|
||||
'''Метод для установки текущего контейнера параметров.'''
|
||||
self._parameters_container = parameters_container
|
||||
self._added_parameters = set()
|
||||
|
@ -264,7 +267,7 @@ class ParametersProcessor:
|
|||
return self._for_package
|
||||
|
||||
@for_package.setter
|
||||
def for_package(self, package: Package) -> NoReturn:
|
||||
def for_package(self, package: Package) -> None:
|
||||
self._for_package = package
|
||||
|
||||
def __getattr__(self, parameter_name: str) -> Any:
|
||||
|
@ -278,7 +281,7 @@ class ParametersProcessor:
|
|||
|
||||
def check_template_parameter(self, parameter_name: str,
|
||||
parameter_value: Any,
|
||||
template_type: int, lineno: int) -> NoReturn:
|
||||
template_type: int, lineno: int) -> None:
|
||||
'''Метод, проверяющий указанный параметр.'''
|
||||
self.lineno = lineno
|
||||
self.template_type = template_type
|
||||
|
@ -313,7 +316,7 @@ class ParametersProcessor:
|
|||
self._parameters_container.set_parameter({parameter_name:
|
||||
checked_value})
|
||||
|
||||
def check_postparse_parameters(self) -> NoReturn:
|
||||
def check_postparse_parameters(self) -> None:
|
||||
'''Метод, запускающий проверку параметров после их разбора.'''
|
||||
for parameter, parameter_checker in\
|
||||
self.postparse_checkers_list.items():
|
||||
|
@ -326,7 +329,7 @@ class ParametersProcessor:
|
|||
result)
|
||||
|
||||
def check_template_parameters(self, parameters: dict,
|
||||
template_type: int, lineno: int) -> NoReturn:
|
||||
template_type: int, lineno: int) -> None:
|
||||
'''Метод, запускающий проверку указанных параметров.'''
|
||||
self.template_type = template_type
|
||||
self.lineno = lineno
|
||||
|
@ -638,7 +641,7 @@ class ParametersProcessor:
|
|||
return parameter_value
|
||||
|
||||
# Методы для проверки параметров после разбора всего шаблона.
|
||||
def check_postparse_append(self, parameter_value: str) -> NoReturn:
|
||||
def check_postparse_append(self, parameter_value: str) -> None:
|
||||
if parameter_value == 'link':
|
||||
if 'source' not in self._parameters_container:
|
||||
raise IncorrectParameter("append = 'link' without source "
|
||||
|
@ -652,7 +655,7 @@ class ParametersProcessor:
|
|||
raise IncorrectParameter("'append' parameter is not 'compatible' "
|
||||
"with the 'exec' parameter")
|
||||
|
||||
def check_postparse_run(self, parameter_value: str) -> NoReturn:
|
||||
def check_postparse_run(self, parameter_value: str) -> None:
|
||||
if self._parameters_container.append:
|
||||
raise IncorrectParameter("'run' parameter is not 'compatible' "
|
||||
"with the 'append' parameter")
|
||||
|
@ -661,7 +664,7 @@ class ParametersProcessor:
|
|||
raise IncorrectParameter("'run' parameter is not 'compatible' "
|
||||
"with the 'exec' parameter")
|
||||
|
||||
def check_postparse_exec(self, parameter_value: str) -> NoReturn:
|
||||
def check_postparse_exec(self, parameter_value: str) -> None:
|
||||
if self._parameters_container.append:
|
||||
raise IncorrectParameter("'exec' parameter is not 'compatible' "
|
||||
"with the 'append' parameter")
|
||||
|
@ -672,7 +675,7 @@ class ParametersProcessor:
|
|||
|
||||
def check_postparse_source(self,
|
||||
parameter_value: Union[str, Tuple[bool, str]]
|
||||
) -> NoReturn:
|
||||
) -> None:
|
||||
# Если файл по пути source не существует, но присутствует параметр
|
||||
# mirror -- пропускаем шаблон для того, чтобы целевой файл мог быть
|
||||
# удален в исполнительном модуле.
|
||||
|
@ -692,12 +695,12 @@ class ParametersProcessor:
|
|||
"append = 'link' for directory template")
|
||||
)
|
||||
|
||||
def check_postparse_autoupdate(self, parameter_value: bool) -> NoReturn:
|
||||
def check_postparse_autoupdate(self, parameter_value: bool) -> None:
|
||||
if self._parameters_container.unbound:
|
||||
raise IncorrectParameter("'unbound' parameter is incompatible"
|
||||
" with 'autoupdate' parameter")
|
||||
|
||||
def check_postparse_handler(self, parameter_value: bool) -> NoReturn:
|
||||
def check_postparse_handler(self, parameter_value: bool) -> None:
|
||||
if self._parameters_container.merge:
|
||||
raise IncorrectParameter("'merge' parameter is not available"
|
||||
" in handler templates")
|
||||
|
@ -706,7 +709,7 @@ class ParametersProcessor:
|
|||
raise IncorrectParameter("'package' parameter is not available"
|
||||
" in handler templates")
|
||||
|
||||
def check_postparse_package(self, parameter_value: str) -> NoReturn:
|
||||
def check_postparse_package(self, parameter_value: str) -> None:
|
||||
groups = []
|
||||
package_atom = PackageAtomParser.parse_atom_name(parameter_value)
|
||||
|
||||
|
@ -760,19 +763,19 @@ class ParametersProcessor:
|
|||
return True
|
||||
return False
|
||||
|
||||
def check_postparse_convert(self, parameter_value: str) -> NoReturn:
|
||||
def check_postparse_convert(self, parameter_value: str) -> None:
|
||||
template_format = self._parameters_container.format
|
||||
if not template_format or template_format != "backgrounds":
|
||||
raise IncorrectParameter("'convert' parameter available for"
|
||||
" 'backgrounds' format only.")
|
||||
|
||||
def check_postparse_stretch(self, parameter_value: str) -> NoReturn:
|
||||
def check_postparse_stretch(self, parameter_value: str) -> None:
|
||||
template_format = self._parameters_container.format
|
||||
if not template_format or template_format != "backgrounds":
|
||||
raise IncorrectParameter("'stretch' parameter available for"
|
||||
" 'backgrounds' format only.")
|
||||
|
||||
def check_postparse_execsql(self, parameter_value: str) -> NoReturn:
|
||||
def check_postparse_execsql(self, parameter_value: str) -> None:
|
||||
template_format = self._parameters_container.format
|
||||
if not template_format or template_format != "sqlite":
|
||||
raise IncorrectParameter("'execsql' parameter available for"
|
||||
|
@ -876,7 +879,7 @@ class ParametersProcessor:
|
|||
format(group_file_path))
|
||||
|
||||
@classmethod
|
||||
def _inspect_formats_package(cls) -> NoReturn:
|
||||
def _inspect_formats_package(cls) -> None:
|
||||
'''Метод для определения множества доступных форматов и
|
||||
предоставляемых ими параметров.'''
|
||||
if cls.format_is_inspected:
|
||||
|
@ -901,6 +904,7 @@ class ParametersProcessor:
|
|||
module = import_module('calculate.templates.format.{}'.
|
||||
format(module_name))
|
||||
for obj in dir(module):
|
||||
# TODO изменить способ выборки классов форматов.
|
||||
if obj.endswith('Format') and obj != 'BaseFormat':
|
||||
format_class = getattr(module, obj, False)
|
||||
|
||||
|
@ -954,6 +958,11 @@ class CalculateContext(Context):
|
|||
сохранять их.'''
|
||||
_env_set = set()
|
||||
|
||||
def __init__(self, environment: Environment,
|
||||
parent: Union[Context, Dict[str, Any]],
|
||||
name: str, blocks: Dict[str, Any], **kwargs):
|
||||
super().__init__(environment, parent, name, blocks, **kwargs)
|
||||
|
||||
def resolve(self, key: str) -> Any:
|
||||
if self._legacy_resolve_mode:
|
||||
rv = resolve_or_missing(self, key,
|
||||
|
@ -987,19 +996,19 @@ class ParametersContainer(MutableMapping):
|
|||
else:
|
||||
self.__inheritable: dict = {}
|
||||
|
||||
def set_parameter(self, item_to_add: dict) -> NoReturn:
|
||||
def set_parameter(self, item_to_add: dict) -> None:
|
||||
self.__parameters.update(item_to_add)
|
||||
|
||||
def set_inheritable(self, item_to_add: dict) -> NoReturn:
|
||||
def set_inheritable(self, item_to_add: dict) -> None:
|
||||
self.__inheritable.update(item_to_add)
|
||||
|
||||
def get_inheritables(self) -> "ParametersContainer":
|
||||
return ParametersContainer(copy.deepcopy(self.__inheritable))
|
||||
|
||||
def remove_not_inheritable(self) -> NoReturn:
|
||||
def remove_not_inheritable(self) -> None:
|
||||
self.__parameters.clear()
|
||||
|
||||
def print_parameters_for_debug(self) -> NoReturn:
|
||||
def print_parameters_for_debug(self) -> None:
|
||||
print('Parameters:')
|
||||
pprint(self.__parameters)
|
||||
|
||||
|
@ -1010,19 +1019,19 @@ class ParametersContainer(MutableMapping):
|
|||
return (parameter_name not in self.__parameters
|
||||
and parameter_name in self.__inheritable)
|
||||
|
||||
def remove_parameter(self, parameter_name: str) -> NoReturn:
|
||||
def remove_parameter(self, parameter_name: str) -> None:
|
||||
if parameter_name in self.__parameters:
|
||||
self.__parameters.pop(parameter_name)
|
||||
elif parameter_name in self.__inheritable:
|
||||
self.__inheritable.pop(parameter_name)
|
||||
|
||||
def change_parameter(self, parameter: str, value: Any) -> NoReturn:
|
||||
def change_parameter(self, parameter: str, value: Any) -> None:
|
||||
if parameter in self.__parameters:
|
||||
self.__parameters.update({parameter: value})
|
||||
elif parameter in self.__inheritable:
|
||||
self.__inheritable.update({parameter: value})
|
||||
|
||||
def _clear_container(self) -> NoReturn:
|
||||
def _clear_container(self) -> None:
|
||||
self.__parameters.clear()
|
||||
self.__inheritable.clear()
|
||||
|
||||
|
@ -1047,10 +1056,10 @@ class ParametersContainer(MutableMapping):
|
|||
else:
|
||||
return False
|
||||
|
||||
def __setitem__(self, name: str, value: Any) -> NoReturn:
|
||||
def __setitem__(self, name: str, value: Any) -> None:
|
||||
self.__parameters[name] = value
|
||||
|
||||
def __delitem__(self, name: str) -> NoReturn:
|
||||
def __delitem__(self, name: str) -> None:
|
||||
if name in self.__parameters:
|
||||
del self.__parameters[name]
|
||||
|
||||
|
@ -1092,9 +1101,9 @@ class CalculateExtension(Extension):
|
|||
self.environment: Environment = environment
|
||||
self.package_atom_parser = PackageAtomParser(chroot_path=chroot_path)
|
||||
|
||||
self.environment.globals.update({'pkg': self.pkg})
|
||||
self.environment.globals.update({'grep': self.grep})
|
||||
self.environment.globals.update({'exists': self.exists})
|
||||
self.environment.globals.update({'pkg': self.pkg,
|
||||
'grep': self.grep,
|
||||
'exists': self.exists})
|
||||
|
||||
self._datavars = datavars_module
|
||||
self.parameters_processor = parameters_processor
|
||||
|
@ -1104,7 +1113,7 @@ class CalculateExtension(Extension):
|
|||
# того, чтобы проверять единственность тега calculate.
|
||||
self.calculate_parsed: bool = False
|
||||
|
||||
self.tags = {'calculate', 'save', 'set_var'}
|
||||
self.tags = {'calculate', 'save'}
|
||||
self.CONDITION_TOKENS_TYPES = {'eq', 'ne', 'lt', 'gt', 'lteq', 'gteq'}
|
||||
self.CONDITION_NAME_TOKENS = {'not'}
|
||||
self.LITERAL_TOKENS_TYPES = {'string', 'integer', 'float'}
|
||||
|
@ -1121,6 +1130,8 @@ class CalculateExtension(Extension):
|
|||
def __call__(self, env: Environment) -> "CalculateExtension":
|
||||
# Необходимо для обеспечения возможности передать готовый объект
|
||||
# расширения, а не его класс.
|
||||
# Через функцию-фабрику такое не получается устроить, потому что при
|
||||
# добавлении расширения jinja2 ищет у его класса атрибут identifier.
|
||||
return self
|
||||
|
||||
def parse(self, parser: Parser) -> List[nodes.Output]:
|
||||
|
@ -1285,8 +1296,10 @@ class CalculateExtension(Extension):
|
|||
.format(str(error)),
|
||||
lineno=self.stream.current.lineno)
|
||||
|
||||
def check_conditions(self, conditions: List[Template]) -> NoReturn:
|
||||
def check_conditions(self, conditions: List[Template]) -> None:
|
||||
for condition in conditions:
|
||||
# TODO Добавить различие провала условие из синтаксической ошибки
|
||||
# или при отрицательном условии.
|
||||
self.condition_result = False
|
||||
try:
|
||||
condition.render(__datavars__=self._datavars)
|
||||
|
@ -1423,7 +1436,7 @@ class CalculateExtension(Extension):
|
|||
def _modify_variables(self, variable: List[str], namespace: NamespaceNode,
|
||||
new_value: Any, optype: int,
|
||||
target_file: Optional[str] = None,
|
||||
modify_only: bool = True) -> NoReturn:
|
||||
modify_only: bool = True) -> None:
|
||||
'''Метод для модификации значения переменной.'''
|
||||
variable_name = variable[-1]
|
||||
|
||||
|
@ -1457,7 +1470,7 @@ class CalculateExtension(Extension):
|
|||
# DEPRECATED
|
||||
def _modify_hash(self, variable_name: List[str],
|
||||
hash_variable: VariableNode, new_value, optype,
|
||||
target_file: Optional[str] = None) -> NoReturn:
|
||||
target_file: Optional[str] = None) -> None:
|
||||
'''Метод для модификации значения в переменной-хэше.'''
|
||||
value_name = variable_name[-1]
|
||||
hash_value = hash_variable.get_value().get_hash()
|
||||
|
@ -1477,7 +1490,7 @@ class CalculateExtension(Extension):
|
|||
|
||||
def _save_to_target(self, namespace_name: List[str],
|
||||
variable_name: str, value: Any, target_file: str
|
||||
) -> NoReturn:
|
||||
) -> None:
|
||||
'''Метод для добавления переменной в список переменных, значение
|
||||
которых было установлено через тег save и при этом должно быть
|
||||
сохранено в указанном файле: save.target_file.'''
|
||||
|
@ -1677,7 +1690,7 @@ class CalculateExtension(Extension):
|
|||
|
||||
|
||||
class TemplateEngine:
|
||||
def __init__(self, directory_path: Union[str, None] = None,
|
||||
def __init__(self, directory_path: Optional[str] = None,
|
||||
datavars_module: Union[Datavars,
|
||||
NamespaceNode,
|
||||
Variables] = Variables(),
|
||||
|
@ -1728,16 +1741,16 @@ class TemplateEngine:
|
|||
return self.parameters_processor.for_package
|
||||
|
||||
@for_package.setter
|
||||
def for_package(self, package: Package) -> NoReturn:
|
||||
def for_package(self, package: Package) -> None:
|
||||
self.parameters_processor.for_package = package
|
||||
|
||||
def change_directory(self, directory_path: str) -> NoReturn:
|
||||
def change_directory(self, directory_path: str) -> None:
|
||||
'''Метод для смены директории в загрузчике.'''
|
||||
self.environment.loader = FileSystemLoader(directory_path)
|
||||
|
||||
def process_template(self, template_path: str, template_type: str,
|
||||
parameters: Optional[ParametersContainer] = None
|
||||
) -> NoReturn:
|
||||
) -> None:
|
||||
'''Метод для обработки файла шаблона, расположенного по указанному
|
||||
пути.'''
|
||||
if parameters is not None:
|
||||
|
@ -1767,7 +1780,7 @@ class TemplateEngine:
|
|||
def process_template_from_string(
|
||||
self, string: str, template_type: int,
|
||||
parameters: Optional[ParametersContainer] = None
|
||||
) -> NoReturn:
|
||||
) -> None:
|
||||
'''Метод для обработки текста шаблона.'''
|
||||
if parameters is not None:
|
||||
self._parameters_object = parameters
|
||||
|
|
|
@ -43,7 +43,6 @@ from typing import (
|
|||
List,
|
||||
Tuple,
|
||||
Iterator,
|
||||
NoReturn,
|
||||
Optional,
|
||||
Callable
|
||||
)
|
||||
|
@ -123,21 +122,21 @@ class CalculateConfigFile:
|
|||
self._unsaved_changes = False
|
||||
return config_dictionary
|
||||
|
||||
def set_files_md5(self, file_path: str, file_md5: str) -> NoReturn:
|
||||
def set_files_md5(self, file_path: str, file_md5: str) -> None:
|
||||
'''Метод для установки в config соответствия файла некоторой
|
||||
контрольной сумме.'''
|
||||
file_path = self._remove_chroot(file_path)
|
||||
self._config_dictionary[file_path] = file_md5
|
||||
self._unsaved_changes = True
|
||||
|
||||
def remove_file(self, file_path: str) -> NoReturn:
|
||||
def remove_file(self, file_path: str) -> None:
|
||||
'''Метод для удаления файла из config.'''
|
||||
file_path = self._remove_chroot(file_path)
|
||||
if file_path in self._config_dictionary:
|
||||
self._config_dictionary.pop(file_path)
|
||||
self._unsaved_changes = True
|
||||
|
||||
def compare_md5(self, file_path: str, file_md5: str) -> NoReturn:
|
||||
def compare_md5(self, file_path: str, file_md5: str) -> None:
|
||||
'''Метод для сравнения хэш-суммы из config и некоторой заданной.'''
|
||||
file_path = self._remove_chroot(file_path)
|
||||
if file_path in self._config_dictionary:
|
||||
|
@ -145,7 +144,7 @@ class CalculateConfigFile:
|
|||
else:
|
||||
return False
|
||||
|
||||
def save_changes(self) -> NoReturn:
|
||||
def save_changes(self) -> None:
|
||||
'''Метод для записи изменений, внессенных в файл config.'''
|
||||
if not self._unsaved_changes:
|
||||
return
|
||||
|
@ -168,7 +167,6 @@ class CalculateConfigFile:
|
|||
class TemplateWrapper:
|
||||
'''Класс связывающий шаблон с целевым файлом и определяющий параметры
|
||||
наложения шаблона, обусловленные состоянием целевого файла.'''
|
||||
|
||||
type_checks: Dict[int,
|
||||
Callable[[str], bool]] = {DIR: os.path.isdir,
|
||||
FILE: os.path.isfile}
|
||||
|
@ -264,7 +262,7 @@ class TemplateWrapper:
|
|||
raise TemplateExecutorError("'format' parameter is not set"
|
||||
" file template.")
|
||||
|
||||
# Если по этому пути что-то есть -- проверяем конфликты.
|
||||
# Если по этому пути что-то есть -- проверяем тип этого.
|
||||
if os.path.exists(target_file_path):
|
||||
for file_type, checker in self.type_checks.items():
|
||||
if checker(target_file_path):
|
||||
|
@ -312,7 +310,7 @@ class TemplateWrapper:
|
|||
# self.parameters.append == "replace"):
|
||||
# self.remove_original = True
|
||||
|
||||
def _check_type_conflicts(self) -> NoReturn:
|
||||
def _check_type_conflicts(self) -> None:
|
||||
'''Метод для проверки конфликтов типов.'''
|
||||
if self.parameters.append == 'link':
|
||||
if self.parameters.force:
|
||||
|
@ -386,7 +384,7 @@ class TemplateWrapper:
|
|||
raise TemplateTypeConflict("the target file is a directory"
|
||||
" while the template is a file")
|
||||
|
||||
def _check_package_collision(self) -> NoReturn:
|
||||
def _check_package_collision(self) -> None:
|
||||
'''Метод для проверки на предмет коллизии, то есть конфликта пакета
|
||||
шаблона и целевого файла.'''
|
||||
if self.parameters.package:
|
||||
|
@ -475,7 +473,9 @@ class TemplateWrapper:
|
|||
def _compare_packages(self, lpackage: PackageAtomName,
|
||||
rpackage: PackageAtomName
|
||||
) -> Union[None, PackageAtomName]:
|
||||
'''Метод, сравнивающий пакеты по их именам, возвращает старший.'''
|
||||
'''Метод, сравнивающий пакеты по их именам, возвращает старший, если
|
||||
пакеты с одинаковыми именами, но разными версиями, или None, если их
|
||||
имена и категории не равны.'''
|
||||
if lpackage.category != rpackage.category:
|
||||
return None
|
||||
|
||||
|
@ -487,7 +487,7 @@ class TemplateWrapper:
|
|||
else:
|
||||
return rpackage
|
||||
|
||||
def _check_user_changes(self) -> NoReturn:
|
||||
def _check_user_changes(self) -> None:
|
||||
'''Метод для проверки наличия пользовательских изменений в
|
||||
конфигурационных файлах.'''
|
||||
# Эта проверка только для файлов.
|
||||
|
@ -520,8 +520,7 @@ class TemplateWrapper:
|
|||
# Путь к архивной версии файла.
|
||||
self.archive_path = self._get_archive_path(self.target_path)
|
||||
|
||||
self.contents_matching = (self.parameters.autoupdate
|
||||
or self.parameters.force)
|
||||
self.contents_matching = (self.parameters.autoupdate)
|
||||
|
||||
if not self.protected:
|
||||
self.contents_matching = True
|
||||
|
@ -545,6 +544,8 @@ class TemplateWrapper:
|
|||
# Если файл по целевому пути не относится к какому-либо пакету.
|
||||
# self.contents_matching = False
|
||||
pass
|
||||
elif self.target_type is DIR and self.parameters.force:
|
||||
self.contents_matching = True
|
||||
elif not self.contents_matching:
|
||||
# Если файл есть и он относится к текущему пакету.
|
||||
# Если по каким-то причинам уже нужно считать, что хэш-суммы
|
||||
|
@ -613,7 +614,7 @@ class TemplateWrapper:
|
|||
|
||||
return new_cfg_path
|
||||
|
||||
def remove_from_contents(self) -> NoReturn:
|
||||
def remove_from_contents(self) -> None:
|
||||
'''Метод для удаления целевого файла из CONTENTS.'''
|
||||
if self.target_package is None:
|
||||
return
|
||||
|
@ -623,13 +624,13 @@ class TemplateWrapper:
|
|||
elif self.template_type == FILE:
|
||||
self.target_package.remove_obj(self.target_path)
|
||||
|
||||
def clear_dir_contents(self) -> NoReturn:
|
||||
def clear_dir_contents(self) -> None:
|
||||
'''Метод для удаления из CONTENTS всего содержимого директории после
|
||||
применения append = "clear".'''
|
||||
if self.template_type == DIR and self.target_package is not None:
|
||||
self.target_package.clear_dir(self.target_path)
|
||||
|
||||
def add_to_contents(self, file_md5: Optional[str] = None) -> NoReturn:
|
||||
def add_to_contents(self, file_md5: Optional[str] = None) -> None:
|
||||
'''Метод для добавления целевого файла в CONTENTS.'''
|
||||
if self.target_package is None:
|
||||
return
|
||||
|
@ -652,7 +653,7 @@ class TemplateWrapper:
|
|||
elif self.template_type == FILE:
|
||||
self.target_package.add_obj(source_path, file_md5=file_md5)
|
||||
|
||||
def update_contents_from_list(self, changed_list: dict) -> NoReturn:
|
||||
def update_contents_from_list(self, changed_list: dict) -> None:
|
||||
'''Метод для изменения CONTENTS по списку измененных файлов.'''
|
||||
print("UPDATE CONTENTS FROM LIST")
|
||||
if self.target_package is None:
|
||||
|
@ -676,7 +677,7 @@ class TemplateWrapper:
|
|||
self.target_package.add_dir(file_path)
|
||||
|
||||
@classmethod
|
||||
def _set_protected(cls, chroot_path: str) -> NoReturn:
|
||||
def _set_protected(cls, chroot_path: str) -> None:
|
||||
'''Метод для получения множества защищенных директорий.'''
|
||||
cls._protected_set = set()
|
||||
cls._unprotected_set = set()
|
||||
|
@ -699,7 +700,7 @@ class TemplateWrapper:
|
|||
|
||||
cls._protected_is_set = True
|
||||
|
||||
def save_changes(self) -> NoReturn:
|
||||
def save_changes(self) -> None:
|
||||
'''Метод для сохранения изменений внесенных в CONTENTS.'''
|
||||
if self.target_package:
|
||||
self.target_package.remove_empty_directories()
|
||||
|
@ -864,7 +865,7 @@ class TemplateExecutor:
|
|||
|
||||
return self.executor_output
|
||||
|
||||
def save_changes(self) -> NoReturn:
|
||||
def save_changes(self) -> None:
|
||||
'''Метод для сохранения чего-нибудь после выполнения всех шаблонов.'''
|
||||
# Пока сохраняем только получившееся содержимое config-файла.
|
||||
self.calculate_config_file.save_changes()
|
||||
|
@ -892,7 +893,7 @@ class TemplateExecutor:
|
|||
template_object.add_to_contents()
|
||||
|
||||
def _append_remove_directory(self, template_object: TemplateWrapper
|
||||
) -> NoReturn:
|
||||
) -> None:
|
||||
'''Метод описывающий действия для append = "remove", если шаблон --
|
||||
директория. Удаляет директорию со всем содержимым, если она есть.'''
|
||||
if template_object.target_type is not None:
|
||||
|
@ -914,11 +915,11 @@ class TemplateExecutor:
|
|||
template_object.remove_from_contents()
|
||||
|
||||
def _append_skip_directory(self,
|
||||
template_object: TemplateWrapper) -> NoReturn:
|
||||
template_object: TemplateWrapper) -> None:
|
||||
pass
|
||||
|
||||
def _append_clear_directory(self,
|
||||
template_object: TemplateWrapper) -> NoReturn:
|
||||
template_object: TemplateWrapper) -> None:
|
||||
'''Метод описывающий действия для append = "clear", если шаблон --
|
||||
директория. Удаляет все содержимое директории, если она есть.'''
|
||||
if template_object.target_type is not None:
|
||||
|
@ -949,7 +950,7 @@ class TemplateExecutor:
|
|||
template_object.clear_dir_contents()
|
||||
|
||||
def _append_link_directory(self, template_object: TemplateWrapper
|
||||
) -> NoReturn:
|
||||
) -> None:
|
||||
'''Метод описывающий действия для append = "link", если шаблон --
|
||||
директория. Создает ссылку на директорию, если она есть.'''
|
||||
self._link_directory(template_object.parameters.source,
|
||||
|
@ -974,7 +975,7 @@ class TemplateExecutor:
|
|||
template_object.add_to_contents()
|
||||
|
||||
def _append_replace_directory(self, template_object: TemplateWrapper
|
||||
) -> NoReturn:
|
||||
) -> None:
|
||||
'''Метод описывающий действия для append = "replace", если шаблон --
|
||||
директория. Очищает директорию или создает, если ее нет.'''
|
||||
if template_object.target_type is None:
|
||||
|
@ -1006,7 +1007,7 @@ class TemplateExecutor:
|
|||
|
||||
def _append_join_file(self, template_object: TemplateWrapper,
|
||||
join_before: bool = False, replace: bool = False
|
||||
) -> NoReturn:
|
||||
) -> None:
|
||||
'''Метод описывающий действия при append = "join", если шаблон -- файл.
|
||||
Объединяет шаблон с целевым файлом.'''
|
||||
output_path = template_object.output_path
|
||||
|
@ -1105,6 +1106,7 @@ class TemplateExecutor:
|
|||
chroot_path=self.chroot_path)
|
||||
# Удаляем форматный объект входного файла.
|
||||
# del(parsed_template)
|
||||
|
||||
# Если исполняемый формат выдал список измененных файлов для
|
||||
# изменения CONTENTS и при этом задан пакет -- обновляем
|
||||
# CONTENTS.
|
||||
|
@ -1279,32 +1281,32 @@ class TemplateExecutor:
|
|||
chown = self.file_default_parameters.get('chown', False)
|
||||
return chown
|
||||
|
||||
def _append_after_file(self, template_object: TemplateWrapper) -> NoReturn:
|
||||
def _append_after_file(self, template_object: TemplateWrapper) -> None:
|
||||
'''Метод описывающий действия при append = "after", если шаблон --
|
||||
файл. Объединяет шаблон с целевым файлом так, чтобы текст добавлялся
|
||||
в конец файла и в конец каждой секции файла.'''
|
||||
self._append_join_file(template_object, join_before=False)
|
||||
|
||||
def _append_before_file(self, template_object: TemplateWrapper
|
||||
) -> NoReturn:
|
||||
) -> None:
|
||||
'''Метод описывающий действия при append = "after", если шаблон --
|
||||
файл. Объединяет шаблон с целевым файлом так, чтобы текст добавлялся
|
||||
в начало файла и в начало каждой секции файла.'''
|
||||
self._append_join_file(template_object, join_before=True)
|
||||
|
||||
def _append_skip_file(self, template_object: TemplateWrapper) -> NoReturn:
|
||||
def _append_skip_file(self, template_object: TemplateWrapper) -> None:
|
||||
'''Метод описывающий действия при append = "skip". Пока никаких
|
||||
действий.'''
|
||||
pass
|
||||
|
||||
def _append_replace_file(self, template_object: TemplateWrapper
|
||||
) -> NoReturn:
|
||||
) -> None:
|
||||
'''Метод описывающий действия при append = "replace", если шаблон --
|
||||
файл. Очищает файл и затем накладывает на него шаблон.'''
|
||||
self._append_join_file(template_object, replace=True)
|
||||
|
||||
def _append_remove_file(self, template_object: TemplateWrapper
|
||||
) -> NoReturn:
|
||||
) -> None:
|
||||
'''Метод описывающий действия при append = "remove", если шаблон --
|
||||
файл. Удаляет файл.'''
|
||||
if template_object.target_type is not None:
|
||||
|
@ -1320,7 +1322,7 @@ class TemplateExecutor:
|
|||
if self.dbpkg:
|
||||
template_object.remove_from_contents()
|
||||
|
||||
def _append_clear_file(self, template_object: TemplateWrapper) -> NoReturn:
|
||||
def _append_clear_file(self, template_object: TemplateWrapper) -> None:
|
||||
'''Метод описывающий действия при append = "clear", если шаблон --
|
||||
файл. Очищает файл.'''
|
||||
if template_object.target_type is not None:
|
||||
|
@ -1342,7 +1344,7 @@ class TemplateExecutor:
|
|||
if self.dbpkg:
|
||||
template_object.add_to_contents()
|
||||
|
||||
def _append_link_file(self, template_object: TemplateWrapper) -> NoReturn:
|
||||
def _append_link_file(self, template_object: TemplateWrapper) -> None:
|
||||
'''Метод описывающий действия при append = "link", если шаблон --
|
||||
файл. Создает ссылку на файл, указанный в параметре source.'''
|
||||
input_path = template_object.input_path
|
||||
|
@ -1424,7 +1426,7 @@ class TemplateExecutor:
|
|||
return hashlib.md5(source_path.encode()).hexdigest()
|
||||
|
||||
def _create_directory(self, template_object: TemplateWrapper,
|
||||
path_to_create: Optional[str] = None) -> NoReturn:
|
||||
path_to_create: Optional[str] = None) -> None:
|
||||
'''Метод для создания директории и, при необходимости, изменения
|
||||
владельца и доступа все директорий на пути к целевой.'''
|
||||
if path_to_create is None:
|
||||
|
@ -1481,7 +1483,7 @@ class TemplateExecutor:
|
|||
'Failed to create directory: {}, reason: {}'.
|
||||
format(create_path, str(error)))
|
||||
|
||||
def _remove_directory(self, target_path: str) -> NoReturn:
|
||||
def _remove_directory(self, target_path: str) -> None:
|
||||
'''Метод для удаления директории.'''
|
||||
if os.path.exists(target_path):
|
||||
if os.path.isdir(target_path):
|
||||
|
@ -1505,7 +1507,7 @@ class TemplateExecutor:
|
|||
"reason: {1}").format(target_path,
|
||||
error_message))
|
||||
|
||||
def _clear_directory(self, target_path: str) -> NoReturn:
|
||||
def _clear_directory(self, target_path: str) -> None:
|
||||
'''Метод для очистки содержимого целевой директории.'''
|
||||
if os.path.exists(target_path):
|
||||
if os.path.isdir(target_path):
|
||||
|
@ -1526,9 +1528,9 @@ class TemplateExecutor:
|
|||
error_message))
|
||||
|
||||
def _link_directory(self, source: str, target_path: str,
|
||||
force: bool = False) -> NoReturn:
|
||||
force: bool = False) -> None:
|
||||
'''Метод для создания по целевому пути ссылки на директорию
|
||||
расположенную на пути, указанному в source.'''
|
||||
расположенную на пути, указанном в source.'''
|
||||
try:
|
||||
os.symlink(source, target_path, target_is_directory=True)
|
||||
except OSError as error:
|
||||
|
@ -1536,7 +1538,7 @@ class TemplateExecutor:
|
|||
"failed to create symlink: {0} -> {1}, reason: {2}".
|
||||
format(target_path, source, str(error)))
|
||||
|
||||
def _remove_file(self, target_path: str) -> NoReturn:
|
||||
def _remove_file(self, target_path: str) -> None:
|
||||
'''Метод для удаления файлов.'''
|
||||
if os.path.exists(target_path):
|
||||
if os.path.isfile(target_path):
|
||||
|
@ -1567,7 +1569,7 @@ class TemplateExecutor:
|
|||
"reason: {1}").format(target_path,
|
||||
error_message))
|
||||
|
||||
def _clear_file(self, target_path: str) -> NoReturn:
|
||||
def _clear_file(self, target_path: str) -> None:
|
||||
'''Метод для очистки файлов.'''
|
||||
if os.path.exists(target_path):
|
||||
if os.path.isfile(target_path):
|
||||
|
@ -1586,7 +1588,7 @@ class TemplateExecutor:
|
|||
"reason: {1}").format(target_path,
|
||||
error_message))
|
||||
|
||||
def _link_file(self, source: str, target_path: str) -> NoReturn:
|
||||
def _link_file(self, source: str, target_path: str) -> None:
|
||||
'''Метод для создания по целевому пути ссылки на файл расположенный на
|
||||
пути, указанному в source.'''
|
||||
try:
|
||||
|
@ -1596,7 +1598,7 @@ class TemplateExecutor:
|
|||
"failed to create symlink to the file: {0} -> {1}, reason: {2}".
|
||||
format(target_path, source, str(error)))
|
||||
|
||||
def _run_template(self, template_object: TemplateWrapper) -> NoReturn:
|
||||
def _run_template(self, template_object: TemplateWrapper) -> None:
|
||||
'''Метод для сохранения текста шаблонов, который должен быть исполнен
|
||||
интерпретатором указанным в run прямо во время обработки шаблонов.'''
|
||||
text_to_run = template_object.template_text
|
||||
|
@ -1634,7 +1636,7 @@ class TemplateExecutor:
|
|||
" interpreter '{}', reason: {}").
|
||||
format(interpreter, str(error)))
|
||||
|
||||
def _exec_template(self, template_object: TemplateWrapper) -> NoReturn:
|
||||
def _exec_template(self, template_object: TemplateWrapper) -> None:
|
||||
'''Метод для сохранения текста шаблонов, который должен быть исполнен
|
||||
интерпретатором указанным в exec после выполнения всех прочих шаблонов.
|
||||
'''
|
||||
|
@ -1720,7 +1722,7 @@ class TemplateExecutor:
|
|||
format(interpreter, str(error)))
|
||||
|
||||
def _chown_directory(self, target_path: str, chown_value: dict
|
||||
) -> NoReturn:
|
||||
) -> None:
|
||||
"""Метод для смены владельца директории."""
|
||||
try:
|
||||
if os.path.exists(target_path):
|
||||
|
@ -1738,7 +1740,7 @@ class TemplateExecutor:
|
|||
chown_value['gid']),
|
||||
str(error)))
|
||||
|
||||
def _chmod_directory(self, target_path: str, chmod_value: int) -> NoReturn:
|
||||
def _chmod_directory(self, target_path: str, chmod_value: int) -> None:
|
||||
'''Метод для смены прав доступа к директории.'''
|
||||
if isinstance(chmod_value, tuple) and not chmod_value[1]:
|
||||
chmod_value = chmod_value[0]
|
||||
|
@ -1759,7 +1761,7 @@ class TemplateExecutor:
|
|||
'Can not chmod directory: {0}, reason: {1}'.
|
||||
format(target_path, str(error)))
|
||||
|
||||
def _chown_file(self, target_path: str, chown_value: dict) -> NoReturn:
|
||||
def _chown_file(self, target_path: str, chown_value: dict) -> None:
|
||||
'''Метод для смены владельца файла.'''
|
||||
try:
|
||||
if os.path.exists(target_path):
|
||||
|
@ -1777,7 +1779,7 @@ class TemplateExecutor:
|
|||
chown_value['gid']),
|
||||
str(error)))
|
||||
|
||||
def _chmod_file(self, target_path: str, chmod_value: int) -> NoReturn:
|
||||
def _chmod_file(self, target_path: str, chmod_value: int) -> None:
|
||||
'''Метод для смены прав доступа к директории.'''
|
||||
try:
|
||||
if not os.path.exists(target_path):
|
||||
|
@ -1802,13 +1804,15 @@ class TemplateExecutor:
|
|||
current_mode: Optional[int] = None) -> int:
|
||||
'''Метод для наложения X-маски, необходимой для получения значения
|
||||
chmod, c учетом возможности наличия в нем значения "X".'''
|
||||
if not chmod[1]:
|
||||
return chmod[0]
|
||||
chmod, x_mask = chmod
|
||||
|
||||
if not x_mask:
|
||||
return chmod
|
||||
|
||||
if current_mode is None:
|
||||
return chmod[0] ^ chmod[1]
|
||||
return chmod ^ x_mask
|
||||
else:
|
||||
return chmod[0] ^ (current_mode & chmod[1])
|
||||
return chmod ^ (current_mode & x_mask)
|
||||
|
||||
def _get_file_mode(self, file_path: str) -> int:
|
||||
'''Метод для получения прав доступа для указанного файла.'''
|
||||
|
@ -1847,7 +1851,7 @@ class TemplateExecutor:
|
|||
if self.chroot_path == '/':
|
||||
group_name = grp.getgrgid(gid).gr_name
|
||||
else:
|
||||
group_name = self._get_group_name_form_gid(gid)
|
||||
group_name = self._get_group_name_from_gid(gid)
|
||||
except (TypeError, KeyError):
|
||||
group_name = str(gid)
|
||||
|
||||
|
@ -1875,7 +1879,7 @@ class TemplateExecutor:
|
|||
else:
|
||||
return str(uid)
|
||||
|
||||
def _get_group_name_form_gid(self, gid: int) -> str:
|
||||
def _get_group_name_from_gid(self, gid: int) -> str:
|
||||
'''Метод для получения названия группы по его gid.'''
|
||||
group_file_path = os.path.join(self.chroot_path, 'etc/group')
|
||||
group_dictionary = dict()
|
||||
|
@ -1927,7 +1931,7 @@ class DirectoryTree:
|
|||
self.base_directory = base_directory
|
||||
self._tree = {}
|
||||
|
||||
def update_tree(self, tree: dict) -> NoReturn:
|
||||
def update_tree(self, tree: dict) -> None:
|
||||
'''Метод, инициирующий наложение заданного дерева каталогов на данный
|
||||
экземпляр дерева.'''
|
||||
self._update(self._tree, tree)
|
||||
|
@ -1943,7 +1947,7 @@ class DirectoryTree:
|
|||
original_tree[parent] = child
|
||||
return original_tree
|
||||
|
||||
def show_tree(self) -> NoReturn:
|
||||
def show_tree(self) -> None:
|
||||
pprint(self._tree)
|
||||
|
||||
def get_directory_tree(self, directory: str) -> "DirectoryTree":
|
||||
|
@ -1956,13 +1960,13 @@ class DirectoryTree:
|
|||
directory_tree._tree = self._tree[directory]
|
||||
return directory_tree
|
||||
|
||||
def __getitem__(self, name: str) -> dict:
|
||||
def __getitem__(self, name: str) -> Union[None, dict]:
|
||||
if name in self._tree:
|
||||
return self._tree[name]
|
||||
else:
|
||||
return None
|
||||
|
||||
def __setitem__(self, name: str, value: Union[None, dict]) -> NoReturn:
|
||||
def __setitem__(self, name: str, value: Union[None, dict]) -> None:
|
||||
self._tree[name] = value
|
||||
|
||||
def __iter__(self) -> Iterator[str]:
|
||||
|
@ -2149,7 +2153,7 @@ class DirectoryProcessor:
|
|||
return path_to_add
|
||||
|
||||
def _add_package_to_group(self, group_name: str,
|
||||
package_atom: str) -> NoReturn:
|
||||
package_atom: str) -> None:
|
||||
try:
|
||||
groups_namespace = self.datavars_module.main.cl.groups
|
||||
except (VariableNotFoundError, AttributeError):
|
||||
|
@ -2230,7 +2234,7 @@ class DirectoryProcessor:
|
|||
return -1
|
||||
return -2
|
||||
|
||||
def _make_current_template_var(self) -> NoReturn:
|
||||
def _make_current_template_var(self) -> None:
|
||||
var_path = ['main', 'cl']
|
||||
namespace = self.datavars_module
|
||||
|
||||
|
@ -2251,7 +2255,7 @@ class DirectoryProcessor:
|
|||
else:
|
||||
namespace['current_template'] = ""
|
||||
|
||||
def process_template_directories(self) -> NoReturn:
|
||||
def process_template_directories(self) -> None:
|
||||
'''Метод для обхода шаблонов, содержащихся в каталогах из
|
||||
main.cl_template.path.'''
|
||||
# Режим заполнения очередей директорий пакетов, необходимых для более
|
||||
|
@ -2319,12 +2323,11 @@ class DirectoryProcessor:
|
|||
PackageCreator.save_all()
|
||||
return self.template_executor.changed_files
|
||||
|
||||
def _run_template_from_base_directory(
|
||||
self, template_names: List[str],
|
||||
base_directory: str,
|
||||
package: Optional[Package] = None,
|
||||
directory_tree: Optional[dict] = None
|
||||
) -> NoReturn:
|
||||
def _run_template_from_base_directory(self, template_names: List[str],
|
||||
base_directory: str,
|
||||
package: Optional[Package] = None,
|
||||
directory_tree: Optional[dict] = None
|
||||
) -> None:
|
||||
'''Метод для запуска шаблонов файлов находящихся в базовой директории.
|
||||
'''
|
||||
self.template_engine.change_directory(base_directory)
|
||||
|
@ -2380,6 +2383,7 @@ class DirectoryProcessor:
|
|||
else:
|
||||
template_package = package
|
||||
|
||||
# Дефолтные значения разные для упрощения написания шаблонов.
|
||||
if not parameters.run and not parameters.exec:
|
||||
if not parameters.format:
|
||||
parameters.set_parameter({'format': 'raw'})
|
||||
|
@ -2396,7 +2400,7 @@ class DirectoryProcessor:
|
|||
template_text=template_text,
|
||||
package=template_package)
|
||||
|
||||
def _execute_handlers(self) -> NoReturn:
|
||||
def _execute_handlers(self) -> None:
|
||||
'''Метод для запуска обработчиков добавленных в очередь обработчиков
|
||||
с помощью параметра notify.'''
|
||||
self.output.set_info('Processing handlers...')
|
||||
|
@ -2462,7 +2466,7 @@ class DirectoryProcessor:
|
|||
FILE, handler_path,
|
||||
template_text=handler_text)
|
||||
|
||||
def _merge_packages(self) -> NoReturn:
|
||||
def _merge_packages(self) -> None:
|
||||
'''Метод для выполнения шаблонов относящихся к пакетам, указанным во
|
||||
всех встреченных значениях параметра merge.'''
|
||||
not_merged_packages = []
|
||||
|
@ -2527,7 +2531,7 @@ class DirectoryProcessor:
|
|||
else:
|
||||
self.output.set_success('All packages are merged.')
|
||||
|
||||
def _run_exec_files(self) -> NoReturn:
|
||||
def _run_exec_files(self) -> None:
|
||||
'''Метод для выполнения скриптов, полученных в результате обработки
|
||||
шаблонов с параметром exec.'''
|
||||
for exec_file_path, exec_info in\
|
||||
|
@ -2554,7 +2558,7 @@ class DirectoryProcessor:
|
|||
current_target_path: str,
|
||||
directory_parameters: ParametersContainer,
|
||||
directory_tree: Union[dict, DirectoryTree] = {},
|
||||
package: Optional[Package] = None) -> NoReturn:
|
||||
package: Optional[Package] = None) -> None:
|
||||
'''Метод для рекурсивного обхода директорий с шаблонами, а также, при
|
||||
необходимости, заполнения деревьев директорий шаблонов, с помощью
|
||||
которых далее выполняются шаблоны пакетов из merge.'''
|
||||
|
@ -2805,7 +2809,6 @@ class DirectoryProcessor:
|
|||
|
||||
if self.fill_trees:
|
||||
directory_tree = {}
|
||||
return
|
||||
|
||||
def _scan_directory(self, directory_path: str
|
||||
) -> Tuple[List[str], List[str]]:
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
from pyparsing import Literal, Word, ZeroOrMore, Group, Dict, Optional,\
|
||||
restOfLine, empty, printables, OneOrMore, oneOf, nums,\
|
||||
lineno, line, col, Keyword, SkipTo, LineEnd, Combine
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class CalculateIniParser:
|
||||
'''Класс парсера calculate.ini файлов.'''
|
||||
class Define(Enum):
|
||||
assign = 0
|
||||
append = 1
|
||||
remove = 2
|
||||
|
||||
def __init__(self):
|
||||
self.operations = {"=": self.Define.assign,
|
||||
"+=": self.Define.append,
|
||||
"-=": self.Define.remove}
|
||||
lbrack = Literal("[")
|
||||
rbrack = Literal("]")
|
||||
|
||||
# comma = Literal(",").suppress()
|
||||
comment_symbol = Literal(';') | Literal('#')
|
||||
|
||||
# Define = self.Define
|
||||
value_operation = (Literal("=") | Combine(Literal("+") + Literal("="))
|
||||
| Combine(Literal("-") + Literal("=")))
|
||||
|
||||
comment = comment_symbol + Optional(restOfLine)
|
||||
|
||||
section_name = Word(printables+'\t', excludeChars='[]')
|
||||
|
||||
value_name = Word(printables+'\t', excludeChars='=-+')
|
||||
|
||||
# non_comma = Word(printables+'\t', excludeChars=',')
|
||||
clear_section = lbrack.suppress() + Group(empty) + rbrack.suppress()
|
||||
|
||||
section_start = Group(OneOrMore(lbrack.suppress() + section_name
|
||||
+ rbrack.suppress())
|
||||
+ (clear_section | ~lbrack())
|
||||
+ LineEnd().suppress())
|
||||
|
||||
# Если содержимое ini-файла не предваряется заголовком секции,
|
||||
# значит эта строка ошибочна.
|
||||
unexpected = Group(~section_start + SkipTo(LineEnd(),
|
||||
include=True))("error")
|
||||
unexpected.setParseAction(self._unexpected_token)
|
||||
|
||||
key_value = (~lbrack + value_name
|
||||
+ value_operation + empty
|
||||
+ restOfLine + LineEnd().suppress())
|
||||
|
||||
def strip_key_value(tokens):
|
||||
tokens[0] = tokens[0].strip()
|
||||
tokens[1] = tokens[1].strip()
|
||||
|
||||
key_value.setParseAction(strip_key_value)
|
||||
|
||||
self.ini_section_parser = (section_start
|
||||
+ Group(ZeroOrMore(
|
||||
Group(key_value | unexpected)))
|
||||
| unexpected)
|
||||
self.ini_section_parser.ignore(comment)
|
||||
|
||||
def _unexpected_token(self, string, location, tokens):
|
||||
'''Метод вызываемый парсером, если обнаружена некорректная строка,
|
||||
предназначен для получения некорректной строки и ее дальнейшего
|
||||
разбора.'''
|
||||
error_line = line(location, string).strip()
|
||||
if error_line:
|
||||
return (error_line, lineno(location, string),
|
||||
col(location, string))
|
||||
|
||||
def parse(self, data):
|
||||
for tokens, start, end in self.ini_section_parser.scanString(data):
|
||||
if tokens.getName() == "error":
|
||||
print('error tokens: {}'.format(tokens))
|
||||
yield {'error': tokens}
|
||||
|
||||
section, defkeys = tokens
|
||||
|
||||
section_list = section.asList()
|
||||
if section_list[-1] == []:
|
||||
self.clear_section(section_list[:-1])
|
||||
yield {'clear_section': (section_list[:-1], )}
|
||||
|
||||
self.start_section(section.asList())
|
||||
for defkey in defkeys:
|
||||
yield {'define_key': (section.asList(), defkey[0], defkey[2],
|
||||
self.operations[defkey[1]])}
|
|
@ -1,14 +0,0 @@
|
|||
{% for file_name, contents_values in document_dictionary.items() %}
|
||||
{% if contents_values is mapping %}
|
||||
{% set contents_values = (contents_values.values()|list) %}
|
||||
{% else %}
|
||||
{% set contents_values = (contents_values[0]|list) %}
|
||||
{% set file_name = file_name[-1] %}
|
||||
{% endif %}
|
||||
{% if contents_values[0] == 'sym' %}
|
||||
{{ contents_values[0] }} {{ file_name }} -> {{ contents_values[1:]|join(' ') }}
|
||||
{% else %}
|
||||
{{ contents_values[0] }} {{ file_name }}{% if contents_values[1:] %} {{ contents_values[1:]|join(' ') }}{% endif %}
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
|
@ -1,22 +1,23 @@
|
|||
import os
|
||||
import re
|
||||
from typing import Dict
|
||||
from calculate.utils.files import read_file_lines
|
||||
|
||||
|
||||
class ProfileWalker:
|
||||
'''Объект обходящий все директории профиля через parent файлы.'''
|
||||
def __init__(self, filename, repositories):
|
||||
self.repositories = repositories
|
||||
self.filename = filename
|
||||
self.re_reppath = re.compile("^({0})+:".format(
|
||||
def __init__(self, filename: str, repositories: Dict[str, str]):
|
||||
self.repositories: Dict[str, str] = repositories
|
||||
self.filename: str = filename
|
||||
self.re_reppath: re.Pattern = re.compile("^({0})+:".format(
|
||||
"|".join(self.repositories.keys())))
|
||||
|
||||
def interpolate(self, path):
|
||||
def interpolate(self, path: str):
|
||||
def subfunc(m):
|
||||
return "{0}/profiles/".format(self.repositories.get(m.group(1)))
|
||||
return self.re_reppath.sub(subfunc, path)
|
||||
|
||||
def find(self, directory):
|
||||
def find(self, directory: str):
|
||||
'''Метод для поиска по профилю всех файлов с именем, указанным в
|
||||
self.filename.'''
|
||||
parent_file_path = os.path.join(directory, "parent")
|
||||
|
|
|
@ -7,7 +7,6 @@ from collections import OrderedDict
|
|||
from .files import read_file, read_link, join_paths, FilesError
|
||||
from typing import (
|
||||
Generator,
|
||||
NoReturn,
|
||||
Optional,
|
||||
Dict,
|
||||
Tuple,
|
||||
|
@ -15,16 +14,6 @@ from typing import (
|
|||
List,
|
||||
Any
|
||||
)
|
||||
from pyparsing import (
|
||||
Literal,
|
||||
Regex,
|
||||
Word,
|
||||
nums,
|
||||
alphanums,
|
||||
LineEnd,
|
||||
SkipTo,
|
||||
)
|
||||
from jinja2 import PackageLoader, Environment
|
||||
from calculate.utils.tools import Singleton
|
||||
import hashlib
|
||||
|
||||
|
@ -59,6 +48,10 @@ class PackageNotFound(Exception):
|
|||
|
||||
|
||||
class PackageCreator(type):
|
||||
"""Метакласс для создания классов пакетов, напоминающий метакласс
|
||||
Singleton. Следит за тем, чтобы для каждого пакета создавался только один
|
||||
экземляр объекта Package."""
|
||||
# TODO Перенести этот функционал в класс Package.
|
||||
_instances: Dict["PackageAtomName", "Package"] = {}
|
||||
|
||||
def __call__(cls, *args, **kwargs):
|
||||
|
@ -74,14 +67,14 @@ class PackageCreator(type):
|
|||
return cls._instances[args[0]]
|
||||
|
||||
@classmethod
|
||||
def save_all(cls) -> NoReturn:
|
||||
def save_all(cls) -> None:
|
||||
for atom, pkg in cls._instances.items():
|
||||
pkg.remove_empty_directories()
|
||||
pkg.write_contents_file()
|
||||
|
||||
@classmethod
|
||||
def clear_instances(cls) -> NoReturn:
|
||||
"""Метод для очистки текущего кэша пакетный объектов.
|
||||
def clear_instances(cls) -> None:
|
||||
"""Метод для очистки текущего кэша пакетных объектов.
|
||||
Необходим при тестировании."""
|
||||
cls._instances.clear()
|
||||
|
||||
|
@ -370,62 +363,6 @@ class ContentsParser(metaclass=Singleton):
|
|||
return "\n".join(lines)
|
||||
|
||||
|
||||
class OldContentsParser(metaclass=Singleton):
|
||||
def __init__(self):
|
||||
sym_keyword = Literal('sym')
|
||||
dir_keyword = Literal('dir')
|
||||
obj_keyword = Literal('obj')
|
||||
|
||||
symlink_arrow = Literal('->')
|
||||
|
||||
file_path = Regex(r'\S+')
|
||||
|
||||
time_value = Word(nums)
|
||||
|
||||
md5 = Word(alphanums)
|
||||
|
||||
sym_line = (sym_keyword('type') + file_path('path')
|
||||
+ symlink_arrow.suppress() + file_path('target')
|
||||
+ time_value('mtime') + LineEnd().suppress())
|
||||
|
||||
dir_line = (dir_keyword('type') + file_path('path')
|
||||
+ LineEnd().suppress())
|
||||
|
||||
obj_line = (obj_keyword('type') + file_path('path')
|
||||
+ md5('md5') + time_value('mtime') + LineEnd().suppress())
|
||||
unexpected = SkipTo(LineEnd(), include=True)
|
||||
|
||||
self._parser = (dir_line | sym_line |
|
||||
obj_line | unexpected('unexpected')
|
||||
).setParseAction(self._parser_method)
|
||||
|
||||
def _parser_method(self, parse_result):
|
||||
if parse_result.getName() == 'unexpected':
|
||||
return [None]
|
||||
result_dict = parse_result.asDict()
|
||||
path = result_dict.pop('path')
|
||||
value = ({path: result_dict})
|
||||
return value
|
||||
|
||||
def parse(self, contents_text: str) -> OrderedDict:
|
||||
output_dictionary = OrderedDict()
|
||||
for tokens, start, end in self._parser.scanString(contents_text):
|
||||
parse_result = tokens[0]
|
||||
if parse_result is None:
|
||||
continue
|
||||
output_dictionary.update(parse_result)
|
||||
return output_dictionary
|
||||
|
||||
def render(self, contents_dictionary: dict) -> str:
|
||||
file_loader = PackageLoader('calculate', 'utils')
|
||||
environment = Environment(loader=file_loader,
|
||||
trim_blocks=True,
|
||||
lstrip_blocks=True)
|
||||
template = environment.get_template('contents_template')
|
||||
text = template.render(document_dictionary=contents_dictionary)
|
||||
return text
|
||||
|
||||
|
||||
class PackageAtomName:
|
||||
'''Класс для хранения результата определения пакета. Для определения пакета
|
||||
использует путь к его pkg директории.'''
|
||||
|
@ -686,7 +623,7 @@ class PackageAtomParser:
|
|||
return atom_dictionary
|
||||
|
||||
def _check_slot_value(self, pkg_path: str,
|
||||
atom_dictionary: dict) -> NoReturn:
|
||||
atom_dictionary: dict) -> None:
|
||||
'''Метод для проверки полученного из параметра package значения slot.
|
||||
'''
|
||||
if atom_dictionary['slot']:
|
||||
|
@ -699,7 +636,7 @@ class PackageAtomParser:
|
|||
errno=NOTEXIST)
|
||||
|
||||
def _check_use_flags_value(self, pkg_path: str,
|
||||
atom_dictionary: dict) -> NoReturn:
|
||||
atom_dictionary: dict) -> None:
|
||||
'''Метод для проверки полученных из параметра package значений
|
||||
use-флагов.'''
|
||||
if atom_dictionary['use_flags']:
|
||||
|
@ -742,7 +679,7 @@ class PackageAtomParser:
|
|||
yield path
|
||||
|
||||
def _check_version(self, atom_dictionary: dict, pkg_version: Version
|
||||
) -> NoReturn:
|
||||
) -> None:
|
||||
condition = atom_dictionary['condition']
|
||||
|
||||
if condition == '=':
|
||||
|
@ -769,6 +706,7 @@ class PackageAtomParser:
|
|||
def get_file_package(self, file_path: str) -> PackageAtomName:
|
||||
'''Метод для определения пакета, которому принадлежит файл.'''
|
||||
# Удаляем часть пути соответствующую chroot_path
|
||||
# TODO предусмотреть кэширование результата.
|
||||
if self.chroot_path != '/' and file_path.startswith(self.chroot_path):
|
||||
file_path = file_path[len(self.chroot_path):]
|
||||
|
||||
|
@ -919,7 +857,7 @@ class Package(metaclass=PackageCreator):
|
|||
else:
|
||||
return False
|
||||
|
||||
def write_contents_file(self) -> NoReturn:
|
||||
def write_contents_file(self) -> None:
|
||||
'''Метод для записи файла CONTENTS.'''
|
||||
with open(self.contents_file_path, 'w') as contents_file:
|
||||
contents_text = self.render_contents_file()
|
||||
|
@ -943,7 +881,7 @@ class Package(metaclass=PackageCreator):
|
|||
return self.contents_dictionary[file_path]['type']
|
||||
return None
|
||||
|
||||
def sort_contents_dictionary(self) -> NoReturn:
|
||||
def sort_contents_dictionary(self) -> None:
|
||||
'''Метод для сортировки словаря, полученного в результате разбора и
|
||||
изменения CONTENTS-файла.'''
|
||||
tree = {}
|
||||
|
@ -980,7 +918,7 @@ class Package(metaclass=PackageCreator):
|
|||
if level[part]:
|
||||
yield from self._make_paths(part_path, level[part])
|
||||
|
||||
def add_dir(self, file_name: str) -> NoReturn:
|
||||
def add_dir(self, file_name: str) -> None:
|
||||
'''Метод для добавления в CONTENTS директорий.'''
|
||||
file_name = self.remove_chroot_path(file_name)
|
||||
|
||||
|
@ -992,7 +930,7 @@ class Package(metaclass=PackageCreator):
|
|||
self.contents_dictionary[file_name] = contents_item
|
||||
|
||||
def add_sym(self, file_name: str, target_path: Optional[str] = None,
|
||||
mtime: Optional[str] = None) -> NoReturn:
|
||||
mtime: Optional[str] = None) -> None:
|
||||
'''Метод для добавления в CONTENTS символьных ссылок.'''
|
||||
real_path = file_name
|
||||
|
||||
|
@ -1018,7 +956,7 @@ class Package(metaclass=PackageCreator):
|
|||
self.contents_dictionary[file_name] = contents_item
|
||||
|
||||
def add_obj(self, file_name: str, file_md5: Optional[str] = None,
|
||||
mtime: Optional[str] = None) -> NoReturn:
|
||||
mtime: Optional[str] = None) -> None:
|
||||
'''Метод для добавления в CONTENTS обычных файлов как obj.'''
|
||||
real_path = file_name
|
||||
file_name = self.remove_chroot_path(file_name)
|
||||
|
@ -1043,7 +981,7 @@ class Package(metaclass=PackageCreator):
|
|||
'mtime': mtime})
|
||||
self.contents_dictionary[file_name] = contents_item
|
||||
|
||||
def add_file(self, file_name: str) -> NoReturn:
|
||||
def add_file(self, file_name: str) -> None:
|
||||
'''Метод для добавления в CONTENTS файла любого типа.'''
|
||||
if file_name != '/':
|
||||
real_path = file_name
|
||||
|
@ -1197,7 +1135,7 @@ class Package(metaclass=PackageCreator):
|
|||
return '<Package: {}/{}>'.format(self.package_name.category,
|
||||
self.package_name.fullname)
|
||||
|
||||
# def __del__(self) -> NoReturn:
|
||||
# def __del__(self) -> None:
|
||||
# if self.autosave and os.path.exists(self.contents_file_path):
|
||||
# self.remove_empty_directories()
|
||||
# self.write_contents_file()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from calculate.utils.files import grep_file
|
||||
import os
|
||||
|
||||
|
||||
class SystemType:
|
||||
"""
|
||||
Тип контейнера текущей системы
|
||||
|
@ -21,12 +22,12 @@ class SystemType:
|
|||
if grep_file("/proc/cpuinfo", "UML"):
|
||||
return cls.Uml
|
||||
elif grep_file("/proc/self/status",
|
||||
"(s_context|VxID):\s*[1-9]"):
|
||||