Browse Source

Fixed force parameter. Added new worker ipc, some pydantic response and requests schemas and server api description.

master
Иванов Денис 4 months ago
parent
commit
c0a552a1cf
70 changed files with 5733 additions and 2069 deletions
  1. +2
    -2
      calculate/commands/commands.py
  2. +3
    -3
      calculate/parameters/parameters.py
  3. +10
    -10
      calculate/scripts/scripts.py
  4. +0
    -0
      calculate/server/database/db.py
  5. +0
    -0
      calculate/server/database/users.py
  6. +0
    -0
      calculate/server/database/workers.py
  7. +830
    -0
      calculate/server/interfaces.txt
  8. +165
    -0
      calculate/server/ipc_draft/async_client.py
  9. +1
    -0
      calculate/server/ipc_draft/cl-command-0
  10. +1
    -0
      calculate/server/ipc_draft/cl-command-1
  11. +1
    -0
      calculate/server/ipc_draft/cl-command-2
  12. +1
    -0
      calculate/server/ipc_draft/cl-command-3
  13. +183
    -0
      calculate/server/ipc_draft/client.py
  14. +77
    -0
      calculate/server/ipc_draft/commands.py
  15. +121
    -0
      calculate/server/ipc_draft/daemon.py
  16. +143
    -0
      calculate/server/ipc_draft/example/ipc.py
  17. +830
    -0
      calculate/server/ipc_draft/interfaces.txt
  18. +100
    -0
      calculate/server/ipc_draft/io_module.py
  19. +202
    -0
      calculate/server/ipc_draft/schemas.py
  20. +55
    -0
      calculate/server/ipc_draft/scripts.py
  21. +282
    -0
      calculate/server/ipc_draft/server.py
  22. +199
    -0
      calculate/server/ipc_draft/utils.py
  23. +413
    -0
      calculate/server/ipc_draft/worker.py
  24. +428
    -0
      calculate/server/ipc_draft/worker_old.py
  25. +57
    -47
      calculate/server/routers/commands.py
  26. +0
    -0
      calculate/server/schemas/commands.py
  27. +22
    -0
      calculate/server/schemas/requests.py
  28. +180
    -0
      calculate/server/schemas/responses.py
  29. +49
    -22
      calculate/server/server.py
  30. +111
    -18
      calculate/server/server_data.py
  31. +1
    -1
      calculate/server/utils/auth.py
  32. +5
    -0
      calculate/server/utils/daemon.py
  33. +122
    -0
      calculate/server/utils/responses.py
  34. +25
    -14
      calculate/server/utils/users.py
  35. +470
    -152
      calculate/server/utils/workers.py
  36. +2
    -2
      calculate/templates/format/backgrounds_format.py
  37. +2
    -0
      calculate/templates/format/base_format.py
  38. +18
    -4
      calculate/templates/format/bind_format.py
  39. +1
    -1
      calculate/templates/format/dovecot_format.py
  40. +13
    -4
      calculate/templates/format/kde_format.py
  41. +11
    -4
      calculate/templates/format/kernel_format.py
  42. +1
    -2
      calculate/templates/format/patch_format.py
  43. +13
    -3
      calculate/templates/format/procmail_format.py
  44. +0
    -2
      calculate/templates/format/samba_format.py
  45. +67
    -54
      calculate/templates/template_engine.py
  46. +74
    -71
      calculate/templates/template_processor.py
  47. +0
    -89
      calculate/utils/calculateini.py
  48. +0
    -14
      calculate/utils/contents_template
  49. +7
    -6
      calculate/utils/gentoo.py
  50. +0
    -0
      calculate/utils/ldap.py
  51. +18
    -80
      calculate/utils/package.py
  52. +3
    -2
      calculate/utils/system.py
  53. +3
    -0
      calculate/utils/tools.py
  54. +15
    -11
      calculate/variables/datavars.py
  55. +65
    -55
      calculate/variables/loader.py
  56. +2
    -4
      calculate/vars/main/__init__.py
  57. +1
    -0
      calculate/vars/main/cl/__init__.py
  58. +5
    -0
      calculate/vars/main/os/__init__.py
  59. +0
    -1339
      output
  60. +2
    -0
      pytest.ini
  61. +6
    -3
      run_server.py
  62. +3
    -0
      run_templates.py
  63. +7
    -7
      tests/parameters/test_parameters.py
  64. +43
    -19
      tests/scripts/test_scripts.py
  65. +232
    -0
      tests/server/test_responses.py
  66. +25
    -23
      tests/server/test_server.py
  67. +1
    -1
      tests/templates/test_template_executor.py
  68. +2
    -0
      tests/templates/test_template_filters.py
  69. +1
    -0
      tests/templates/testfiles/test_executor_root/var/db/pkg/test-category/test-package-1.0/CONTENTS
  70. +1
    -0
      tests/variables/test_calculateini.py

+ 2
- 2
calculate/commands/commands.py View File

@@ -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


+ 3
- 3
calculate/parameters/parameters.py View File

@@ -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


+ 10
- 10
calculate/scripts/scripts.py View File

@@ -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 = '',


calculate/server/models/database.py → calculate/server/database/db.py View File


calculate/server/models/users.py → calculate/server/database/users.py View File


calculate/server/models/workers.py → calculate/server/database/workers.py View File


+ 830
- 0
calculate/server/interfaces.txt View 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
- 0
calculate/server/ipc_draft/async_client.py View 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
- 0
calculate/server/ipc_draft/cl-command-0 View File

@@ -0,0 +1 @@
client.py

+ 1
- 0
calculate/server/ipc_draft/cl-command-1 View File

@@ -0,0 +1 @@
client.py

+ 1
- 0
calculate/server/ipc_draft/cl-command-2 View File

@@ -0,0 +1 @@
client.py

+ 1
- 0
calculate/server/ipc_draft/cl-command-3 View File

@@ -0,0 +1 @@
client.py

+ 183
- 0
calculate/server/ipc_draft/client.py View 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
- 0
calculate/server/ipc_draft/commands.py View 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
- 0
calculate/server/ipc_draft/daemon.py View 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
- 0
calculate/server/ipc_draft/example/ipc.py View 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
- 0
calculate/server/ipc_draft/interfaces.txt View 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"
}
}
}
}