فهرست منبع

SVI Перенос кода; 100.0%

SVI 1 سال پیش
والد
کامیت
d3589a5b2f
100فایلهای تغییر یافته به همراه7257 افزوده شده و 1 حذف شده
  1. 11 0
      .env
  2. 21 0
      .gitignore
  3. 116 0
      LICENSE.md
  4. 25 0
      Makefile
  5. 90 1
      README.md
  6. 25 0
      cmd/demo/main.go
  7. 20 0
      cmd/demo/main_test.go
  8. 12 0
      demo.sh
  9. 20 0
      docs/img/coverage.svg
  10. 31 0
      go.mod
  11. 164 0
      go.sum
  12. 76 0
      kc/helpers/helpers.go
  13. 126 0
      kc/helpers/helpers_test.go
  14. 74 0
      kc/helpers/result.txt
  15. 63 0
      kc/local_ctx/ctx_value/ctx_value.go
  16. 47 0
      kc/local_ctx/ctx_value/ctx_value_test.go
  17. 104 0
      kc/local_ctx/local_ctx.go
  18. 83 0
      kc/local_ctx/local_ctx_test.go
  19. 119 0
      kc/local_ctx/lst_sort/lst_sort.go
  20. 94 0
      kc/local_ctx/lst_sort/lst_sort_test.go
  21. 227 0
      kc/log_buf/log_buf.go
  22. 48 0
      kc/log_buf/log_buf_test.go
  23. 71 0
      kc/log_buf/log_msg/log_msg.go
  24. 55 0
      kc/log_buf/log_msg/log_msg_test.go
  25. 41 0
      kc/safe_bool/safe_bool.go
  26. 59 0
      kc/safe_bool/safe_bool_test.go
  27. 41 0
      kc/safe_int/safe_int.go
  28. 59 0
      kc/safe_int/safe_int_test.go
  29. 48 0
      kc/safe_string/safe_string.go
  30. 60 0
      kc/safe_string/safe_string_test.go
  31. 125 0
      kern.go
  32. 115 0
      kern_test.go
  33. 19 0
      krn/kalias/kalias.go
  34. 7 0
      krn/kalias/kalias_test.go
  35. 57 0
      krn/kbus/dict_sub_hook/dict_sub_hook.go
  36. 78 0
      krn/kbus/dict_sub_hook/dict_sub_hook_test.go
  37. 143 0
      krn/kbus/dict_topic_serve/dict_topic_serve.go
  38. 159 0
      krn/kbus/dict_topic_serve/dict_topic_serve_test.go
  39. 79 0
      krn/kbus/dict_topic_sub/dict_topic_sub.go
  40. 147 0
      krn/kbus/dict_topic_sub/dict_topic_sub_test.go
  41. 146 0
      krn/kbus/kbus_base/kbus_base.go
  42. 191 0
      krn/kbus/kbus_base/kbus_base_test.go
  43. 219 0
      krn/kbus/kbus_http/client_bus_http/client_bus_http.go
  44. 391 0
      krn/kbus/kbus_http/client_bus_http/client_bus_http_test.go
  45. 204 0
      krn/kbus/kbus_http/kbus_http.go
  46. 549 0
      krn/kbus/kbus_http/kbus_http_test.go
  47. 20 0
      krn/kbus/kbus_local/client_bus_local/client_bus_local.go
  48. 32 0
      krn/kbus/kbus_local/client_bus_local/client_bus_local_test.go
  49. 27 0
      krn/kbus/kbus_local/kbus_local.go
  50. 49 0
      krn/kbus/kbus_local/kbus_local_test.go
  51. 31 0
      krn/kbus/kbus_msg/msg_pub/msg_pub.go
  52. 63 0
      krn/kbus/kbus_msg/msg_pub/msg_pub_test.go
  53. 32 0
      krn/kbus/kbus_msg/msg_serve/msg_serve.go
  54. 63 0
      krn/kbus/kbus_msg/msg_serve/msg_serve_test.go
  55. 33 0
      krn/kbus/kbus_msg/msg_sub/msg_sub.go
  56. 63 0
      krn/kbus/kbus_msg/msg_sub/msg_sub_test.go
  57. 30 0
      krn/kbus/kbus_msg/msg_unsub/msg_unsub.go
  58. 62 0
      krn/kbus/kbus_msg/msg_unsub/msg_unsub_test.go
  59. 82 0
      krn/kctx/kctx.go
  60. 53 0
      krn/kctx/kctx_test.go
  61. 80 0
      krn/kctx/kernel_keeper/kernel_keeper.go
  62. 77 0
      krn/kctx/kernel_keeper/kernel_keeper_test.go
  63. 137 0
      krn/kctx/kwg/kwg.go
  64. 156 0
      krn/kctx/kwg/kwg_test.go
  65. 126 0
      krn/kmodule/kmodule.go
  66. 107 0
      krn/kmodule/kmodule_test.go
  67. 80 0
      krn/kmodule/mod_stat/mod_stat.go
  68. 137 0
      krn/kmodule/mod_stat/mod_stat_day/mod_stat_day.go
  69. 45 0
      krn/kmodule/mod_stat/mod_stat_day/mod_stat_day_test.go
  70. 137 0
      krn/kmodule/mod_stat/mod_stat_minute/mod_stat_minute.go
  71. 45 0
      krn/kmodule/mod_stat/mod_stat_minute/mod_stat_minute_test.go
  72. 159 0
      krn/kmodule/mod_stat/mod_stat_sec/mod_stat_sec.go
  73. 53 0
      krn/kmodule/mod_stat/mod_stat_sec/mod_stat_sec_test.go
  74. 60 0
      krn/kmodule/mod_stat/mod_stat_test.go
  75. 124 0
      krn/kmonolit/kmonolit.go
  76. 122 0
      krn/kmonolit/kmonolit_test.go
  77. 147 0
      krn/kserv_http/kserv_http.go
  78. 119 0
      krn/kserv_http/kserv_http_test.go
  79. 4 0
      krn/kserv_http/static/css/bootstrap-grid.min.css
  80. 0 0
      krn/kserv_http/static/css/bootstrap-grid.min.css.map
  81. 4 0
      krn/kserv_http/static/css/bootstrap-grid.rtl.min.css
  82. 0 0
      krn/kserv_http/static/css/bootstrap-grid.rtl.min.css.map
  83. 4 0
      krn/kserv_http/static/css/bootstrap-reboot.min.css
  84. 0 0
      krn/kserv_http/static/css/bootstrap-reboot.min.css.map
  85. 4 0
      krn/kserv_http/static/css/bootstrap-reboot.rtl.min.css
  86. 0 0
      krn/kserv_http/static/css/bootstrap-reboot.rtl.min.css.map
  87. 4 0
      krn/kserv_http/static/css/bootstrap-utilities.min.css
  88. 0 0
      krn/kserv_http/static/css/bootstrap-utilities.min.css.map
  89. 4 0
      krn/kserv_http/static/css/bootstrap-utilities.rtl.min.css
  90. 0 0
      krn/kserv_http/static/css/bootstrap-utilities.rtl.min.css.map
  91. 4 0
      krn/kserv_http/static/css/bootstrap.min.css
  92. 0 0
      krn/kserv_http/static/css/bootstrap.min.css.map
  93. 4 0
      krn/kserv_http/static/css/bootstrap.rtl.min.css
  94. 0 0
      krn/kserv_http/static/css/bootstrap.rtl.min.css.map
  95. 5 0
      krn/kserv_http/static/js/bootstrap.bundle.min.js
  96. 0 0
      krn/kserv_http/static/js/bootstrap.bundle.min.js.map
  97. 5 0
      krn/kserv_http/static/js/bootstrap.esm.min.js
  98. 0 0
      krn/kserv_http/static/js/bootstrap.esm.min.js.map
  99. 5 0
      krn/kserv_http/static/js/bootstrap.min.js
  100. 0 0
      krn/kserv_http/static/js/bootstrap.min.js.map

+ 11 - 0
.env

@@ -0,0 +1,11 @@
+
+# Предыдущую пустую строку НЕ УДАЛЯТЬ!!! НУЖНА ДЛЯ ТЕСТОВ!!!
+# Переменная окружения [local, prod]
+STAGE=local
+
+# URL для локального HTTP-сервера
+LOCAL_HTTP_URL="http://localhost:18200/"
+
+# Путь для локального хранилища (нужен, если локальное хранилище используется)
+LOCAL_STORE_PATH=/store
+

+ 21 - 0
.gitignore

@@ -0,0 +1,21 @@
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+# Go workspace file
+go.work
+bin_dev
+.token
+.vscode

+ 116 - 0
LICENSE.md

@@ -0,0 +1,116 @@
+# Универсальная коммунистическая лицензия
+
+`Версия 1.0.6` `2024-10-15 10:13:11 MSK +03`
+
+Сокращённое название:
+
+- русский: УТК;
+- английский: UCL.
+
+## Определения
+
+**Трудящийся**: человек, для которого приоритетным способом обеспечения своей жизни является реализация своего производительного труда каким-либо образом.
+
+**Капиталист**: человек, который является владельцем материальных и трудовых ресурсов, инструментов производства и присваивает в личное владение хотя бы часть прибавочной стоимости в ходе товарного производства.
+
+**Объект лицензирования**: любой результат труда, в отношение которого устанавливаются правовые отношения настоящей лицензией (далее `ОЛ`); `ОЛ` не может быть ничего, кроме артефакта, который потребовал для своего создания производительного труда.
+
+**Автор ОЛ**: человек (или несколько человек), который является автором `ОЛ` и всех сопутствующих материалов, которые он создал непосредственно.
+
+**Потребитель ОЛ**: `трудящийся` или `капиталист`, который использует настоящий `ОЛ`, с соблюдением требований данной лицензии или с нарушением требований данной лицензии.
+
+**Производный объект лицензирования**: `ОЛ` со всеми сопутствующими материалами, в котором доля модификации не превышает 50%; производный `ОЛ` подпадает под действие этих же лицензионных требований в полном объёме.
+
+**Лицензионные требования**:требования на условия использование ОЛ, которые предъявляет автор ОЛ при его использовании потребителем ОЛ. `Лицензионные требования` автоматически вступают в силу, если потребитель ОЛ использует объект лицензирования в любым способом.
+
+```mermaid
+flowchart TD
+  subgraph Потребитель
+    Трудящийся
+    Капиталист
+  end
+  subgraph Права
+    subgraph отчуждаемые
+      ПК[копирование]
+      ПМ[модификация]
+      ПИ[использование]
+    end
+    subgraph неотчуждаемые
+      ПА[авторское]
+      ПВ[владение]
+    end
+    ПК --> ОП[Отчуждаемые права]
+    ПМ --> ОП
+    ПИ --> ОП
+    ПА --> НП[Неотчуждаемые права]
+    ПВ --> НП
+  end
+  ОЛ[Объект лицензирования] --> Права
+  ОП --> Трудящийся
+  ОП --> Автор
+  ОП -.-x Капиталист
+  Автор --> Капиталист
+  НП --> Автор
+```
+
+## Права
+
+**Неотчуждаемое право**: такой вид права, который при любых условиях и/или обстоятельствах не может быть утрачен.
+
+**Авторское право**: неотчуждаемое право автора (или группы авторов) на своё произведение ни при каких условиях; авторское право означает, что автором `ОЛ` является конкретный человек (или группа людей) и ничего более.
+
+**Право владения**: особое, неотчуждаемое право в условиях капитализма, по непосредственному владению `ОЛ` `автором`; возможно отчуждаемое право в условиях социализма на свободном решении отказа  `автора` от права владения `ОЛ` в общественное пользование `трудящимися`.
+
+**Отчуждаемое право**: такой вид права, который может быть передан или отозван по различным основаниям.
+
+**Свободное решение**: решение двух сторон по обладанию отчуждаемыми правами без какого-либо внешнего давления, которое не принуждает любую из сторон  (с явной потерей своей пользы в явную пользу другой стороны или наоборот) передавать или принимать права любой из сторон.
+
+**Право копирования**:  возможное отчуждаемое право, по  `свободному решению`` сторон; копирование -- создание копии ПО. подлежащее данным лицензионным требованиям.
+
+**Право модификации**: возможное отчуждаемое право, по `свободному решению`` сторон; модификация -- доработка, декомпиляция. патчинг и другие виды изменений в отношении ПО, которое подлежит данному лицензированию.
+
+**Право использования**: возможное отчуждаемое право, по свободному решению сторон; использование -- использование `ОЛ` по функциональному назначению _и все действия_, связанные с технологией использования такого `ОЛ`, подлежащему данным лицензионным требованиям; право использования для `капиталиста` -- -- особое индивидуальное разрешение `капиталисту` на использование конкретного `ЛО` или его частью, которое подлежит данным лицензионным требованиям.
+
+## Обязанности сторон
+
+`Автор` в рамках определений в настоящих лицензионных требований _разрешает_ всем `трудящимся` мира использовать права на настоящий `ОЛ`:
+
+- копирования;
+- использования;
+- модификации.
+
+`Автор` в рамках определений настоящих лицензионных требований _запрещает_ всем `капиталистам` мира право копирования, использования, модификации настоящего `ОЛ` и любые права не указанные здесь, если это не оговорено особо или передача прав не является свободной.
+
+`Автор` _явно_ запрещает использование `ОЛ`, в случае его применения любым способом против `трудящихся`.
+
+`Автор` не даёт _никаких_ гарантий в отношении настоящего `ОЛ`:
+
+- необходимого функционала;
+- необходимой архитектуры;
+- необходимых архитектурных свойств;
+- необходимого качества и/или кодирования, материалов;
+- необходимого количества тестирования;
+- необходимых возможностей масштабирования;
+- отсутствия возможного ущерба;
+- возможной упущенной выгоды;
+- иных возможных потерь, которые не упомянуты здесь.
+
+`Автор` может дать дополнительные гарантии по своему усмотрению или договорённости с `потребителем`; подобные договорённости оформляются отдельным приложением и оно имеет приоритет надо отказом от гарантий, перечисленными (или не упомянутыми) выше.
+
+`Автор` гарантирует, что не включает в свой код деструктивный функционал, но оставляет за собой право ограничивать `потребителей` данного `ОЛ` в праве его использовать в случаях нарушения требований настоящей лицензии.
+
+`Трудящиеся` _обязаны_ не передавать права на настоящий `ОЛ` `капиталистам` и не препятствовать передаче этих же прав `трудящимся`, кроме случаев _явного_ использования `трудящимися` настоящего `ОЛ` в пользу `капиталистов`.
+
+В случае использования настоящего `ОЛ` `пользователи` автоматически принимают требования текущей УКЛ, и соглашаются со всеми положениями настоящих лицензионных требований.
+
+Настоящая лицензия _должна_ сопровождать `ОЛ`, в случае передачи отчуждаемых прав.
+
+## Защита лицензионных требований
+
+`Автор` может в любой момент изменить состав `ОЛ` в любых целях, кроме откровенно деструктивных, для защиты лицензионных требований.
+`Автор` может в любой момент изменить версию настоящих лицензионных требований, из-за внесённых им каких-либо изменений в `ОЛ`, но в рамках совместимости с предыдущими лицензионными требованиями.
+`Автор` может в любой момент отозвать отчуждаемые права (кроме особых случаев отчуждаемого права использования), если посчитает, что `потребитель` нарушает настоящие лицензионные требования.
+
+## Заключительные положения
+
+Если, какая-либо сторона настоящих лицензионных требований не оговорена или допускает какую-либо трактовку -- в таком случае, ситуация трактуется приоритетно в пользу `автора`, во вторую очередь в пользу `трудящихся` и никогда в пользу `капиталистов`. Данное требование не распространяется на особые случаи использования с отдельными договорённостями и только на основе свободного решения.

+ 25 - 0
Makefile

@@ -0,0 +1,25 @@
+demo:
+	clear
+	go fmt ./...
+	go build -race -o ./bin_dev/demo ./cmd/demo/main.go
+	./demo.sh
+mod:
+	clear
+	go get -u ./...
+	go mod tidy -compat=1.24.0
+	go mod vendor
+	go fmt ./...
+
+.PHONY: test
+test:
+	clear
+	go fmt ./...
+	go test -race -shuffle=on -timeout=30s -coverprofile=./cover.out ./...
+	go tool cover -func=./cover.out
+
+lint:
+	clear
+	go fmt ./...
+	golangci-lint run ./...
+	gocyclo -over 11 .
+	gosec ./...

+ 90 - 1
README.md

@@ -1,3 +1,92 @@
 # kern
 
-kern -- фреймворк повышенной надёжности для модульных монолитов и модульных микросервисов.
+![Coverage](./docs/img/coverage.svg)
+![MX Linux](https://img.shields.io/badge/-MX%20Linux-%23000000?style=for-the-badge&logo=MXlinux&logoColor=white)
+![Go](https://img.shields.io/badge/go-%2300ADD8.svg?style=for-the-badge&logo=go&logoColor=white)
+![GitHub](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)
+![Visual Studio Code](https://img.shields.io/badge/Visual%20Studio%20Code-0078d7.svg?style=for-the-badge&logo=visual-studio-code&logoColor=white)
+[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
+![Stat](https://starchart.cc/prospero78/kern.svg)
+
+**kern** -- фреймворк модульных компонентов ядра с высокой надёжностью для любого микросервиса или модульного монолита. Позволяет создавать изолированные модули в составе монолита (полезно в начале разработки). А затем модули можно выносить в отдельные микросервисы (после отладки).
+
+## Контакты
+
+Пишите запросы в соответствующем [разделе](https://gitp78su.ipnodns.ru/svi/kern/issue?status=).
+
+## Статус проекта
+
+* готовность: `92%`;
+* покрытие тестами: `100%`;
+* линтеры: `no errors`;
+* цикломатическая сложность: `< 11`.
+
+## Состав
+
+Команда вывода дерева:
+
+```bash
+tree -I vendor -I bin_dev -d
+```
+
+```bash
+.
+├── kc                # Вспомогательные компоненты
+│   ├── helpers       # Жёсткий и мягкий assert
+│   ├── local_ctx     # Локальный контекст
+│   │   └── ctx_value # Переменная контекста с метаинформацией
+│   ├── log_buf       # Буферизованный лог
+│   │   └── log_msg   # Сообщение буферизованного лога
+│   └── safe_bool     # Потокобезопасная булева переменная
+├── krn                          # Компоненты ядра
+│   ├── kalias                   # Алиасы типов ядра
+│   ├── kbus                     # Шина данных ядра
+│   │   ├── dict_sub_hook        # Словарь обработчиков подписок
+│   │   ├── dict_topic_serve     # Список топиков для обработчиков входящих запросов
+│   │   ├── dict_topic_sub       # Список топиков подписки
+│   │   ├── kbus_base            # БазоваЯ шина данных
+│   │   ├── kbus_http            # Шина данных поверх HTTP
+│   │   │   └── client_bus_http  # Клиент для шины данных поверх HTTP
+│   │   ├── kbus_local           # Локальная шина данных
+│   │   │   └── client_bus_local # Клиент для локальной шины данных
+│   │   └── kbus_msg          # Сообщения для всех сетевых шин
+│   │       ├── msg_pub       # Сообщения для публикации
+│   │       ├── msg_serve     # Сообщения для запросов
+│   │       ├── msg_sub       # Сообщения для подписки
+│   │       └── msg_unsub     # Сообщения для отписки
+│   ├── kctx              # Контекст ядра
+│   │   ├── kernel_keeper # Сторож сигналов ОС
+│   │   └── kwg           # Именованный ожидатель групп
+│   ├── kmodule   # Компонент модуль ядра
+│   ├── kmonolit  # Компонент модульного монолита ядра
+│   ├── kserv_http      # Компонент встроенного быстрого HTTP-сервера (fiber)
+│   │   └── static      # Встраиваемые статические файлы
+│   │       ├── css     # Встраиваемые стили (bootstrap)
+│   │       └── js      # Встраиваемые скрипты (htmx, hyperscript, bootstrap)
+│   ├── kstore_kv   # Встраиваемое быстрое key:value хранилище (Badger)
+│   └── ktypes  # Интерфейсы ядра
+├── mds   # Типовые модули ядра
+└── mock        # Мок-объекты для тестирования и экспериментов
+    ├── mock_env            # Мок-окружение для запуска компонентов ядра
+    ├── mock_hand_serve     # Мок-обработчик входящих запросов
+    ├── mock_hand_sub_http  # Мок-обработчик подписки через HTTP-шину
+    └── mock_hand_sub_local # Мок-обработчик подписки через локальную шину
+```
+
+## Версия компилятора
+
+Не ниже `go 1.24.0`
+
+## Лицензия
+
+Код открытый, [лицензия MIT](./LICENSE.txt)
+
+## Команды сборки
+
+```bash
+make      # Запуск демо
+make demo # -//-
+make mod  # Обновление зависимостей
+make test # Запуск тестов
+make lint # Запуск линтеров
+```

+ 25 - 0
cmd/demo/main.go

@@ -0,0 +1,25 @@
+// package main -- пускач для демонстратора монолита
+package main
+
+import (
+	"gitp78su.ipnodns.ru/svi/kern"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+var app IKernelMonolit
+
+func main() {
+	app = kern.NewMonolitLocal("Demo monolit")
+
+	modServHttp := kern.NewModuleServHttp()
+	app.Add(modServHttp)
+
+	modKernelCtx := kern.NewModuleKernelCtx()
+	app.Add(modKernelCtx)
+
+	modKernKeep := kern.NewModuleKernelKeeper()
+	app.Add(modKernKeep)
+
+	app.Run()
+	app.Wait()
+}

+ 20 - 0
cmd/demo/main_test.go

@@ -0,0 +1,20 @@
+package main
+
+import (
+	"os"
+	"testing"
+	"time"
+
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_env"
+)
+
+func TestMain(t *testing.T) {
+	_ = mock_env.MakeEnv()
+	_ = os.Unsetenv("LOCAL_HTTP_URL")
+	os.Setenv("LOCAL_HTTP_URL", "http://localhost:18330/")
+	go main()
+	time.Sleep(time.Second * 2)
+	kCtx := kctx.GetKernelCtx()
+	kCtx.Cancel()
+}

+ 12 - 0
demo.sh

@@ -0,0 +1,12 @@
+# Предыдущую пустую строку НЕ УДАЛЯТЬ!!! НУЖНА ДЛЯ ТЕСТОВ!!!
+# Переменная окружения [local, prod]
+export STAGE=local
+
+# URL для локального HTTP-сервера
+export LOCAL_HTTP_URL="http://localhost:18200/"
+
+# Путь для локального хранилища (нужен, если локальное хранилище используется)
+export LOCAL_STORE_PATH=/store
+
+cd ./bin_dev && \
+./demo

+ 20 - 0
docs/img/coverage.svg

@@ -0,0 +1,20 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="103.3" height="20" viewBox="0 0 1033 200" role="img" aria-label="coverage: 100%">
+  <title>coverage: 100%</title>
+  <linearGradient id="enYYm" x2="0" y2="100%">
+    <stop offset="0" stop-opacity=".1" stop-color="#EEE"/>
+    <stop offset="1" stop-opacity=".1"/>
+  </linearGradient>
+  <mask id="XTWfx"><rect width="1033" height="200" rx="30" fill="#FFF"/></mask>
+  <g mask="url(#XTWfx)">
+    <rect width="603" height="200" fill="#555"/>
+    <rect width="430" height="200" fill="#3C1" x="603"/>
+    <rect width="1033" height="200" fill="url(#enYYm)"/>
+  </g>
+  <g aria-hidden="true" fill="#fff" text-anchor="start" font-family="Verdana,DejaVu Sans,sans-serif" font-size="110">
+    <text x="60" y="148" textLength="503" fill="#000" opacity="0.25">coverage</text>
+    <text x="50" y="138" textLength="503">coverage</text>
+    <text x="658" y="148" textLength="330" fill="#000" opacity="0.25">100%</text>
+    <text x="648" y="138" textLength="330">100%</text>
+  </g>
+
+</svg>

+ 31 - 0
go.mod

@@ -0,0 +1,31 @@
+module gitp78su.ipnodns.ru/svi/kern
+
+go 1.24.0
+
+require (
+	github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b
+	github.com/dgraph-io/badger/v4 v4.5.1
+	github.com/gofiber/fiber/v2 v2.52.6
+	github.com/google/uuid v1.6.0
+)
+
+require (
+	github.com/andybalholm/brotli v1.1.1 // indirect
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/dgraph-io/ristretto/v2 v2.1.0 // indirect
+	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
+	github.com/google/flatbuffers v25.2.10+incompatible // indirect
+	github.com/klauspost/compress v1.18.0 // indirect
+	github.com/mattn/go-colorable v0.1.14 // indirect
+	github.com/mattn/go-isatty v0.0.20 // indirect
+	github.com/mattn/go-runewidth v0.0.16 // indirect
+	github.com/pkg/errors v0.9.1 // indirect
+	github.com/rivo/uniseg v0.4.7 // indirect
+	github.com/valyala/bytebufferpool v1.0.0 // indirect
+	github.com/valyala/fasthttp v1.59.0 // indirect
+	go.opencensus.io v0.24.0 // indirect
+	golang.org/x/net v0.35.0 // indirect
+	golang.org/x/sys v0.30.0 // indirect
+	google.golang.org/protobuf v1.36.5 // indirect
+)

+ 164 - 0
go.sum

@@ -0,0 +1,164 @@
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
+github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY=
+github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk=
+github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b h1:slYM766cy2nI3BwyRiyQj/Ud48djTMtMebDqepE95rw=
+github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM=
+github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
+github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
+github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dgraph-io/badger/v4 v4.5.1 h1:7DCIXrQjo1LKmM96YD+hLVJ2EEsyyoWxJfpdd56HLps=
+github.com/dgraph-io/badger/v4 v4.5.1/go.mod h1:qn3Be0j3TfV4kPbVoK0arXCD1/nr1ftth6sbL5jxdoA=
+github.com/dgraph-io/ristretto/v2 v2.1.0 h1:59LjpOJLNDULHh8MC4UaegN52lC4JnO2dITsie/Pa8I=
+github.com/dgraph-io/ristretto/v2 v2.1.0/go.mod h1:uejeqfYXpUomfse0+lO+13ATz4TypQYLJZzBSAemuB4=
+github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WAFKLNi6ZS0675eEUC9y3AlwSbQu1Y=
+github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/gofiber/fiber/v2 v2.52.6 h1:Rfp+ILPiYSvvVuIPvxrBns+HJp8qGLDnLJawAu27XVI=
+github.com/gofiber/fiber/v2 v2.52.6/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q=
+github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
+github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
+github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
+github.com/valyala/fasthttp v1.59.0 h1:Qu0qYHfXvPk1mSLNqcFtEk6DpxgA26hy6bmydotDpRI=
+github.com/valyala/fasthttp v1.59.0/go.mod h1:GTxNb9Bc6r2a9D0TWNSPwDz78UxnTGBViY3xZNEqyYU=
+github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
+github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0=
+go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
+golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
+golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
+golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
+google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=

+ 76 - 0
kc/helpers/helpers.go

@@ -0,0 +1,76 @@
+// package helpers -- содержит всякие полезняшки
+//
+// Пакет импортировать где нужно в нотации `. "gitlab.c2g.pw/back/uaj-abstract-client/pkg/helpers"`
+package helpers
+
+import (
+	"fmt"
+	"os"
+	"time"
+
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+)
+
+var (
+	IsStageLocal bool
+	IsStageProd  bool
+)
+
+// Assert -- проверка на правильность утверждения с падением в панику на локальном стенде (soft assert)
+func Assert(isCond bool, msgFormat string, args ...interface{}) {
+	if isCond {
+		return
+	}
+	msg := fmt.Sprintf("SOFT ASSERT "+msgFormat+"\n", args...)
+	if IsStageLocal {
+		panic(msg)
+	}
+	fmt.Print(msg)
+}
+
+// Hassert -- проверка на правильность утверждения с безусловным падением в панику (hard assert)
+func Hassert(isCond bool, msgFormat string, args ...interface{}) {
+	if isCond {
+		return
+	}
+	msg := fmt.Sprintf("HARD ASSERT "+msgFormat+"\n", args...)
+	panic(msg)
+}
+
+// TimeNowStr -- возвращает стандартную строку локального сейчас-времени "2006-01-02 15:04:05.000 -07 MST"
+func TimeNowStr() ATime {
+	strTime := time.Now().Local().Format("2006-01-02 15:04:05.000 -07 MST")
+	return ATime(strTime)
+}
+
+// TimeNow -- возвращает Unix сейчас-время (мсек, не зависит от положения)
+func TimeNow() int64 {
+	timeNow := time.Now().Local().UnixMilli()
+	return timeNow
+}
+
+// SleepMs -- спит миллисекунду
+func SleepMs() {
+	time.Sleep(time.Millisecond * 1)
+}
+
+func init_() {
+	strStage := os.Getenv("STAGE")
+	switch strStage {
+	case "local":
+		IsStageLocal = true
+		IsStageProd = false
+	case "prod":
+		IsStageProd = true
+		IsStageLocal = false
+	case "":
+		IsStageLocal = true
+		IsStageProd = false
+	default:
+		panic(fmt.Sprintf("lepers.init_(): unknown env STAGE (%v)\n", strStage))
+	}
+}
+
+func init() {
+	init_()
+}

+ 126 - 0
kc/helpers/helpers_test.go

@@ -0,0 +1,126 @@
+package helpers
+
+import (
+	"os"
+	"testing"
+)
+
+type tester struct {
+	t *testing.T
+}
+
+func TestHelpers(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.assert()
+	sf.hassert()
+	sf.init_()
+}
+
+// Неизвестное значение STAGE
+func (sf *tester) init_() {
+	sf.t.Log("init_")
+	sf.initBad1()
+	_ = os.Unsetenv("STAGE")
+	_ = os.Setenv("STAGE", "local")
+	init_()
+}
+
+func (sf *tester) initBad1() {
+	sf.t.Log("initBad1")
+	defer func() {
+		if panic_ := recover(); panic_ == nil {
+			sf.t.Fatalf("initBad1(): panic==nil")
+		}
+	}()
+	_ = os.Unsetenv("STAGE")
+	_ = os.Setenv("STAGE", "tra-lala")
+	init_()
+}
+
+// Проверка мягкого ассерта
+func (sf *tester) hassert() {
+	sf.t.Log("assert")
+	sf.hassertLocal()
+	sf.hassertProd()
+	sf.hassertProdGood1()
+	if strTime := TimeNowStr(); strTime == "" {
+		sf.t.Fatalf("hassert(): strTime==''")
+	}
+	if unixTime := TimeNow(); unixTime == 0 {
+		sf.t.Fatalf("hassert(): unixTime==0")
+	}
+	SleepMs()
+}
+
+// Мягкая ТВЁРДАЯ проверка на ок
+func (sf *tester) hassertProdGood1() {
+	sf.t.Log("hassertProdGood1")
+	Hassert(true, "tra-la-la")
+}
+
+// Твёрдая проверка
+func (sf *tester) hassertProd() {
+	sf.t.Log("hassertProd")
+	err := os.Setenv("STAGE", "prod")
+	if err != nil {
+		sf.t.Fatalf("hassertProd(): err=%v", err)
+	}
+	init_()
+	defer func() {
+		if panic_ := recover(); panic_ == nil {
+			sf.t.Fatalf("assertLocal(): panic==nil")
+		}
+	}()
+	Hassert(false, "tra-la-la")
+}
+
+// ТВЁРДАЯ проверка
+func (sf *tester) hassertLocal() {
+	sf.t.Log("hassertLocal")
+	defer func() {
+		if panic_ := recover(); panic_ == nil {
+			sf.t.Fatalf("assertLocal(): panic==nil")
+		}
+	}()
+	Hassert(false, "tra-la-la")
+}
+
+//========================================================================
+
+// Проверка мягкого ассерта
+func (sf *tester) assert() {
+	sf.t.Log("assert")
+	sf.assertLocal()
+	sf.assertProd()
+	sf.assertProdGood1()
+}
+
+// Мягкая ТВЁРДАЯ проверка на ок
+func (sf *tester) assertProdGood1() {
+	sf.t.Log("assertProdGood1")
+	Assert(true, "tra-la-la")
+}
+
+// Мягкая мягка проверка (на проде)
+func (sf *tester) assertProd() {
+	sf.t.Log("assertProd")
+	err := os.Setenv("STAGE", "prod")
+	if err != nil {
+		sf.t.Fatalf("assertProd(): err=%v", err)
+	}
+	init_()
+	Assert(false, "tra-la-la")
+}
+
+// Мягкая ТВЁРДАЯ локальная проверка (локально)
+func (sf *tester) assertLocal() {
+	sf.t.Log("assertLocal")
+	defer func() {
+		if panic_ := recover(); panic_ == nil {
+			sf.t.Fatalf("assertLocal(): panic==nil")
+		}
+	}()
+	Assert(false, "tra-la-la")
+}

+ 74 - 0
kc/helpers/result.txt

@@ -0,0 +1,74 @@
+package helpers
+
+// Result — аналог Result<T, E> из Rust
+//
+// Может быть либотолько полезное значение, либо только ошибка
+type Result[T any] struct {
+	value T     // Полезное значение
+	err   error // Ошибка
+}
+
+// NewResult -- возвращает успешный Result с значением
+func NewResult[T *any](result T) *Result[T] {
+	Hassert(result != nil, "NewResult(): result==nil")
+	return &Result[T]{
+		value: result,
+	}
+}
+
+// NewResultErr -- возвращает Result с ошибкой
+func NewResultErr[T any](err error) *Result[T] {
+	Hassert(err != nil, "NewError(): err==nil")
+	return &Result[T]{
+		err: err,
+	}
+}
+
+// IsOk -- возвращает true, если Result содержит значение
+func (sf *Result[T]) IsOk() bool {
+	return sf.err == nil
+}
+
+// IsErr -- возвращает true, если Result содержит ошибку
+func (sf *Result[T]) IsErr() bool {
+	return sf.err != nil
+}
+
+// Unwrap -- возвращает значение, если оно есть, иначе паникует
+func (sf *Result[T]) Unwrap() T {
+	if sf.err != nil {
+		panic(sf.err)
+	}
+	return sf.value
+}
+
+// UnwrapOr -- возвращает значение, если оно есть, или значение по умолчанию
+func (sf *Result[T]) UnwrapOr(defaultValue T) T {
+	if sf.IsErr() {
+		return defaultValue
+	}
+	return sf.value
+}
+
+// UnwrapOrElse -- возвращает значение, если оно есть, или результат выполнения функции
+func (sf *Result[T]) UnwrapOrElse(f func() T) T {
+	if sf.IsErr() {
+		return f()
+	}
+	return sf.value
+}
+
+// Error -- возвращает ошибку, если она есть
+func (sf *Result[T]) Error() error {
+	return sf.err
+}
+
+// HAssert -- проверяет, что нет ошибки (с паникой)
+func (sf *Result[T]) Hassert(msg string) {
+	Hassert(sf.err != nil, msg+", err=\n\t%v\n", sf.err)
+}
+
+// Assert -- проверяет, что нет ошибки (с паникой только на локальном стенде)
+func (sf *Result[T]) Assert(msg string) {
+	Assert(sf.err != nil, msg+", err=\n\t%v\n", sf.err)
+}

+ 63 - 0
kc/local_ctx/ctx_value/ctx_value.go

@@ -0,0 +1,63 @@
+// package ctx_value -- потокобезопасное значение локального контекста
+package ctx_value
+
+import (
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+// CtxValue -- потокобезопасное значение локального контекста
+type CtxValue struct {
+	sync.RWMutex
+	key       string
+	createAt  ATime
+	Val_      any
+	UpdateAt_ ATime
+	Comment_  string
+}
+
+// NewCtxValue -- возвращает новое потокобезопасное значение локального контекста
+func NewCtxValue(key string, val any, comment string) ICtxValue {
+	Hassert(key != "", "NewCtxValue(): key is empty")
+	sf := &CtxValue{
+		key:      key,
+		createAt: TimeNowStr(),
+		Val_:     val,
+		Comment_: comment,
+	}
+	return sf
+}
+
+// Key -- возвращает ключ значения
+func (sf *CtxValue) Key() string {
+	return sf.key
+}
+
+// Val -- возвращает хранимое значение
+func (sf *CtxValue) Val() any {
+	sf.RLock()
+	defer sf.RUnlock()
+	return sf.Val_
+}
+
+// UpdateAt -- возвращает время обновления значения
+func (sf *CtxValue) UpdateAt() ATime {
+	sf.RLock()
+	defer sf.RUnlock()
+	return sf.UpdateAt_
+}
+
+// CreateAt -- возвращает время создания значения
+func (sf *CtxValue) CreateAt() ATime {
+	return sf.createAt
+}
+
+// Comment -- возвращает комментарий значения
+func (sf *CtxValue) Comment() string {
+	sf.RLock()
+	defer sf.RUnlock()
+	return sf.Comment_
+}

+ 47 - 0
kc/local_ctx/ctx_value/ctx_value_test.go

@@ -0,0 +1,47 @@
+package ctx_value
+
+import (
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+type tester struct {
+	t      *testing.T
+	val    ICtxValue
+	create ATime
+}
+
+func TestCtxValue(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+}
+
+// Создаёт значение локального контекста
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.val = NewCtxValue("test_key", 5, "test_value")
+	if sf.val == nil {
+		sf.t.Fatalf("new(): val==nil")
+	}
+	if val := sf.val.Val().(int); val != 5 {
+		sf.t.Fatalf("new(): val(%v)!=5", val)
+	}
+	create := sf.val.CreateAt()
+	if create == "" {
+		sf.t.Fatalf("new(): create is empty")
+	}
+	sf.create = create
+	if comment := sf.val.Comment(); comment != "test_value" {
+		sf.t.Fatalf("new(): comment(%v)!='test_value'", comment)
+	}
+	if key := sf.val.Key(); key != "test_key" {
+		sf.t.Fatalf("new(): key(%v)!='test_key'", key)
+	}
+	if update := sf.val.UpdateAt(); update != "" {
+		sf.t.Fatalf("new(): update not empty")
+	}
+}

+ 104 - 0
kc/local_ctx/local_ctx.go

@@ -0,0 +1,104 @@
+// package local_ctx -- локальный контекст
+package local_ctx
+
+import (
+	"context"
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/kc/local_ctx/ctx_value"
+	"gitp78su.ipnodns.ru/svi/kern/kc/local_ctx/lst_sort"
+	"gitp78su.ipnodns.ru/svi/kern/kc/log_buf"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+// LocalCtx -- локальный контекст
+type LocalCtx struct {
+	sync.RWMutex
+	ctx      context.Context // Отменяемый контекст
+	fnCancel func()          // Функция отмены контекста
+
+	dictVal map[string]ICtxValue // Словарь различных значений
+	lstSort *lst_sort.LstSort    // Сортированный список значений
+	log     ILogBuf              // Локальный буфер
+}
+
+// NewLocalCtx -- возвращает новый локальный контекст
+func NewLocalCtx(ctx context.Context) ILocalCtx {
+	Hassert(ctx != nil, "NewLocalCtx(): ctx==nil")
+	_ctx, fnCancel := context.WithCancel(ctx)
+	sf := &LocalCtx{
+		ctx:      _ctx,
+		fnCancel: fnCancel,
+		dictVal:  map[string]ICtxValue{},
+		lstSort:  lst_sort.NewLstSort(),
+		log:      log_buf.NewLogBuf(),
+	}
+	return sf
+}
+
+// Size -- возвращает размер контекста
+func (sf *LocalCtx) Size() int {
+	sf.RLock()
+	defer sf.RUnlock()
+	return len(sf.dictVal)
+}
+
+// SortedList -- возвращает сортированный список значений
+func (sf *LocalCtx) SortedList() []ICtxValue {
+	return sf.lstSort.List()
+}
+
+// Log -- возвращает локальный буферный лог
+func (sf *LocalCtx) Log() ILogBuf {
+	return sf.log
+}
+
+// Get -- возвращает хранимое значение
+func (sf *LocalCtx) Get(key string) ICtxValue {
+	sf.RLock()
+	defer sf.RUnlock()
+	Hassert(key != "", "localCtx.Get(): key is empty")
+	sf.log.Debug("localCtx.Get(): key='%v'", key)
+	return sf.dictVal[key]
+}
+
+// Del -- удаляет значение из контекста
+func (sf *LocalCtx) Del(key string) {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.log.Debug("localCtx.Del(): key='%v'", key)
+	val := sf.dictVal[key]
+	delete(sf.dictVal, key)
+	sf.lstSort.Del(val)
+}
+
+// Set -- добавляет значение в контекст
+func (sf *LocalCtx) Set(key string, val any, comment string) {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.log.Debug("localCtx.Set(): key='%v'", key)
+	_val, isOk := sf.dictVal[key]
+	if isOk {
+		val0 := _val.(*ctx_value.CtxValue)
+		val0.Lock()
+		val0.UpdateAt_ = TimeNowStr()
+		val0.Val_ = val
+		val0.Unlock()
+		return
+	}
+	_val = ctx_value.NewCtxValue(key, val, comment)
+	sf.dictVal[key] = _val
+	sf.lstSort.Add(_val)
+}
+
+// Done -- блокирующий вызов ожидания отмены контекста
+func (sf *LocalCtx) Done() {
+	<-sf.ctx.Done()
+}
+
+// Cancel -- отменяет контекст
+func (sf *LocalCtx) Cancel() {
+	sf.log.Warn("localCtx.Cancel()")
+	sf.fnCancel()
+}

+ 83 - 0
kc/local_ctx/local_ctx_test.go

@@ -0,0 +1,83 @@
+package local_ctx
+
+import (
+	"context"
+	"testing"
+)
+
+type tester struct {
+	t   *testing.T
+	ctx *LocalCtx
+}
+
+func TestLocalCtx(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+	sf.set()
+	sf.get()
+	sf.del()
+	sf.done()
+}
+
+// Ожидает отмены контекста
+func (sf *tester) done() {
+	sf.t.Log("done")
+	_ = sf.ctx.Size()
+	go sf.ctx.Cancel()
+	sf.ctx.Done()
+}
+
+// Удаляет несуществующий ключ из локального контекста
+func (sf *tester) del() {
+	sf.t.Log("del")
+	sf.ctx.Del("123")
+	sf.ctx.Del("count")
+}
+
+// Возвращает хранимое значение
+func (sf *tester) get() {
+	sf.t.Log("get")
+	val := sf.ctx.Get("count")
+	count := val.Val().(int)
+	if count == 15 {
+		return
+	}
+}
+
+// Устанавливает значение
+func (sf *tester) set() {
+	sf.t.Log("set")
+	sf.ctx.Set("count", 5, "test_val")
+	sf.ctx.Set("count", 15, "test_val1")
+}
+
+// Создание нового локального контекста
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.newBad1()
+	sf.newGood1()
+}
+
+func (sf *tester) newGood1() {
+	sf.t.Log("newGood1")
+	ctx := context.Background()
+	sf.ctx = NewLocalCtx(ctx).(*LocalCtx)
+	_ = sf.ctx.Log()
+	if lst := sf.ctx.SortedList(); lst == nil {
+		sf.t.Fatalf("newGood1(): lst==nil")
+	}
+}
+
+// Нет контекста ядра
+func (sf *tester) newBad1() {
+	sf.t.Log("newBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("newBad1(): panic==nil")
+		}
+	}()
+	var ctx context.Context
+	sf.ctx = NewLocalCtx(ctx).(*LocalCtx)
+}

+ 119 - 0
kc/local_ctx/lst_sort/lst_sort.go

@@ -0,0 +1,119 @@
+// package lst_sort -- сортированный список значений контекста
+package lst_sort
+
+import (
+	"sort"
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+// LstSort -- сортированный список значений контекста
+type LstSort struct {
+	sync.RWMutex
+	lstVal []ICtxValue // Сортированный список значений
+}
+
+// NewLstSort -- возвращает новый сортированный список значений контекста
+func NewLstSort() *LstSort {
+	sf := &LstSort{
+		lstVal: []ICtxValue{},
+	}
+	return sf
+}
+
+// Add -- добавляет значение в список
+func (sf *LstSort) Add(val ICtxValue) {
+	sf.Lock()
+	defer sf.Unlock()
+	Hassert(val != nil, "LstSort.Add(): ICtxValue==nil")
+	sf.lstVal = append(sf.lstVal, val)
+	sf.sort()
+}
+
+// Del -- удаляет элемент из списка
+func (sf *LstSort) Del(val ICtxValue) {
+	sf.Lock()
+	defer sf.Unlock()
+	if val == nil {
+		return
+	}
+	sf.del(val)
+}
+
+// List -- возвращает сортированный список
+func (sf *LstSort) List() []ICtxValue {
+	sf.RLock()
+	defer sf.RUnlock()
+	return sf.list()
+}
+
+// Size -- возвращает длину списка
+func (sf *LstSort) Size() int {
+	sf.RLock()
+	defer sf.RUnlock()
+	return len(sf.lstVal)
+}
+
+// Get -- возвращает по индексу
+func (sf *LstSort) Get(ind int) ICtxValue {
+	sf.RLock()
+	defer sf.RUnlock()
+	Hassert(ind >= 0, "LstSort.Get(): ind(%v)<0", ind)
+	Hassert(ind < len(sf.lstVal), "LstSort.Get(): ind(%v)>=len(%v)", ind, len(sf.lstVal))
+	return sf.lstVal[ind]
+}
+
+// удаляет элемент из списка
+func (sf *LstSort) del(val ICtxValue) {
+	var (
+		ind  int
+		_val ICtxValue
+	)
+	for ind, _val = range sf.lstVal {
+		if val == _val {
+			break
+		}
+		_val = nil
+	}
+	if _val == nil {
+		return
+	}
+	lst0 := sf.lstVal[:ind]
+	lst1 := []ICtxValue{}
+	if ind < len(sf.lstVal)-1 {
+		lst1 = sf.lstVal[ind+1:]
+	}
+	sf.lstVal = sf.lstVal[:0]
+	sf.lstVal = append(sf.lstVal, lst0...)
+	sf.lstVal = append(sf.lstVal, lst1...)
+	sf.sort()
+}
+
+// возвращает сортированный список
+func (sf *LstSort) list() []ICtxValue {
+	lst := make([]ICtxValue, 0, len(sf.lstVal))
+	lst = append(lst, sf.lstVal...)
+	return lst
+}
+
+// Сортирует элементы в списке
+func (sf *LstSort) sort() {
+	sort.Sort(sf)
+}
+
+// Swap -- НЕ ИСПОЛЬЗОВАТЬ меняет местами два элемента
+func (sf *LstSort) Swap(ind1, ind2 int) {
+	sf.lstVal[ind1], sf.lstVal[ind2] = sf.lstVal[ind2], sf.lstVal[ind1]
+}
+
+// Less -- НЕ ИСПОЛЬЗОВАТЬ сравнивает элементы по индексам
+func (sf *LstSort) Less(ind1, ind2 int) bool {
+	return sf.lstVal[ind1].Key() < sf.lstVal[ind2].Key()
+}
+
+// Len -- НЕ ИСПОЛЬЗОВАТЬ возвращает длину списка
+func (sf *LstSort) Len() int {
+	return len(sf.lstVal)
+}

+ 94 - 0
kc/local_ctx/lst_sort/lst_sort_test.go

@@ -0,0 +1,94 @@
+package lst_sort
+
+import (
+	"testing"
+
+	"gitp78su.ipnodns.ru/svi/kern/kc/local_ctx/ctx_value"
+)
+
+type tester struct {
+	t   *testing.T
+	lst *LstSort
+}
+
+func TestLstSort(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+	sf.add()
+	sf.del()
+	sf.del2()
+	sf.del3()
+}
+
+// Нет такого элемента
+func (sf *tester) del3() {
+	sf.t.Log("del3")
+	val5 := ctx_value.NewCtxValue("val5", 5, "test 5")
+	sf.lst.Del(val5)
+	sf.lst.Del(nil)
+}
+
+// Удаляет элемент в середине
+func (sf *tester) del2() {
+	sf.t.Log("del2")
+	val3 := sf.lst.Get(3)
+	sf.lst.Del(val3)
+	if _len := sf.lst.Size(); _len != 4 {
+		sf.t.Fatalf("del2(): len(%v)!=4", _len)
+	}
+	val4 := sf.lst.Get(3)
+	if val4.Key() != "val4" {
+		sf.t.Fatalf("del2(): key(%v)!='val4'", val4.Key())
+	}
+	sf.lst.Add(val3)
+	if _len := sf.lst.Size(); _len != 5 {
+		sf.t.Fatalf("del2(): len(%v)!=5", _len)
+	}
+}
+
+// Удаляет элемент из списка в конце
+func (sf *tester) del() {
+	sf.t.Log("del")
+
+	val4 := sf.lst.Get(4)
+
+	sf.lst.Del(val4)
+	if _len := sf.lst.Size(); _len != 4 {
+		sf.t.Fatalf("del(): len(%v)!=4", _len)
+	}
+	val3 := sf.lst.Get(3)
+	if val3.Key() != "val3" {
+		sf.t.Fatalf("del(): key(%v)!='val3'", val3.Key())
+	}
+	sf.lst.Add(val4)
+	if _len := sf.lst.Size(); _len != 5 {
+		sf.t.Fatalf("del(): len(%v)!=5", _len)
+	}
+	_ = sf.lst.List()
+}
+
+// Добавление элементов в список
+func (sf *tester) add() {
+	sf.t.Log("add")
+	val0 := ctx_value.NewCtxValue("val0", 0, "test 0")
+	val1 := ctx_value.NewCtxValue("val1", 1, "test 1")
+	val2 := ctx_value.NewCtxValue("val2", 2, "test 2")
+	val3 := ctx_value.NewCtxValue("val3", 3, "test 3")
+	val4 := ctx_value.NewCtxValue("val4", 4, "test 4")
+	sf.lst.Add(val1)
+	sf.lst.Add(val4)
+	sf.lst.Add(val0)
+	sf.lst.Add(val2)
+	sf.lst.Add(val3)
+}
+
+// Создание сортированного списка
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.lst = NewLstSort()
+	if sf.lst == nil {
+		sf.t.Fatalf("new(): lst==nil")
+	}
+}

+ 227 - 0
kc/log_buf/log_buf.go

@@ -0,0 +1,227 @@
+// package log_buf -- потокобезопасный буфер лога
+package log_buf
+
+import (
+	"fmt"
+
+	"gitp78su.ipnodns.ru/svi/kern/kc/log_buf/log_msg"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+// logBuf -- потокобезопасный буфер лога
+type logBuf struct {
+	chGetIn  chan int
+	chGetOut chan ILogMsg
+
+	chDebugIn  chan tMsg
+	chDebugOut chan int
+
+	chInfoIn  chan tMsg
+	chInfoOut chan int
+
+	chWarnIn  chan tMsg
+	chWarnOut chan int
+
+	chErrorIn chan tMsg
+
+	lst []ILogMsg
+
+	chGetErrIn  chan int
+	chGetErrOut chan ILogMsg
+	lstErr      []ILogMsg
+
+	chSizeIn  chan int
+	chSizeOut chan int
+}
+
+// NewLogBuf -- возвращает новый потокобезопасный буфер лога
+func NewLogBuf() ILogBuf {
+	sf := &logBuf{
+		chGetIn:  make(chan int, 2),
+		chGetOut: make(chan ILogMsg, 2),
+
+		chDebugIn:  make(chan tMsg, 2),
+		chDebugOut: make(chan int, 2),
+
+		chInfoIn:  make(chan tMsg, 2),
+		chInfoOut: make(chan int, 2),
+
+		chWarnIn:  make(chan tMsg, 2),
+		chWarnOut: make(chan int, 2),
+
+		chErrorIn: make(chan tMsg, 2),
+
+		lst: []ILogMsg{},
+
+		chGetErrIn:  make(chan int, 2),
+		chGetErrOut: make(chan ILogMsg, 2),
+		lstErr:      []ILogMsg{},
+
+		chSizeIn:  make(chan int, 2),
+		chSizeOut: make(chan int, 2),
+	}
+	go sf.run()
+	return sf
+}
+
+// GetErr -- возвращает сообщение ошибки по номеру
+func (sf *logBuf) GetErr(num int) ILogMsg {
+	sf.chGetErrIn <- num
+	return <-sf.chGetErrOut
+}
+
+// Get -- возвращает сообщение по номеру
+func (sf *logBuf) Get(num int) ILogMsg {
+	sf.chGetIn <- num
+	return <-sf.chGetOut
+}
+
+type tMsg struct {
+	text string
+	args []any
+}
+
+// Debug -- сообщение отладки
+func (sf *logBuf) Debug(fMsg string, args ...any) {
+	msg := tMsg{
+		text: fMsg,
+		args: args,
+	}
+	sf.chDebugIn <- msg
+	<-sf.chDebugOut
+}
+
+// Info -- информационные сообщения
+func (sf *logBuf) Info(fMsg string, args ...any) {
+	msg := tMsg{
+		text: fMsg,
+		args: args,
+	}
+	sf.chInfoIn <- msg
+	<-sf.chInfoOut
+}
+
+// Warn -- предупреждающие сообщения
+func (sf *logBuf) Warn(fMsg string, args ...any) {
+	msg := tMsg{
+		text: fMsg,
+		args: args,
+	}
+	sf.chWarnIn <- msg
+	<-sf.chWarnOut
+}
+
+// Err -- сообщения об ошибках
+func (sf *logBuf) Err(fMsg string, args ...any) {
+	msg := tMsg{
+		text: fMsg,
+		args: args,
+	}
+	sf.chErrorIn <- msg
+}
+
+// Size -- возвращает размер буфера
+func (sf *logBuf) Size() int {
+	sf.chSizeIn <- 1
+	return <-sf.chSizeOut
+}
+
+func (sf *logBuf) run() {
+	for {
+		select {
+		case num := <-sf.chGetErrIn:
+			sf.chGetErrOut <- sf.getErr(num)
+		case num := <-sf.chGetIn:
+			sf.chGetOut <- sf.get(num)
+		case msg := <-sf.chDebugIn:
+			sf.debug(msg)
+			sf.chDebugOut <- 1
+		case msg := <-sf.chInfoIn:
+			sf.info(msg)
+			sf.chInfoOut <- 1
+		case msg := <-sf.chWarnIn:
+			sf.warn(msg)
+			sf.chWarnOut <- 1
+		case msg := <-sf.chErrorIn:
+			sf.err(msg)
+		case <-sf.chSizeIn:
+			sf.chSizeOut <- len(sf.lst)
+		}
+	}
+}
+
+// Возвращает сообщение ошибки по номеру
+func (sf *logBuf) getErr(num int) ILogMsg {
+	if len(sf.lstErr) == 0 {
+		return log_msg.NewLogMsg(log_msg.DEBUG, "not error msg")
+	}
+	if num >= len(sf.lstErr) {
+		return sf.lstErr[len(sf.lstErr)-1]
+	}
+	if num <= 0 {
+		return sf.lstErr[0]
+	}
+	return sf.lstErr[num]
+}
+
+// возвращает сообщение по номеру
+func (sf *logBuf) get(num int) ILogMsg {
+	if len(sf.lst) == 0 {
+		return log_msg.NewLogMsg(log_msg.DEBUG, "*no msg*")
+	}
+	if num >= len(sf.lst) {
+		return log_msg.NewLogMsg(log_msg.DEBUG, "*no msg*")
+	}
+	if num <= 0 {
+		return log_msg.NewLogMsg(log_msg.DEBUG, "*no msg*")
+	}
+	return sf.lst[num]
+}
+
+// сообщение отладки
+func (sf *logBuf) debug(msg tMsg) {
+	strMsg := fmt.Sprintf(msg.text, msg.args...)
+	_msg := log_msg.NewLogMsg(log_msg.DEBUG, strMsg)
+	sf.lst = append(sf.lst, _msg)
+	sf.checkLen()
+}
+
+// информационные сообщения
+func (sf *logBuf) info(msg tMsg) {
+	strMsg := fmt.Sprintf(msg.text, msg.args...)
+	_msg := log_msg.NewLogMsg(log_msg.INFO, strMsg)
+	sf.lst = append(sf.lst, _msg)
+	sf.checkLen()
+}
+
+// предупреждающие сообщения
+func (sf *logBuf) warn(msg tMsg) {
+	strMsg := fmt.Sprintf(msg.text, msg.args...)
+	_msg := log_msg.NewLogMsg(log_msg.WARN, strMsg)
+	sf.lst = append(sf.lst, _msg)
+	sf.checkLen()
+}
+
+// сообщения об ошибках
+func (sf *logBuf) err(msg tMsg) {
+	strMsg := fmt.Sprintf(msg.text, msg.args...)
+	_msg := log_msg.NewLogMsg(log_msg.ERROR, strMsg)
+	sf.lst = append(sf.lst, _msg)
+	sf.lstErr = append(sf.lstErr, _msg)
+	sf.checkLen()
+	sf.checkLenErr()
+}
+
+// Проверяет длину общую лога
+func (sf *logBuf) checkLen() {
+	for len(sf.lst) > 100 {
+		sf.lst = sf.lst[1:]
+	}
+}
+
+// Проверяет длину лога ошибок
+func (sf *logBuf) checkLenErr() {
+	for len(sf.lstErr) > 100 {
+		sf.lstErr = sf.lstErr[1:]
+	}
+}

+ 48 - 0
kc/log_buf/log_buf_test.go

@@ -0,0 +1,48 @@
+package log_buf
+
+import (
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+type tester struct {
+	t   *testing.T
+	log ILogBuf
+}
+
+func TestLogBuf(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+}
+
+// Создаёт новый буферный лог
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.log = NewLogBuf()
+	if sf.log == nil {
+		sf.t.Fatalf("new(): log==nil")
+	}
+	msg := sf.log.Get(-1)
+	_ = sf.log.GetErr(-1)
+	if msg == nil {
+		sf.t.Fatalf("new(): msg==nil")
+	}
+	sf.log.Debug("test msg: %v", 45)
+	sf.log.Info("test msg: %v", 46)
+	sf.log.Warn("test msg: %v", 47)
+	for i := range 120 {
+		sf.log.Err("test err: %v", i)
+	}
+	_ = sf.log.Get(120)
+	_ = sf.log.GetErr(120)
+
+	_ = sf.log.Get(-1)
+	_ = sf.log.GetErr(-1)
+
+	_ = sf.log.Get(19)
+	_ = sf.log.GetErr(20)
+	_ = sf.log.Size()
+}

+ 71 - 0
kc/log_buf/log_msg/log_msg.go

@@ -0,0 +1,71 @@
+// package log_msg -- сообщение логгера
+package log_msg
+
+import (
+	"fmt"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+const (
+	DEBUG = -3
+	INFO  = -2
+	WARN  = -1
+	ERROR = 0
+)
+
+// logMsg -- сообщение логгера
+type logMsg struct {
+	level    string
+	createAt ATime
+	msg      string
+}
+
+// NewLogMsg -- возвращает новое сообщение логгера
+func NewLogMsg(level int, msg string) ILogMsg {
+	sf := &logMsg{
+		createAt: TimeNowStr(),
+		msg:      msg,
+	}
+	sf.check(level)
+	return sf
+}
+
+// String -- возвращает форматированное сообщение лога
+func (sf *logMsg) String() string {
+	strOut := fmt.Sprintf("%v   %v  %v", sf.level, sf.createAt, sf.msg)
+	return strOut
+}
+
+// Msg -- возвращает хранимое сообщение
+func (sf *logMsg) Msg() string {
+	return sf.msg
+}
+
+// Level -- возвращает уровень сообщения
+func (sf *logMsg) Level() string {
+	return sf.level
+}
+
+// CreateAt -- когда сообщение создано
+func (sf *logMsg) CreateAt() ATime {
+	return sf.createAt
+}
+
+// Проверяет правильность своего состава
+func (sf *logMsg) check(level int) {
+	switch level {
+	case DEBUG:
+		sf.level = "DEBU"
+	case INFO:
+		sf.level = "INFO"
+	case WARN:
+		sf.level = "WARN"
+	case ERROR:
+		sf.level = "ERRO"
+	default:
+		Hassert(false, "logMsg.check(): unknown level(%v)", level)
+	}
+}

+ 55 - 0
kc/log_buf/log_msg/log_msg_test.go

@@ -0,0 +1,55 @@
+package log_msg
+
+import (
+	"strings"
+	"testing"
+)
+
+type tester struct {
+	t *testing.T
+}
+
+func TestLogMsg(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+}
+
+// Создаёт сообщение
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.newBad1()
+	sf.newGood1()
+}
+
+func (sf *tester) newGood1() {
+	sf.t.Log("newGood1")
+	_ = NewLogMsg(-3, "test msg -3")
+	_ = NewLogMsg(-2, "test msg -2")
+	_ = NewLogMsg(-1, "test msg -1")
+	msg := NewLogMsg(0, "test msg 0")
+	if msg := msg.Msg(); msg != "test msg 0" {
+		sf.t.Fatalf("newGood1(): msg(%v)!='test msg 0'", msg)
+	}
+	if lvl := msg.Level(); lvl != "ERRO" {
+		sf.t.Fatalf("newGood1(): lvl(%v)!='ERRO'", lvl)
+	}
+	if create := msg.CreateAt(); create == "" {
+		sf.t.Fatalf("newGood1(): create is empty")
+	}
+	if str := msg.String(); !strings.Contains(str, "ERRO   2") {
+		sf.t.Fatalf("newGood1(): str(%v)!=`ERRO   2`", str)
+	}
+}
+
+// Неправильный уровень сообщения
+func (sf *tester) newBad1() {
+	sf.t.Log("newBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("newBad1(): panic==nil")
+		}
+	}()
+	_ = NewLogMsg(-10, "test msg")
+}

+ 41 - 0
kc/safe_bool/safe_bool.go

@@ -0,0 +1,41 @@
+// package safe_bool -- потокобезопасный булевый признак
+package safe_bool
+
+import (
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+// safeBool -- потокобезопасный булевый признак
+type safeBool struct {
+	sync.RWMutex
+	val bool
+}
+
+// NewSafeBool -- возвращает новый потокобезопасный булевый признак
+func NewSafeBool() ISafeBool {
+	sf := &safeBool{}
+	return sf
+}
+
+// Get -- возвращает хранимый булевый признак
+func (sf *safeBool) Get() bool {
+	sf.RLock()
+	defer sf.RUnlock()
+	return sf.val
+}
+
+// Set -- устанавливает булевый признак
+func (sf *safeBool) Set() {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.val = true
+}
+
+// Reset -- сбрасывает булевый признак
+func (sf *safeBool) Reset() {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.val = false
+}

+ 59 - 0
kc/safe_bool/safe_bool_test.go

@@ -0,0 +1,59 @@
+package safe_bool
+
+import (
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+type tester struct {
+	t  *testing.T
+	sb ISafeBool
+}
+
+func TestSAfeBool(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.create()
+	sf.set()
+	sf.reset()
+}
+
+// Сбрасывает хранимое значение
+func (sf *tester) reset() {
+	sf.t.Log("reset")
+	sf.sb.Reset()
+	if sf.sb.Get() {
+		sf.t.Fatalf("reset(): SafeBool==true")
+	}
+	sf.sb.Reset()
+	if sf.sb.Get() {
+		sf.t.Fatalf("reset(): SafeBool==true")
+	}
+}
+
+// Установка хранимого значения
+func (sf *tester) set() {
+	sf.t.Log("set")
+	sf.sb.Set()
+	if !sf.sb.Get() {
+		sf.t.Fatalf("set(): SafeBool==true")
+	}
+	sf.sb.Set()
+	if !sf.sb.Get() {
+		sf.t.Fatalf("set(): SafeBool==true")
+	}
+}
+
+// Создаёт потокобезопасный булевый признак
+func (sf *tester) create() {
+	sf.t.Log("create")
+	sf.sb = NewSafeBool()
+	if sf.sb == nil {
+		sf.t.Fatalf("create(): SafeBool==nil")
+	}
+	if sf.sb.Get() {
+		sf.t.Fatalf("create(): SafeBool==true")
+	}
+}

+ 41 - 0
kc/safe_int/safe_int.go

@@ -0,0 +1,41 @@
+// package safe_int -- потокобезопасный целое
+package safe_int
+
+import (
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+// safeInt -- потокобезопасное целое
+type safeInt struct {
+	sync.RWMutex
+	val int
+}
+
+// NewSafeInt -- возвращает новое потокобезопасное целое
+func NewSafeInt() ISafeInt {
+	sf := &safeInt{}
+	return sf
+}
+
+// Get -- возвращает хранимое целое
+func (sf *safeInt) Get() int {
+	sf.RLock()
+	defer sf.RUnlock()
+	return sf.val
+}
+
+// Set -- устанавливает целое
+func (sf *safeInt) Set(val int) {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.val = val
+}
+
+// Reset -- сбрасывает целое
+func (sf *safeInt) Reset() {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.val = 0
+}

+ 59 - 0
kc/safe_int/safe_int_test.go

@@ -0,0 +1,59 @@
+package safe_int
+
+import (
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+type tester struct {
+	t  *testing.T
+	si ISafeInt
+}
+
+func TestSafeInt(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+	sf.set()
+	sf.reset()
+}
+
+// Сбрасывает хранимое значение
+func (sf *tester) reset() {
+	sf.t.Log("reset")
+	sf.si.Reset()
+	if sf.si.Get() != 0 {
+		sf.t.Fatalf("reset(): ISafeInt!=0")
+	}
+	sf.si.Reset()
+	if sf.si.Get() != 0 {
+		sf.t.Fatalf("reset(): ISafeInt!=0")
+	}
+}
+
+// Установка хранимого значения
+func (sf *tester) set() {
+	sf.t.Log("set")
+	sf.si.Set(77)
+	if sf.si.Get() != 77 {
+		sf.t.Fatalf("set(): ISafeInt!=77")
+	}
+	sf.si.Set(-56)
+	if sf.si.Get() != -56 {
+		sf.t.Fatalf("set(): ISafeInt!=-56")
+	}
+}
+
+// Создаёт потокобезопасный булевый признак
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.si = NewSafeInt()
+	if sf.si == nil {
+		sf.t.Fatalf("new(): ISafeInt==nil")
+	}
+	if sf.si.Get() != 0 {
+		sf.t.Fatalf("new(): ISafeInt!=0")
+	}
+}

+ 48 - 0
kc/safe_string/safe_string.go

@@ -0,0 +1,48 @@
+// package safe_string -- потокобезопасная строка
+package safe_string
+
+import (
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+// safeString -- потокобезопасная строка
+type safeString struct {
+	sync.RWMutex
+	val string
+}
+
+// NewSafeString -- возвращает новую потокобезопасную строку
+func NewSafeString() ISafeString {
+	sf := &safeString{}
+	return sf
+}
+
+// Byte -- возвращает байтовое представление строки
+func (sf *safeString) Byte() []byte {
+	sf.RLock()
+	defer sf.RUnlock()
+	return []byte(sf.val)
+}
+
+// Get -- возвращает хранимую строку
+func (sf *safeString) Get() string {
+	sf.RLock()
+	defer sf.RUnlock()
+	return sf.val
+}
+
+// Set -- устанавливает строку
+func (sf *safeString) Set(val string) {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.val = val
+}
+
+// Reset -- сбрасывает строку
+func (sf *safeString) Reset() {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.val = ""
+}

+ 60 - 0
kc/safe_string/safe_string_test.go

@@ -0,0 +1,60 @@
+package safe_string
+
+import (
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+type tester struct {
+	t  *testing.T
+	ss ISafeString
+}
+
+func TestSafeString(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+	sf.set()
+	sf.reset()
+}
+
+// Сбрасывает хранимое значение
+func (sf *tester) reset() {
+	sf.t.Log("reset")
+	sf.ss.Reset()
+	if sf.ss.Get() != "" {
+		sf.t.Fatalf("reset(): ISafeString!=''")
+	}
+	sf.ss.Reset()
+	if sf.ss.Get() != "" {
+		sf.t.Fatalf("reset(): ISafeString!=''")
+	}
+	_ = sf.ss.Byte()
+}
+
+// Установка хранимого значения
+func (sf *tester) set() {
+	sf.t.Log("set")
+	sf.ss.Set("77")
+	if sf.ss.Get() != "77" {
+		sf.t.Fatalf("set(): ISafeString!='77'")
+	}
+	sf.ss.Set("-56")
+	if sf.ss.Get() != "-56" {
+		sf.t.Fatalf("set(): ISafeString!='-56'")
+	}
+}
+
+// Создаёт потокобезопасный булевый признак
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.ss = NewSafeString()
+	if sf.ss == nil {
+		sf.t.Fatalf("new(): ISafeString==nil")
+	}
+	if sf.ss.Get() != "" {
+		sf.t.Fatalf("new(): ISafeString!=''")
+	}
+}

+ 125 - 0
kern.go

@@ -0,0 +1,125 @@
+// package kern -- библиотека гибкого универсального облегчённого ядра для любого микросервиса
+package kern
+
+import (
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/kc/safe_bool"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_http"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_http/client_bus_http"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_local"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_local/client_bus_local"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kmodule"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kmonolit"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kserv_http"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kstore_kv"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/mds/mod_kctx"
+	"gitp78su.ipnodns.ru/svi/kern/mds/mod_keeper"
+	"gitp78su.ipnodns.ru/svi/kern/mds/mod_serv_http"
+)
+
+// NewKernelCtx -- возвращает контекст ядра
+func NewKernelCtx() IKernelCtx {
+	ctx := kctx.GetKernelCtx()
+	return ctx
+}
+
+// NewKernelStoreKv -- возвращает быстрое key-value хранилище ядра
+func NewKernelStoreKv() IKernelStoreKv {
+	store := kstore_kv.GetKernelStore()
+	return store
+}
+
+// NewKernelServerHttp -- возвращает веб-сервер ядра
+func NewKernelServerHttp() IKernelServerHttp {
+	kernServHttp := kserv_http.GetKernelServHttp()
+	return kernServHttp
+}
+
+// NewSafeBool -- возвращает новый потокобезопасный булевый признак
+func NewSafeBool() ISafeBool {
+	sb := safe_bool.NewSafeBool()
+	return sb
+}
+
+// NewKernelBusLocal -- возвращает локальную шину данных
+func NewKernelBusLocal() IKernelBus {
+	ctx := kctx.GetKernelCtx()
+	ctx.Set("monolitName", "unknown monolit", "GetKernelBusLocal()")
+	bus := kbus_local.GetKernelBusLocal()
+	return bus
+}
+
+// NewKernelBusHttp -- возвращает HTTP шину данных
+func NewKernelBusHttp() IKernelBus {
+	bus := kbus_http.GetKernelBusHttp()
+	return bus
+}
+
+// NewMonolitLocal -- возвращает монолит с локальной шиной
+func NewMonolitLocal(name string) IKernelMonolit {
+	ctx := kctx.GetKernelCtx()
+	ctx.Set("isLocal", true, "bus type")
+	for {
+		SleepMs()
+		if ctx.Get("isLocal") != nil {
+			break
+		}
+	}
+	monolit := kmonolit.GetMonolit(name)
+	_ = kbus_local.GetKernelBusLocal()
+	return monolit
+}
+
+// NewMonolitHttp -- возвращает монолит с локальной шиной поверх HTTP
+func NewMonolitHttp(name string) IKernelMonolit {
+	ctx := kctx.GetKernelCtx()
+	_ = kbus_http.GetKernelBusHttp()
+	ctx.Set("isLocal", false, "bus type")
+	for {
+		SleepMs()
+		if ctx.Get("isLocal") != nil {
+			break
+		}
+	}
+	monolit := kmonolit.GetMonolit(name)
+	return monolit
+}
+
+// NewKernelModule -- возвращает новый модуль на ядре
+func NewKernelModule(name AModuleName) IKernelModule {
+	mod := kmodule.NewKernelModule(name)
+	return mod
+}
+
+// NewClientBusLocal -- возвращает клиент для работы с локальной шиной
+func NewClientBusLocal() IBusClient {
+	client := client_bus_local.NewClientBusLocal()
+	return client
+}
+
+// NewClientBusHttp -- возвращает клиент для работы с шиной поверх HTTP
+func NewClientBusHttp(url string) IBusClient {
+	client := client_bus_http.NewClientBusHttp(url)
+	return client
+}
+
+// NewModuleServHttp -- возвращает новый модуль для IKernelServHttp
+func NewModuleServHttp() IKernelModule {
+	modServHttp := mod_serv_http.NewModuleServHttp()
+	return modServHttp
+}
+
+// NewModuleKernelCtx -- возвращает новый модуль для IKernelCtx
+func NewModuleKernelCtx() IKernelModule {
+	modKernelCtx := mod_kctx.NewModuleKernelCtx()
+	return modKernelCtx
+}
+
+// NewModuleKernelKeeper -- возвращает новый модуль для IKernelKeeper
+func NewModuleKernelKeeper() IKernelModule {
+	modKernelKeeper := mod_keeper.NewModuleKeeper()
+	return modKernelKeeper
+}

+ 115 - 0
kern_test.go

@@ -0,0 +1,115 @@
+package kern
+
+import (
+	"os"
+	"testing"
+
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_env"
+)
+
+type tester struct {
+	t  *testing.T
+	me *mock_env.MockEnv
+}
+
+func TestBuilders(t *testing.T) {
+	sf := &tester{
+		t:  t,
+		me: mock_env.MakeEnv(),
+	}
+	_ = os.Unsetenv("LOCAL_STORE_PATH")
+	_ = os.Setenv("LOCAL_STORE_PATH", "/store/store_builder")
+	_ = os.Unsetenv("LOCAL_HTTP_URL")
+	os.Setenv("LOCAL_HTTP_URL", "http://localhost:18311/")
+	fnClear := func() {
+		pwd := sf.me.Pwd() + "/store/store_builder"
+		_ = os.RemoveAll(pwd)
+	}
+	fnClear()
+	fnClear()
+	sf.new()
+	sf.newModBad()
+}
+
+func (sf *tester) newModBad() {
+	sf.t.Log("newModBad")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("newModBad(): panic==nil")
+		}
+	}()
+	_ = NewKernelModule("")
+}
+
+// создание компонентов
+func (sf *tester) new() {
+	sf.t.Log("new")
+	ctx := NewKernelCtx()
+	if ctx == nil {
+		sf.t.Fatalf("new(): IKernelCtx==nil")
+	}
+	store := NewKernelStoreKv()
+	err := store.Delete("test_builders")
+	if err != nil {
+		sf.t.Fatalf("new(): get empty key, store, err=%v", err)
+	}
+
+	safeBool := NewSafeBool()
+	if safeBool == nil {
+		sf.t.Fatalf("new(): ISafeBool==nil")
+	}
+
+	kernBus := NewKernelBusLocal()
+	if kernBus == nil {
+		sf.t.Fatalf("new(): (local) IKernelBus==nil")
+	}
+
+	kernBusHttp := NewKernelBusHttp()
+	if kernBusHttp == nil {
+		sf.t.Fatalf("new(): (http) IKernelBus==nil")
+	}
+
+	monLocal := NewMonolitLocal("mon_local")
+	if monLocal == nil {
+		sf.t.Fatalf("new(): (local) IKernelMonolit==nil")
+	}
+
+	monHttp := NewMonolitHttp("mon_http")
+	if monHttp == nil {
+		sf.t.Fatalf("new(): (http) IKernelMonolit==nil")
+	}
+
+	mod := NewKernelModule("test_mod")
+	if mod == nil {
+		sf.t.Fatalf("new(): IKernelModule==nil")
+	}
+
+	clientLocal := NewClientBusLocal()
+	if clientLocal == nil {
+		sf.t.Fatalf("new(): (local) IBusClient==nil")
+	}
+	clientHttp := NewClientBusHttp("test_url")
+	if clientHttp == nil {
+		sf.t.Fatalf("new(): (http) IBusClient==nil")
+	}
+
+	modServHttp := NewModuleServHttp()
+	if modServHttp == nil {
+		sf.t.Fatalf("new(): modServHttp==nil")
+	}
+
+	modKernelCtx := NewModuleKernelCtx()
+	if modKernelCtx == nil {
+		sf.t.Fatalf("new(): modKernelCtx==nil")
+	}
+	modKernelKeeper := NewModuleKernelKeeper()
+	if modKernelKeeper == nil {
+		sf.t.Fatalf("new(): modKernelKeeper==nil")
+	}
+	kernServHttp := NewKernelServerHttp()
+	go kernServHttp.Run()
+	ctx.Cancel()
+	ctx.Wg().Wait()
+	ctx.Cancel()
+	ctx.Wg().Wait()
+}

+ 19 - 0
krn/kalias/kalias.go

@@ -0,0 +1,19 @@
+// package kalias -- алиасы типов ядра
+package kalias
+
+// ATime -- метка времени
+//
+// Вывод: "2006-01-02 15:04:05.000 -07 MST")
+type ATime string
+
+// AStreamName -- имя потока для ожидания
+type AStreamName string
+
+// ATopic -- имя топика в шине
+type ATopic string
+
+// AHandlerName -- имя функции обработчика
+type AHandlerName string
+
+// AModuleName -- уникальное имя модуля
+type AModuleName string

+ 7 - 0
krn/kalias/kalias_test.go

@@ -0,0 +1,7 @@
+package kalias
+
+import (
+	"testing"
+)
+
+func TestKernelAlias(t *testing.T) {}

+ 57 - 0
krn/kbus/dict_sub_hook/dict_sub_hook.go

@@ -0,0 +1,57 @@
+// package dict_sub_hook -- словарь потребителей топика по подписке
+package dict_sub_hook
+
+import (
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+// dictSubHook -- словарь потребителей топика по подписке
+type dictSubHook struct {
+	ctx   IKernelCtx
+	dict  map[AHandlerName]bool // В качестве ключа -- URL веб-хука
+	block sync.RWMutex
+}
+
+// NewDictSubHook -- возвращает новый словарь веб-хуков одного топика
+func NewDictSubHook() IDictSubHook {
+	sf := &dictSubHook{
+		ctx:  kctx.GetKernelCtx(),
+		dict: map[AHandlerName]bool{},
+	}
+	return sf
+}
+
+// Unsubscribe -- удаляет из словаря подписки обработчик
+func (sf *dictSubHook) Unsubscribe(handler IBusHandlerSubscribe) {
+	sf.block.Lock()
+	defer sf.block.Unlock()
+	Hassert(handler != nil, "dictSubHook.Unsubscribe(): handler==nil")
+	handlerName := handler.Name()
+	delete(sf.dict, handlerName)
+	sf.ctx.Del(string(handlerName))
+}
+
+// Subscribe -- добавляет в словарь подписки новый обработчик
+func (sf *dictSubHook) Subscribe(handler IBusHandlerSubscribe) {
+	sf.block.Lock()
+	defer sf.block.Unlock()
+	Hassert(handler != nil, "dictSubHook.Subscribe(): handler==nil")
+	handlerName := handler.Name()
+	sf.dict[handlerName] = true
+	sf.ctx.Set(string(handlerName), handler, "subscribe handler")
+}
+
+// Read -- вызывает все обработчики словаря подписок
+func (sf *dictSubHook) Read(binMsg []byte) {
+	sf.block.RLock()
+	defer sf.block.RUnlock()
+	for handlerName := range sf.dict {
+		handler := sf.ctx.Get(string(handlerName)).Val().(IBusHandlerSubscribe)
+		go handler.FnBack(binMsg)
+	}
+}

+ 78 - 0
krn/kbus/dict_sub_hook/dict_sub_hook_test.go

@@ -0,0 +1,78 @@
+package dict_sub_hook
+
+import (
+	"testing"
+
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_hand_sub_local"
+)
+
+type tester struct {
+	t        *testing.T
+	dict     *dictSubHook
+	handSub  *mock_hand_sub_local.MockHandlerSub
+	handSub2 *mock_hand_sub_local.MockHandlerSub
+}
+
+func TestDictSubWebHook(t *testing.T) {
+	sf := &tester{
+		t:        t,
+		handSub:  mock_hand_sub_local.NewMockHandlerSub("hand_topic1", "hand_name1"),
+		handSub2: mock_hand_sub_local.NewMockHandlerSub("hand_topic2", "hand_name2"),
+	}
+	sf.new()
+	sf.add()
+	sf.read()
+	sf.unsub()
+}
+
+// Отписка обработчика от топика
+func (sf *tester) unsub() {
+	sf.t.Log("unsub")
+	sf.dict.Unsubscribe(sf.handSub)
+}
+
+// Чтение входящего сообщения по подписке
+func (sf *tester) read() {
+	sf.t.Log("read")
+	sf.dict.Read([]byte("test_msg"))
+}
+
+// Добавляет хуки ыв словарь
+func (sf *tester) add() {
+	sf.t.Log("add")
+	sf.addBad1()
+	sf.addGood1()
+}
+
+func (sf *tester) addGood1() {
+	sf.t.Log("addGood1")
+	sf.dict.Subscribe(sf.handSub)
+	sf.dict.Subscribe(sf.handSub)
+	sf.dict.Subscribe(sf.handSub2)
+	if _len := len(sf.dict.dict); _len != 2 {
+		sf.t.Fatalf("new(): len(%v)!=2", _len)
+	}
+}
+
+// нет веб-хука для добавления
+func (sf *tester) addBad1() {
+	sf.t.Log("addBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("addBad1(): panic==nil")
+		}
+	}()
+	sf.dict.Subscribe(nil)
+}
+
+// Создание словаря
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.dict = NewDictSubHook().(*dictSubHook)
+	if sf.dict == nil {
+		sf.t.Fatalf("new(): dict==nil")
+	}
+	if _len := len(sf.dict.dict); _len != 0 {
+		sf.t.Fatalf("new(): len(%v)!=0", _len)
+	}
+}

+ 143 - 0
krn/kbus/dict_topic_serve/dict_topic_serve.go

@@ -0,0 +1,143 @@
+// package dict_topic_serve -- словарь топиков обработчиков запросов
+package dict_topic_serve
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_msg/msg_serve"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+// dictServe -- потокобезопасный словарь обработчиков запросов
+//
+// Допускается только один обработчик запросов на один топик
+type dictServe struct {
+	ctx IKernelCtx
+
+	chUnregisterIn chan IBusHandlerServe
+	dictServe      map[ATopic]IBusHandlerServe
+
+	chSendRequestIn  chan *msg_serve.ServeReq
+	chSendRequestOut chan *serveResp
+
+	chRegisterIn  chan IBusHandlerServe
+	chRegisterOut chan bool
+}
+
+// NewDictServe -- возвращает потокобезопасный словарь обработчиков запросов
+func NewDictServe() IDictTopicServe {
+	sf := &dictServe{
+		ctx: kctx.GetKernelCtx(),
+
+		chUnregisterIn: make(chan IBusHandlerServe, 5),
+		dictServe:      map[ATopic]IBusHandlerServe{},
+
+		chSendRequestIn:  make(chan *msg_serve.ServeReq, 5),
+		chSendRequestOut: make(chan *serveResp, 5),
+
+		chRegisterIn:  make(chan IBusHandlerServe, 5),
+		chRegisterOut: make(chan bool, 5),
+	}
+	go sf.run()
+	return sf
+}
+
+// Register -- регистрирует обработчик запросов
+func (sf *dictServe) Register(handler IBusHandlerServe) {
+	Hassert(handler != nil, "dictServe.Register(): IBusHandlerSubscribe==nil")
+	topic := handler.Topic()
+	Hassert(topic != "", "dictServe.Register(): empty topic of handler")
+	sf.chRegisterIn <- handler
+	isTwinRegister := <-sf.chRegisterOut
+	Hassert(!isTwinRegister, "dictServe.Register(): handler of topic (%v) already register", handler.Topic())
+}
+
+// Unregister -- удаляет обработчик запросов из словаря
+func (sf *dictServe) Unregister(handler IBusHandlerServe) {
+	Hassert(handler != nil, "dictServe.Unregister(): IBusHandlerSubscribe==nil")
+	sf.chUnregisterIn <- handler
+}
+
+type serveResp struct {
+	binResp []byte
+	err     error
+}
+
+// SendRequest -- вызывает обработчик при поступлении запроса
+func (sf *dictServe) SendRequest(topic ATopic, binReq []byte) ([]byte, error) {
+	req := &msg_serve.ServeReq{
+		Topic_:  topic,
+		BinReq_: binReq,
+	}
+	sf.chSendRequestIn <- req
+	resp := <-sf.chSendRequestOut
+	return resp.binResp, resp.err
+}
+
+func (sf *dictServe) run() {
+	for {
+		select {
+		case handler := <-sf.chUnregisterIn:
+			delete(sf.dictServe, handler.Topic())
+		case reqServe := <-sf.chSendRequestIn:
+			binResp, err := sf.sendRequest(reqServe)
+			resp := &serveResp{
+				err:     err,
+				binResp: binResp,
+			}
+			sf.chSendRequestOut <- resp
+		case handler := <-sf.chRegisterIn:
+			sf.chRegisterOut <- sf.register(handler)
+		}
+	}
+}
+
+var TimeoutDefault = 15000
+
+// вызывает обработчик при поступлении запроса
+func (sf *dictServe) sendRequest(req *msg_serve.ServeReq) ([]byte, error) {
+	handler, isOk := sf.dictServe[req.Topic_]
+	if !isOk {
+		return nil, fmt.Errorf("dictServe.sendRequest(): handler for topic (%v) not exists", req.Topic_)
+	}
+	var (
+		chErr  = make(chan error, 2)
+		binRes []byte
+	)
+	ctx, fnCancel := context.WithTimeout(sf.ctx.BaseCtx(), time.Millisecond*time.Duration(TimeoutDefault))
+	defer fnCancel()
+	fnCall := func() {
+		defer close(chErr)
+		var err error
+		binRes, err = handler.FnBack(req.BinReq_)
+		if err != nil {
+			chErr <- err
+		}
+	}
+	go fnCall()
+	select {
+	case <-ctx.Done():
+		return nil, fmt.Errorf("dictServe.sendRequest(): in call for topic (%v), err=\n\t%w", req.Topic_, ctx.Err())
+	case err := <-chErr:
+		if err != nil {
+			return nil, fmt.Errorf("dictServe.sendRequest(): error in call for topic (%v), err=\n\t%w", req.Topic_, err)
+		}
+	}
+	return binRes, nil
+}
+
+// регистрирует обработчик запросов
+func (sf *dictServe) register(handler IBusHandlerServe) bool {
+	topic := handler.Topic()
+	_, isOk := sf.dictServe[topic]
+	if isOk {
+		return true
+	}
+	sf.dictServe[topic] = handler
+	return false
+}

+ 159 - 0
krn/kbus/dict_topic_serve/dict_topic_serve_test.go

@@ -0,0 +1,159 @@
+package dict_topic_serve
+
+import (
+	"testing"
+
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_hand_serve"
+)
+
+type tester struct {
+	t    *testing.T
+	dict *dictServe
+	hand *mock_hand_serve.MockHandlerServe
+}
+
+func TestDictSub(t *testing.T) {
+	sf := &tester{
+		t:    t,
+		hand: mock_hand_serve.NewMockHandlerServe("topic_dict_serve", "name_dict_serve"),
+	}
+	sf.new()
+	sf.addBad1()
+	sf.addGood1()
+	sf.addBad2()
+	sf.sendBad1()
+	sf.sendBad2()
+	sf.sendGood1()
+	sf.delBad1()
+	sf.delGood2()
+	sf.callBad3()
+}
+
+// Работа ядра завершена
+func (sf *tester) callBad3() {
+	sf.t.Log("callBad3")
+	ctx := kctx.GetKernelCtx()
+	ctx.Cancel()
+	ctx.Wg().Wait()
+	sf.dict.Register(sf.hand)
+	binMsg, err := sf.dict.SendRequest(sf.hand.Topic(), []byte("test"))
+	if err == nil {
+		sf.t.Fatalf("callBad3(): err==nil")
+	}
+	if binMsg != nil {
+		sf.t.Fatalf("callBad3(): binMsg!=nil")
+	}
+}
+
+func (sf *tester) delGood2() {
+	sf.t.Log("delGood2()")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("delGood2(): panic=%v", _panic)
+		}
+	}()
+	sf.dict.Unregister(sf.hand)
+	sf.dict.Unregister(sf.hand)
+}
+
+// Удаляет, чего нет
+func (sf *tester) delBad1() {
+	sf.t.Log("delBad1()")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("delBad1(): panic==nil")
+		}
+	}()
+	sf.dict.Unregister(nil)
+}
+
+func (sf *tester) sendGood1() {
+	sf.t.Log("sendGood1")
+	TimeoutDefault = 5000
+	binMsg, err := sf.dict.SendRequest(sf.hand.Topic(), []byte("test_good"))
+	if err != nil {
+		sf.t.Fatalf("sendGood1(): err=%v", err)
+	}
+	if binMsg == nil {
+		sf.t.Fatalf("sendGood1(): binMsg==nil")
+	}
+}
+
+// Обработчик вернул ошибку
+func (sf *tester) sendBad2() {
+	sf.t.Log("sendBad2")
+	sf.hand.IsBad_.Set()
+	binMsg, err := sf.dict.SendRequest(sf.hand.Topic(), []byte("test"))
+	if err == nil {
+		sf.t.Fatalf("sendBad2(): err==nil")
+	}
+	if binMsg != nil {
+		sf.t.Fatalf("sendBad2(): binMsg!=nil")
+	}
+	sf.hand.IsBad_.Reset()
+}
+
+// повторное добавление обработчика
+func (sf *tester) addBad2() {
+	sf.t.Log("addBad2")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("addGood1(): panic==nil")
+		}
+	}()
+	sf.dict.Register(sf.hand)
+}
+
+// Правильное добавление обработчика подписки
+func (sf *tester) addGood1() {
+	sf.t.Log("addGood1()")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("addGood1(): panic=%v", _panic)
+		}
+	}()
+	sf.dict.Register(sf.hand)
+}
+
+// Вместо обработчика пустышка
+func (sf *tester) addBad1() {
+	sf.t.Log("addBad1()")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("addBad1(): panic==nil")
+		}
+	}()
+	sf.dict.Register(nil)
+}
+
+// Вызов несуществующего топика
+func (sf *tester) sendBad1() {
+	sf.t.Log("sendBad1")
+	binRes, err := sf.dict.SendRequest("test_bad_topic", []byte("test"))
+	if err == nil {
+		sf.t.Fatalf("sendBad1(): err==nil")
+	}
+	if binRes != nil {
+		sf.t.Fatalf("sendBad1(): binRes!=nil")
+	}
+}
+
+// Создание словаря подписчиков
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.newGood1()
+}
+
+func (sf *tester) newGood1() {
+	sf.t.Log("newGood1")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("newGood1(): panic=%v", _panic)
+		}
+	}()
+	sf.dict = NewDictServe().(*dictServe)
+	if sf.dict == nil {
+		sf.t.Fatalf("newGood1(): DictServe==nil")
+	}
+}

+ 79 - 0
krn/kbus/dict_topic_sub/dict_topic_sub.go

@@ -0,0 +1,79 @@
+// package dict_topic_sub -- потокобезопасный словарь подписчиков локальной шины
+package dict_topic_sub
+
+import (
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/dict_sub_hook"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+type tReadReq struct {
+	topic  ATopic
+	binMsg []byte
+}
+
+// dictTopicSub -- потокобезопасный словарь подписчиков
+type dictTopicSub struct {
+	sync.RWMutex
+	ctx           IKernelCtx
+	dictTopicHook map[ATopic]IDictSubHook
+}
+
+// NewDictTopicSub -- возвращает потокобезопасный словарь подписчиков
+func NewDictTopicSub() IDictTopicSub {
+	sf := &dictTopicSub{
+		ctx:           kctx.GetKernelCtx(),
+		dictTopicHook: map[ATopic]IDictSubHook{},
+	}
+	return sf
+}
+
+// Read -- вызывает обработчики при поступлении события
+func (sf *dictTopicSub) Read(topic ATopic, binMsg []byte) {
+	sf.RLock()
+	defer sf.RUnlock()
+	Hassert(topic != "", "dictTopicSub.Read(): topic is empty")
+	msg := &tReadReq{
+		topic:  topic,
+		binMsg: binMsg,
+	}
+	dictHook := sf.dictTopicHook[msg.topic]
+	if dictHook == nil {
+		return
+	}
+	dictHook.Read(msg.binMsg)
+}
+
+// Subscribe -- подписывает обработчик на топик
+func (sf *dictTopicSub) Subscribe(handler IBusHandlerSubscribe) {
+	sf.Lock()
+	defer sf.Unlock()
+	Hassert(handler != nil, "dictTopicSub.Subscribe(): IBusHandlerSubscribe==nil")
+	topic := handler.Topic()
+	Hassert(topic != "", "dictTopicSub.Subscribe(): topic is empty")
+	dictSubHook := sf.dictTopicHook[topic]
+	if dictSubHook == nil {
+		dictSubHook = dict_sub_hook.NewDictSubHook()
+		sf.dictTopicHook[topic] = dictSubHook
+	}
+	dictSubHook.Subscribe(handler)
+}
+
+// Unsubscribe -- отписывает обработчик
+func (sf *dictTopicSub) Unsubscribe(handler IBusHandlerSubscribe) {
+	sf.Lock()
+	defer sf.Unlock()
+	if handler == nil {
+		return
+	}
+	topic := handler.Topic()
+	dictSubHook := sf.dictTopicHook[topic]
+	if dictSubHook == nil {
+		return
+	}
+	dictSubHook.Unsubscribe(handler)
+}

+ 147 - 0
krn/kbus/dict_topic_sub/dict_topic_sub_test.go

@@ -0,0 +1,147 @@
+package dict_topic_sub
+
+import (
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_hand_sub_local"
+)
+
+type tester struct {
+	t    *testing.T
+	dict *dictTopicSub
+	hand *mock_hand_sub_local.MockHandlerSub
+}
+
+func TestDictSub(t *testing.T) {
+	sf := &tester{
+		t:    t,
+		hand: mock_hand_sub_local.NewMockHandlerSub("topic_dict_sub", "name_dict_sub"),
+	}
+	sf.new()
+	sf.addBad1()
+	sf.addGood1()
+	sf.addGood2()
+	sf.callGood10()
+	sf.callGood1()
+	sf.callBad2()
+	sf.delBad1()
+	sf.delGood2()
+	sf.unsub1()
+}
+
+// Прямой вызов отписки от топика которого точно нет
+func (sf *tester) unsub1() {
+	sf.t.Log("unsub1")
+	sf.dict.Unsubscribe(sf.hand)
+	sf.dict.Read("test_test", []byte("test test"))
+	hand := mock_hand_sub_local.NewMockHandlerSub("topic_dict_sub1", "name_dict_sub")
+	sf.dict.Unsubscribe(hand)
+	count := 0
+	for count < 100 {
+		SleepMs()
+		count++
+	}
+	sf.dict.Read("topic_dict_sub", []byte("test test"))
+	count = 0
+	for count < 200 {
+		SleepMs()
+		count++
+	}
+}
+
+func (sf *tester) delGood2() {
+	sf.t.Log("delGood2()")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("delGood2(): panic=%v", _panic)
+		}
+	}()
+	sf.dict.Unsubscribe(sf.hand)
+	sf.dict.Unsubscribe(sf.hand)
+}
+
+// Удаляет, чего нет
+func (sf *tester) delBad1() {
+	sf.t.Log("delBad1()")
+	sf.dict.Unsubscribe(nil)
+}
+
+func (sf *tester) callGood1() {
+	sf.t.Log("callGood1")
+	sf.dict.Read(sf.hand.Topic(), []byte("test_good"))
+}
+
+// повторное добавление обработчика
+func (sf *tester) addGood2() {
+	sf.t.Log("addGood2")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("addGood2(): panic=%v", _panic)
+		}
+	}()
+	sf.dict.Subscribe(sf.hand)
+}
+
+// Правильное добавление обработчика подписки
+func (sf *tester) addGood1() {
+	sf.t.Log("addGood1()")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("addGood1(): panic=%v", _panic)
+		}
+	}()
+	sf.dict.Subscribe(sf.hand)
+}
+
+// Вместо обработчика пустышка
+func (sf *tester) addBad1() {
+	sf.t.Log("addBad1()")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("addBad1(): panic==nil")
+		}
+	}()
+	sf.dict.Subscribe(nil)
+}
+
+// Нет топика
+func (sf *tester) callBad2() {
+	sf.t.Log("callBad2")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("callBad2(): panic==nil")
+		}
+	}()
+	sf.dict.Read("", []byte("test_msg"))
+}
+
+// Нет данных в топике
+func (sf *tester) callGood10() {
+	sf.t.Log("callGood10")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("callGood10(): panic=%v", _panic)
+		}
+	}()
+	sf.dict.Read("test_bad_topic", []byte("test"))
+}
+
+// Создание словаря подписчиков
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.newGood1()
+}
+
+func (sf *tester) newGood1() {
+	sf.t.Log("newGood1")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("newGood1(): panic=%v", _panic)
+		}
+	}()
+	sf.dict = NewDictTopicSub().(*dictTopicSub)
+	if sf.dict == nil {
+		sf.t.Fatalf("newGood1(): DictSub==nil")
+	}
+}

+ 146 - 0
krn/kbus/kbus_base/kbus_base.go

@@ -0,0 +1,146 @@
+// package kbus_base -- базовая часть шины данных
+package kbus_base
+
+import (
+	"fmt"
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/kc/local_ctx"
+	"gitp78su.ipnodns.ru/svi/kern/kc/safe_bool"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/dict_topic_serve"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/dict_topic_sub"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+const (
+	strBusBaseStream = "bus_base"
+)
+
+// KBusBase -- базовая часть шины данных
+type KBusBase struct {
+	Ctx_      IKernelCtx
+	IsWork_   ISafeBool
+	ctx       ILocalCtx
+	log       ILogBuf
+	dictSub   IDictTopicSub
+	dictServe IDictTopicServe
+}
+
+var (
+	Bus_  *KBusBase
+	block sync.Mutex
+)
+
+// GetKernelBusBase -- возвращает базовую шину сообщений
+func GetKernelBusBase() *KBusBase {
+	block.Lock()
+	defer block.Unlock()
+	if Bus_ != nil {
+		return Bus_
+	}
+	ctx := kctx.GetKernelCtx()
+	Bus_ = &KBusBase{
+		Ctx_:      ctx,
+		IsWork_:   safe_bool.NewSafeBool(),
+		dictSub:   dict_topic_sub.NewDictTopicSub(),
+		dictServe: dict_topic_serve.NewDictServe(),
+		ctx:       local_ctx.NewLocalCtx(ctx.BaseCtx()),
+	}
+	Bus_.log = Bus_.ctx.Log()
+	go Bus_.close()
+	go Bus_.run()
+	Bus_.IsWork_.Set()
+	err := Bus_.Ctx_.Wg().Add(strBusBaseStream)
+	Hassert(err == nil, "GetKernelBusBase(): in add name stream(%v), err=\n\t%v", strBusBaseStream, err)
+	ctx.Set("kernBusBase", Bus_, "base of data bus")
+	_ = IKernelBus(Bus_)
+	return Bus_
+}
+
+// Log -- возвращает лог шины
+func (sf *KBusBase) Log() ILogBuf {
+	return sf.log
+}
+
+func (sf *KBusBase) run() {
+	sf.log.Debug("KBusBase.run()")
+	for {
+		break
+	}
+}
+
+// Unsubscribe -- отписывает обработчик от топика
+func (sf *KBusBase) Unsubscribe(handler IBusHandlerSubscribe) {
+	sf.log.Debug("KBusBase.Unsubscribe(): handler='%v'", handler.Name())
+	sf.dictSub.Unsubscribe(handler)
+}
+
+// Subscribe -- подписывает обработчик на топик
+func (sf *KBusBase) Subscribe(handler IBusHandlerSubscribe) error {
+	sf.log.Debug("KBusBase.Subscribe(): handler='%v'", handler.Name())
+	if !sf.IsWork_.Get() {
+		err := fmt.Errorf("KBusBase.Subscribe():  handler='%v', bus already closed", handler.Name())
+		sf.log.Err(err.Error())
+		return err
+	}
+	sf.dictSub.Subscribe(handler)
+	return nil
+}
+
+// SendRequest -- отправляет запрос в шину данных
+func (sf *KBusBase) SendRequest(topic ATopic, binReq []byte) ([]byte, error) {
+	sf.log.Debug("KBusBase.SendRequest(): topic='%v'", topic)
+	if !sf.IsWork_.Get() {
+		err := fmt.Errorf("KBusBase.SendRequest():  topic='%v', bus already closed", topic)
+		sf.log.Err(err.Error())
+		return nil, err
+	}
+	binResp, err := sf.dictServe.SendRequest(topic, binReq)
+	if err != nil {
+		err := fmt.Errorf("KBusBase.SendRequest(): topic='%v', err=\n\t%w", topic, err)
+		sf.log.Err(err.Error())
+		return nil, err
+	}
+	return binResp, nil
+}
+
+// RegisterServe -- регистрирует обработчики входящих запросов
+func (sf *KBusBase) RegisterServe(handler IBusHandlerServe) {
+	Hassert(handler != nil, "KBusBase.RegisterServe(): IBusHandlerSubscribe==nil")
+	sf.log.Debug("KBusBase.RegisterServe(): handler='%v'", handler.Name())
+	sf.dictServe.Register(handler)
+}
+
+// Publish -- публикует сообщение в шину
+func (sf *KBusBase) Publish(topic ATopic, binMsg []byte) (err error) {
+	sf.log.Debug("KBusBase.Publish(): topic='%v'", topic)
+	if !sf.IsWork_.Get() {
+		err := fmt.Errorf("KBusBase.Publish(): topic='%v',bus already closed", topic)
+		sf.log.Err(err.Error())
+		return err
+	}
+	// Асинхронный запуск чтения
+	go sf.dictSub.Read(topic, binMsg)
+	return nil
+}
+
+// IsWork -- возвращает признак работы шины
+func (sf *KBusBase) IsWork() bool {
+	return sf.IsWork_.Get()
+}
+
+// Ожидает закрытия шины в отдельном потоке
+func (sf *KBusBase) close() {
+	sf.Ctx_.Done()
+	sf.Ctx_.Lock()
+	defer sf.Ctx_.Unlock()
+	if !sf.IsWork_.Get() {
+		return
+	}
+	sf.IsWork_.Reset()
+	sf.Ctx_.Wg().Done(strBusBaseStream)
+	sf.log.Debug("KBusBase.close(): done")
+}

+ 191 - 0
krn/kbus/kbus_base/kbus_base_test.go

@@ -0,0 +1,191 @@
+package kbus_base
+
+import (
+	"testing"
+
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_hand_serve"
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_hand_sub_local"
+)
+
+type tester struct {
+	t        *testing.T
+	bus      IKernelBus
+	handSub  *mock_hand_sub_local.MockHandlerSub
+	handServ *mock_hand_serve.MockHandlerServe
+}
+
+func TestKernelBusLocal(t *testing.T) {
+	sf := &tester{
+		t:        t,
+		handSub:  mock_hand_sub_local.NewMockHandlerSub("topic_hand_sub", "mock_hand_sub"),
+		handServ: mock_hand_serve.NewMockHandlerServe("topic_hand_serv", "mock_hand_serv"),
+	}
+	sf.new()
+	sf.subBad1()
+	sf.subGood1()
+	sf.pubGood10()
+	sf.reqBad1()
+	sf.servBad1()
+	sf.servGood1()
+	sf.reqGood1()
+	sf.close()
+	sf.unsubBad1()
+	sf.unsubGood1()
+}
+
+func (sf *tester) unsubGood1() {
+	sf.t.Log("unsubBad1")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("unsubGood1(): panic=%v", _panic)
+		}
+	}()
+	sf.bus.Unsubscribe(sf.handSub)
+	sf.bus.Unsubscribe(sf.handSub)
+}
+
+// Отписка от топика, нет обработчика
+func (sf *tester) unsubBad1() {
+	sf.t.Log("unsubBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("unsubBad1(): panic==nil")
+		}
+	}()
+	sf.bus.Unsubscribe(nil)
+}
+
+func (sf *tester) reqGood1() {
+	sf.t.Log("reqGood1")
+	binMsg, err := sf.bus.SendRequest(sf.handServ.Topic_, []byte("test_msg"))
+	if err != nil {
+		sf.t.Fatalf("reqGood1(): err=%v", err)
+	}
+	if binMsg == nil {
+		sf.t.Fatalf("reqGood1(): binMsg==nil")
+	}
+}
+
+func (sf *tester) servGood1() {
+	sf.t.Log("servGood1")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("servGood1(): panic=%v", _panic)
+		}
+	}()
+	sf.bus.RegisterServe(sf.handServ)
+}
+
+// Нет обработчика для обслуживания запросов
+func (sf *tester) servBad1() {
+	sf.t.Log("servBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("servBad1(): panic==nil")
+		}
+	}()
+	sf.bus.RegisterServe(nil)
+}
+
+// Нет такого топика
+func (sf *tester) reqBad1() {
+	sf.t.Log("reqBad1")
+	binMsg, err := sf.bus.SendRequest("test_topic1", []byte("test_msg"))
+	if err == nil {
+		sf.t.Fatalf("reqBad1(): err==nil")
+	}
+	if binMsg != nil {
+		sf.t.Fatalf("reqBad1(): binMsg!=nil")
+	}
+}
+
+// Нет читателей топика
+func (sf *tester) pubGood10() {
+	sf.t.Log("pubGood10")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("pubGood10(): panic=%v", _panic)
+		}
+	}()
+	err := sf.bus.Publish("test_topic1", []byte("test_msg"))
+	if err != nil {
+		sf.t.Fatalf("pubGood10(): err=%v", nil)
+	}
+}
+
+func (sf *tester) subGood1() {
+	sf.t.Log("subGood1")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("subGood1(): panic=%v", _panic)
+		}
+	}()
+	err := sf.bus.Subscribe(sf.handSub)
+	if err != nil {
+		sf.t.Fatalf("subGood1(): err=%v", err)
+	}
+}
+
+// Нет обработчик подписки
+func (sf *tester) subBad1() {
+	sf.t.Log("subBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("subBad1(): panic==nil")
+		}
+	}()
+	_ = sf.bus.Subscribe(nil)
+}
+
+// Создание локальной шины
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.newGood1()
+
+}
+
+// Закрытие шины
+func (sf *tester) close() {
+	sf.t.Log("close")
+	ctx := kctx.GetKernelCtx()
+	ctx.Cancel()
+	ctx.Wg().Wait()
+	sf.bus.(*KBusBase).close()
+	if sf.bus.IsWork() {
+		sf.t.Fatalf("close(): bus work")
+	}
+	err := sf.bus.Subscribe(sf.handSub)
+	if err == nil {
+		sf.t.Fatalf("close(): err==nil")
+	}
+	err = sf.bus.Publish("test_topic1", []byte("test_msg"))
+	if err == nil {
+		sf.t.Fatalf("close(): err==nil")
+	}
+	_, err = sf.bus.SendRequest("test_topic", []byte("test_msg"))
+	if err == nil {
+		sf.t.Fatalf("close(): err==nil")
+	}
+}
+
+func (sf *tester) newGood1() {
+	sf.t.Log("newGood1")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("newGood1(): panic=%v", _panic)
+		}
+	}()
+	sf.bus = GetKernelBusBase()
+	sf.bus = GetKernelBusBase()
+	if sf.bus == nil {
+		sf.t.Fatalf("newGood1(): IKernelBus==nil")
+	}
+	if !sf.bus.IsWork() {
+		sf.t.Fatalf("newGood1(): bus not work")
+	}
+	if log := sf.bus.Log(); log == nil {
+		sf.t.Fatalf("newGood1(): log==nil")
+	}
+}

+ 219 - 0
krn/kbus/kbus_http/client_bus_http/client_bus_http.go

@@ -0,0 +1,219 @@
+// package client_bus_http -- клиент HTTP-шины
+package client_bus_http
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+	"strings"
+
+	"github.com/google/uuid"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/kc/local_ctx"
+	"gitp78su.ipnodns.ru/svi/kern/kc/safe_bool"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_http"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_msg/msg_pub"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_msg/msg_serve"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_msg/msg_sub"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_msg/msg_unsub"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_hand_sub_http"
+)
+
+// ClientBusHttp -- клиент HTTP-шины
+type ClientBusHttp struct {
+	bus       IKernelBus
+	ctx       ILocalCtx
+	log       ILogBuf
+	isWork    ISafeBool
+	urlRemote string // URL дистанционной шины
+	urlLocal  string // URL локальной шины
+}
+
+// NewClientBusHttp - -возвращает новый клиент HTTP-шины
+func NewClientBusHttp(url string) IBusClient {
+	Hassert(url != "", "NewClientBusHttp(): url is empty")
+	kCtx := kctx.GetKernelCtx()
+	urlLocal := os.Getenv("LOCAL_HTTP_URL")
+	Hassert(urlLocal != "", "NewClientBusHttp(): env LOCAL_HTTP_URL not set")
+	sf := &ClientBusHttp{
+		ctx:       local_ctx.NewLocalCtx(kCtx.BaseCtx()),
+		bus:       kbus_http.GetKernelBusHttp(),
+		isWork:    safe_bool.NewSafeBool(),
+		urlRemote: strings.TrimSuffix(url, "/"),
+		urlLocal:  strings.TrimSuffix(urlLocal, "/"),
+	}
+	sf.log = sf.ctx.Log()
+	return sf
+}
+
+// Unsubscribe -- отписывается от топика в дистанционной шине
+func (sf *ClientBusHttp) Unsubscribe(handler IBusHandlerSubscribe) {
+	_uuid, err := uuid.NewV6()
+	Hassert(err == nil, "ClientBusHttp.Unsubscribe(): in generate UUID v6, err=\n\t%v", err)
+
+	req := &msg_unsub.UnsubReq{
+		Name_: handler.Name(),
+		Uuid_: _uuid.String(),
+	}
+	req.SelfCheck()
+	binReq, _ := json.MarshalIndent(req, "", "  ")
+	body := strings.NewReader(string(binReq))
+
+	hReq, err := http.NewRequest("POST", sf.urlRemote+"/bus/unsub", body)
+	Hassert(err == nil, "ClientBusHttp.Unsubscribe(): in new request, err=\n\t%v")
+
+	binBody, err := sf.makePost(hReq)
+	if err != nil {
+		sf.log.Err("ClientBusHttp.Unsubscribe(): in make request, err=\n\t%v")
+		return
+	}
+	resp := &msg_unsub.UnsubResp{}
+	err = json.Unmarshal(binBody, resp)
+	Hassert(err == nil, "ClientBusHttp.Unsubscribe(): in unmarshal response,  err=\n\t%v", err)
+	if string(resp.Status_) != "ok" {
+		sf.log.Err("ClientBusHttp.Unsubscribe(): resp!='ok', err=\n\t%v", resp.Status_)
+	}
+	Hassert(resp.Uuid_ == req.Uuid_, "ClientBusHttp.Unsubscribe(): resp uuid(%v) bad", resp.Uuid_)
+}
+
+// Subscribe -- подписывается на топик в дистанционной шине
+func (sf *ClientBusHttp) Subscribe(handler IBusHandlerSubscribe) error {
+	_uuid, err := uuid.NewV6()
+	Hassert(err == nil, "ClientBusHttp.Subscribe(): in generate UUID v6, err=\n\t%v", err)
+	req := &msg_sub.SubscribeReq{
+		Topic_:   handler.Topic(),
+		Uuid_:    _uuid.String(),
+		WebHook_: sf.urlLocal + "/bus/pub",
+	}
+	req.SelfCheck()
+	binReq, _ := json.MarshalIndent(req, "", "  ")
+	body := strings.NewReader(string(binReq))
+
+	hReq, err := http.NewRequest("POST", sf.urlRemote+"/bus/sub", body)
+	Hassert(err == nil, "ClientBusHttp.Subscribe(): in new request, err=\n\t%v")
+
+	binBody, err := sf.makePost(hReq)
+	if err != nil {
+		err := fmt.Errorf("ClientBusHttp.Subscribe(): in make request, err=\n\t%w", err)
+		sf.log.Err(err.Error())
+		return err
+	}
+	resp := &msg_sub.SubscribeResp{}
+	err = json.Unmarshal(binBody, resp)
+	Hassert(err == nil, "ClientBusHttp.Subscribe(): in unmarshal response,  err=\n\t%v", err)
+	if string(resp.Status_) != "ok" {
+		err := fmt.Errorf("ClientBusHttp.Subscribe(): resp!='ok', err=\n\t%v", resp.Status_)
+		sf.log.Err(err.Error())
+		return err
+	}
+	Hassert(resp.Uuid_ == req.Uuid_, "ClientBusHttp.Subscribe(): resp uuid(%v) bad", resp.Uuid_)
+	// FIXME: вот тут похоже дичь
+	_handler := handler.(*mock_hand_sub_http.MockHandSubHttp)
+	_handler.SetName(resp.Name_)
+	err = sf.bus.Subscribe(_handler)
+	return err
+}
+
+// SendRequest -- отправляет в дистанционную шину запрос
+func (sf *ClientBusHttp) SendRequest(topic ATopic, binReq []byte) ([]byte, error) {
+	_uuid, err := uuid.NewV6()
+	Hassert(err == nil, "ClientBusHttp.SendRequest(): in generate UUID v6, err=\n\t%v", err)
+	req := &msg_serve.ServeReq{
+		Topic_:  topic,
+		Uuid_:   _uuid.String(),
+		BinReq_: binReq,
+	}
+	req.SelfCheck()
+	_binReq, _ := json.MarshalIndent(req, "", "  ")
+	body := strings.NewReader(string(_binReq))
+
+	hReq, err := http.NewRequest("POST", sf.urlRemote+"/bus/request", body)
+	Hassert(err == nil, "ClientBusHttp.SendRequest(): in new request, err=\n\t%v")
+
+	binBody, err := sf.makePost(hReq)
+	if err != nil {
+		err := fmt.Errorf("ClientBusHttp.SendRequest(): in make request, err=\n\t%w", err)
+		sf.log.Err(err.Error())
+		return nil, err
+	}
+	resp := &msg_serve.ServeResp{}
+	err = json.Unmarshal(binBody, resp)
+	Hassert(err == nil, "ClientBusHttp.SendRequest(): in unmarshal response,  err=\n\t%v", err)
+	if string(resp.Status_) != "ok" {
+		err := fmt.Errorf("ClientBusHttp.SendRequest(): resp!='ok', err=\n\t%v", resp.Status_)
+		sf.log.Err(err.Error())
+		return nil, err
+	}
+	Hassert(resp.Uuid_ == req.Uuid_, "ClientBusHttp.SendRequest(): resp uuid(%v) bad", resp.Uuid_)
+	return resp.BinResp_, nil
+}
+
+// RegisterServe -- регистрирует в локальной шине обработчик
+func (sf *ClientBusHttp) RegisterServe(handler IBusHandlerServe) {
+	Hassert(handler != nil, "ClientBusHttp.RegisterServe(): handler==nil")
+	sf.bus.RegisterServe(handler)
+}
+
+// Publish -- публикует сообщение в дистанционной шину
+func (sf *ClientBusHttp) Publish(topic ATopic, binMsg []byte) error {
+	_uuid, err := uuid.NewV6()
+	Hassert(err == nil, "ClientBusHttp.Publish(): in generate UUID v6, err=\n\t%v", err)
+	req := &msg_pub.PublishReq{
+		Topic_:  topic,
+		Uuid_:   _uuid.String(),
+		BinMsg_: binMsg,
+	}
+	req.SelfCheck()
+	binReq, _ := json.MarshalIndent(req, "", "  ")
+	body := strings.NewReader(string(binReq))
+
+	hReq, err := http.NewRequest("POST", sf.urlRemote+"/bus/pub", body)
+	Hassert(err == nil, "ClientBusHttp.Publish(): in new request, err=\n\t%v")
+
+	binBody, err := sf.makePost(hReq)
+	if err != nil {
+		err := fmt.Errorf("ClientBusHttp.Publish(): in make request, err=\n\t%w", err)
+		sf.log.Err(err.Error())
+		return err
+	}
+	resp := &msg_pub.PublishResp{}
+	err = json.Unmarshal(binBody, resp)
+	Hassert(err == nil, "ClientBusHttp.Publish(): in unmarshal response,  err=\n\t%v", err)
+	if string(resp.Status_) != "ok" {
+		err := fmt.Errorf("ClientBusHttp.Publish(): resp!='ok', err=\n\t%v", resp.Status_)
+		sf.log.Err(err.Error())
+		return err
+	}
+	Hassert(resp.Uuid_ == req.Uuid_, "ClientBusHttp.Publish(): resp uuid(%v) bad", resp.Uuid_)
+	return nil
+}
+
+// Единый обработчик запросов
+func (sf *ClientBusHttp) makePost(hReq *http.Request) ([]byte, error) {
+	hReq.Header.Add("Content-Type", "application/json")
+	_resp, err := http.DefaultClient.Do(hReq)
+	if err != nil {
+		err := fmt.Errorf("ClientBusHttp.makePost(): after request, err=\n\t%w", err)
+		sf.log.Err(err.Error())
+		return nil, err
+	}
+	defer _resp.Body.Close()
+	binBody, _ := io.ReadAll(_resp.Body)
+	return binBody, nil
+}
+
+// Log -- возвращает локальный лог клиента
+func (sf *ClientBusHttp) Log() ILogBuf {
+	return sf.log
+}
+
+// IsWork -- возвращает признак работы
+func (sf *ClientBusHttp) IsWork() bool {
+	return sf.bus.IsWork()
+}

+ 391 - 0
krn/kbus/kbus_http/client_bus_http/client_bus_http_test.go

@@ -0,0 +1,391 @@
+package client_bus_http
+
+import (
+	"os"
+	"strings"
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_base"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kserv_http"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_env"
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_hand_serve"
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_hand_sub_http"
+)
+
+type tester struct {
+	t        *testing.T
+	ctx      IKernelCtx
+	cl       *ClientBusHttp
+	handSub  *mock_hand_sub_http.MockHandSubHttp
+	handServ *mock_hand_serve.MockHandlerServe
+	bus      *kbus_base.KBusBase
+}
+
+func TestClientBusHttp(t *testing.T) {
+	handSub := mock_hand_sub_http.NewMockHandSubHttp("test_topic_sub", "http://localhost:18314/").(*mock_hand_sub_http.MockHandSubHttp)
+	handServ := mock_hand_serve.NewMockHandlerServe("test_topic_serv", "local_hook")
+	sf := &tester{
+		t:        t,
+		ctx:      kctx.GetKernelCtx(),
+		handSub:  handSub,
+		handServ: handServ,
+		bus:      kbus_base.GetKernelBusBase(),
+	}
+	sf.new()
+	sf.unsub()
+	sf.sub()
+	sf.pub()
+	sf.unsubGood1()
+	sf.reg()
+	sf.send()
+}
+
+// Отправка запросов
+func (sf *tester) send() {
+	sf.t.Log("send")
+	sf.sendBad1()
+	sf.sendBad2()
+	sf.sendGood1()
+}
+
+func (sf *tester) sendGood1() {
+	sf.t.Log("sendGood1")
+	binResp, err := sf.cl.SendRequest("test_topic_serv", []byte("test msg 456"))
+	if err != nil {
+		sf.t.Fatalf("sendGood1(): err=%v", err)
+	}
+	if binResp == nil {
+		sf.t.Fatalf("sendGood1(): binResp==nil")
+	}
+	strResp := string(binResp)
+	if strResp != "test msg 456" {
+		sf.t.Fatalf("sendGood1(): strResp(%v)!='test msg 456'", strResp)
+	}
+}
+
+// Левый адрес
+func (sf *tester) sendBad2() {
+	sf.t.Log("sendBad2")
+	urlRemote := sf.cl.urlRemote
+	sf.cl.urlRemote = "tra-ta-ta"
+	defer func() {
+		sf.cl.urlRemote = urlRemote
+	}()
+	binResp, err := sf.cl.SendRequest("test_topic_serv", []byte("test msg"))
+	if err == nil {
+		sf.t.Fatalf("sendBad2(): err==nil")
+	}
+	if binResp != nil {
+		sf.t.Fatalf("sendBad2(): binResp!=nil")
+	}
+}
+
+// Нет такого топика
+func (sf *tester) sendBad1() {
+	sf.t.Log("sendBad1")
+	binResp, err := sf.cl.SendRequest("test_bad_topic", []byte("test msg"))
+	if err == nil {
+		sf.t.Fatalf("sendBad1(): err==nil")
+	}
+	if binResp != nil {
+		sf.t.Fatalf("sendBad1(): binResp!=nil")
+	}
+}
+
+// Регистрация серверного обработчика
+func (sf *tester) reg() {
+	sf.t.Log("reg")
+	sf.regBad1()
+	sf.regGood1()
+}
+
+func (sf *tester) regGood1() {
+	sf.t.Log("regGood1")
+	sf.cl.RegisterServe(sf.handServ)
+}
+
+// Нет серверного обработчика
+func (sf *tester) regBad1() {
+	sf.t.Log("regBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("regBad1(): panic==nil")
+		}
+	}()
+	sf.cl.RegisterServe(nil)
+}
+
+// Правильная отписка
+func (sf *tester) unsubGood1() {
+	sf.t.Log("unsubGood1")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("unsubGood1(): panic=%v", _panic)
+		}
+	}()
+	sf.cl.Unsubscribe(sf.handSub)
+}
+
+// Публикация сообщения
+func (sf *tester) pub() {
+	sf.t.Log("pub")
+	sf.pubBad1()
+	sf.pubGood1()
+	sf.pubBad2()
+}
+
+// С шиной что-то случилось
+func (sf *tester) pubBad2() {
+	sf.t.Log("pubBad2")
+	sf.bus.IsWork_.Reset()
+	defer sf.bus.IsWork_.Set()
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("pubBad2(): panic=%v", _panic)
+		}
+	}()
+	sf.handSub.BinMsg_ = []byte{}
+	err := sf.cl.Publish("test_topic_sub", []byte("test_msg_456"))
+	if err != nil {
+		if strings.Contains(err.Error(), "topic='test_topic_sub',bus already closed") {
+			return
+		}
+		sf.t.Fatalf("pubBad2(): err=%v", err)
+	}
+	for {
+		SleepMs()
+		if sf.handSub.Msg() != "" {
+			break
+		}
+	}
+	if msg := sf.handSub.Msg(); msg != "test_msg_456" {
+		sf.t.Fatalf("bad msg(%v)", msg)
+	}
+}
+
+func (sf *tester) pubGood1() {
+	sf.t.Log("pubGood1")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("pubGood1(): panic=%v", _panic)
+		}
+	}()
+	sf.handSub.BinMsg_ = []byte{}
+	err := sf.cl.Publish("test_topic_sub", []byte("test_msg_456"))
+	if err != nil {
+		if strings.Contains(err.Error(), "topic='test_topic_sub',bus already closed") {
+			return
+		}
+		sf.t.Fatalf("pubGood1(): err=%v", err)
+	}
+	for {
+		SleepMs()
+		if sf.handSub.Msg() != "" {
+			break
+		}
+	}
+	if msg := sf.handSub.Msg(); msg != "test_msg_456" {
+		sf.t.Fatalf("bad msg(%v)", msg)
+	}
+}
+
+// Левый адрес
+func (sf *tester) pubBad1() {
+	sf.t.Log("pubBad1")
+	urlRemote := sf.cl.urlRemote
+	sf.cl.urlRemote = "tra-ta-ta"
+	defer func() {
+		sf.cl.urlRemote = urlRemote
+	}()
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("pubBad1(): panic=%v", _panic)
+		}
+	}()
+	err := sf.cl.Publish("test_topic", []byte("test_msg"))
+	if err == nil {
+		sf.t.Fatalf("pubBad1(): err==nil")
+	}
+}
+
+// Подписка на топик
+func (sf *tester) sub() {
+	sf.t.Log("sub")
+	sf.subBad1()
+	sf.subBad2()
+	sf.subBad3()
+	sf.subGood1()
+}
+
+// С шиной что-то случилось
+func (sf *tester) subBad3() {
+	sf.t.Log("subBad3")
+	bus := kbus_base.Bus_
+	bus.IsWork_.Reset()
+	defer bus.IsWork_.Set()
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("subBad3(): panic=%v", _panic)
+		}
+	}()
+	err := sf.cl.Subscribe(sf.handSub)
+	if err != nil {
+		if strings.Contains(err.Error(), "bus already closed") {
+			return
+		}
+		sf.t.Fatalf("subBad3(): err=%v", err)
+	}
+}
+
+func (sf *tester) subGood1() {
+	sf.t.Log("subGood1")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("subGood1(): panic=%v", _panic)
+		}
+	}()
+	err := sf.cl.Subscribe(sf.handSub)
+	if err != nil {
+		if strings.Contains(err.Error(), "bus already closed") {
+			return
+		}
+		sf.t.Fatalf("subGood1(): err=%v", err)
+	}
+}
+
+// Левый адрес
+func (sf *tester) subBad2() {
+	sf.t.Log("subBad2")
+	urlRemote := sf.cl.urlRemote
+	sf.cl.urlRemote = "tra-ta-ta"
+	defer func() {
+		sf.cl.urlRemote = urlRemote
+	}()
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("subBad2(): panic=%v", _panic)
+		}
+	}()
+	err := sf.cl.Subscribe(sf.handSub)
+	if err == nil {
+		sf.t.Fatalf("subBad2(): err==nil")
+	}
+}
+
+// Нет обработчика
+func (sf *tester) subBad1() {
+	sf.t.Log("subBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("subBad1(): panic==nil")
+		}
+	}()
+	_ = sf.cl.Subscribe(nil)
+}
+
+// Отписка от топика
+func (sf *tester) unsub() {
+	sf.t.Log("unsub")
+	sf.unsubBad1()
+	sf.unsubBad2()
+	sf.unsubBad3()
+}
+
+// Левый адрес
+func (sf *tester) unsubBad3() {
+	sf.t.Log("unsubBad3")
+	urlRemote := sf.cl.urlRemote
+	sf.cl.urlRemote = "tra-ta-ta"
+	defer func() {
+		sf.cl.urlRemote = urlRemote
+	}()
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("unsubBad3(): panic=%v", _panic)
+		}
+	}()
+	sf.cl.Unsubscribe(sf.handSub)
+}
+
+// Нет такой подписки
+func (sf *tester) unsubBad2() {
+	sf.t.Log("unsubBad2")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("unsubBad2(): panic=%v", _panic)
+		}
+	}()
+	sf.cl.Unsubscribe(sf.handSub)
+}
+
+// Нет обработчика
+func (sf *tester) unsubBad1() {
+	sf.t.Log("unsubBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("unsubBad1(): panic==nil")
+		}
+	}()
+	sf.cl.Unsubscribe(nil)
+}
+
+// Создание нового клиента
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.newBad1()
+	sf.newBad2()
+	sf.newGood1()
+}
+
+func (sf *tester) newGood1() {
+	sf.t.Log("newGood1")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("newGood1(): panic=%v", _panic)
+		}
+	}()
+	_ = mock_env.MakeEnv()
+	_ = os.Unsetenv("LOCAL_HTTP_URL")
+	_ = os.Setenv("LOCAL_HTTP_URL", "http://localhost:18314/")
+	sf.ctx.Set("monolitName", "test_monolit", "comment")
+	sf.cl = NewClientBusHttp("http://localhost:18314/").(*ClientBusHttp)
+	kServHttp := kserv_http.GetKernelServHttp()
+	go kServHttp.Run()
+	for {
+		SleepMs()
+		if kServHttp.IsWork() {
+			break
+		}
+	}
+	if log := sf.cl.Log(); log == nil {
+		sf.t.Fatalf("newGood1(): log==nil")
+	}
+	if isWork := sf.cl.IsWork(); !isWork {
+		sf.t.Fatalf("newGood1(): isWork==false")
+	}
+}
+
+func (sf *tester) newBad2() {
+	sf.t.Log("newBad2")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("newBad2(): panic==nil")
+		}
+	}()
+	_ = mock_env.MakeEnv()
+	_ = NewClientBusHttp("")
+}
+
+// Нет ничего для HTTP-шины
+func (sf *tester) newBad1() {
+	sf.t.Log("newBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("newBad1(): panic==nil")
+		}
+	}()
+	_ = NewClientBusHttp("url")
+}

+ 204 - 0
krn/kbus/kbus_http/kbus_http.go

@@ -0,0 +1,204 @@
+// package kbus_http -- шина сообщений поверх HTTP
+package kbus_http
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/gofiber/fiber/v2"
+
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_base"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_msg/msg_pub"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_msg/msg_serve"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_msg/msg_sub"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_msg/msg_unsub"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kserv_http"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_hand_sub_http"
+)
+
+// kBusHttp -- шина данных поверх HTTP
+type kBusHttp struct {
+	*kbus_base.KBusBase
+	log ILogBuf
+}
+
+var (
+	bus *kBusHttp
+)
+
+// GetKernelBusHttp -- возвращает шину HTTP
+func GetKernelBusHttp() IKernelBus {
+	if bus != nil {
+		return bus
+	}
+	ctx := kctx.GetKernelCtx()
+	bus = &kBusHttp{
+		KBusBase: kbus_base.GetKernelBusBase(),
+	}
+	bus.log = bus.Log()
+	ctx.Set("kernBus", bus, "http data bus")
+	fibApp := kserv_http.GetKernelServHttp().Fiber()
+	fibApp.Post("/bus/sub", bus.postSub)             // Топик подписки, IN
+	fibApp.Post("/bus/unsub", bus.postUnsub)         // Топик отписки, IN
+	fibApp.Post("/bus/request", bus.postSendRequest) // Топик входящих запросов, IN
+	fibApp.Post("/bus/pub", bus.postPublish)         // Топик публикаций подписки, IN
+	return bus
+}
+
+// Входящий запрос HTTP на подписку
+func (sf *kBusHttp) postSub(ctx *fiber.Ctx) error {
+	ctx.Set("Content-type", "text/html; charset=utf8")
+	ctx.Set("Content-type", "text/json")
+	ctx.Set("Cache-Control", "no-cache")
+	sf.log.Debug("kBusHttp.postSub()")
+	req := &msg_sub.SubscribeReq{}
+	err := ctx.BodyParser(req)
+	if err != nil {
+		resp := &msg_sub.SubscribeResp{
+			Status_: fmt.Sprintf("kernelBusHttp.postSub(): in parse request, err=\n\t%v\n", err),
+			Uuid_:   req.Uuid_,
+		}
+		resp.SelfCheck()
+		ctx.Response().SetStatusCode(http.StatusBadRequest)
+		sf.log.Err(resp.Status_)
+		return ctx.JSON(resp)
+	}
+	resp := sf.processSubscribe(req)
+	resp.SelfCheck()
+	return ctx.JSON(resp)
+}
+
+// Процесс подписки веб-хука
+func (sf *kBusHttp) processSubscribe(req *msg_sub.SubscribeReq) *msg_sub.SubscribeResp {
+	req.SelfCheck()
+	handler := mock_hand_sub_http.NewMockHandSubHttp(req.Topic_, req.WebHook_)
+	resp := &msg_sub.SubscribeResp{
+		Status_: "ok",
+		Uuid_:   req.Uuid_,
+		Name_:   handler.Name(),
+	}
+	err := sf.Subscribe(handler)
+	if err != nil {
+		resp.Status_ = fmt.Sprintf("kernelBusHttp.processSubscribe(): err=\n\t%v", err)
+		return resp
+	}
+	return resp
+}
+
+// Входящая публикация
+func (sf *kBusHttp) postPublish(ctx *fiber.Ctx) error {
+	sf.log.Debug("kBusHttp.postPublish()")
+	ctx.Set("Content-type", "text/html; charset=utf8")
+	ctx.Set("Content-type", "text/json")
+	ctx.Set("Cache-Control", "no-cache")
+	req := &msg_pub.PublishReq{}
+	err := ctx.BodyParser(req)
+	if err != nil {
+		resp := &msg_pub.PublishResp{
+			Status_: fmt.Sprintf("kernelBusHttp.postPublish(): in parse request, err=\n\t%v\n", err),
+			Uuid_:   req.Uuid_,
+		}
+		resp.SelfCheck()
+		ctx.Response().SetStatusCode(http.StatusBadRequest)
+		sf.log.Err(resp.Status_)
+		return ctx.JSON(resp)
+	}
+	resp := sf.processPublish(req)
+	resp.SelfCheck()
+	return ctx.JSON(resp)
+}
+
+// Выполняет процесс публикации
+func (sf *kBusHttp) processPublish(req *msg_pub.PublishReq) *msg_pub.PublishResp {
+	req.SelfCheck()
+	err := sf.Publish(req.Topic_, req.BinMsg_)
+	resp := &msg_pub.PublishResp{
+		Status_: "ok",
+		Uuid_:   req.Uuid_,
+	}
+	if err != nil {
+		resp.Status_ = fmt.Sprintf("kernelBusHttp.processPublish(): err=\n\t%v", err)
+	}
+	return resp
+}
+
+// Входящий запрос
+func (sf *kBusHttp) postSendRequest(ctx *fiber.Ctx) error {
+	sf.log.Debug("kBusHttp.postSendRequest()")
+	ctx.Set("Content-type", "text/html; charset=utf8")
+	ctx.Set("Content-type", "text/json")
+	ctx.Set("Cache-Control", "no-cache")
+	req := &msg_serve.ServeReq{}
+	err := ctx.BodyParser(req)
+	if err != nil {
+		resp := &msg_serve.ServeResp{
+			Status_: fmt.Sprintf("kernelBusHttp.postSendRequest(): err=\n\t%v", err),
+			Uuid_:   req.Uuid_,
+		}
+		resp.SelfCheck()
+		ctx.Response().SetStatusCode(http.StatusBadRequest)
+		sf.log.Err(resp.Status_)
+		return ctx.JSON(resp)
+	}
+	resp := sf.processSendRequest(req)
+	resp.SelfCheck()
+	return ctx.JSON(resp)
+}
+
+// Обрабатывает входящий запрос
+func (sf *kBusHttp) processSendRequest(req *msg_serve.ServeReq) *msg_serve.ServeResp {
+	req.SelfCheck()
+	binResp, err := sf.SendRequest(req.Topic_, req.BinReq_)
+	resp := &msg_serve.ServeResp{
+		Status_:  "ok",
+		Uuid_:    req.Uuid_,
+		BinResp_: binResp,
+	}
+	if err != nil {
+		resp.Status_ = fmt.Sprintf("kernelBusHttp.processSendRequest(): err=\n\t%v", err)
+	}
+
+	return resp
+}
+
+// Входящая отписка от топика по HTTP
+func (sf *kBusHttp) postUnsub(ctx *fiber.Ctx) error {
+	sf.log.Debug("kBusHttp.postUnsub()")
+	ctx.Set("Content-type", "text/html; charset=utf8")
+	ctx.Set("Content-type", "text/json")
+	ctx.Set("Cache-Control", "no-cache")
+	req := &msg_unsub.UnsubReq{}
+	err := ctx.BodyParser(req)
+	if err != nil {
+		resp := &msg_serve.ServeResp{
+			Status_: fmt.Sprintf("kernelBusHttp.postSendRequest(): err=\n\t%v", err),
+			Uuid_:   req.Uuid_,
+		}
+		resp.SelfCheck()
+		ctx.Response().SetStatusCode(http.StatusBadRequest)
+		sf.log.Err(resp.Status_)
+		return ctx.JSON(resp)
+	}
+	resp := sf.processUnsubRequest(req)
+	resp.SelfCheck()
+	return ctx.JSON(resp)
+}
+
+// Процесс отписки от топика
+func (sf *kBusHttp) processUnsubRequest(req *msg_unsub.UnsubReq) *msg_unsub.UnsubResp {
+	req.SelfCheck()
+	_hand := sf.Ctx_.Get(string(req.Name_))
+	resp := &msg_unsub.UnsubResp{
+		Status_: "ok",
+		Uuid_:   req.Uuid_,
+	}
+	if _hand == nil {
+		resp.Status_ = fmt.Sprintf("kernelBusHttp.processUnsubRequest(): handler name(%v) not exists", req.Name_)
+		return resp
+	}
+	hand := _hand.Val().(IBusHandlerSubscribe)
+	sf.Unsubscribe(hand)
+	return resp
+}

+ 549 - 0
krn/kbus/kbus_http/kbus_http_test.go

@@ -0,0 +1,549 @@
+package kbus_http
+
+import (
+	"encoding/json"
+	"io"
+	"net/http"
+	"os"
+	"strings"
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_msg/msg_pub"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_msg/msg_serve"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_msg/msg_sub"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_msg/msg_unsub"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kserv_http"
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_env"
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_hand_serve"
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_hand_sub_local"
+)
+
+type tester struct {
+	t        *testing.T
+	handSub  *mock_hand_sub_local.MockHandlerSub
+	handServ *mock_hand_serve.MockHandlerServe
+}
+
+func TestKernelBusHttp(t *testing.T) {
+	sf := &tester{
+		t:        t,
+		handSub:  mock_hand_sub_local.NewMockHandlerSub("topic_sub", "http://localhost:18200/bus/pub"),
+		handServ: mock_hand_serve.NewMockHandlerServe("topic_serv", "name_serv"),
+	}
+	ctx := kctx.GetKernelCtx()
+	ctx.Set("monolitName", "test_monolit", "comment")
+	sf.get()
+	sf.req()
+	sf.sub()
+	sf.pub()
+	sf.unsub()
+}
+
+// Запрос на отписку
+func (sf *tester) unsub() {
+	sf.t.Log("unsub")
+	sf.unsubBad1()
+	sf.unsubBad2()
+	sf.unsubGood1()
+	sf.unsubBad3()
+	sf.unsubGood2()
+}
+
+func (sf *tester) unsubGood2() {
+	sf.t.Log("unsubGood2")
+	err := bus.Subscribe(sf.handSub)
+	if err != nil {
+		sf.t.Fatalf("unsubGood1(): err=%v", err)
+	}
+	req := &msg_unsub.UnsubReq{
+		Name_: sf.handSub.Name_,
+		Uuid_: "test_uuid",
+	}
+	binReq, _ := json.MarshalIndent(req, "", "  ")
+	body := strings.NewReader(string(binReq))
+	fibApp := kserv_http.GetKernelServHttp().Fiber()
+	hReq, err := http.NewRequest("POST", "/bus/unsub", body)
+	if err != nil {
+		sf.t.Fatalf("unsubGood2(): err=%v", err)
+	}
+	hReq.Header.Add("Content-Type", "application/json")
+
+	_resp, err := fibApp.Test(hReq)
+	if err != nil {
+		sf.t.Fatalf("unsubGood2(): after request, err=%v", err)
+	}
+	if _resp.StatusCode != 200 {
+		sf.t.Fatalf("unsubGood2(): statusCode(%v)!=200", _resp.StatusCode)
+	}
+	defer _resp.Body.Close()
+	binBody, _ := io.ReadAll(_resp.Body)
+	resp := &msg_unsub.UnsubResp{}
+	err = json.Unmarshal(binBody, resp)
+	if err != nil {
+		sf.t.Fatalf("unsubGood2(): err=%v", err)
+	}
+	if string(resp.Status_) != "ok" {
+		sf.t.Fatalf("unsubGood2(): resp(%v)!='ok'", string(resp.Status_))
+	}
+}
+
+// Кривой запрос
+func (sf *tester) unsubBad3() {
+	sf.t.Log("unsubBad3")
+	req := "tra-la-la"
+	binReq, _ := json.MarshalIndent(req, "", "  ")
+	body := strings.NewReader(string(binReq))
+	fibApp := kserv_http.GetKernelServHttp().Fiber()
+	hReq, err := http.NewRequest("POST", "/bus/unsub", body)
+	hReq.Header.Add("Content-Type", "application/json")
+	if err != nil {
+		sf.t.Fatalf("unsubBad3(): err=%v", err)
+	}
+	_resp, err := fibApp.Test(hReq)
+	if err != nil {
+		sf.t.Fatalf("unsubBad3(): after request, err=%v", err)
+	}
+	if _resp.StatusCode == 200 {
+		sf.t.Fatalf("unsubBad3(): statusCode(%v)==200", _resp.StatusCode)
+	}
+}
+
+func (sf *tester) unsubGood1() {
+	sf.t.Log("unsubGood1")
+	err := bus.Subscribe(sf.handSub)
+	if err != nil {
+		sf.t.Fatalf("unsubGood1(): err=%v", err)
+	}
+	count := 0
+	for count < 100 {
+		SleepMs()
+		count++
+	}
+	req := &msg_unsub.UnsubReq{
+		Name_: sf.handSub.Name_,
+		Uuid_: "test_uuid",
+	}
+	resp := bus.processUnsubRequest(req)
+	if resp.Status_ != "ok" {
+		sf.t.Fatalf("unsubGood1(): status(%v)!='ok'", resp.Status_)
+	}
+}
+
+// Все поля на месте, нет подписчика
+func (sf *tester) unsubBad2() {
+	sf.t.Log("unsubBad2")
+	bus.Unsubscribe(sf.handSub)
+	count := 0
+	for count < 100 {
+		SleepMs()
+		count++
+	}
+	req := &msg_unsub.UnsubReq{
+		Name_: sf.handSub.Name_,
+		Uuid_: "test_uuid",
+	}
+	resp := bus.processUnsubRequest(req)
+	if resp.Status_ == "ok" {
+		sf.t.Fatalf("unsubBad2(): status(%v)=='ok'", resp.Status_)
+	}
+}
+
+// Нет полей для процесса отписки
+func (sf *tester) unsubBad1() {
+	sf.t.Log("unsubBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("unsubBad1(): panic==nil")
+		}
+	}()
+	req := &msg_unsub.UnsubReq{}
+	_ = bus.processUnsubRequest(req)
+}
+
+// Запрос на публикацию
+func (sf *tester) pub() {
+	sf.t.Log("pub")
+	sf.pubBad1()
+	sf.pubGood1()
+	sf.pubBad2()
+	sf.pubBad3()
+	sf.pubGood2()
+}
+
+func (sf *tester) pubGood2() {
+	sf.t.Log("pubGood2")
+	req := &msg_pub.PublishReq{
+		Topic_:  "topic_sub",
+		Uuid_:   "test_uuid",
+		BinMsg_: []byte("http_pub"),
+	}
+	binReq, _ := json.MarshalIndent(req, "", "  ")
+	body := strings.NewReader(string(binReq))
+	fibApp := kserv_http.GetKernelServHttp().Fiber()
+	hReq, err := http.NewRequest("POST", "/bus/pub", body)
+	hReq.Header.Add("Content-Type", "application/json")
+	if err != nil {
+		sf.t.Fatalf("pubGood2(): err=%v", err)
+	}
+	_resp, err := fibApp.Test(hReq)
+	if err != nil {
+		sf.t.Fatalf("pubGood2(): after request, err=%v", err)
+	}
+	if _resp.StatusCode != 200 {
+		sf.t.Fatalf("pubGood2(): statusCode(%v)!=200", _resp.StatusCode)
+	}
+	defer _resp.Body.Close()
+	binBody, _ := io.ReadAll(_resp.Body)
+	resp := &msg_pub.PublishResp{}
+	err = json.Unmarshal(binBody, resp)
+	if err != nil {
+		sf.t.Fatalf("pubGood2(): err=%v", err)
+	}
+	if string(resp.Status_) != "ok" {
+		sf.t.Fatalf("pubGood2(): resp(%v)!='ok'", string(resp.Status_))
+	}
+}
+
+// Кривой запрос
+func (sf *tester) pubBad3() {
+	sf.t.Log("pubBad3")
+	req := "tra-la-la"
+	binReq, _ := json.MarshalIndent(req, "", "  ")
+	body := strings.NewReader(string(binReq))
+	fibApp := kserv_http.GetKernelServHttp().Fiber()
+	hReq, err := http.NewRequest("POST", "/bus/pub", body)
+	hReq.Header.Add("Content-Type", "application/json")
+	if err != nil {
+		sf.t.Fatalf("pubBad3(): err=%v", err)
+	}
+	_resp, err := fibApp.Test(hReq)
+	if err != nil {
+		sf.t.Fatalf("pubBad3(): after request, err=%v", err)
+	}
+	if _resp.StatusCode == 200 {
+		sf.t.Fatalf("pubBad3(): statusCode(%v)==200", _resp.StatusCode)
+	}
+}
+
+// Что-то случилось внутри шины
+func (sf *tester) pubBad2() {
+	sf.t.Log("pubBad2")
+	bus.IsWork_.Reset()
+	defer bus.IsWork_.Set()
+
+	req := &msg_pub.PublishReq{
+		Topic_:  "topic_sub",
+		Uuid_:   "test_uuid",
+		BinMsg_: []byte("test_pub"),
+	}
+	resp := bus.processPublish(req)
+	if resp.Status_ == "ok" {
+		sf.t.Fatalf("pubBad2(): status(%v)=='ok'", resp.Status_)
+	}
+}
+
+// Все поля на месте
+func (sf *tester) pubGood1() {
+	sf.t.Log("pubGood1")
+	err := bus.Subscribe(sf.handSub)
+	if err != nil {
+		sf.t.Fatalf("pubGood1(): err=%v", err)
+	}
+	req := &msg_pub.PublishReq{
+		Topic_:  "topic_sub",
+		Uuid_:   "test_uuid",
+		BinMsg_: []byte("test_pub"),
+	}
+	_ = bus.processPublish(req)
+	for {
+		SleepMs()
+		msg := string(sf.handSub.Msg())
+		if msg != "" {
+			break
+		}
+	}
+	msg := string(sf.handSub.Msg())
+	if msg != "test_pub" {
+		sf.t.Fatalf("pubGood1(): msg(%v)!='test_pub'", msg)
+	}
+}
+
+// Нет полей для процесса публикации
+func (sf *tester) pubBad1() {
+	sf.t.Log("pubBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("pubBad1(): panic==nil")
+		}
+	}()
+	req := &msg_pub.PublishReq{}
+	_ = bus.processPublish(req)
+}
+
+// Запрос подписки на топик
+func (sf *tester) sub() {
+	sf.t.Log("sub")
+	sf.subBad1()
+	sf.subBad2()
+	sf.subGood1()
+	sf.subBad3()
+	sf.subGood2()
+}
+
+// Полный процесс подписки
+func (sf *tester) subGood2() {
+	sf.t.Log("subGood2")
+	req := &msg_sub.SubscribeReq{
+		Topic_:   "topic_serv",
+		Uuid_:    "test_uuid",
+		WebHook_: "http://localhost:18200/bus/pub/",
+	}
+	binReq, _ := json.MarshalIndent(req, "", "  ")
+	body := strings.NewReader(string(binReq))
+	fibApp := kserv_http.GetKernelServHttp().Fiber()
+	hReq, err := http.NewRequest("POST", "/bus/sub", body)
+	hReq.Header.Add("Content-Type", "application/json")
+	if err != nil {
+		sf.t.Fatalf("subBad1(): err=%v", err)
+	}
+	_resp, err := fibApp.Test(hReq)
+	if err != nil {
+		sf.t.Fatalf("subBad1(): after request, err=%v", err)
+	}
+	if _resp.StatusCode != 200 {
+		sf.t.Fatalf("subBad1(): statusCode(%v)!=200", _resp.StatusCode)
+	}
+	defer _resp.Body.Close()
+	binBody, _ := io.ReadAll(_resp.Body)
+	resp := &msg_sub.SubscribeResp{}
+	err = json.Unmarshal(binBody, resp)
+	if err != nil {
+		sf.t.Fatalf("subBad1(): err=%v", err)
+	}
+	if string(resp.Status_) != "ok" {
+		sf.t.Fatalf("subBad1(): resp(%v)!='ok'", string(resp.Status_))
+	}
+}
+
+// Отключена базовая шина
+func (sf *tester) subBad3() {
+	sf.t.Log("subBad3")
+	req := &msg_sub.SubscribeReq{
+		Topic_:   "topic_serv",
+		Uuid_:    "test_uuid",
+		WebHook_: "http://localhost:18200/bus/pub/",
+	}
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("subBad3(): panic!=nil")
+		}
+	}()
+	// _bus := kernel_bus_base.GetKernelBusBase()
+	bus.IsWork_.Reset()
+	defer bus.IsWork_.Set()
+	resp := bus.processSubscribe(req)
+	if resp.Status_ == "ok" {
+		sf.t.Fatalf("subBad3(): resp==ok")
+	}
+}
+
+// Проверка полей запроса в процессе подписки
+func (sf *tester) subGood1() {
+	sf.t.Log("subGood1")
+	req := &msg_sub.SubscribeReq{
+		Topic_:   "topic_serv",
+		Uuid_:    "test_uuid",
+		WebHook_: "http://localhost:18200/bus/",
+	}
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("subGood1(): panic!=nil")
+		}
+	}()
+	_ = bus.processSubscribe(req)
+}
+
+// Проверка кривых полей запроса в процессе подписки
+func (sf *tester) subBad2() {
+	sf.t.Log("subBad2")
+	req := &msg_sub.SubscribeReq{
+		Topic_: "",
+	}
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("subBad2(): panic==nil")
+		}
+	}()
+	_ = bus.processSubscribe(req)
+}
+
+// Кривой запрос
+func (sf *tester) subBad1() {
+	sf.t.Log("subBad1")
+	req := "tra-ta-ta"
+	binReq, _ := json.MarshalIndent(req, "", "  ")
+	body := strings.NewReader(string(binReq))
+	fibApp := kserv_http.GetKernelServHttp().Fiber()
+	hReq, err := http.NewRequest("POST", "/bus/sub", body)
+	hReq.Header.Add("Content-Type", "application/json")
+	if err != nil {
+		sf.t.Fatalf("subBad1(): err=%v", err)
+	}
+	_resp, err := fibApp.Test(hReq)
+	if err != nil {
+		sf.t.Fatalf("subBad1(): after request, err=%v", err)
+	}
+	if _resp.StatusCode != 400 {
+		sf.t.Fatalf("subBad1(): statusCode(%v)!=400", _resp.StatusCode)
+	}
+}
+
+// Входящий запрос
+func (sf *tester) req() {
+	sf.t.Log("req")
+	sf.reqBad1()
+	sf.reqBad2()
+	sf.reqBad3()
+	sf.reqGood1()
+	sf.reqBad4()
+}
+
+// Что-то с обработчиком
+func (sf *tester) reqBad4() {
+	sf.t.Log("reqBad4")
+	sf.handServ.IsBad_.Set()
+	defer sf.handServ.IsBad_.Reset()
+	req := &msg_serve.ServeReq{
+		Topic_:  sf.handServ.Topic_,
+		Uuid_:   "test_uuid",
+		BinReq_: []byte("test_msg"),
+	}
+	binReq, _ := json.MarshalIndent(req, "", "  ")
+	body := strings.NewReader(string(binReq))
+	fibApp := kserv_http.GetKernelServHttp().Fiber()
+	hReq, err := http.NewRequest("POST", "/bus/request", body)
+	hReq.Header.Add("Content-Type", "application/json")
+	if err != nil {
+		sf.t.Fatalf("reqBad4(): err=%v", err)
+	}
+	_resp, err := fibApp.Test(hReq)
+	if err != nil {
+		sf.t.Fatalf("reqBad4(): after request, err=%v", err)
+	}
+	if _resp.StatusCode != 200 {
+		sf.t.Fatalf("reqBad4(): statusCode(%v)!=200", _resp.StatusCode)
+	}
+	defer _resp.Body.Close()
+	binBody, _ := io.ReadAll(_resp.Body)
+	resp := &msg_serve.ServeResp{}
+	err = json.Unmarshal(binBody, resp)
+	if err != nil {
+		sf.t.Fatalf("reqBad4(): err=%v", err)
+	}
+	if string(resp.Status_) == "ok" {
+		sf.t.Fatalf("reqBad4(): status(%v)=='ok'", string(resp.Status_))
+	}
+}
+
+func (sf *tester) reqGood1() {
+	sf.t.Log("reqGood1")
+	req := &msg_serve.ServeReq{
+		Topic_:  sf.handServ.Topic_,
+		Uuid_:   "test_uuid",
+		BinReq_: []byte("test_msg"),
+	}
+	binReq, _ := json.MarshalIndent(req, "", "  ")
+	body := strings.NewReader(string(binReq))
+	fibApp := kserv_http.GetKernelServHttp().Fiber()
+	hReq, err := http.NewRequest("POST", "/bus/request", body)
+	hReq.Header.Add("Content-Type", "application/json")
+	if err != nil {
+		sf.t.Fatalf("reqGood1(): err=%v", err)
+	}
+	_resp, err := fibApp.Test(hReq)
+	if err != nil {
+		sf.t.Fatalf("reqGood1(): after request, err=%v", err)
+	}
+	if _resp.StatusCode != 200 {
+		sf.t.Fatalf("reqGood1(): statusCode(%v)!=200", _resp.StatusCode)
+	}
+	defer _resp.Body.Close()
+	binBody, _ := io.ReadAll(_resp.Body)
+	resp := &msg_serve.ServeResp{}
+	err = json.Unmarshal(binBody, resp)
+	if err != nil {
+		sf.t.Fatalf("reqGood1(): err=%v", err)
+	}
+	if string(resp.BinResp_) != "test_msg" {
+		sf.t.Fatalf("reqGood1(): resp(%v)!='test_msg'", string(resp.BinResp_))
+	}
+}
+
+// Нет такого топика для запросов
+func (sf *tester) reqBad3() {
+	sf.t.Log("reqBad3")
+	req := &msg_serve.ServeReq{
+		Topic_:  "bad_topic",
+		Uuid_:   "test_uuid",
+		BinReq_: []byte("test_msg"),
+	}
+	binReq, _ := json.MarshalIndent(req, "", "  ")
+	body := strings.NewReader(string(binReq))
+	fibApp := kserv_http.GetKernelServHttp().Fiber()
+	hReq, err := http.NewRequest("POST", "/bus/request", body)
+	if err != nil {
+		sf.t.Fatalf("reqBad3(): err=%v", err)
+	}
+	resp, err := fibApp.Test(hReq)
+	if err != nil {
+		sf.t.Fatalf("reqBad3(): after request, err=%v", err)
+	}
+	if resp.StatusCode != 400 {
+		sf.t.Fatalf("reqBad3(): statusCode(%v)!=400", resp.StatusCode)
+	}
+}
+
+// Нет тела запроса
+func (sf *tester) reqBad2() {
+	sf.t.Log("reqBad1")
+	body := strings.NewReader("test_msg")
+	fibApp := kserv_http.GetKernelServHttp().Fiber()
+	hReq, err := http.NewRequest("POST", "/bus/request", body)
+	if err != nil {
+		sf.t.Fatalf("reqBad1(): err=%v", err)
+	}
+	resp, err := fibApp.Test(hReq)
+	if err != nil {
+		sf.t.Fatalf("reqBad1(): after request, err=%v", err)
+	}
+	if resp.StatusCode != 400 {
+		sf.t.Fatalf("reqBad1(): statusCode(%v)!=400", resp.StatusCode)
+	}
+}
+
+// Отсутствуют поля в запросе
+func (sf *tester) reqBad1() {
+	sf.t.Log("reqBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("reqBad1(): panic==nil")
+		}
+	}()
+	bus.processSendRequest(nil)
+}
+
+// Получает локальную шину
+func (sf *tester) get() {
+	sf.t.Log("get")
+	_ = mock_env.MakeEnv()
+	_ = os.Unsetenv("LOCAL_HTTP_URL")
+	os.Setenv("LOCAL_HTTP_URL", "http://localhost:18312/")
+	_ = GetKernelBusHttp()
+	if bus == nil {
+		sf.t.Fatalf("get(): bus==nil")
+	}
+	_ = GetKernelBusHttp()
+	bus.RegisterServe(sf.handServ)
+}

+ 20 - 0
krn/kbus/kbus_local/client_bus_local/client_bus_local.go

@@ -0,0 +1,20 @@
+// package client_bus_local -- клиент локальной шины
+package client_bus_local
+
+import (
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_local"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+// ClientBusLocal -- клиент локальной шины
+type ClientBusLocal struct {
+	IKernelBus
+}
+
+// NewClientBusLocal -- клиент локальной шины
+func NewClientBusLocal() IBusClient {
+	sf := &ClientBusLocal{
+		IKernelBus: kbus_local.GetKernelBusLocal(),
+	}
+	return sf
+}

+ 32 - 0
krn/kbus/kbus_local/client_bus_local/client_bus_local_test.go

@@ -0,0 +1,32 @@
+package client_bus_local
+
+import (
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+type tester struct {
+	t  *testing.T
+	cl IBusClient
+}
+
+func TestClientBusLocal(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+}
+
+// Создание нового клиента
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.cl = NewClientBusLocal()
+	if sf.cl == nil {
+		sf.t.Fatalf("new(): client==nil")
+	}
+	err := sf.cl.Publish("local_topic", []byte("test_msg"))
+	if err != nil {
+		sf.t.Fatalf("new(): err=%v", err)
+	}
+}

+ 27 - 0
krn/kbus/kbus_local/kbus_local.go

@@ -0,0 +1,27 @@
+// package kbus_local -- реализация локальной шины сообщений
+package kbus_local
+
+import (
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_base"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+// Локальная шина данных
+type kernelBusLocal struct {
+	*kbus_base.KBusBase
+}
+
+var (
+	bus *kernelBusLocal
+)
+
+// GetKernelBusLocal -- возвращает локальную шину сообщений
+func GetKernelBusLocal() IKernelBus {
+	if bus != nil {
+		return bus
+	}
+	bus = &kernelBusLocal{
+		KBusBase: kbus_base.GetKernelBusBase(),
+	}
+	return bus
+}

+ 49 - 0
krn/kbus/kbus_local/kbus_local_test.go

@@ -0,0 +1,49 @@
+package kbus_local
+
+import (
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_hand_serve"
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_hand_sub_local"
+)
+
+type tester struct {
+	t        *testing.T
+	bus      IKernelBus
+	handSub  *mock_hand_sub_local.MockHandlerSub
+	handServ *mock_hand_serve.MockHandlerServe
+}
+
+func TestKernelBusLocal(t *testing.T) {
+	sf := &tester{
+		t:        t,
+		handSub:  mock_hand_sub_local.NewMockHandlerSub("topic_hand_sub", "mock_hand_sub"),
+		handServ: mock_hand_serve.NewMockHandlerServe("topic_hand_serv", "mock_hand_serv"),
+	}
+	sf.new()
+}
+
+// Создание локальной шины
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.newGood1()
+
+}
+
+func (sf *tester) newGood1() {
+	sf.t.Log("newGood1")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("newGood1(): panic=%v", _panic)
+		}
+	}()
+	sf.bus = GetKernelBusLocal()
+	sf.bus = GetKernelBusLocal()
+	if sf.bus == nil {
+		sf.t.Fatalf("newGood1(): IKernelBus==nil")
+	}
+	if !sf.bus.IsWork() {
+		sf.t.Fatalf("newGood1(): bus not work")
+	}
+}

+ 31 - 0
krn/kbus/kbus_msg/msg_pub/msg_pub.go

@@ -0,0 +1,31 @@
+// package msg_pub -- сообщения публикации
+package msg_pub
+
+import (
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+)
+
+// PublishReq -- запрос на публикацию
+type PublishReq struct {
+	Topic_  ATopic `json:"topic"`
+	Uuid_   string `json:"uuid"`
+	BinMsg_ []byte `json:"msg"`
+}
+
+// SelfCheck -- проверяет правильность своих полей
+func (sf *PublishReq) SelfCheck() {
+	Hassert(sf.Topic_ != "", "PublishReq.SelfCheck(): topic is empty")
+	Hassert(sf.Uuid_ != "", "PublishReq.SelfCheck(): uuid is empty")
+}
+
+// PublishResp -- ответ на запрос публикации
+type PublishResp struct {
+	Status_ string `json:"status"`
+	Uuid_   string `json:"uuid"`
+}
+
+// SelfCheck -- проверяет правильность своих полей
+func (sf *PublishResp) SelfCheck() {
+	Hassert(sf.Status_ != "", "PublishResp.SelfCheck(): status is empty")
+}

+ 63 - 0
krn/kbus/kbus_msg/msg_pub/msg_pub_test.go

@@ -0,0 +1,63 @@
+package msg_pub
+
+import (
+	"testing"
+)
+
+type tester struct {
+	t *testing.T
+}
+
+func TestPublishMsg(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.req()
+	sf.resp()
+}
+
+// Работа с ответом
+func (sf *tester) resp() {
+	sf.t.Log("resp")
+	sf.respBad1()
+	resp := &PublishResp{
+		Status_: "test_ok",
+	}
+	resp.SelfCheck()
+}
+
+// Кривые поля ответа
+func (sf *tester) respBad1() {
+	sf.t.Log("respBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("respBad1(): panic==nil")
+		}
+	}()
+	resp := &PublishResp{}
+	resp.SelfCheck()
+}
+
+// Работа с запросом
+func (sf *tester) req() {
+	sf.t.Log("req")
+	sf.reqBad1()
+	req := &PublishReq{
+		Topic_:  "test_topic",
+		Uuid_:   "test_uuid",
+		BinMsg_: []byte("test msg"),
+	}
+	req.SelfCheck()
+}
+
+// Кривые поля
+func (sf *tester) reqBad1() {
+	sf.t.Log("reqBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("reqBad1(): panic==nil")
+		}
+	}()
+	req := &PublishReq{}
+	req.SelfCheck()
+}

+ 32 - 0
krn/kbus/kbus_msg/msg_serve/msg_serve.go

@@ -0,0 +1,32 @@
+// package msg_serve -- сообщения на обслуживание входящих запросов
+package msg_serve
+
+import (
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+)
+
+// ServeReq -- входящий запрос на обслуживание
+type ServeReq struct {
+	Topic_  ATopic `json:"topic"`
+	Uuid_   string `json:"uuid"`
+	BinReq_ []byte `json:"req"`
+}
+
+// SelfCheck -- проверяет структуру на правильность полей
+func (sf *ServeReq) SelfCheck() {
+	Hassert(sf.Topic_ != "", "ServeReq.SelfCheck(): topic is empty")
+	Hassert(sf.Uuid_ != "", "ServeReq.SelfCheck(): uuid is empty")
+}
+
+// ServeResp -- ответ на входящий запрос
+type ServeResp struct {
+	Status_  string `json:"status"`
+	Uuid_    string `json:"uuid"`
+	BinResp_ []byte `json:"resp"`
+}
+
+// SelfCheck -- проверяет правильность своих полей
+func (sf *ServeResp) SelfCheck() {
+	Hassert(sf.Status_ != "", "ServeResp.SelfCheck(): status is empty")
+}

+ 63 - 0
krn/kbus/kbus_msg/msg_serve/msg_serve_test.go

@@ -0,0 +1,63 @@
+package msg_serve
+
+import (
+	"testing"
+)
+
+type tester struct {
+	t *testing.T
+}
+
+func TestServeMsg(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.req()
+	sf.resp()
+}
+
+// Работа с ответом
+func (sf *tester) resp() {
+	sf.t.Log("resp")
+	sf.respBad1()
+	resp := &ServeResp{
+		Status_: "test_ok",
+	}
+	resp.SelfCheck()
+}
+
+// Кривые поля ответа
+func (sf *tester) respBad1() {
+	sf.t.Log("respBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("respBad1(): panic==nil")
+		}
+	}()
+	resp := &ServeResp{}
+	resp.SelfCheck()
+}
+
+// Работа с запросом
+func (sf *tester) req() {
+	sf.t.Log("req")
+	sf.reqBad1()
+	req := &ServeReq{
+		Topic_:  "test_topic",
+		Uuid_:   "test_uuid",
+		BinReq_: []byte("test msg"),
+	}
+	req.SelfCheck()
+}
+
+// Кривые поля
+func (sf *tester) reqBad1() {
+	sf.t.Log("reqBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("reqBad1(): panic==nil")
+		}
+	}()
+	req := &ServeReq{}
+	req.SelfCheck()
+}

+ 33 - 0
krn/kbus/kbus_msg/msg_sub/msg_sub.go

@@ -0,0 +1,33 @@
+// package msg_sub -- сообщения для подписки
+package msg_sub
+
+import (
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+)
+
+// SubscribeReq -- входящий запрос на подписку
+type SubscribeReq struct {
+	Topic_   ATopic `json:"topic"` // Топик, на который надо подписаться
+	Uuid_    string `json:"uuid"`
+	WebHook_ string `json:"web_hook"` // Веб-хук для обратного вызова
+}
+
+// SelfCheck -- проверяет поля на правильность
+func (sf *SubscribeReq) SelfCheck() {
+	Hassert(sf.Topic_ != "", "SubscribeReq.SelfCheck(): topic is empty")
+	Hassert(sf.Uuid_ != "", "SubscribeReq.SelfCheck(): uuid is empty")
+	Hassert(sf.WebHook_ != "", "SubscribeReq.SelfCheck(): WebHook_ is empty")
+}
+
+// SubscribeResp -- ответ на запрос подписки
+type SubscribeResp struct {
+	Status_ string       `json:"status"`
+	Uuid_   string       `json:"uuid"`
+	Name_   AHandlerName `json:"name"` // Уникальное имя подписки
+}
+
+// SelfCheck -- проверяет правильность своих полей
+func (sf *SubscribeResp) SelfCheck() {
+	Hassert(sf.Status_ != "", "SubscribeResp.SelfCheck(): status is empty")
+}

+ 63 - 0
krn/kbus/kbus_msg/msg_sub/msg_sub_test.go

@@ -0,0 +1,63 @@
+package msg_sub
+
+import (
+	"testing"
+)
+
+type tester struct {
+	t *testing.T
+}
+
+func TestPublishMsg(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.req()
+	sf.resp()
+}
+
+// Работа с ответом
+func (sf *tester) resp() {
+	sf.t.Log("resp")
+	sf.respBad1()
+	resp := &SubscribeResp{
+		Status_: "test_ok",
+	}
+	resp.SelfCheck()
+}
+
+// Кривые поля ответа
+func (sf *tester) respBad1() {
+	sf.t.Log("respBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("respBad1(): panic==nil")
+		}
+	}()
+	resp := &SubscribeResp{}
+	resp.SelfCheck()
+}
+
+// Работа с запросом
+func (sf *tester) req() {
+	sf.t.Log("req")
+	sf.reqBad1()
+	req := &SubscribeReq{
+		Topic_:   "test_topic",
+		Uuid_:    "test_uuid",
+		WebHook_: "test msg",
+	}
+	req.SelfCheck()
+}
+
+// Кривые поля
+func (sf *tester) reqBad1() {
+	sf.t.Log("reqBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("reqBad1(): panic==nil")
+		}
+	}()
+	req := &SubscribeReq{}
+	req.SelfCheck()
+}

+ 30 - 0
krn/kbus/kbus_msg/msg_unsub/msg_unsub.go

@@ -0,0 +1,30 @@
+// package msg_unsub -- сообщения отписки
+package msg_unsub
+
+import (
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+)
+
+// UnsubReq -- запрос на отписку от топика
+type UnsubReq struct {
+	Name_ AHandlerName `json:"name"` // Уникальная метка подписки
+	Uuid_ string       `json:"uuid"`
+}
+
+// SelfCheck -- проверка запроса на правильность полей
+func (sf *UnsubReq) SelfCheck() {
+	Hassert(sf.Name_ != "", "UnsubReq.SelfCheck(): name is empty")
+	Hassert(sf.Uuid_ != "", "UnsubReq.SelfCheck(): uuid is empty")
+}
+
+// UnsubResp -- ответ на запрос отписки
+type UnsubResp struct {
+	Status_ string `json:"status"`
+	Uuid_   string `json:"uuid"`
+}
+
+// SelfCheck -- проверяет правильность своих полей
+func (sf *UnsubResp) SelfCheck() {
+	Hassert(sf.Status_ != "", "UnsubResp.SelfCheck(): status is empty")
+}

+ 62 - 0
krn/kbus/kbus_msg/msg_unsub/msg_unsub_test.go

@@ -0,0 +1,62 @@
+package msg_unsub
+
+import (
+	"testing"
+)
+
+type tester struct {
+	t *testing.T
+}
+
+func TestPublishMsg(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.req()
+	sf.resp()
+}
+
+// Работа с ответом
+func (sf *tester) resp() {
+	sf.t.Log("resp")
+	sf.respBad1()
+	resp := &UnsubResp{
+		Status_: "test_ok",
+	}
+	resp.SelfCheck()
+}
+
+// Кривые поля ответа
+func (sf *tester) respBad1() {
+	sf.t.Log("respBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("respBad1(): panic==nil")
+		}
+	}()
+	resp := &UnsubResp{}
+	resp.SelfCheck()
+}
+
+// Работа с запросом
+func (sf *tester) req() {
+	sf.t.Log("req")
+	sf.reqBad1()
+	req := &UnsubReq{
+		Name_: "test_topic",
+		Uuid_: "test_uuid",
+	}
+	req.SelfCheck()
+}
+
+// Кривые поля
+func (sf *tester) reqBad1() {
+	sf.t.Log("reqBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("reqBad1(): panic==nil")
+		}
+	}()
+	req := &UnsubReq{}
+	req.SelfCheck()
+}

+ 82 - 0
krn/kctx/kctx.go

@@ -0,0 +1,82 @@
+// package kctx -- контекст ядра
+package kctx
+
+import (
+	"context"
+	"sync"
+
+	"gitp78su.ipnodns.ru/svi/kern/kc/local_ctx"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx/kernel_keeper"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx/kwg"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+// kCtx -- контекст ядра
+type kCtx struct {
+	ILocalCtx
+	log        ILogBuf
+	ctxBg      context.Context // Неотменяемый контекст ядра
+	ctx        context.Context // Отменяемый контекст ядра
+	fnCancel   func()          // Функция отмены контекста ядра
+	kernKeeper IKernelKeeper   // Встроенный сторож отмены контекста системным сигналом
+	kernWg     IKernelWg       // Встроенный ожидатель потока
+}
+
+var (
+	kernCtx *kCtx // Глобальный объект контекста приложения
+	block   sync.Mutex
+)
+
+// GetKernelCtx -- возвращает контекст ядра
+func GetKernelCtx() IKernelCtx {
+	block.Lock()
+	defer block.Unlock()
+	if kernCtx != nil {
+		return kernCtx
+	}
+	ctxBg := context.Background()
+	ctx, fnCancel := context.WithCancel(ctxBg)
+	sf := &kCtx{
+		ctxBg:    ctxBg,
+		ctx:      ctx,
+		fnCancel: fnCancel,
+	}
+	sf.ILocalCtx = local_ctx.NewLocalCtx(sf.ctx)
+	sf.log = sf.Log()
+	sf.kernWg = kwg.GetKernelWg(sf.ctx)
+	sf.kernKeeper = kernel_keeper.GetKernelKeeper(sf.ctx, sf.fnCancel, sf.kernWg)
+	kernCtx = sf
+	return kernCtx
+}
+
+// Keeper -- возвращает сторож системных сигналов
+func (sf *kCtx) Keeper() IKernelKeeper {
+	return sf.kernKeeper
+}
+
+// Wg -- возвращает ожидатель потоков
+func (sf *kCtx) Wg() IKernelWg {
+	return sf.kernWg
+}
+
+// Done -- блокирующий вызов ожидания отмены контекста ядра
+func (sf *kCtx) Done() {
+	<-sf.ctx.Done()
+	sf.log.Debug("kCtx.Done()")
+}
+
+// CtxBg -- возвращает неотменяемый контекст ядра (лучше не использовать)
+func (sf *kCtx) CtxBg() context.Context {
+	return sf.ctxBg
+}
+
+// BaseCtx -- возвращает контекст ядра
+func (sf *kCtx) BaseCtx() context.Context {
+	return sf.ctx
+}
+
+// Cancel -- отменяет контекст ядра
+func (sf *kCtx) Cancel() {
+	sf.fnCancel()
+	sf.log.Debug("kCtx.Cancel()")
+}

+ 53 - 0
krn/kctx/kctx_test.go

@@ -0,0 +1,53 @@
+package kctx
+
+import (
+	"testing"
+)
+
+type tester struct {
+	t *testing.T
+}
+
+func TestKernelCtx(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+}
+
+// Создание контекста ядра
+func (sf *tester) new() {
+	sf.t.Log("new")
+	ctx := GetKernelCtx()
+	if ctx == nil {
+		sf.t.Fatalf("new(): KernelCtx==nil")
+	}
+	if ctx := ctx.CtxBg(); ctx != kernCtx.ctxBg {
+		sf.t.Fatalf("new(): ctx!=ctxBg")
+	}
+	if ctx := ctx.BaseCtx(); ctx != kernCtx.ctx {
+		sf.t.Fatalf("new(): ctx!=kernel.ctx")
+	}
+	ctx.Set("counter", 5, "test_counter")
+	if ctx.Get("counter") == nil {
+		sf.t.Fatalf("new(): counter==nil")
+	}
+	counter := ctx.Get("counter").Val().(int)
+	if counter != 5 {
+		sf.t.Fatalf("new(): counter(%v)!=5", counter)
+	}
+	ctx.Del("counter")
+	ctx.Cancel()
+	ctx.Done()
+	ctx = GetKernelCtx()
+	if ctx == nil {
+		sf.t.Fatalf("new(): KernelCtx==nil")
+	}
+	if wg := ctx.Wg(); wg == nil {
+		sf.t.Fatalf("new(): IKernelWg==nil")
+	}
+	if keep := ctx.Keeper(); keep == nil {
+		sf.t.Fatalf("new(): IKernelKeeper==nil")
+	}
+
+}

+ 80 - 0
krn/kctx/kernel_keeper/kernel_keeper.go

@@ -0,0 +1,80 @@
+// package kernel_keeper -- сторож системных сигналов
+package kernel_keeper
+
+import (
+	"context"
+	"os"
+	"os/signal"
+	"sync"
+	"syscall"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/kc/log_buf"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+// kernelKeeper -- сторож системных сигналов
+type kernelKeeper struct {
+	ctx      context.Context
+	fnCancel func()
+	wg       IKernelWg
+	log      ILogBuf
+	chSys_   chan os.Signal
+}
+
+var (
+	kernKeep *kernelKeeper
+	block    sync.Mutex
+)
+
+// GetKernelKeeper -- возвращает новый сторож системных сигналов
+func GetKernelKeeper(ctx context.Context, fnCancel func(), wg IKernelWg) *kernelKeeper {
+	block.Lock()
+	defer block.Unlock()
+	if kernKeep != nil {
+		kernKeep.log.Debug("GetKernelKeeper()")
+		return kernKeep
+	}
+	Hassert(ctx != nil, "NewKernelCtx(): ctx==nil")
+	Hassert(wg != nil, "NewKernelCtx(): IKernelWg==nil")
+	Hassert(fnCancel != nil, "NewKernelCtx(): fnCancel==nil")
+	sf := &kernelKeeper{
+		ctx:      ctx,
+		fnCancel: fnCancel,
+		wg:       wg,
+		log:      log_buf.NewLogBuf(),
+		chSys_:   make(chan os.Signal, 2),
+	}
+	sf.log.Debug("GetKernelKeeper(): first run")
+	err := sf.wg.Add("kernel_keeper")
+	Hassert(err == nil, "NewKernelCtx(): in add stream kernel keeper in IKernelWg, err=\n\t%v,err")
+
+	go sf.run(sf.chSys_)
+	kernKeep = sf
+	_ = IKernelKeeper(sf)
+	return sf
+}
+
+// Log -- возвращает лог сторожа системных сигналов
+func (sf *kernelKeeper) Log() ILogBuf {
+	return sf.log
+}
+
+// Работает в отдельном потоке и ждёт сигналов прерываний работы
+func (sf *kernelKeeper) run(chSys chan os.Signal) {
+	sf.log.Debug("kernelKeeper.run()")
+
+	// Регистрируем сигналы SIGINT (Ctrl+C) и SIGTERM (завершение процесса)
+	// syscall.SIGHUP: Сигнал, отправляемый при закрытии терминала.
+	// syscall.SIGQUIT: Сигнал, отправляемый при нажатии **Ctrl+**.
+	signal.Notify(chSys, syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP, syscall.SIGQUIT)
+	select {
+	case sig := <-chSys: // системный сигнал
+		sf.log.Debug("kernelKeeper.run(): system signal, sig=%v\n", sig)
+		sf.fnCancel()
+	case <-sf.ctx.Done(): // сигнал от приложения
+		sf.log.Debug("kernelKeeper.run(): cancel app context, err=\n\t%v\n", sf.ctx.Err())
+	}
+	sf.wg.Done("kernel_keeper")
+	sf.log.Debug("kernelKeeper.run(): end")
+}

+ 77 - 0
krn/kctx/kernel_keeper/kernel_keeper_test.go

@@ -0,0 +1,77 @@
+package kernel_keeper
+
+import (
+	"context"
+	"os"
+	"testing"
+	"time"
+
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx/kwg"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+type tester struct {
+	t        *testing.T
+	ctx      context.Context
+	fnCancel func()
+	wg       IKernelWg
+}
+
+func TestKernelKeeper(t *testing.T) {
+	ctxBg := context.Background()
+	ctx, fnCancel := context.WithCancel(ctxBg)
+	wg := kwg.GetKernelWg(ctx)
+	defer fnCancel()
+	sf := &tester{
+		t:        t,
+		ctx:      ctx,
+		fnCancel: fnCancel,
+		wg:       wg,
+	}
+	sf.get()
+	sf.get2()
+	sf.done()
+}
+
+// Отмена контекста приложения
+func (sf *tester) done() {
+	sf.t.Log("done")
+	sf.fnCancel()
+	time.Sleep(time.Millisecond * 10)
+	chSys := make(chan os.Signal, 1)
+	kernKeep.run(chSys)
+}
+
+type sysSig struct {
+}
+
+func (sf *sysSig) String() string {
+	return "test_sig"
+}
+
+func (sf *sysSig) Signal() {
+}
+func (sf *tester) get2() {
+	sf.t.Log("get2")
+	chSys := make(chan os.Signal, 2)
+	sig := &sysSig{}
+	chSys <- sig
+	go kernKeep.run(chSys)
+	sf.fnCancel()
+	sf.wg.Wait()
+}
+
+// Получает сторож ядра
+func (sf *tester) get() {
+	sf.t.Log("get")
+	keep := GetKernelKeeper(sf.ctx, sf.fnCancel, sf.wg)
+	if keep == nil {
+		sf.t.Fatalf("get(): IKernelKeeper==nil")
+	}
+	_ = GetKernelKeeper(sf.ctx, sf.fnCancel, sf.wg)
+	if log := keep.Log(); log == nil {
+		sf.t.Fatalf("get(): log==nil")
+	}
+	close(keep.chSys_)
+	time.Sleep(time.Millisecond * 10)
+}

+ 137 - 0
krn/kctx/kwg/kwg.go

@@ -0,0 +1,137 @@
+// package kwg -- именованный ожидатель потоков ядра
+//
+// Не позволяет завершиться ядру, если есть хоть один работающий поток
+package kwg
+
+import (
+	"context"
+	"fmt"
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/kc/log_buf"
+	"gitp78su.ipnodns.ru/svi/kern/kc/safe_bool"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+// kernelWg -- именованный ожидатель потоков ядра
+type kernelWg struct {
+	sync.RWMutex
+	ctx        context.Context
+	dictStream map[AStreamName]bool // Словарь имён потоков с признаком работы
+	isWork     ISafeBool
+	log        ILogBuf
+}
+
+var (
+	kernWg *kernelWg // Глобальный объект
+	block  sync.Mutex
+)
+
+// GetKernelWg -- возвращает новый именованный ожидатель потоков ядра
+func GetKernelWg(ctx context.Context) IKernelWg {
+	block.Lock()
+	defer block.Unlock()
+	if kernWg != nil {
+		kernWg.log.Debug("GetKernelWg()")
+		return kernWg
+	}
+	Hassert(ctx != nil, "GetKernelWg(): ctx==nil")
+	sf := &kernelWg{
+		ctx:        ctx,
+		dictStream: map[AStreamName]bool{},
+		isWork:     safe_bool.NewSafeBool(),
+		log:        log_buf.NewLogBuf(),
+	}
+	sf.log.Debug("GetKernelWg(): run")
+	go sf.close()
+	sf.isWork.Set()
+	kernWg = sf
+	return kernWg
+}
+
+// Log -- возвращает лог ожидателя потоков
+func (sf *kernelWg) Log() ILogBuf {
+	return sf.log
+}
+
+// Len -- возвращает размер списка ожидания потоков
+func (sf *kernelWg) Len() int {
+	sf.RLock()
+	defer sf.RUnlock()
+	return len(sf.dictStream)
+}
+
+// IsWork -- возвращает признак работы ядра
+func (sf *kernelWg) IsWork() bool {
+	return sf.isWork.Get()
+}
+
+// List -- возвращает список имён потоков на ожидании
+func (sf *kernelWg) List() []AStreamName {
+	sf.RLock()
+	defer sf.RUnlock()
+	lst := []AStreamName{}
+	for name := range sf.dictStream {
+		lst = append(lst, name)
+	}
+	return lst
+}
+
+// Done -- удаляет поток из ожидания
+func (sf *kernelWg) Done(name AStreamName) {
+	sf.Lock()
+	defer sf.Unlock()
+	delete(sf.dictStream, name)
+	sf.log.Debug("kernelWg.Done(): stream(%v) done", name)
+}
+
+// Wait -- блокирующий вызов; возвращает управление, только когда все потоки завершили работу
+func (sf *kernelWg) Wait() {
+	for {
+		SleepMs()
+		if !sf.isWork.Get() {
+			break
+		}
+	}
+	sf.log.Debug("kernelWg.Wait(): done")
+}
+
+// Add -- добавляет поток в ожидание
+func (sf *kernelWg) Add(name AStreamName) error {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.log.Debug("kernelWg.Add(): stream='%v'", name)
+	if !sf.isWork.Get() {
+		return fmt.Errorf("kernelWg.Add(): stream=%v, work end", name)
+	}
+	Hassert(name != "", "kernelWg.Add(): name stream is empty")
+	_, isOk := sf.dictStream[name]
+	Hassert(!isOk, "kernelWg.Add(): stream '%v' already exists", name)
+	sf.dictStream[name] = true
+	return nil
+}
+
+// Ожидает окончания работы ожидателя групп
+func (sf *kernelWg) close() {
+	<-sf.ctx.Done()
+	fnDone := func() bool {
+		sf.Lock()
+		defer sf.Unlock()
+		return len(sf.dictStream) == 0
+	}
+	for {
+		SleepMs()
+		if fnDone() {
+			break
+		}
+	}
+	sf.Lock()
+	defer sf.Unlock()
+	if !sf.isWork.Get() {
+		return
+	}
+	sf.isWork.Reset()
+	sf.log.Debug("kernelWg.close(): end")
+}

+ 156 - 0
krn/kctx/kwg/kwg_test.go

@@ -0,0 +1,156 @@
+package kwg
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+type tester struct {
+	t        *testing.T
+	ctx      context.Context
+	fnCancel func()
+	wg       IKernelWg
+}
+
+func TestKernelWG(t *testing.T) {
+	ctxBg := context.Background()
+	ctx, fnCancel := context.WithCancel(ctxBg)
+	defer fnCancel()
+	sf := &tester{
+		t:        t,
+		ctx:      ctx,
+		fnCancel: fnCancel,
+	}
+	sf.new()
+	sf.add()
+	sf.done()
+	sf.wait()
+	sf.addBad3()
+}
+
+// Попытка добавления после закрытия ожидателя
+func (sf *tester) addBad3() {
+	sf.t.Log("addBad3")
+	err := sf.wg.Add("test_stream")
+	if err == nil {
+		sf.t.Fatalf("addBad3(): err==nil")
+	}
+	_len := sf.wg.Len()
+	if _len != 0 {
+		sf.t.Fatalf("addBad3(): len(%v)!=0", _len)
+	}
+	sf.fnCancel()
+	kernWg.close()
+}
+
+// Убирает имя потока из ожидателя
+func (sf *tester) done() {
+	sf.t.Log("done")
+	sf.wg.Done("test_stream")
+	lst := sf.wg.List()
+	if len(lst) != 0 {
+		sf.t.Fatalf("addBad1(): len(lst)!=0, lst=%#v", lst)
+	}
+}
+
+// Добавление потока ожидания
+func (sf *tester) add() {
+	sf.t.Log("add")
+	sf.addGood1()
+	sf.addBad1()
+	sf.addBad2()
+}
+
+// Уже есть такое имя потока
+func (sf *tester) addBad2() {
+	sf.t.Log("addBad2")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("addBad1(): panic==nil")
+		}
+		lst := sf.wg.List()
+		if len(lst) != 1 {
+			sf.t.Fatalf("addBad1(): len(lst)!=1, lst=%#v", lst)
+		}
+	}()
+	_ = sf.wg.Add("test_stream")
+}
+
+// Пустое имя потока
+func (sf *tester) addBad1() {
+	sf.t.Log("addBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("addBad1(): panic==nil")
+		}
+	}()
+	_ = sf.wg.Add("")
+}
+
+func (sf *tester) addGood1() {
+	sf.t.Log("addGood1")
+	err := sf.wg.Add("test_stream")
+	if err != nil {
+		sf.t.Fatalf("addGood1(): err=%v", err)
+	}
+}
+
+// Ожидание завершения ожидателя потоков
+func (sf *tester) wait() {
+	sf.t.Log("wait")
+	go sf.wg.Wait()
+	time.Sleep(time.Millisecond * 5)
+	sf.fnCancel()
+	for {
+		time.Sleep(time.Millisecond * 10)
+		if !sf.wg.IsWork() {
+			break
+		}
+	}
+}
+
+// Создаёт ожидатель потоков ядра
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.newBad1()
+	sf.newGood1()
+}
+
+func (sf *tester) newGood1() {
+	sf.t.Log("newGood1")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("newGood1(): panic=%v", _panic)
+		}
+	}()
+	sf.wg = GetKernelWg(sf.ctx)
+	if sf.wg == nil {
+		sf.t.Fatalf("newGood1(): KernelWg==nil")
+	}
+	if !sf.wg.IsWork() {
+		sf.t.Fatalf("wait(): isWork==false")
+	}
+	wg := GetKernelWg(sf.ctx)
+	if sf.wg != wg {
+		sf.t.Fatalf("newGood1(): bad IKernelWg")
+	}
+	if log := sf.wg.Log(); log == nil {
+		sf.t.Fatalf("newGood1(): log==nil")
+	}
+}
+
+// Нет контекста ядра
+func (sf *tester) newBad1() {
+	sf.t.Log("newBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("newBad1(): panic=nil")
+		}
+	}()
+	// defer sf.panicUse("newBad1(): ")
+	var ctx context.Context
+	_ = GetKernelWg(ctx)
+}

+ 126 - 0
krn/kmodule/kmodule.go

@@ -0,0 +1,126 @@
+// package kmodule -- модуль на основе ядра
+package kmodule
+
+import (
+	"time"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/kc/local_ctx"
+	"gitp78su.ipnodns.ru/svi/kern/kc/safe_int"
+	"gitp78su.ipnodns.ru/svi/kern/kc/safe_string"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kbus/kbus_local"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kmodule/mod_stat"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+// kModule -- модуль на основе ядра
+type kModule struct {
+	kCtx      IKernelCtx
+	ctx       ILocalCtx
+	name      AModuleName
+	bus       IKernelBus
+	timePhase ISafeInt
+	strLive   ISafeString
+	stat      IModuleStat
+}
+
+// NewKernelModule -- возвращает новый модуль на основе ядра
+func NewKernelModule(name AModuleName) IKernelModule {
+	Hassert(name != "", "NewKernelModule(): name is empty")
+	kCtx := kctx.GetKernelCtx()
+	sf := &kModule{
+		kCtx:      kCtx,
+		ctx:       local_ctx.NewLocalCtx(kCtx.BaseCtx()),
+		name:      name,
+		bus:       kbus_local.GetKernelBusLocal(),
+		timePhase: safe_int.NewSafeInt(),
+		strLive:   safe_string.NewSafeString(),
+		stat:      mod_stat.NewModStat(name),
+	}
+	sf.timePhase.Set(1000) // 1000 msec
+	go sf.sigLive()
+	return sf
+}
+
+// Stat -- возвращает статистику модуля
+func (sf *kModule) Stat() IModuleStat {
+	return sf.stat
+}
+
+// Log -- возвращает буферный лог
+func (sf *kModule) Log() ILogBuf {
+	return sf.ctx.Log()
+}
+
+// Ctx -- возвращает контекст модуля
+func (sf *kModule) Ctx() ILocalCtx {
+	return sf.ctx
+}
+
+// Run -- запускает модуль в работу
+func (sf *kModule) Run() {
+	Hassert(false, "kModule.Run(): module='%v', parent not realised this method", sf.name)
+}
+
+// Name -- возвращает уникальное имя модуля
+func (sf *kModule) Name() AModuleName {
+	return sf.name
+}
+
+// IsWork -- возвращает признак состояния работы
+func (sf *kModule) IsWork() bool {
+	Hassert(false, "kModule.IsWork(): module='%v', parent not realised this method", sf.name)
+	return false
+}
+
+// Live -- возвращает индикатор жизни модуля
+func (sf *kModule) Live() string {
+	return sf.strLive.Get()
+}
+
+// Сигнал жизни, каждые 5 сек публикует в шину метку
+func (sf *kModule) sigLive() {
+	var (
+		topic  = sf.name + "_live"
+		iPhase = 0
+		err    error
+	)
+	fnPhase := func() {
+		switch iPhase {
+		case 0:
+			sf.strLive.Set("|")
+			err = sf.bus.Publish(ATopic(topic), sf.strLive.Byte())
+		case 1:
+			sf.strLive.Set("/")
+			err = sf.bus.Publish(ATopic(topic), sf.strLive.Byte())
+		case 2:
+			sf.strLive.Set("-")
+			err = sf.bus.Publish(ATopic(topic), sf.strLive.Byte())
+		case 3:
+			sf.strLive.Set("\\")
+			err = sf.bus.Publish(ATopic(topic), sf.strLive.Byte())
+			iPhase = -1
+		}
+		sf.recErr(err)
+		iPhase++
+		sf.stat.Add(1)
+		time.Sleep(time.Millisecond * time.Duration(sf.timePhase.Get()))
+	}
+	for {
+		select {
+		case <-sf.kCtx.BaseCtx().Done():
+			return
+		default:
+			fnPhase()
+		}
+	}
+}
+
+// Регистрирует ошибку обработчика при публикации лайв сигнала, если была
+func (sf *kModule) recErr(err error) {
+	if err != nil {
+		sf.Log().Err("kModule.recErr(): name=%v, in publish live, err=\n\t%v", err)
+	}
+}

+ 107 - 0
krn/kmodule/kmodule_test.go

@@ -0,0 +1,107 @@
+package kmodule
+
+import (
+	"fmt"
+	"testing"
+	"time"
+
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+type tester struct {
+	t   *testing.T
+	mod IKernelModule
+}
+
+func TestKernelModule(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+	sf.run()
+	sf.isWork()
+	sf.done()
+	sf.recErr()
+}
+
+// Регистрация ошибки
+func (sf *tester) recErr() {
+	sf.t.Log("recErr")
+	mod := sf.mod.(*kModule)
+	err := fmt.Errorf("tra-la-la")
+	mod.recErr(err)
+	if live := sf.mod.Live(); live == "" {
+		sf.t.Fatalf("recErr(): live empty")
+	}
+	if stat := sf.mod.Stat(); stat == nil {
+		sf.t.Fatalf("recErr(): stat==nil")
+	}
+}
+
+// Работа после остановки локальной шины
+func (sf *tester) done() {
+	sf.t.Log("done")
+	kCtx := kctx.GetKernelCtx()
+
+	time.Sleep(time.Millisecond * 250)
+	kCtx.Cancel()
+	kCtx.Done()
+	time.Sleep(time.Millisecond * 200)
+}
+
+// Проверить признак работы
+func (sf *tester) isWork() {
+	sf.t.Log("isWork")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("isWork(): panic==nil")
+		}
+	}()
+	_ = sf.mod.IsWork()
+}
+
+// Запускает модуль в работу
+func (sf *tester) run() {
+	sf.t.Log("run")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("run(): panic==nil")
+		}
+	}()
+	mod := sf.mod.(*kModule)
+	mod.timePhase.Set(5) // Настройка переменной модуля
+	sf.mod.Run()
+}
+
+// Создание нового модуля ядра
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.newBad1()
+	sf.newGood1()
+}
+
+func (sf *tester) newGood1() {
+	sf.t.Log("newGood1")
+	sf.mod = NewKernelModule("test_module")
+	if name := sf.mod.Name(); name != "test_module" {
+		sf.t.Fatalf("newGood1(): name(%v)!='test_module'", name)
+	}
+	if ctx := sf.mod.Ctx(); ctx == nil {
+		sf.t.Fatalf("newGood1(): ctx==nil")
+	}
+	if _log := sf.mod.Log(); _log == nil {
+		sf.t.Fatalf("newGood1(): log==nil")
+	}
+}
+
+// Нет имени модуля
+func (sf *tester) newBad1() {
+	sf.t.Log("newBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("newBad1(): panic==nil")
+		}
+	}()
+	_ = NewKernelModule("")
+}

+ 80 - 0
krn/kmodule/mod_stat/mod_stat.go

@@ -0,0 +1,80 @@
+// package mod_stat -- статистика модуля
+//
+// Подробная статистика по периодам:
+//   60 сек -- первая минута
+//   60 минут -- первый час
+//   48 получасов -- первые сутки
+//   4 часа -- первые 14 суток
+
+package mod_stat
+
+import (
+	"time"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/kc/safe_int"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kmodule/mod_stat/mod_stat_day"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kmodule/mod_stat/mod_stat_sec"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+// ModStat -- статистика модуля
+type ModStat struct {
+	statSec    *mod_stat_sec.ModStatSec // Объект статистики 60 секунд
+	timeMinute ISafeInt                 // Интервал ожидания минутного таймера, мсек
+	statMin    *mod_stat_day.ModStatDay // Объект статистики 60 минут
+	statDay    *mod_stat_day.ModStatDay // Объект статистики за последние 24 часа
+	name       AModuleName
+}
+
+// NewModStat -- возвращает новую статистику модуля
+func NewModStat(name AModuleName) *ModStat {
+	Hassert(name != "", "NewModuleStat(): name module is empty")
+	sf := &ModStat{
+		statSec:    mod_stat_sec.NewModStatSec(),
+		statMin:    mod_stat_day.NewModStatDay(),
+		statDay:    mod_stat_day.NewModStatDay(),
+		timeMinute: safe_int.NewSafeInt(),
+		name:       name,
+	}
+	sf.timeMinute.Set(60 * 1000)
+	go sf.eventMinute()
+	return sf
+}
+
+// Срабатывает раз в минуту
+func (sf *ModStat) eventMinute() {
+	countPartHour := 20
+	for {
+		time.Sleep(time.Millisecond * time.Duration(sf.timeMinute.Get()))
+		sum := sf.statSec.Sum()
+		sf.statMin.Add(sum)
+		countPartHour--
+		if countPartHour == 0 {
+			sum := sf.statMin.Sum()
+			sf.statDay.Add(sum)
+			countPartHour = 20
+		}
+	}
+}
+
+// Add -- добавляет значение в статистику
+func (sf *ModStat) Add(val int) {
+	sf.statSec.Add(val)
+}
+
+// SvgSec -- возвращает посекундную SVG за последнюю минуту
+func (sf *ModStat) SvgSec() string {
+	return sf.statSec.Svg()
+}
+
+// SvgMin -- возвращает поминутную SVG за последнюю минуту
+func (sf *ModStat) SvgMin() string {
+	return sf.statMin.Svg()
+}
+
+// SvgDay -- возвращает SVG за последние сутки по часам
+func (sf *ModStat) SvgDay() string {
+	return sf.statDay.Svg()
+}

+ 137 - 0
krn/kmodule/mod_stat/mod_stat_day/mod_stat_day.go

@@ -0,0 +1,137 @@
+// package mod_stat_day -- статистика модуля за первые 24 часа
+package mod_stat_day
+
+import (
+	"bytes"
+	"fmt"
+	"math"
+
+	"sync"
+	"time"
+
+	svg "github.com/ajstarks/svgo"
+)
+
+// ModStatВфн -- статистика модуля за первые 24 часа
+type ModStatDay struct {
+	sync.RWMutex
+	lst    []int // Список значений за первые 24 часа
+	bufSvg *bytes.Buffer
+}
+
+// NewModStatDay -- возвращает новую статистику модуля за первые 24 часа
+func NewModStatDay() *ModStatDay {
+	sf := &ModStatDay{
+		lst:    []int{},
+		bufSvg: bytes.NewBufferString(""),
+	}
+	return sf
+}
+
+// Sum -- возвращает сумму элементов по требованию
+func (sf *ModStatDay) Sum() int {
+	sf.RLock()
+	defer sf.RUnlock()
+	sum := 0
+	for _, val := range sf.lst {
+		sum += val
+	}
+	return sum
+}
+
+// Add -- добавляет значение в часовой срез
+func (sf *ModStatDay) Add(val int) {
+	sf.Lock()
+	defer sf.Unlock()
+
+	for len(sf.lst) < 72 {
+		sf.lst = append(sf.lst, math.MinInt64)
+	}
+
+	sf.lst = append(sf.lst, val)
+	if len(sf.lst) > 72 {
+		sf.lst = sf.lst[1:]
+	}
+}
+
+// Svg -- возвращает сгенерированный SVG по часовому срезу
+func (sf *ModStatDay) Svg() string {
+	sf.RLock()
+	defer sf.RUnlock()
+	sf.bufSvg.Reset()
+	cnv := svg.New(sf.bufSvg)
+	cnv.Start(480, 320)
+	cnv.Title("Last 24 hours")
+	cnv.Desc("Graphic of last 24 hours")
+	cnv.Text(20, 20, "Last 24 hours", "")
+	var (
+		valMin = math.MaxInt64
+		valMax = math.MinInt64
+	)
+	fnGetMinMax := func() { // Вычисляет максимальное и минимальное значение в графике
+		for _, val := range sf.lst {
+			if val < valMin {
+				valMin = val
+			}
+			if val > valMax {
+				valMax = val
+			}
+		}
+	}
+	fnGetMinMax()
+	for i, val := range sf.lst {
+		x1 := int(float32(i)*5) + 42
+		y1 := int(240 * float32(valMax) / float32(val))
+		cnv.Rect(x1, 280-y1, 4, y1, "fill:true;stroke:red;")
+	}
+	fnDrawNet := func() {
+		// Метки величины
+		if valMin == math.MaxInt64 || valMin == math.MinInt64 {
+			valMin = 0
+		}
+		cnv.Text(25, 285, fmt.Sprint(valMin), "")
+		if valMax == math.MinInt64 || valMax == math.MaxInt64 {
+			valMax = 1
+		}
+		cnv.Text(25, 45, fmt.Sprint(valMax), "")
+		// Метки времени
+		timeNow := time.Now().Local()
+		timeSub24 := timeNow.Add(-24 * time.Hour).Format("15")
+		cnv.Text(40, 295, timeSub24, "")
+
+		timeSub45 := timeNow.Add(-18 * time.Hour).Local().Format("15")
+		cnv.Text(128, 295, timeSub45, "")
+
+		timeSub30 := timeNow.Add(-12 * time.Hour).Local().Format("15")
+		cnv.Text(216, 295, timeSub30, "")
+
+		timeSub15 := timeNow.Add(-6 * time.Hour).Local().Format("15")
+		cnv.Text(304, 295, timeSub15, "")
+
+		timeSub0 := time.Now().Local().Format("15")
+		cnv.Text(392, 295, timeSub0, "")
+
+		cnv.Line(40, 280, 40, 38, "fill:true;stroke:black;stroke-width:4")
+		cnv.Line(40, 280, 442, 280, "fill:true;stroke:black;stroke-width:4")
+		count := 0
+		for x := 40; x < 460; x += 20 {
+			cnv.Line(x, 40, x, 280, "fill:true;stroke:gray")
+			if count%5 == 0 {
+				cnv.Text(x+3, 278, fmt.Sprint(x), "fill:white;stroke:gray")
+			}
+			count++
+		}
+		count = 0
+		for y := 40; y < 300; y += 20 {
+			cnv.Line(40, y, 440, y, "fill:true;stroke:gray")
+			if count%5 == 0 {
+				cnv.Text(43, y+12, fmt.Sprint(y), "fill:white;stroke:gray")
+			}
+			count++
+		}
+	}
+	fnDrawNet()
+	cnv.End()
+	strOut := sf.bufSvg.String()
+	return strOut
+}

+ 45 - 0
krn/kmodule/mod_stat/mod_stat_day/mod_stat_day_test.go

@@ -0,0 +1,45 @@
+package mod_stat_day
+
+import (
+	"testing"
+)
+
+type tester struct {
+	t    *testing.T
+	stat *ModStatDay
+}
+
+func TestModStatMinute(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+	sf.add()
+}
+
+// Добавляет событие в стату
+func (sf *tester) add() {
+	sf.t.Log("add")
+	sf.stat.Add(12)
+	sf.stat.Add(11)
+	sf.stat.Add(3)
+	if svg := sf.stat.Svg(); svg == "" {
+		sf.t.Fatal("add(): svg is empty")
+	}
+	if sum := sf.stat.Sum(); sum == 0 {
+		sf.t.Fatalf("add(): sum==0")
+	}
+}
+
+// Создаёт новую секундную статистику модуля
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.stat = NewModStatDay()
+	if sf.stat == nil {
+		sf.t.Fatalf("new(): stat==nil")
+	}
+	if svg := sf.stat.Svg(); svg == "" {
+		sf.t.Fatal("new(): svg is empty")
+	}
+
+}

+ 137 - 0
krn/kmodule/mod_stat/mod_stat_minute/mod_stat_minute.go

@@ -0,0 +1,137 @@
+// package mod_stat_minute -- статистика модуля за первые 60 мин
+package mod_stat_minute
+
+import (
+	"bytes"
+	"fmt"
+	"math"
+
+	"sync"
+	"time"
+
+	svg "github.com/ajstarks/svgo"
+)
+
+// ModStatMinutes -- статистика модуля за первые 60 мин
+type ModStatMinutes struct {
+	sync.RWMutex
+	lst    []int // Список значений за последние 60 мин
+	bufSvg *bytes.Buffer
+}
+
+// NewModStatMinute -- возвращает новую статистику модуля за последние 60 мин
+func NewModStatMinute() *ModStatMinutes {
+	sf := &ModStatMinutes{
+		lst:    []int{},
+		bufSvg: bytes.NewBufferString(""),
+	}
+	return sf
+}
+
+// Sum -- возвращает сумму элементов по требованию
+func (sf *ModStatMinutes) Sum() int {
+	sf.RLock()
+	defer sf.RUnlock()
+	sum := 0
+	for _, val := range sf.lst {
+		sum += val
+	}
+	return sum
+}
+
+// Add -- добавляет значение в часовой срез
+func (sf *ModStatMinutes) Add(val int) {
+	sf.Lock()
+	defer sf.Unlock()
+
+	for len(sf.lst) < 60 {
+		sf.lst = append(sf.lst, math.MinInt64)
+	}
+
+	sf.lst = append(sf.lst, val)
+	if len(sf.lst) > 60 {
+		sf.lst = sf.lst[1:]
+	}
+}
+
+// Svg -- возвращает сгенерированный SVG по часовому срезу
+func (sf *ModStatMinutes) Svg() string {
+	sf.RLock()
+	defer sf.RUnlock()
+	sf.bufSvg.Reset()
+	cnv := svg.New(sf.bufSvg)
+	cnv.Start(480, 320)
+	cnv.Title("Last 60 minute")
+	cnv.Desc("Graphic of last 60 minute")
+	cnv.Text(20, 20, "Last 60 minute", "")
+	var (
+		valMin = math.MaxInt64
+		valMax = math.MinInt64
+	)
+	fnGetMinMax := func() { // Вычисляет максимальное и минимальное значение в графике
+		for _, val := range sf.lst {
+			if val < valMin {
+				valMin = val
+			}
+			if val > valMax {
+				valMax = val
+			}
+		}
+	}
+	fnGetMinMax()
+	for i, val := range sf.lst {
+		x1 := int(float32(i)*6) + 42
+		y1 := int(240 * float32(valMax) / float32(val))
+		cnv.Rect(x1, 280-y1, 5, y1, "fill:true;stroke:red;")
+	}
+	fnDrawNet := func() {
+		// Метки величины
+		if valMin == math.MaxInt64 || valMin == math.MinInt64 {
+			valMin = 0
+		}
+		cnv.Text(25, 285, fmt.Sprint(valMin), "")
+		if valMax == math.MinInt64 || valMax == math.MaxInt64 {
+			valMax = 1
+		}
+		cnv.Text(25, 45, fmt.Sprint(valMax), "")
+		// Метки времени
+		timeNow := time.Now().Local()
+		timeSub60 := timeNow.Add(-60 * time.Minute).Format("04")
+		cnv.Text(40, 295, timeSub60, "")
+
+		timeSub45 := timeNow.Add(-45 * time.Minute).Format("04")
+		cnv.Text(128, 295, timeSub45, "")
+
+		timeSub30 := timeNow.Add(-30 * time.Minute).Format("04")
+		cnv.Text(216, 295, timeSub30, "")
+
+		timeSub15 := timeNow.Add(-15 * time.Minute).Format("04")
+		cnv.Text(304, 295, timeSub15, "")
+
+		timeSub0 := time.Now().Format("04")
+		cnv.Text(392, 295, timeSub0, "")
+
+		cnv.Line(40, 280, 40, 38, "fill:true;stroke:black;stroke-width:4")
+		cnv.Line(40, 280, 442, 280, "fill:true;stroke:black;stroke-width:4")
+		count := 0
+		for x := 40; x < 460; x += 20 {
+			cnv.Line(x, 40, x, 280, "fill:true;stroke:gray")
+			if count%5 == 0 {
+				cnv.Text(x+3, 278, fmt.Sprint(x), "fill:white;stroke:gray")
+			}
+			count++
+		}
+		count = 0
+		for y := 40; y < 300; y += 20 {
+			cnv.Line(40, y, 440, y, "fill:true;stroke:gray")
+			if count%5 == 0 {
+				cnv.Text(43, y+12, fmt.Sprint(y), "fill:white;stroke:gray")
+			}
+			count++
+		}
+	}
+	fnDrawNet()
+	cnv.End()
+	strOut := sf.bufSvg.String()
+	return strOut
+}

+ 45 - 0
krn/kmodule/mod_stat/mod_stat_minute/mod_stat_minute_test.go

@@ -0,0 +1,45 @@
+package mod_stat_minute
+
+import (
+	"testing"
+)
+
+type tester struct {
+	t    *testing.T
+	stat *ModStatMinutes
+}
+
+func TestModStatMinute(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+	sf.add()
+}
+
+// Добавляет событие в стату
+func (sf *tester) add() {
+	sf.t.Log("add")
+	sf.stat.Add(12)
+	sf.stat.Add(11)
+	sf.stat.Add(3)
+	if svg := sf.stat.Svg(); svg == "" {
+		sf.t.Fatal("add(): svg is empty")
+	}
+	if sum := sf.stat.Sum(); sum == 0 {
+		sf.t.Fatalf("add(): sum==0")
+	}
+}
+
+// Создаёт новую секундную статистику модуля
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.stat = NewModStatMinute()
+	if sf.stat == nil {
+		sf.t.Fatalf("new(): stat==nil")
+	}
+	if svg := sf.stat.Svg(); svg == "" {
+		sf.t.Fatal("new(): svg is empty")
+	}
+
+}

+ 159 - 0
krn/kmodule/mod_stat/mod_stat_sec/mod_stat_sec.go

@@ -0,0 +1,159 @@
+// package mod_stat_sec -- статистика модуля за первые 60 сек
+package mod_stat_sec
+
+import (
+	"bytes"
+	"fmt"
+	"math"
+
+	// "strings"
+	"sync"
+	"time"
+
+	"github.com/ajstarks/svgo"
+)
+
+// ModStatSec -- статистика модуля за первые 60 сек
+type ModStatSec struct {
+	sync.RWMutex
+	lst      []int // Список значений за последние 60 сек
+	momentAt int64 // Метка времени для учёта текущей секунды
+	bufSvg   *bytes.Buffer
+}
+
+// NewModStatSec -- возвращает новую статистику модуля за последние 60 сек
+func NewModStatSec() *ModStatSec {
+	sf := &ModStatSec{
+		lst:      []int{},
+		bufSvg:   bytes.NewBufferString(""),
+		momentAt: time.Now().Local().Unix(),
+	}
+	return sf
+}
+
+// Sum -- возвращает сумму элементов по требованию
+func (sf *ModStatSec) Sum() int {
+	sf.RLock()
+	defer sf.RUnlock()
+	sum := 0
+	for _, val := range sf.lst {
+		sum += val
+	}
+	return sum
+}
+
+// Add -- добавляет значение в минутный срез
+func (sf *ModStatSec) Add(val int) {
+	sf.Lock()
+	defer sf.Unlock()
+
+	for len(sf.lst) < 60 {
+		sf.lst = append(sf.lst, 0)
+	}
+
+	momentNow := time.Now().Local().Unix()
+	isSameSec := momentNow == sf.momentAt
+	switch isSameSec {
+	case true:
+		_val := sf.lst[59]
+		_val += val
+		sf.lst[59] = _val
+	default:
+		sf.momentAt = momentNow
+		sf.lst = append(sf.lst, val)
+		if len(sf.lst) > 60 {
+			sf.lst = sf.lst[1:]
+		}
+	}
+}
+
+// const (
+// 	strCut = `<?xml version="1.0"?>
+// <!-- Generated by SVGo -->`
+// )
+
+// Svg -- возвращает сгенерированный SVG по минутному срезу
+func (sf *ModStatSec) Svg() string {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.bufSvg.Reset()
+	cnv := svg.New(sf.bufSvg)
+	cnv.Start(480, 320)
+	cnv.Title("Last 60 sec")
+	cnv.Desc("Graphic of last 60 sec")
+	// cnv.Text(20, 20, "Last 60 sec", "text-anchor:middle;font-size:14px;fill:white")
+	cnv.Text(20, 20, "Last 60 sec", "")
+	// cnv.Circle(250, 250, 125, "fill:none;stroke:black")
+	var (
+		valMin = math.MaxInt64
+		valMax = math.MinInt64
+	)
+	fnGetMinMax := func() { // Вычисляет максимальное и минимальное значение в графике
+		for _, val := range sf.lst {
+			if val < valMin {
+				valMin = val
+			}
+			if val > valMax {
+				valMax = val
+			}
+		}
+	}
+	fnGetMinMax()
+	for i, val := range sf.lst {
+		x1 := int(float32(i)*6) + 42
+		y1 := int(240 * float32(valMax) / float32(val))
+		cnv.Rect(x1, 280-y1, 5, y1, "fill:true;stroke:red;")
+	}
+	fnDrawNet := func() {
+		// Метки величины
+		if valMin == math.MaxInt64 {
+			valMin = 0
+		}
+
+		if valMax == math.MinInt64 {
+			valMax = 1
+		}
+		cnv.Text(25, 285, fmt.Sprint(valMin), "")
+		cnv.Text(25, 45, fmt.Sprint(valMax), "")
+
+		// Метки времени
+		timeNow := time.Now().Local()
+		timeSub60 := timeNow.Add(-60 * time.Second).Format("05")
+		cnv.Text(40, 295, timeSub60, "")
+
+		timeSub45 := timeNow.Add(-45 * time.Second).Format("05")
+		cnv.Text(128, 295, timeSub45, "")
+
+		timeSub30 := timeNow.Add(-30 * time.Second).Format("05")
+		cnv.Text(216, 295, timeSub30, "")
+
+		timeSub15 := timeNow.Add(-15 * time.Second).Format("05")
+		cnv.Text(304, 295, timeSub15, "")
+
+		timeSub0 := time.Now().Format("05")
+		cnv.Text(392, 295, timeSub0, "")
+		cnv.Line(40, 280, 40, 38, "fill:true;stroke:black;stroke-width:4")
+		cnv.Line(40, 280, 442, 280, "fill:true;stroke:black;stroke-width:4")
+		count := 0
+		for x := 40; x < 460; x += 20 {
+			cnv.Line(x, 40, x, 280, "fill:true;stroke:gray")
+			if count%5 == 0 {
+				cnv.Text(x+3, 278, fmt.Sprint(x), "fill:white;stroke:gray")
+			}
+			count++
+		}
+		count = 0
+		for y := 40; y < 300; y += 20 {
+			cnv.Line(40, y, 440, y, "fill:true;stroke:gray")
+			if count%5 == 0 {
+				cnv.Text(43, y+12, fmt.Sprint(y), "fill:white;stroke:gray")
+			}
+			count++
+		}
+	}
+	fnDrawNet()
+	cnv.End()
+	strOut := sf.bufSvg.String()
+	// strOut = strings.ReplaceAll(strOut, strCut, "")
+	return strOut
+}

+ 53 - 0
krn/kmodule/mod_stat/mod_stat_sec/mod_stat_sec_test.go

@@ -0,0 +1,53 @@
+package mod_stat_sec
+
+import (
+	"testing"
+)
+
+type tester struct {
+	t    *testing.T
+	stat *ModStatSec
+}
+
+func TestModStatSec(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+	sf.add()
+	sf.sum()
+}
+
+// Возвращает сумму значений за минуту
+func (sf *tester) sum() {
+	sf.t.Log("sum")
+	if sum := sf.stat.Sum(); sum == 0 {
+		sf.t.Fatalf("sum(): sum==0")
+	}
+}
+
+// Добавляет событие в стату
+func (sf *tester) add() {
+	sf.t.Log("add")
+	sf.stat.Add(12)
+	sf.stat.momentAt = 0
+	sf.stat.Add(11)
+	sf.stat.momentAt = 0
+	sf.stat.Add(3)
+	sf.stat.Add(3)
+	if svg := sf.stat.Svg(); svg == "" {
+		sf.t.Fatal("add(): svg is empty")
+	}
+}
+
+// Создаёт новую секундную статистику модуля
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.stat = NewModStatSec()
+	if sf.stat == nil {
+		sf.t.Fatalf("new(): stat==nil")
+	}
+	if svg := sf.stat.Svg(); svg == "" {
+		sf.t.Fatal("new(): svg is empty")
+	}
+}

+ 60 - 0
krn/kmodule/mod_stat/mod_stat_test.go

@@ -0,0 +1,60 @@
+package mod_stat
+
+import (
+	"testing"
+	"time"
+)
+
+type tester struct {
+	t    *testing.T
+	stat *ModStat
+}
+
+func TestModStat(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+	sf.event()
+}
+
+// Проверка генерации меток времени
+func (sf *tester) event() {
+	sf.t.Log("event")
+	time.Sleep(time.Millisecond * 100)
+}
+
+// Создание статистики модуля
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.newBad1()
+	sf.newGood1()
+}
+
+func (sf *tester) newGood1() {
+	sf.t.Log("newGood1")
+	sf.stat = NewModStat("test")
+	// Обязательно установить время сразу, для покрытия тестами
+	sf.stat.timeMinute.Set(1)
+	sf.stat.Add(23)
+	if svg := sf.stat.SvgSec(); svg == "" {
+		sf.t.Fatalf("newGood1(): svg is empty")
+	}
+	if svg := sf.stat.SvgMin(); svg == "" {
+		sf.t.Fatalf("newGood1(): svg is empty")
+	}
+	if svg := sf.stat.SvgDay(); svg == "" {
+		sf.t.Fatalf("newGood1(): svg is empty")
+	}
+}
+
+// Нет имени статистики
+func (sf *tester) newBad1() {
+	sf.t.Log("newBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("newBad1(): panic==nil")
+		}
+	}()
+	_ = NewModStat("")
+}

+ 124 - 0
krn/kmonolit/kmonolit.go

@@ -0,0 +1,124 @@
+// package kmonolit -- модульный монолит на основе ядра
+package kmonolit
+
+import (
+	"fmt"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/kc/local_ctx"
+	"gitp78su.ipnodns.ru/svi/kern/kc/safe_bool"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+// kMonolit -- объект модульного монолита
+type kMonolit struct {
+	kCtx    IKernelCtx
+	ctx     ILocalCtx
+	log     ILogBuf
+	name    string
+	isLocal bool
+	isWork  ISafeBool
+	isEnd   ISafeBool
+	dict    map[AModuleName]IKernelModule // Словарь модулей монолита
+}
+
+var (
+	mon *kMonolit
+)
+
+// GetMonolit -- возвращает монолит
+func GetMonolit(name string) IKernelMonolit {
+	if mon != nil {
+		return mon
+	}
+	Hassert(name != "", "NewMonolit(): name is empty")
+	kCtx := kctx.GetKernelCtx()
+	sf := &kMonolit{
+		kCtx:    kCtx,
+		ctx:     local_ctx.NewLocalCtx(kCtx.BaseCtx()),
+		name:    name,
+		dict:    map[AModuleName]IKernelModule{},
+		isWork:  safe_bool.NewSafeBool(),
+		isEnd:   safe_bool.NewSafeBool(),
+		isLocal: kCtx.Get("isLocal").Val().(bool),
+	}
+	sf.log = sf.ctx.Log()
+	sf.kCtx.Set("monolitName", name, "name of monolit")
+	sf.kCtx.Set("monolit", sf, "monolit-app")
+	sf.ctx.Set("monolitName", name, "name of monolit")
+	mon = sf
+	return sf
+}
+
+// Ctx -- возвращает контекст монолита
+func (sf *kMonolit) Ctx() ILocalCtx {
+	return sf.ctx
+}
+
+// Log -- возвращает лог монолита
+func (sf *kMonolit) Log() ILogBuf {
+	return sf.ctx.Log()
+}
+
+// Name -- возвращает имя монолита
+func (sf *kMonolit) Name() string {
+	return sf.name
+}
+
+// Add -- добавляет модуль в монолит
+func (sf *kMonolit) Add(module IKernelModule) {
+	sf.kCtx.RLock()
+	defer sf.kCtx.RUnlock()
+	Hassert(module != nil, "kMonolit.Add(): module==nil")
+	_, isOk := sf.dict[module.Name()]
+	Hassert(!isOk, "kMonolit.Add(): module(%v) already exists", module.Name())
+	sf.dict[module.Name()] = module
+	sf.log.Debug("kMonolit.Add(): module='%v'", module.Name())
+	if sf.isWork.Get() {
+		go module.Run()
+		sf.log.Debug("kMonolit.Add(): module='%v' is run", module.Name())
+	}
+	key := fmt.Sprintf("module_%v", len(sf.dict))
+	moduleName := string(module.Name())
+	sf.ctx.Set(key, module, "kMonolit.Add(): module="+moduleName)
+}
+
+// Run -- запускает монолит в работу
+func (sf *kMonolit) Run() {
+	sf.kCtx.RLock()
+	defer sf.kCtx.RUnlock()
+	if sf.isEnd.Get() {
+		return
+	}
+	if sf.isWork.Get() {
+		return
+	}
+	sf.isWork.Set()
+	for _, module := range sf.dict {
+		go module.Run()
+	}
+	sf.log.Debug("kMonolit.Run()")
+}
+
+// IsLocal -- возвращает признак локальной шины
+func (sf *kMonolit) IsLocal() bool {
+	return sf.isLocal
+}
+
+// IsWork -- возвращает признак работы монолита
+func (sf *kMonolit) IsWork() bool {
+	return sf.isWork.Get()
+}
+
+// Ожидание завершения работы монолита
+func (sf *kMonolit) Wait() {
+	sf.kCtx.Done()
+	sf.kCtx.Wg().Wait()
+	sf.kCtx.Lock()
+	defer sf.kCtx.Unlock()
+	sf.isWork.Reset()
+	sf.isEnd.Set()
+	sf.log.Debug("kMonolit.close(): end")
+}

+ 122 - 0
krn/kmonolit/kmonolit_test.go

@@ -0,0 +1,122 @@
+package kmonolit
+
+import (
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kmodule"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+type tester struct {
+	t   *testing.T
+	mon IKernelMonolit
+}
+
+func TestKernMono(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+	sf.run()
+	sf.add()
+	sf.done()
+}
+
+func (sf *tester) done() {
+	sf.t.Log("done")
+	ctx := kctx.GetKernelCtx()
+	ctx.Cancel()
+	ctx.Wg().Wait()
+	sf.mon.Run()
+	sf.mon.Wait()
+	sf.mon.Run()
+}
+
+// Добавление модуля в монолит
+func (sf *tester) add() {
+	sf.t.Log("add")
+	sf.addGood1()
+}
+
+type mod struct {
+	IKernelModule
+}
+
+func newMod(name AModuleName) IKernelModule {
+	sf := &mod{
+		IKernelModule: kmodule.NewKernelModule(name),
+	}
+	return sf
+}
+
+func (sf *mod) Run() {}
+
+func (sf *tester) addGood1() {
+	sf.t.Log("addGood1")
+	mod := newMod("test_module")
+	sf.mon.Add(mod)
+}
+
+func (sf *tester) run() {
+	sf.t.Log("run")
+	mod := newMod("test_mod1")
+	sf.mon.Add(mod)
+	sf.mon.Run()
+	isWork := sf.mon.IsWork()
+	if !isWork {
+		sf.t.Fatalf("newGood1(): isWork==false")
+	}
+}
+
+// Создаёт новый монолит
+func (sf *tester) new() {
+	sf.t.Log("new")
+	sf.newBad1()
+	sf.newGood1()
+}
+
+func (sf *tester) newGood1() {
+	sf.t.Log("newGood1")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("newGood1(): panic=%v", _panic)
+		}
+	}()
+	ctx := kctx.GetKernelCtx()
+	ctx.Set("isLocal", true, "type bus")
+	for {
+		SleepMs()
+		if ctx.Get("isLocal") != nil {
+			break
+		}
+	}
+	sf.mon = GetMonolit("test_monolit")
+	isLocal := sf.mon.IsLocal()
+	if !isLocal {
+		sf.t.Fatalf("newGood1(): isLocal==false")
+	}
+	if name := sf.mon.Name(); name != "test_monolit" {
+		sf.t.Fatalf("newGood1(): name(%v)!='test_monolit'", name)
+	}
+	if log := sf.mon.Log(); log == nil {
+		sf.t.Fatalf("newGood1(): log==nil")
+	}
+	if ctx := sf.mon.Ctx(); ctx == nil {
+		sf.t.Fatalf("newGood1(): ctx==nil")
+	}
+	sf.mon = GetMonolit("")
+}
+
+// Нет признака локальности
+func (sf *tester) newBad1() {
+	sf.t.Log("newBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("newBad1(): panic==nil")
+		}
+	}()
+	_ = GetMonolit("test_32")
+}

+ 147 - 0
krn/kserv_http/kserv_http.go

@@ -0,0 +1,147 @@
+// package kserv_http -- встроенный HTTP-сервер
+package kserv_http
+
+import (
+	"embed"
+	"fmt"
+	"net/http"
+	"os"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/gofiber/fiber/v2"
+	"github.com/gofiber/fiber/v2/middleware/compress"
+	"github.com/gofiber/fiber/v2/middleware/filesystem"
+	"github.com/gofiber/fiber/v2/middleware/monitor"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/kc/local_ctx"
+	"gitp78su.ipnodns.ru/svi/kern/kc/safe_bool"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+)
+
+const (
+	streamName = "kernel_server_http" // Контрольная строка для ожидателя потока
+)
+
+// kServHttp -- встроенный HTTP-сервер
+type kServHttp struct {
+	kCtx     IKernelCtx
+	ctx      ILocalCtx
+	log      ILogBuf
+	strUrl   string // URL, на котором слушает HTTP-сервер
+	fiberApp *fiber.App
+	isWork   ISafeBool
+	isEnd    ISafeBool
+}
+
+//go:embed static/*
+var embedDirStatic embed.FS
+
+var (
+	kernServHttp *kServHttp
+	block        sync.Mutex
+)
+
+// GetKernelServHttp -- возвращает  встроенный HTTP-сервер
+func GetKernelServHttp() IKernelServerHttp {
+	block.Lock()
+	defer block.Unlock()
+	if kernServHttp != nil {
+		kernServHttp.log.Debug("GetKernelServHttp()")
+		return kernServHttp
+	}
+	ctx := kctx.GetKernelCtx()
+
+	strUrl := os.Getenv("LOCAL_HTTP_URL")
+	Hassert(strUrl != "", "GetKernelServHttp(): env LOCAL_HTTP_URL not set")
+	confFiber := fiber.Config{
+		ServerHeader:      ctx.Get("monolitName").Val().(string),
+		UnescapePath:      true,
+		ReadTimeout:       time.Second * 15,
+		WriteTimeout:      time.Second * 15,
+		AppName:           ctx.Get("monolitName").Val().(string),
+		Network:           "tcp4",
+		EnablePrintRoutes: true,
+	}
+	sf := &kServHttp{
+		kCtx:     ctx,
+		ctx:      local_ctx.NewLocalCtx(ctx.BaseCtx()),
+		strUrl:   strUrl,
+		fiberApp: fiber.New(confFiber),
+		isWork:   safe_bool.NewSafeBool(),
+		isEnd:    safe_bool.NewSafeBool(),
+	}
+	sf.log = sf.ctx.Log()
+	sf.log.Debug("GetKernelServHttp(): first run")
+	sf.fiberApp.Use(compress.New(compress.Config{
+		Level: compress.LevelBestCompression, // 2
+	}))
+	sf.fiberApp.Use("/static", filesystem.New(filesystem.Config{
+		Root:       http.FS(embedDirStatic),
+		PathPrefix: "static",
+		Browse:     true,
+		MaxAge:     3600 * 24,
+	}))
+	sf.fiberApp.Get("/monitor", monitor.New(monitor.Config{Title: ctx.Get("monolitName").Val().(string)}))
+	err := sf.kCtx.Wg().Add(streamName)
+	Hassert(err == nil, "NewKernelServHttp(): in add stream %v, err=\n\t%v", streamName, err)
+	ctx.Set("fiberApp", sf.fiberApp, "kServHttp: internal fiber app")
+	kernServHttp = sf
+	ctx.Set("kServHttp", kernServHttp, "kServHttp")
+	return kernServHttp
+}
+
+// IsWork -- возвращает признак работы
+func (sf *kServHttp) IsWork() bool {
+	return sf.isWork.Get()
+}
+
+// Log -- возвращает локальный лог
+func (sf *kServHttp) Log() ILogBuf {
+	return sf.log
+}
+
+// Fiber -- возвращает объект веб-приложения fiber
+func (sf *kServHttp) Fiber() *fiber.App {
+	return sf.fiberApp
+}
+
+// Run -- запускает сервер в работу (блокирующий вызов)
+func (sf *kServHttp) Run() {
+	if sf.isEnd.Get() {
+		return
+	}
+	if sf.isWork.Get() {
+		return
+	}
+	go sf.close()
+	sf.isWork.Set()
+	sf.log.Debug("kServHttp.Run(): url='%v'", sf.strUrl)
+	lstPort := strings.Split(sf.strUrl, ":")
+	strPort := lstPort[len(lstPort)-1]
+	strPort = strings.ReplaceAll(strPort, "/", "")
+	strPort = strings.ReplaceAll(strPort, `"`, "")
+	err := sf.fiberApp.Listen(":" + strPort)
+	strOut := fmt.Sprintf("kServHttp.Run(): in listen, err=\n\t%v", err)
+	sf.log.Err(strOut)
+	sf.kCtx.Cancel()
+	sf.isWork.Reset()
+	sf.isEnd.Set()
+}
+
+// Ожидает окончания работы
+func (sf *kServHttp) close() {
+	sf.kCtx.Done()
+	if !sf.isWork.Get() {
+		return
+	}
+	sf.isWork.Reset()
+	sf.isEnd.Set()
+	err := sf.fiberApp.Server().Shutdown()
+	Assert(err == nil, "kServHttp.close(): in close server, err=\n\t%v", err)
+	sf.kCtx.Wg().Done(streamName)
+	sf.log.Debug("kServHttp.close(): end")
+}

+ 119 - 0
krn/kserv_http/kserv_http_test.go

@@ -0,0 +1,119 @@
+package kserv_http
+
+import (
+	"os"
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/mock/mock_env"
+)
+
+type tester struct {
+	t   *testing.T
+	ctx IKernelCtx
+	wg  IKernelWg
+	me  *mock_env.MockEnv
+}
+
+func TestKernelServHttp(t *testing.T) {
+	ctx := kctx.GetKernelCtx()
+	sf := &tester{
+		t:   t,
+		ctx: ctx,
+		wg:  ctx.Wg(),
+	}
+	sf.new()
+	sf.close()
+}
+
+// Закрытие HTTP-сервера
+func (sf *tester) close() {
+	sf.t.Log("close")
+	sf.ctx.Cancel()
+	sf.wg.Wait()
+	kernServHttp.close()
+	if kernServHttp.IsWork() {
+		sf.t.Fatalf("close(): isWork==true")
+	}
+	kernServHttp.Run()
+}
+
+// Создание сервера HTTP
+func (sf *tester) new() {
+	sf.t.Log("new()")
+	sf.newBad1()
+	sf.newBad2()
+	sf.newGood1()
+	sf.newBad3()
+}
+
+// Повторный запуск сервера на том же порту
+func (sf *tester) newBad3() {
+	sf.t.Log("newBad3()")
+	serv := GetKernelServHttp()
+	go serv.Run()
+	count := 0
+	for count < 20 {
+		SleepMs()
+		count++
+	}
+}
+
+func (sf *tester) newGood1() {
+	sf.t.Log("newGood1()")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("newGood1(): panic=%v", _panic)
+		}
+	}()
+	sf.me = mock_env.MakeEnv()
+	_ = os.Unsetenv("LOCAL_HTTP_URL")
+	os.Setenv("LOCAL_HTTP_URL", "http://localhost:18303/")
+	sf.ctx.Set("monolitName", "test_monolit", "comment")
+	for {
+		SleepMs()
+		if sf.ctx.Get("monolitName") != nil {
+			break
+		}
+	}
+	serv := GetKernelServHttp().(*kServHttp)
+	if serv != kernServHttp {
+		sf.t.Fatalf("newGood1(): bad IKernelServHttp")
+	}
+	if webFiber := serv.Fiber(); webFiber != kernServHttp.fiberApp {
+		sf.t.Fatalf("newGood1(): webFiber==serv.appFiber")
+	}
+	if log := serv.Log(); log == nil {
+		sf.t.Fatalf("newGood1(): log==nil")
+	}
+	go serv.Run()
+	count := 0
+	for count < 50 {
+		SleepMs()
+		count++
+	}
+}
+
+// Не указана SERVER_HTTP_PORT
+func (sf *tester) newBad2() {
+	sf.t.Log("newBad2()")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("newBad2(): panic==nil")
+		}
+	}()
+	_ = GetKernelServHttp()
+}
+
+// Нет контекста ядра
+func (sf *tester) newBad1() {
+	sf.t.Log("newBad1()")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("newBad1(): panic==nil")
+		}
+	}()
+	_ = GetKernelServHttp()
+}

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 4 - 0
krn/kserv_http/static/css/bootstrap-grid.min.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
krn/kserv_http/static/css/bootstrap-grid.min.css.map


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 4 - 0
krn/kserv_http/static/css/bootstrap-grid.rtl.min.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
krn/kserv_http/static/css/bootstrap-grid.rtl.min.css.map


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 4 - 0
krn/kserv_http/static/css/bootstrap-reboot.min.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
krn/kserv_http/static/css/bootstrap-reboot.min.css.map


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 4 - 0
krn/kserv_http/static/css/bootstrap-reboot.rtl.min.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
krn/kserv_http/static/css/bootstrap-reboot.rtl.min.css.map


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 4 - 0
krn/kserv_http/static/css/bootstrap-utilities.min.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
krn/kserv_http/static/css/bootstrap-utilities.min.css.map


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 4 - 0
krn/kserv_http/static/css/bootstrap-utilities.rtl.min.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
krn/kserv_http/static/css/bootstrap-utilities.rtl.min.css.map


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 4 - 0
krn/kserv_http/static/css/bootstrap.min.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
krn/kserv_http/static/css/bootstrap.min.css.map


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 4 - 0
krn/kserv_http/static/css/bootstrap.rtl.min.css


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
krn/kserv_http/static/css/bootstrap.rtl.min.css.map


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 5 - 0
krn/kserv_http/static/js/bootstrap.bundle.min.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
krn/kserv_http/static/js/bootstrap.bundle.min.js.map


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 5 - 0
krn/kserv_http/static/js/bootstrap.esm.min.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
krn/kserv_http/static/js/bootstrap.esm.min.js.map


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 5 - 0
krn/kserv_http/static/js/bootstrap.min.js


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 0 - 0
krn/kserv_http/static/js/bootstrap.min.js.map


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است