Explorar el Código

SVI Перенос в правильную папку, исправления

SVI hace 6 meses
padre
commit
e2076853ae
Se han modificado 100 ficheros con 8043 adiciones y 0 borrados
  1. 11 0
      v3/.env
  2. 52 0
      v3/Taskfile.yml
  3. 28 0
      v3/cmd/demo/main.go
  4. 20 0
      v3/cmd/demo/main_test.go
  5. 12 0
      v3/demo.sh
  6. 29 0
      v3/docs/alias.md
  7. 28 0
      v3/docs/helpers.md
  8. 18 0
      v3/docs/idict_topic_serve.md
  9. BIN
      v3/docs/img/coverage.png
  10. 20 0
      v3/docs/img/coverage.svg
  11. BIN
      v3/docs/img/mit.png
  12. 20 0
      v3/docs/img/mit.svg
  13. BIN
      v3/docs/img/ucl.png
  14. 20 0
      v3/docs/img/ucl.svg
  15. 32 0
      v3/docs/index.md
  16. 9 0
      v3/docs/types.md
  17. 35 0
      v3/go.mod
  18. 95 0
      v3/go.sum
  19. 35 0
      v3/ikernel_bus.md
  20. 86 0
      v3/kc/helpers/helpers.go
  21. 161 0
      v3/kc/helpers/helpers_test.go
  22. 63 0
      v3/kc/local_ctx/ctx_value/ctx_value.go
  23. 47 0
      v3/kc/local_ctx/ctx_value/ctx_value_test.go
  24. 109 0
      v3/kc/local_ctx/local_ctx.go
  25. 86 0
      v3/kc/local_ctx/local_ctx_test.go
  26. 119 0
      v3/kc/local_ctx/lst_sort/lst_sort.go
  27. 94 0
      v3/kc/local_ctx/lst_sort/lst_sort_test.go
  28. 196 0
      v3/kc/log_buf/log_buf.go
  29. 59 0
      v3/kc/log_buf/log_buf_test.go
  30. 24 0
      v3/kc/log_buf/log_bus/log_bus.go
  31. 28 0
      v3/kc/log_buf/log_bus/log_bus_test.go
  32. 37 0
      v3/kc/log_buf/log_bus/log_topic/log_topic.go
  33. 54 0
      v3/kc/log_buf/log_bus/log_topic/log_topic_test.go
  34. 71 0
      v3/kc/log_buf/log_msg/log_msg.go
  35. 55 0
      v3/kc/log_buf/log_msg/log_msg_test.go
  36. 93 0
      v3/kc/safe_bool/safe_bool.go
  37. 137 0
      v3/kc/safe_bool/safe_bool_test.go
  38. 120 0
      v3/kc/safe_bool_react/safe_bool_react.go
  39. 169 0
      v3/kc/safe_bool_react/safe_bool_react_test.go
  40. 89 0
      v3/kc/safe_int/safe_int.go
  41. 160 0
      v3/kc/safe_int/safe_int_test.go
  42. 72 0
      v3/kc/safe_string/safe_string.go
  43. 109 0
      v3/kc/safe_string/safe_string_test.go
  44. 200 0
      v3/kern.go
  45. 169 0
      v3/kern_test.go
  46. 19 0
      v3/krn/kalias/kalias.go
  47. 7 0
      v3/krn/kalias/kalias_test.go
  48. 57 0
      v3/krn/kbus/dict_sub_hook/dict_sub_hook.go
  49. 78 0
      v3/krn/kbus/dict_sub_hook/dict_sub_hook_test.go
  50. 93 0
      v3/krn/kbus/dict_topic_serve/dict_topic_serve.go
  51. 150 0
      v3/krn/kbus/dict_topic_serve/dict_topic_serve_test.go
  52. 79 0
      v3/krn/kbus/dict_topic_sub/dict_topic_sub.go
  53. 147 0
      v3/krn/kbus/dict_topic_sub/dict_topic_sub_test.go
  54. 146 0
      v3/krn/kbus/kbus_base/kbus_base.go
  55. 188 0
      v3/krn/kbus/kbus_base/kbus_base_test.go
  56. 215 0
      v3/krn/kbus/kbus_http/client_bus_http/client_bus_http.go
  57. 407 0
      v3/krn/kbus/kbus_http/client_bus_http/client_bus_http_test.go
  58. 215 0
      v3/krn/kbus/kbus_http/kbus_http.go
  59. 549 0
      v3/krn/kbus/kbus_http/kbus_http_test.go
  60. 20 0
      v3/krn/kbus/kbus_local/client_bus_local/client_bus_local.go
  61. 32 0
      v3/krn/kbus/kbus_local/client_bus_local/client_bus_local_test.go
  62. 27 0
      v3/krn/kbus/kbus_local/kbus_local.go
  63. 49 0
      v3/krn/kbus/kbus_local/kbus_local_test.go
  64. 31 0
      v3/krn/kbus/kbus_msg/msg_pub/msg_pub.go
  65. 63 0
      v3/krn/kbus/kbus_msg/msg_pub/msg_pub_test.go
  66. 32 0
      v3/krn/kbus/kbus_msg/msg_serve/msg_serve.go
  67. 63 0
      v3/krn/kbus/kbus_msg/msg_serve/msg_serve_test.go
  68. 33 0
      v3/krn/kbus/kbus_msg/msg_sub/msg_sub.go
  69. 63 0
      v3/krn/kbus/kbus_msg/msg_sub/msg_sub_test.go
  70. 30 0
      v3/krn/kbus/kbus_msg/msg_unsub/msg_unsub.go
  71. 62 0
      v3/krn/kbus/kbus_msg/msg_unsub/msg_unsub_test.go
  72. 77 0
      v3/krn/kctx/kctx.go
  73. 53 0
      v3/krn/kctx/kctx_test.go
  74. 80 0
      v3/krn/kctx/kernel_keeper/kernel_keeper.go
  75. 77 0
      v3/krn/kctx/kernel_keeper/kernel_keeper_test.go
  76. 138 0
      v3/krn/kctx/kwg/kwg.go
  77. 156 0
      v3/krn/kctx/kwg/kwg_test.go
  78. 124 0
      v3/krn/kmodule/kmodule.go
  79. 107 0
      v3/krn/kmodule/kmodule_test.go
  80. 80 0
      v3/krn/kmodule/mod_stat/mod_stat.go
  81. 137 0
      v3/krn/kmodule/mod_stat/mod_stat_day/mod_stat_day.go
  82. 45 0
      v3/krn/kmodule/mod_stat/mod_stat_day/mod_stat_day_test.go
  83. 137 0
      v3/krn/kmodule/mod_stat/mod_stat_minute/mod_stat_minute.go
  84. 45 0
      v3/krn/kmodule/mod_stat/mod_stat_minute/mod_stat_minute_test.go
  85. 159 0
      v3/krn/kmodule/mod_stat/mod_stat_sec/mod_stat_sec.go
  86. 53 0
      v3/krn/kmodule/mod_stat/mod_stat_sec/mod_stat_sec_test.go
  87. 60 0
      v3/krn/kmodule/mod_stat/mod_stat_test.go
  88. 124 0
      v3/krn/kmonolit/kmonolit.go
  89. 140 0
      v3/krn/kmonolit/kmonolit_test.go
  90. 183 0
      v3/krn/kserv_http/kserv_http.go
  91. 122 0
      v3/krn/kserv_http/kserv_http_test.go
  92. 4 0
      v3/krn/kserv_http/static/css/bootstrap-grid.min.css
  93. 0 0
      v3/krn/kserv_http/static/css/bootstrap-grid.min.css.map
  94. 4 0
      v3/krn/kserv_http/static/css/bootstrap-grid.rtl.min.css
  95. 0 0
      v3/krn/kserv_http/static/css/bootstrap-grid.rtl.min.css.map
  96. 4 0
      v3/krn/kserv_http/static/css/bootstrap-reboot.min.css
  97. 0 0
      v3/krn/kserv_http/static/css/bootstrap-reboot.min.css.map
  98. 4 0
      v3/krn/kserv_http/static/css/bootstrap-reboot.rtl.min.css
  99. 0 0
      v3/krn/kserv_http/static/css/bootstrap-reboot.rtl.min.css.map
  100. 4 0
      v3/krn/kserv_http/static/css/bootstrap-utilities.min.css

+ 11 - 0
v3/.env

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

+ 52 - 0
v3/Taskfile.yml

@@ -0,0 +1,52 @@
+# https://taskfile.dev
+
+version: '3'
+
+vars:
+  HELP: Для просмотра всех задач запустите 'task -a'
+
+dotenv: [.env]
+
+tasks:
+  default:
+    desc: Вывод справки
+    cmds:
+      - echo "{{.HELP}}"
+    silent: true
+  demo:
+    desc: Запуск демо-проекта
+    cmds:
+      - clear
+      - go fmt ./...
+      - go build -race -o ./bin_dev/demo ./cmd/demo/main.go
+  mod:
+    desc: Обновление зависимостей
+    cmds:
+      - clear
+      - go fmt ./...
+      - go mod tidy -compat=1.24.2
+      - go mod vendor
+      - go fmt ./...
+  test:
+    desc: Запуск тестов
+    cmds:
+      - clear
+      - go fmt ./...
+      - go test -race -shuffle=on -timeout=30s -coverprofile=./cover.out ./...
+      - go tool cover -func=./cover.out
+  lint:
+    desc: Запуск всех линтеров
+    cmds:
+      - clear
+      - go fmt ./...
+      - go vet ./...
+      # - go install honnef.co/go/tools/cmd/staticcheck@latest
+      # - staticcheck ./...
+      # - go install github.com/MakeNowJust/enumcase/cmd/enumcase@latest
+      # - enumcase ./...
+      - go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
+      - golangci-lint run ./...
+      # - go install github.com/fzipp/gocyclo/cmd/gocyclo@latest
+      # - gocyclo -over 11 .
+      # - go install github.com/securego/gosec/cmd/gosec@latest
+      # - gosec ./...

+ 28 - 0
v3/cmd/demo/main.go

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

+ 20 - 0
v3/cmd/demo/main_test.go

@@ -0,0 +1,20 @@
+package main
+
+import (
+	"os"
+	"testing"
+	"time"
+
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/v3/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
v3/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

+ 29 - 0
v3/docs/alias.md

@@ -0,0 +1,29 @@
+# Алиасы
+
+Алиасы пользовательских типов служат усилению типизации в ядре, что повышает надёжность в-целом.
+
+*Примечание: алиас пользовательского типа в `go` является самостоятельным типом и требует приведения к базовому, если это необходимо*.
+
+## ATime
+
+Метка времени в формате:
+
+```text
+2006-01-02 15:04:05.000 -07 MST
+```
+
+## AStreamName
+
+Имя потока ожидания (в потоках ядра)
+
+## ATopic
+
+Имя топика в шине ядра
+
+## AHandlerName
+
+Имя функции-обработчика в шине ядра
+
+## AModuleName
+
+Имя модуля в модульной структуре

+ 28 - 0
v3/docs/helpers.md

@@ -0,0 +1,28 @@
+# Полезняшки
+
+## Запуск zapret
+
+```bash
+export NFQWS_OPT="
+--filter-tcp=80 --dpi-desync=fake,multisplit --dpi-desync-ttl=0 --dpi-desync-fooling=md5sig,badsum <HOSTLIST> --new
+--filter-tcp=443 --dpi-desync=fake,multidisorder --dpi-desync-split-pos=method+2,midsld,5 --dpi-desync-ttl=0 --dpi-desync-fooling=md5sig,badsum,badseq --dpi-desync-repeats=15 --dpi-desync-fake-tls=/opt/zapret/files/fake/tls_clienthello_www_google_com.bin <HOSTLIST> --new
+--filter-udp=443 --dpi-desync=fake --dpi-desync-repeats=15 --dpi-desync-ttl=0  --dpi-desync-any-protocol --dpi-desync-cutoff=d4 --dpi-desync-fooling=md5sig,badsum --dpi-desync-fake-quic=/opt/zapret/files/fake/quic_initial_www_google_com.bin <HOSTLIST>
+" & sudo /opt/zapret/init.d/sysv/zapret start
+```
+
+Порт 'SOCKS': 987
+
+## Языковые модели
+
+```text
+llama3.1:8b
+qwen2.5-coder:1.5b-base
+qwen2.5-coder:7b (на тестах заявлено, что это самая крутая модель!!!)
+~ (долго думает) deepseek-r1:8b
+```
+
+Запуск сервера `ollama`:
+
+```bash
+ollama serve
+```

+ 18 - 0
v3/docs/idict_topic_serve.md

@@ -0,0 +1,18 @@
+# IDictTopicServe -- интерфейс к обработчику входящих запросов на словарь топиков
+
+## Интерфейс
+
+```go
+// IDictTopicServe -- интерфейс к обработчику входящих запросов на словарь топиков
+//
+// При обслуживании входящих запросов обработчик может быть только ОДИН на КАЖДЫЙ топик.
+// Но обработчик вызывается конкурентно.
+type IDictTopicServe interface {
+    // Register -- регистрирует единственный обработчик на единственный топик
+    Register(IBusHandlerServe)
+    // SendRequest -- выполняет запрос по указанному топику
+    SendRequest(topic ATopic, binReq []byte) Result[[]byte]
+    // Unregister -- удаляет единственный обработчик с единственного топика
+    Unregister(IBusHandlerServe)
+}
+```

BIN
v3/docs/img/coverage.png


+ 20 - 0
v3/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>

BIN
v3/docs/img/mit.png


+ 20 - 0
v3/docs/img/mit.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">License</text>
+    <text x="50" y="138" textLength="503">License</text>
+    <text x="658" y="148" textLength="330" fill="#000" opacity="0.25">MIT</text>
+    <text x="648" y="138" textLength="330">MIT</text>
+  </g>
+
+</svg>

BIN
v3/docs/img/ucl.png


+ 20 - 0
v3/docs/img/ucl.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">License</text>
+    <text x="50" y="138" textLength="503">License</text>
+    <text x="658" y="148" textLength="330" fill="#000" opacity="0.25">UCL</text>
+    <text x="648" y="138" textLength="330">UCL</text>
+  </g>
+
+</svg>

+ 32 - 0
v3/docs/index.md

@@ -0,0 +1,32 @@
+# kern -- модульные компоненты
+
+**kern** позволяет строить проекты на модульном принципе. Этот принцип позволяет бесшовно переходить от модульного монолита к модульным микросервисам. Первый подход (монолит) удобен в стартапах. Второй подход удобен, когда понятно, какие части масштабировать.
+
+## Состав проекта
+
+### Ядро
+
+- [Алиасы](./alias.md)
+- [Типы (интерфейсы)](./types.md)
+- [Шина](./bus.md)
+- [Контекст ядра](./all_context.md)
+- [Модули](./modules.md)
+- [Монолит](./monolit.md)
+- [Сервер HTTP](./server_http.md)
+- [Хранилище key-value](./stjre_kv.md)
+
+## Компоненты
+
+- (Помощники)(./helpers.md)
+- (Локальный контекст)(./local_ctx.md)
+- (Буфер логирования)(./log_buf.md)
+- (Потокобезопасный реактивный `bool`)(./safe_react_bool.md)
+- (Потокобезопасный `int`)(./safe_int.md)
+- (Потокобезопасный `string`)(./safe_string.md)
+
+## Модули
+
+- [Модуль контекста ядра](./module_kern_ctx.md)
+- [Модуль сторожа ядра](./module_kern_keeper.md)
+- [Модуль сервера HTTP](./module_serv_http.md)
+- [Модуль веб-интерфейса](./module_web_interface.md)

+ 9 - 0
v3/docs/types.md

@@ -0,0 +1,9 @@
+# Типы интерфейсов ядра
+
+Интерфейсы введены для ослабления зацепления между объектами и типами.
+
+## Список
+
+- [**IKernelBus**](/ikernel_bus.md) -- шина ядра
+- **IBusClient** -- клиент шины ядра (повторяет интерфейс шины ядра)
+- [**IDictTopicServe**](./idict_topic_serve.md) -- кастомный обработчик входящих запросов

+ 35 - 0
v3/go.mod

@@ -0,0 +1,35 @@
+module gitp78su.ipnodns.ru/svi/kern/v3
+
+go 1.24.2
+
+require (
+	github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b
+	github.com/dgraph-io/badger/v4 v4.8.0
+	github.com/gofiber/fiber/v2 v2.52.9
+	github.com/google/uuid v1.6.0
+)
+
+require (
+	github.com/andybalholm/brotli v1.2.0 // indirect
+	github.com/cespare/xxhash/v2 v2.3.0 // indirect
+	github.com/clipperhouse/stringish v0.1.1 // indirect
+	github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
+	github.com/dgraph-io/ristretto/v2 v2.3.0 // indirect
+	github.com/dustin/go-humanize v1.0.1 // indirect
+	github.com/go-logr/logr v1.4.3 // indirect
+	github.com/go-logr/stdr v1.2.2 // indirect
+	github.com/google/flatbuffers v25.9.23+incompatible // indirect
+	github.com/klauspost/compress v1.18.1 // 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.19 // indirect
+	github.com/valyala/bytebufferpool v1.0.0 // indirect
+	github.com/valyala/fasthttp v1.68.0 // indirect
+	go.opentelemetry.io/auto/sdk v1.2.1 // indirect
+	go.opentelemetry.io/otel v1.38.0 // indirect
+	go.opentelemetry.io/otel/metric v1.38.0 // indirect
+	go.opentelemetry.io/otel/trace v1.38.0 // indirect
+	golang.org/x/net v0.46.0 // indirect
+	golang.org/x/sys v0.37.0 // indirect
+	google.golang.org/protobuf v1.36.10 // indirect
+)

+ 95 - 0
v3/go.sum

@@ -0,0 +1,95 @@
+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.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
+github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
+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/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
+github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
+github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
+github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
+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.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs=
+github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w=
+github.com/dgraph-io/ristretto/v2 v2.3.0 h1:qTQ38m7oIyd4GAed/QkUZyPFNMnvVWyazGXRwvOt5zk=
+github.com/dgraph-io/ristretto/v2 v2.3.0/go.mod h1:gpoRV3VzrEY1a9dWAYV6T1U7YzfgttXdd/ZzL1s9OZM=
+github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38=
+github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/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/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/gofiber/fiber/v2 v2.52.9 h1:YjKl5DOiyP3j0mO61u3NTmK7or8GzzWzCFzkboyP5cw=
+github.com/gofiber/fiber/v2 v2.52.9/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
+github.com/google/flatbuffers v25.9.23+incompatible h1:rGZKv+wOb6QPzIdkM2KxhBZCDrA0DeN6DNmRDrqIsQU=
+github.com/google/flatbuffers v25.9.23+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+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.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
+github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
+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.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
+github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
+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/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+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.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
+github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
+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.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
+go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
+go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
+go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
+go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
+go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
+go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
+go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
+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/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+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.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
+golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
+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-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.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
+golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+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-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-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
+google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+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.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las=

+ 35 - 0
v3/ikernel_bus.md

@@ -0,0 +1,35 @@
+# IKernelBus
+
+Шина ядра.
+
+Может иметь несколько реализаций. Из фреймворка предлагается две реализации :
+
+* локальная
+* поверх HTTP
+
+## Интерфейс
+
+```go
+// IKernelBus -- шина сообщений ядра
+//
+// Публикация и запрос требуют параметров на _передачу_.
+// Подписка и обслуживание входящих запросов требует _обработчиков_.
+type IKernelBus interface {
+    // Publish -- публикует сообщение в шину
+    Publish(topic ATopic, binMsg []byte) Result[bool]
+    // SendRequest -- выполняет запрос по указанному топику
+    SendRequest(topic ATopic, binReq []byte) Result[[]byte]
+
+    // Subscribe -- подписывает обработчик на топик
+    Subscribe(IBusHandlerSubscribe) Result[bool]
+    // Unsubscribe -- отписывается от топика
+    Unsubscribe(IBusHandlerSubscribe)
+    // RegisterServe -- Регистрирует обработчик на обслуживание входящих запросов
+    RegisterServe(IBusHandlerServe)
+
+    // IsWork -- возвращает признак работы шины
+    IsWork() bool
+    // Log -- возвращает буферный лог
+    Log() ILogBuf
+}
+```

+ 86 - 0
v3/kc/helpers/helpers.go

@@ -0,0 +1,86 @@
+// package helpers -- содержит всякие полезняшки
+//
+// Пакет импортировать где нужно в нотации `. "gitlab.c2g.pw/back/uaj-abstract-client/pkg/helpers"`
+package helpers
+
+import (
+	"fmt"
+	"os"
+	"time"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/kalias"
+)
+
+var (
+	IsStageLocal bool
+	IsStageProd  bool
+)
+
+// FnAssert -- проверка на правильность утверждения с падением в панику на локальном стенде (soft assert)
+//
+//	(isCond bool, msgFormat string, args ...any)
+type FnAssert func(isCond bool, msgFormat string, args ...any)
+
+// Assert -- проверка на правильность утверждения с падением в панику на локальном стенде (soft assert)
+func Assert(isCond bool, msgFormat string, args ...any) {
+	if isCond {
+		return
+	}
+	msg := fmt.Sprintf("SOFT ASSERT "+msgFormat+"\n", args...)
+	if IsStageLocal {
+		panic(msg)
+	}
+	fmt.Print(msg)
+}
+
+// FnHassert -- проверяет с жёстким падением условие
+//
+//	(isCond bool, msgFormat string, args ...any)
+type FnHassert func(isCond bool, msgFormat string, args ...any)
+
+// Hassert -- проверка на правильность утверждения с безусловным падением в панику (hard assert)
+func Hassert(isCond bool, msgFormat string, args ...any) {
+	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_()
+}

+ 161 - 0
v3/kc/helpers/helpers_test.go

@@ -0,0 +1,161 @@
+package helpers
+
+import (
+	"os"
+	"testing"
+)
+
+const (
+	baseUrl   = "http://127.0.0.1:18410/"
+	pathStore = "/store/store_helpers"
+)
+
+type tester struct {
+	t *testing.T
+}
+
+func TestHelpers(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	_ = os.Unsetenv("LOCAL_STORE_PATH")
+	_ = os.Setenv("LOCAL_STORE_PATH", pathStore)
+	_ = os.Unsetenv("LOCAL_HTTP_URL")
+	os.Setenv("LOCAL_HTTP_URL", baseUrl)
+	fnClear := func() {
+		_ = os.RemoveAll(pathStore)
+	}
+	fnClear()
+	defer fnClear()
+	sf.assert()
+	sf.hassert()
+	sf.init_()
+}
+
+// Неизвестное значение STAGE
+func (sf *tester) init_() {
+	sf.t.Log("init_")
+	sf.initBad1()
+	sf.initGood1()
+	sf.initGood2()
+	sf.initGood3()
+}
+
+func (sf *tester) initGood3() {
+	sf.t.Log("initGood3")
+	_ = os.Unsetenv("STAGE")
+	_ = os.Setenv("STAGE", "")
+	init_()
+}
+
+func (sf *tester) initGood2() {
+	sf.t.Log("initGood2")
+	_ = os.Unsetenv("STAGE")
+	_ = os.Setenv("STAGE", "prod")
+	init_()
+}
+
+func (sf *tester) initGood1() {
+	sf.t.Log("initGood1")
+	_ = 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")
+}

+ 63 - 0
v3/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/v3/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/kalias"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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
v3/kc/local_ctx/ctx_value/ctx_value_test.go

@@ -0,0 +1,47 @@
+package ctx_value
+
+import (
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/kalias"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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")
+	}
+}

+ 109 - 0
v3/kc/local_ctx/local_ctx.go

@@ -0,0 +1,109 @@
+// package local_ctx -- локальный контекст
+package local_ctx
+
+import (
+	"context"
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/local_ctx/ctx_value"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/local_ctx/lst_sort"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/log_buf"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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(log_buf.OptIsTerm(true), log_buf.OptPrefix("LocalCtx")),
+	}
+	return sf
+}
+
+// Ctx -- возвращает отменяемый контекст
+func (sf *LocalCtx) Ctx() context.Context {
+	return sf.ctx
+}
+
+// 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("Get(): key='%v'", key)
+	return sf.dictVal[key]
+}
+
+// Del -- удаляет значение из контекста
+func (sf *LocalCtx) Del(key string) {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.log.Debug("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("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("Cancel()")
+	sf.fnCancel()
+}

+ 86 - 0
v3/kc/local_ctx/local_ctx_test.go

@@ -0,0 +1,86 @@
+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")
+	}
+	if ctx := sf.ctx.Ctx(); ctx == nil {
+		sf.t.Fatalf("newGood1(): ctx==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
v3/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/v3/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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
v3/kc/local_ctx/lst_sort/lst_sort_test.go

@@ -0,0 +1,94 @@
+package lst_sort
+
+import (
+	"testing"
+
+	"gitp78su.ipnodns.ru/svi/kern/v3/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")
+	}
+}

+ 196 - 0
v3/kc/log_buf/log_buf.go

@@ -0,0 +1,196 @@
+// package log_buf -- потокобезопасный буфер лога
+package log_buf
+
+import (
+	"fmt"
+	"sync"
+
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/log_buf/log_msg"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/safe_bool"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+)
+
+// logBuf -- потокобезопасный буфер лога
+type logBuf struct {
+	sync.RWMutex
+	isTerm ISafeBool // Признак вывода в стандартный поток
+	prefix string    // Префикс для сообщений
+	lst    []ILogMsg
+	lstErr []ILogMsg
+}
+
+// OptionLogBuf -- опция для конфигурирования ILogBuf
+type OptionLogBuf func(logBuf *logBuf)
+
+// OptIsTerm -- устанавливает признак вывода в терминал
+func OptIsTerm(isTerm bool) OptionLogBuf {
+	return func(sf *logBuf) {
+		if isTerm {
+			sf.IsTerm().Set()
+		}
+	}
+}
+
+// OptPrefix -- устанавливает префикс в сообщениях
+func OptPrefix(prefix string) OptionLogBuf {
+	return func(sf *logBuf) {
+		sf.prefix = prefix + "."
+	}
+}
+
+// NewLogBuf -- возвращает новый потокобезопасный буфер лога
+func NewLogBuf(opts ...OptionLogBuf) ILogBuf {
+	sf := &logBuf{
+		isTerm: safe_bool.NewSafeBool(),
+		lst:    []ILogMsg{},
+		lstErr: []ILogMsg{},
+	}
+
+	for _, opt := range opts {
+		opt(sf)
+	}
+	return sf
+}
+
+// IsTerm -- возвращает признак логирования
+func (sf *logBuf) IsTerm() ISafeBool {
+	return sf.isTerm
+}
+
+// GetErr -- возвращает сообщение ошибки по номеру
+func (sf *logBuf) GetErr(num int) ILogMsg {
+	sf.RLock()
+	defer sf.RUnlock()
+	if len(sf.lstErr) == 0 {
+		return log_msg.NewLogMsg(log_msg.DEBUG, sf.prefix+"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]
+}
+
+// Get -- возвращает сообщение по номеру
+func (sf *logBuf) Get(num int) ILogMsg {
+	sf.RLock()
+	defer sf.RUnlock()
+	if len(sf.lst) == 0 {
+		return log_msg.NewLogMsg(log_msg.DEBUG, sf.prefix+"*no msg*")
+	}
+	if num >= len(sf.lst) {
+		return log_msg.NewLogMsg(log_msg.DEBUG, sf.prefix+"*no msg*")
+	}
+	if num <= 0 {
+		return log_msg.NewLogMsg(log_msg.DEBUG, sf.prefix+"*no msg*")
+	}
+	return sf.lst[num]
+}
+
+type tMsg struct {
+	text string
+	args []any
+}
+
+// Debug -- сообщение отладки
+func (sf *logBuf) Debug(fMsg string, args ...any) {
+	sf.Lock()
+	defer sf.Unlock()
+	msg := tMsg{
+		text: fMsg,
+		args: args,
+	}
+	strMsg := fmt.Sprintf(msg.text, msg.args...)
+	_msg := log_msg.NewLogMsg(log_msg.DEBUG, sf.prefix+strMsg)
+	sf.lst = append(sf.lst, _msg)
+	sf.checkLen()
+	sf.printTerm(_msg)
+}
+
+// Info -- информационные сообщения
+func (sf *logBuf) Info(fMsg string, args ...any) {
+	sf.Lock()
+	defer sf.Unlock()
+	msg := tMsg{
+		text: fMsg,
+		args: args,
+	}
+	strMsg := fmt.Sprintf(msg.text, msg.args...)
+	_msg := log_msg.NewLogMsg(log_msg.INFO, sf.prefix+strMsg)
+	sf.lst = append(sf.lst, _msg)
+	sf.checkLen()
+	sf.printTerm(_msg)
+}
+
+// Warn -- предупреждающие сообщения
+func (sf *logBuf) Warn(fMsg string, args ...any) {
+	sf.Lock()
+	defer sf.Unlock()
+	msg := tMsg{
+		text: fMsg,
+		args: args,
+	}
+	strMsg := fmt.Sprintf(msg.text, msg.args...)
+	_msg := log_msg.NewLogMsg(log_msg.WARN, sf.prefix+strMsg)
+	sf.lst = append(sf.lst, _msg)
+	sf.checkLen()
+	sf.printTerm(_msg)
+}
+
+// Err -- сообщения об ошибках
+func (sf *logBuf) Err(fMsg string, args ...any) {
+	sf.Lock()
+	defer sf.Unlock()
+	msg := tMsg{
+		text: fMsg,
+		args: args,
+	}
+	strMsg := fmt.Sprintf(msg.text, msg.args...)
+	_msg := log_msg.NewLogMsg(log_msg.ERROR, sf.prefix+strMsg)
+	sf.lst = append(sf.lst, _msg)
+	sf.lstErr = append(sf.lstErr, _msg)
+	sf.checkLen()
+	sf.checkLenErr()
+	sf.printTerm(_msg)
+}
+
+// Size -- возвращает размер буфера
+func (sf *logBuf) Size() int {
+	sf.RLock()
+	defer sf.RUnlock()
+	return len(sf.lst)
+}
+
+// Проверяет длину общую лога
+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:]
+	}
+}
+
+// Печатает сообщение в терминал, если разрешено
+func (sf *logBuf) printTerm(msg ILogMsg) {
+	if !sf.isTerm.Get() {
+		return
+	}
+	level := msg.Level()
+	if helpers.IsStageProd {
+		switch level {
+		case "ERRO", "WARN":
+			fmt.Printf("%v   %v\n", level, msg.String())
+		default:
+			return
+		}
+	}
+	fmt.Printf("%v\n", msg.String())
+}

+ 59 - 0
v3/kc/log_buf/log_buf_test.go

@@ -0,0 +1,59 @@
+package log_buf
+
+import (
+	"testing"
+
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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(OptIsTerm(true), OptPrefix("tester"))
+	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()
+
+	sf.log.IsTerm().Reset()
+	sf.log.Warn("test msg: %v", 47)
+
+	sf.log.IsTerm().Set()
+	sf.log.Warn("test msg: %v", 47)
+
+	helpers.IsStageProd = true
+	sf.log.Warn("test msg: %v", 47)
+	sf.log.Debug("test msg: %v", 45)
+}

+ 24 - 0
v3/kc/log_buf/log_bus/log_bus.go

@@ -0,0 +1,24 @@
+// package log_bus -- хранитель топиков для логирования
+package log_bus
+
+import (
+	"sync"
+
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+)
+
+type LogDictTopic struct {
+	sync.RWMutex
+	dict   map[kalias.ATopic]bool
+	client ktypes.IBusClient
+}
+
+// NewLogDictTopic -- возвращает новый словарь топиков для логирования
+func NewLogDictTopic(clientBus ktypes.IBusClient) *LogDictTopic {
+	sf := &LogDictTopic{
+		dict:   map[kalias.ATopic]bool{},
+		client: clientBus,
+	}
+	return sf
+}

+ 28 - 0
v3/kc/log_buf/log_bus/log_bus_test.go

@@ -0,0 +1,28 @@
+package log_bus
+
+import (
+	"testing"
+)
+
+type tester struct {
+	t *testing.T
+}
+
+func TestLogDictTopic(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+}
+
+// Создаёт новый словарь топиков для логирования
+func (sf *tester) new() {
+	sf.t.Log("new")
+	log := NewLogDictTopic(nil)
+	if log == nil {
+		sf.t.Fatalf("new(): log!=nil")
+	}
+	if log.client != nil {
+		sf.t.Fatalf("mew(): log.client!=nil")
+	}
+}

+ 37 - 0
v3/kc/log_buf/log_bus/log_topic/log_topic.go

@@ -0,0 +1,37 @@
+// package log_topic -- элемент лога шины топика
+package log_topic
+
+import (
+	"fmt"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/kalias"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+)
+
+// LogTopic -- элемент лога шины
+type LogTopic struct {
+	topic  ATopic     // Топик, куда публиковать лог
+	client IBusClient // С помощью чего публиковать лог
+}
+
+// NewLogTopic -- возвращает новый элемент лога
+func NewLogTopic(topic ATopic, client IBusClient) *LogTopic {
+	Hassert(topic != "", "NewLogTopic(): topic is empty")
+	Hassert(client != nil, "NewLogTopic(): IBusClient==nil")
+	sf := &LogTopic{
+		topic:  topic,
+		client: client,
+	}
+	return sf
+}
+
+// Pub -- публикует сообщение в топик
+func (sf *LogTopic) Pub(binMsg []byte) Result[bool] {
+	res := sf.client.Publish(sf.topic, binMsg)
+	if res.IsErr() {
+		err := fmt.Errorf("LogTopic.Pub(): in pub with client, err=\n\t%v", res.Err())
+		return NewErr[bool](err)
+	}
+	return res
+}

+ 54 - 0
v3/kc/log_buf/log_bus/log_topic/log_topic_test.go

@@ -0,0 +1,54 @@
+package log_topic
+
+import (
+	"testing"
+
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_local/client_bus_local"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+)
+
+type tester struct {
+	t *testing.T
+}
+
+func TestLogTopic(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")
+	busClient := client_bus_local.NewClientBusLocal()
+	client := NewLogTopic("test_topic", busClient)
+	res := client.Pub([]byte("test_msg"))
+	if res.IsErr() {
+		sf.t.Fatalf("newGood1(): err=%v", res.Err())
+	}
+	ctx := kctx.GetKernelCtx()
+	ctx.Cancel()
+	ctx.Wg().Wait()
+	res = client.Pub([]byte("test_msg"))
+	if !res.IsErr() {
+		sf.t.Fatalf("newGood1(): err==nil")
+	}
+}
+
+// Нет топика лога
+func (sf *tester) newBad1() {
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("newBad1(): panic==nil")
+		}
+	}()
+	_ = NewLogTopic("", nil)
+}

+ 71 - 0
v3/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/v3/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/kalias"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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
v3/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")
+}

+ 93 - 0
v3/kc/safe_bool/safe_bool.go

@@ -0,0 +1,93 @@
+// package safe_bool -- потокобезопасный булевый признак
+package safe_bool
+
+import (
+	"fmt"
+	"os"
+	"strings"
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+)
+
+// safeBool -- потокобезопасный булевый признак
+type safeBool struct {
+	sync.RWMutex
+	val bool
+}
+
+// NewSafeBool -- возвращает новый потокобезопасный булевый признак
+func NewSafeBool() ISafeBool {
+	sf := &safeBool{}
+	return sf
+}
+
+// NewSafeBoolFromStr -- возвращает новое потокобезоппсное булево из строки
+func NewSafeBoolFromStr(strVal string) Result[ISafeBool] {
+	sf := NewSafeBool()
+	res := sf.FromStr(strVal)
+	if res.IsErr() {
+		err := fmt.Errorf("NewSafeBoolFromStr(): in parse str(%v), err=\n\t%w", strVal, res.Err())
+		return NewErr[ISafeBool](err)
+	}
+	return NewOk(sf)
+}
+
+// NewSafeBoolGetenv -- возвращает новое потокобезоппсное целое из окружения
+func NewSafeBoolGetenv(env string) Result[ISafeBool] {
+	sf := NewSafeBool()
+	res := sf.Getenv(env)
+	if res.IsErr() {
+		err := fmt.Errorf("NewSafeBoolGetenv(): in parse env(%v), err=\n\t%w", env, res.Err())
+		return NewErr[ISafeBool](err)
+	}
+	return NewOk(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
+}
+
+// FromStr -- получает число из строки
+func (sf *safeBool) FromStr(strVal string) Result[bool] {
+	strVal = strings.ToLower(strVal)
+	sf.Lock()
+	defer sf.Unlock()
+	switch strVal {
+	case "true":
+		sf.val = true
+	case "false":
+		sf.val = false
+	default:
+		return NewErr[bool](fmt.Errorf("safeBool.FromStr(): val(%v) bad", strVal))
+	}
+	return NewOk(sf.val)
+}
+
+// Getenv -- получает значение из окружения
+func (sf *safeBool) Getenv(env string) Result[bool] {
+	strVal := os.Getenv(env)
+	res := sf.FromStr(strVal)
+	if res.IsErr() {
+		err := fmt.Errorf("safeBool.Getenv(): from env %v, err=\n\t%w", env, res.Err())
+		return NewErr[bool](err)
+	}
+	return res
+}

+ 137 - 0
v3/kc/safe_bool/safe_bool_test.go

@@ -0,0 +1,137 @@
+package safe_bool
+
+import (
+	"os"
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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()
+	sf.newFromEnv()
+	sf.newFromStr()
+}
+
+// Булево из строки
+func (sf *tester) newFromStr() {
+	sf.t.Log("newFromStr")
+	sf.newFromStrBad1()
+	sf.newFromStrGood1()
+}
+
+func (sf *tester) newFromStrGood1() {
+	sf.t.Log("newFromStrGood1")
+	res := NewSafeBoolFromStr("tRuE")
+	if res.IsErr() {
+		sf.t.Fatalf("newFromStrGood1(): err=%v", res.Err())
+	}
+	sb := res.Val()
+	if val := sb.Get(); val != true {
+		sf.t.Fatalf("newFromStrGood1(): val==false")
+	}
+}
+
+// Кривое булево значение
+func (sf *tester) newFromStrBad1() {
+	sf.t.Log("newFromStrBad1")
+	res := NewSafeBoolFromStr("0")
+	if res.IsOk() {
+		sf.t.Fatalf("newFromStrBad1(): err==nil")
+	}
+}
+
+// Булево из окружения
+func (sf *tester) newFromEnv() {
+	sf.t.Log("newFromEnv")
+	sf.newFromEnvBad1()
+	sf.newFromEnvGood1()
+	sf.newFromEnvGood2()
+}
+
+func (sf *tester) newFromEnvGood2() {
+	sf.t.Log("newFromEnvGood2")
+	os.Unsetenv("TEST_BOOL")
+	os.Setenv("TEST_BOOL", "false")
+	res := NewSafeBoolGetenv("TEST_BOOL")
+	if res.IsErr() {
+		sf.t.Fatalf("newFromEnvGood2(): err=%v", res.Err())
+	}
+	sb := res.Val()
+	if val := sb.Get(); val != false {
+		sf.t.Fatalf("newFromEnvGood2(): val==true")
+	}
+}
+
+func (sf *tester) newFromEnvGood1() {
+	sf.t.Log("newFromEnvGood1")
+	os.Unsetenv("TEST_BOOL")
+	os.Setenv("TEST_BOOL", "true")
+	res := NewSafeBoolGetenv("TEST_BOOL")
+	if res.IsErr() {
+		sf.t.Fatalf("newFromEnvGood1(): err=%v", res.Err())
+	}
+	sb := res.Val()
+	if val := sb.Get(); val != true {
+		sf.t.Fatalf("newFromEnvGood1(): val==false")
+	}
+}
+
+// Кривое булево значение
+func (sf *tester) newFromEnvBad1() {
+	sf.t.Log("newFromStrBad1")
+	os.Unsetenv("TEST_BOOL")
+	os.Setenv("TEST_BOOL", "bad")
+	res := NewSafeBoolGetenv("TEST_BOOL")
+	if res.IsOk() {
+		sf.t.Fatalf("newFromStrBad1(): err==nil")
+	}
+}
+
+// Сбрасывает хранимое значение
+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")
+	}
+}

+ 120 - 0
v3/kc/safe_bool_react/safe_bool_react.go

@@ -0,0 +1,120 @@
+// package safe_bool_react -- потокобезопасный булевый признак с реакцией на своё изменение
+package safe_bool_react
+
+import (
+	"fmt"
+	"os"
+	"strings"
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+)
+
+// safeBoolReact -- потокобезопасный булевый признак с реакцией на своё изменение
+type safeBoolReact struct {
+	sync.RWMutex
+	dict map[string]func(bool) // Словарь обратных вызовов
+	val  bool
+}
+
+// NewSafeBoolReact -- возвращает новый потокобезопасный булевый признак с реакцией на своё изменение
+func NewSafeBoolReact() ISafeBoolReact {
+	sf := &safeBoolReact{
+		dict: map[string]func(bool){},
+	}
+	return sf
+}
+
+// NewSafeBoolReactFromStr -- возвращает новое потокобезоппсное булево с реакцией из строки
+func NewSafeBoolReactFromStr(strVal string) Result[ISafeBoolReact] {
+	sf := NewSafeBoolReact()
+	res := sf.FromStr(strVal)
+	if res.IsErr() {
+		err := fmt.Errorf("NewSafeBoolReactFromStr(): in parse str(%v), err=\n\t%w", strVal, res.Err())
+		return NewErr[ISafeBoolReact](err)
+	}
+	return NewOk(sf)
+}
+
+// NewSafeBoolGetenv -- возвращает новое потокобезоппсное целое с реакцией из окружения
+func NewSafeBoolGetenv(env string) Result[ISafeBoolReact] {
+	sf := NewSafeBoolReact()
+	res := sf.Getenv(env)
+	if res.IsErr() {
+		err := fmt.Errorf("NewSafeBoolGetenv(): in parse env(%v), err=\n\t%w", env, res.Err())
+		return NewErr[ISafeBoolReact](err)
+	}
+	return NewOk(sf)
+}
+
+// Delete -- удаляет функцию обратного вызова из наблюдения
+func (sf *safeBoolReact) Delete(key string) {
+	sf.Lock()
+	defer sf.Unlock()
+	delete(sf.dict, key)
+}
+
+// Add -- добавляет функцию обратного вызова
+func (sf *safeBoolReact) Add(key string, fn func(bool)) {
+	sf.Lock()
+	defer sf.Unlock()
+	Hassert(key != "", "safeBoolReact.Add(): key is empty")
+	_, isOk := sf.dict[key]
+	Hassert(!isOk, "safeBoolReact.Add(): key already exists")
+	sf.dict[key] = fn
+}
+
+// Get -- возвращает хранимый булевый признак
+func (sf *safeBoolReact) Get() bool {
+	sf.RLock()
+	defer sf.RUnlock()
+	return sf.val
+}
+
+// Set -- устанавливает булевый признак
+func (sf *safeBoolReact) Set() {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.val = true
+	for _, fn := range sf.dict {
+		fn(true)
+	}
+}
+
+// Reset -- сбрасывает булевый признак
+func (sf *safeBoolReact) Reset() {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.val = false
+	for _, fn := range sf.dict {
+		fn(false)
+	}
+}
+
+// FromStr -- получает число из строки
+func (sf *safeBoolReact) FromStr(strVal string) Result[bool] {
+	strVal = strings.ToLower(strVal)
+	sf.Lock()
+	defer sf.Unlock()
+	switch strVal {
+	case "true":
+		sf.val = true
+	case "false":
+		sf.val = false
+	default:
+		return NewErr[bool](fmt.Errorf("safeBoolReact.FromStr(): val(%v) bad", strVal))
+	}
+	return NewOk(sf.val)
+}
+
+// Getenv -- получает значение из окружения
+func (sf *safeBoolReact) Getenv(env string) Result[bool] {
+	strVal := os.Getenv(env)
+	res := sf.FromStr(strVal)
+	if res.IsErr() {
+		err := fmt.Errorf("safeBoolReact.Getenv(): from env %v, err=\n\t%w", env, res.Err())
+		return NewErr[bool](err)
+	}
+	return res
+}

+ 169 - 0
v3/kc/safe_bool_react/safe_bool_react_test.go

@@ -0,0 +1,169 @@
+package safe_bool_react
+
+import (
+	"os"
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+)
+
+type tester struct {
+	t   *testing.T
+	sbr ISafeBoolReact
+	val bool
+}
+
+func TestSafeBoolReact(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.create()
+	sf.add()
+	sf.set()
+	sf.reset()
+	sf.del()
+	sf.newFromEnv()
+	sf.newFromStr()
+}
+
+// Булево из строки
+func (sf *tester) newFromStr() {
+	sf.t.Log("newFromStr")
+	sf.newFromStrBad1()
+	sf.newFromStrGood1()
+}
+
+func (sf *tester) newFromStrGood1() {
+	sf.t.Log("newFromStrGood1")
+	res := NewSafeBoolReactFromStr("tRuE")
+	if res.IsErr() {
+		sf.t.Fatalf("newFromStrGood1(): err=%v", res.Err())
+	}
+	sb := res.Val()
+	if val := sb.Get(); val != true {
+		sf.t.Fatalf("newFromStrGood1(): val==false")
+	}
+}
+
+// Кривое булево значение
+func (sf *tester) newFromStrBad1() {
+	sf.t.Log("newFromStrBad1")
+	res := NewSafeBoolReactFromStr("0")
+	if res.IsOk() {
+		sf.t.Fatalf("newFromStrBad1(): err==nil")
+	}
+}
+
+// Булево из окружения
+func (sf *tester) newFromEnv() {
+	sf.t.Log("newFromEnv")
+	sf.newFromEnvBad1()
+	sf.newFromEnvGood1()
+	sf.newFromEnvGood2()
+}
+
+func (sf *tester) newFromEnvGood2() {
+	sf.t.Log("newFromEnvGood2")
+	os.Unsetenv("TEST_BOOL")
+	os.Setenv("TEST_BOOL", "false")
+	res := NewSafeBoolGetenv("TEST_BOOL")
+	if res.IsErr() {
+		sf.t.Fatalf("newFromEnvGood2(): err=%v", res.Err())
+	}
+	sb := res.Val()
+	if val := sb.Get(); val != false {
+		sf.t.Fatalf("newFromEnvGood2(): val==true")
+	}
+}
+
+func (sf *tester) newFromEnvGood1() {
+	sf.t.Log("newFromEnvGood1")
+	os.Unsetenv("TEST_BOOL")
+	os.Setenv("TEST_BOOL", "true")
+	res := NewSafeBoolGetenv("TEST_BOOL")
+	if res.IsErr() {
+		sf.t.Fatalf("newFromEnvGood1(): err=%v", res.Err())
+	}
+	sb := res.Val()
+	if val := sb.Get(); val != true {
+		sf.t.Fatalf("newFromEnvGood1(): val==false")
+	}
+}
+
+// Кривое булево значение
+func (sf *tester) newFromEnvBad1() {
+	sf.t.Log("newFromStrBad1")
+	os.Unsetenv("TEST_BOOL")
+	os.Setenv("TEST_BOOL", "bad")
+	res := NewSafeBoolGetenv("TEST_BOOL")
+	if res.IsOk() {
+		sf.t.Fatalf("newFromStrBad1(): err==nil")
+	}
+}
+
+// Удаляет функцию обратного вызова
+func (sf *tester) del() {
+	sf.t.Log("del")
+	sf.sbr.Delete("test")
+	sf.val = false
+	sf.sbr.Set()
+	if sf.val {
+		sf.t.Fatalf("del(): val==true")
+	}
+}
+
+// Добавляет функцию обратного вызова
+func (sf *tester) add() {
+	sf.t.Log("add")
+	sf.sbr.Add("test", sf.fnBack)
+}
+
+// Функция обратного вызова для контроля
+func (sf *tester) fnBack(val bool) {
+	sf.val = val
+}
+
+// Сбрасывает хранимое значение
+func (sf *tester) reset() {
+	sf.t.Log("reset")
+	sf.sbr.Reset()
+	if sf.sbr.Get() {
+		sf.t.Fatalf("reset(): SafeBool==true")
+	}
+	sf.val = true
+	sf.sbr.Reset()
+	if sf.sbr.Get() {
+		sf.t.Fatalf("reset(): SafeBool==true")
+	}
+	if sf.val {
+		sf.t.Fatalf("reset(): val==true")
+	}
+}
+
+// Установка хранимого значения
+func (sf *tester) set() {
+	sf.t.Log("set")
+	sf.sbr.Set()
+	if !sf.sbr.Get() {
+		sf.t.Fatalf("set(): SafeBool==true")
+	}
+	sf.sbr.Set()
+	if !sf.sbr.Get() {
+		sf.t.Fatalf("set(): SafeBool==true")
+	}
+	if !sf.val {
+		sf.t.Fatalf("set(): val==false")
+	}
+}
+
+// Создаёт потокобезопасный булевый признак
+func (sf *tester) create() {
+	sf.t.Log("create")
+	sf.sbr = NewSafeBoolReact()
+	if sf.sbr == nil {
+		sf.t.Fatalf("create(): SafeBool==nil")
+	}
+	if sf.sbr.Get() {
+		sf.t.Fatalf("create(): SafeBool==true")
+	}
+}

+ 89 - 0
v3/kc/safe_int/safe_int.go

@@ -0,0 +1,89 @@
+// package safe_int -- потокобезопасный целое
+package safe_int
+
+import (
+	"fmt"
+	"os"
+	"strconv"
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+)
+
+// safeInt -- потокобезопасное целое
+type safeInt struct {
+	sync.RWMutex
+	val int
+}
+
+// NewSafeInt -- возвращает новое потокобезопасное целое
+func NewSafeInt() ISafeInt {
+	sf := &safeInt{}
+	return sf
+}
+
+// NewSafeIntFromStr -- возвращает новое потокобезоппсное целое из строки
+func NewSafeIntFromStr(strVal string) Result[ISafeInt] {
+	sf := NewSafeInt()
+	res := sf.FromStr(strVal)
+	if res.IsErr() {
+		err := fmt.Errorf("NewSafeIntFromStr(): in parse str(%v), err=\n\t%w", strVal, res.Err())
+		return NewErr[ISafeInt](err)
+	}
+	return NewOk(sf)
+}
+
+// NewSafeIntGetenv -- возвращает новое потокобезоппсное целое из окружения
+func NewSafeIntGetenv(env string) Result[ISafeInt] {
+	sf := NewSafeInt()
+	res := sf.Getenv(env)
+	if res.IsErr() {
+		err := fmt.Errorf("NewSafeIntGetenv(): in parse env(%v), err=\n\t%w", env, res.Err())
+		return NewErr[ISafeInt](err)
+	}
+	return NewOk(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
+}
+
+// FromStr -- получает число из строки
+func (sf *safeInt) FromStr(strVal string) Result[int] {
+	iVal, err := strconv.Atoi(strVal)
+	if err != nil {
+		return NewErr[int](fmt.Errorf("safeInt.FromStr(): vak(%v) bad, err=\n\t%w", strVal, err))
+	}
+	sf.Lock()
+	defer sf.Unlock()
+	sf.val = iVal
+	return NewOk(sf.val)
+}
+
+// Getenv -- получает значение из окружения
+func (sf *safeInt) Getenv(env string) Result[int] {
+	strVal := os.Getenv(env)
+	res := sf.FromStr(strVal)
+	if res.IsErr() {
+		err := fmt.Errorf("safeInt.Getenv(): from env %v, err=\n\t%w", env, res.Err())
+		return NewErr[int](err)
+	}
+	return res
+}

+ 160 - 0
v3/kc/safe_int/safe_int_test.go

@@ -0,0 +1,160 @@
+package safe_int
+
+import (
+	"os"
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+)
+
+type tester struct {
+	t  *testing.T
+	si ISafeInt
+}
+
+func TestSafeInt(t *testing.T) {
+	sf := &tester{
+		t: t,
+	}
+	sf.new()
+	sf.newGetenv()
+	sf.newFromStr()
+	sf.set()
+	sf.reset()
+	sf.fromStr()
+	sf.getEnv()
+}
+
+// Получает целое из строки
+func (sf *tester) newFromStr() {
+	sf.t.Log("newFromStr")
+	res := NewSafeIntFromStr("-a52")
+	if res.IsOk() {
+		sf.t.Fatalf("newFromStr(): err==nil")
+	}
+	res = NewSafeIntFromStr("-60")
+	if res.IsErr() {
+		sf.t.Fatalf("newFromStr(): err=%v", res.Err())
+	}
+	if val := res.Val(); val.Get() != -60 {
+		sf.t.Fatalf("newFromStr(): val(%v)!=-60", val.Get())
+	}
+}
+
+// Получает целое из окружения
+func (sf *tester) newGetenv() {
+	sf.t.Log("newGetenv")
+	res := NewSafeIntGetenv("TEST_ENV2")
+	if res.IsOk() {
+		sf.t.Fatalf("newGetenv(): err==nil")
+	}
+	os.Unsetenv("TEST_ENV2")
+	os.Setenv("TEST_ENV2", "-52")
+	res = NewSafeIntGetenv("TEST_ENV2")
+	if res.IsErr() {
+		sf.t.Fatalf("newGetenv(): err=%v", res.Err())
+	}
+	if val := res.Val(); val.Get() != -52 {
+		sf.t.Fatalf("newGetenv(): val(%v)!=-52", val.Get())
+	}
+}
+
+// Получает значение из окружения
+func (sf *tester) getEnv() {
+	sf.t.Log("getEnv")
+	sf.getEnvBad1()
+	sf.getEnvGood1()
+}
+
+func (sf *tester) getEnvGood1() {
+	sf.t.Log("getEnvGood1")
+	os.Unsetenv("TEST_VAL")
+	os.Setenv("TEST_VAL", "45")
+	val := NewSafeInt()
+	res := val.Getenv("TEST_VAL")
+	if res.IsErr() {
+		sf.t.Fatalf("getEnvGood1(): err=%v", res.Err())
+	}
+	if val := res.Val(); val != 45 {
+		sf.t.Fatalf("getEnvGood1(): val(%v)!=45", val)
+	}
+}
+
+// Окружение не число
+func (sf *tester) getEnvBad1() {
+	sf.t.Log("getEnvBad1")
+	os.Unsetenv("TEST_VAL")
+	os.Setenv("TEST_VAL", "")
+	val := NewSafeInt()
+	res := val.Getenv("TEST_VAL")
+	if res.IsOk() {
+		sf.t.Fatalf("getEnvBad1(): res==ok")
+	}
+}
+
+// Получает значение из строки
+func (sf *tester) fromStr() {
+	sf.t.Log("fromStr")
+	sf.fromStrBad1()
+	sf.fromStrGood1()
+}
+
+func (sf *tester) fromStrGood1() {
+	sf.t.Log("fromStrGood1")
+	val := NewSafeInt()
+	res := val.FromStr("45")
+	if res.IsErr() {
+		sf.t.Fatalf("fromStrGood1(): err=%v", res.Err())
+	}
+	if val := res.Val(); val != 45 {
+		sf.t.Fatalf("fromStrGood1(): val(%v)!=45", val)
+	}
+}
+
+// Строка не число
+func (sf *tester) fromStrBad1() {
+	sf.t.Log("fromStrBad1")
+	val := NewSafeInt()
+	res := val.FromStr(" ")
+	if res.IsOk() {
+		sf.t.Fatalf("fromStrBad1(): res==ok")
+	}
+}
+
+// Сбрасывает хранимое значение
+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")
+	}
+}

+ 72 - 0
v3/kc/safe_string/safe_string.go

@@ -0,0 +1,72 @@
+// package safe_string -- потокобезопасная строка
+package safe_string
+
+import (
+	"fmt"
+	"os"
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+)
+
+// safeString -- потокобезопасная строка
+type safeString struct {
+	sync.RWMutex
+	val string
+}
+
+// NewSafeString -- возвращает новую потокобезопасную строку
+func NewSafeString() ISafeString {
+	sf := &safeString{}
+	return sf
+}
+
+// NewSafeStringGetenv -- возвращает новуб потокобезопасную строку из окружения
+func NewSafeStringGetenv(env string) Result[ISafeString] {
+	sf := NewSafeString()
+	res := sf.Getenv(env)
+	if res.IsErr() {
+		err := fmt.Errorf("NewSafeStringGetenv(): in get from env(%v), err=\n\t%w", env, res.Err())
+		return NewErr[ISafeString](err)
+	}
+	return NewOk(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 = ""
+}
+
+// Getenv -- получает значение из окружения
+func (sf *safeString) Getenv(env string) Result[string] {
+	strVal := os.Getenv(env)
+	if strVal == "" {
+		err := fmt.Errorf("safeString.Getenv(): from env(%v), val is empty", env)
+		return NewErr[string](err)
+	}
+	sf.val = strVal
+	return NewOk(strVal)
+}

+ 109 - 0
v3/kc/safe_string/safe_string_test.go

@@ -0,0 +1,109 @@
+package safe_string
+
+import (
+	"os"
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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()
+	sf.newFromEnv()
+}
+
+// Булево из окружения
+func (sf *tester) newFromEnv() {
+	sf.t.Log("newFromEnv")
+	sf.newFromEnvBad1()
+	sf.newFromEnvGood1()
+	sf.newFromEnvGood2()
+}
+
+func (sf *tester) newFromEnvGood2() {
+	sf.t.Log("newFromEnvGood2")
+	os.Unsetenv("TEST_BOOL")
+	os.Setenv("TEST_BOOL", "false")
+	res := NewSafeStringGetenv("TEST_BOOL")
+	if res.IsErr() {
+		sf.t.Fatalf("newFromEnvGood2(): err=%v", res.Err())
+	}
+	sb := res.Val()
+	if val := sb.Get(); val != "false" {
+		sf.t.Fatalf("newFromEnvGood2(): val==true")
+	}
+}
+
+func (sf *tester) newFromEnvGood1() {
+	sf.t.Log("newFromEnvGood1")
+	os.Unsetenv("TEST_BOOL")
+	os.Setenv("TEST_BOOL", "true")
+	res := NewSafeStringGetenv("TEST_BOOL")
+	if res.IsErr() {
+		sf.t.Fatalf("newFromEnvGood1(): err=%v", res.Err())
+	}
+	sb := res.Val()
+	if val := sb.Get(); val != "true" {
+		sf.t.Fatalf("newFromEnvGood1(): val==false")
+	}
+}
+
+// Кривое булево значение
+func (sf *tester) newFromEnvBad1() {
+	sf.t.Log("newFromEnvBad1")
+	os.Unsetenv("TEST_BOOL")
+	os.Setenv("TEST_BOOL", "")
+	res := NewSafeStringGetenv("TEST_BOOL")
+	if res.IsOk() {
+		sf.t.Fatalf("newFromEnvBad1(): err==nil")
+	}
+}
+
+// Сбрасывает хранимое значение
+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!=''")
+	}
+}

+ 200 - 0
v3/kern.go

@@ -0,0 +1,200 @@
+// package kern -- библиотека гибкого универсального облегчённого ядра для любого микросервиса
+package kern
+
+import (
+	"context"
+
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/local_ctx"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/log_buf"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/safe_bool"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/safe_bool_react"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/safe_int"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/safe_string"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_http"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_http/client_bus_http"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_local"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_local/client_bus_local"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kmodule"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kmonolit"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kserv_http"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kstore_kv"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/v3/mds/mod_kctx"
+	"gitp78su.ipnodns.ru/svi/kern/v3/mds/mod_keeper"
+	"gitp78su.ipnodns.ru/svi/kern/v3/mds/mod_serv_http"
+	"gitp78su.ipnodns.ru/svi/kern/v3/mds/mod_wui"
+)
+
+// GetKernelCtx -- возвращает контекст ядра
+func GetKernelCtx() IKernelCtx {
+	ctx := kctx.GetKernelCtx()
+	return ctx
+}
+
+// GetKernelStoreKv -- возвращает быстрое key-value хранилище ядра
+func GetKernelStoreKv() IKernelStoreKv {
+	store := kstore_kv.GetKernelStore()
+	return store
+}
+
+// GetKernelServerHttp -- возвращает веб-сервер ядра
+func GetKernelServerHttp() IKernelServerHttp {
+	kernServHttp := kserv_http.GetKernelServHttp()
+	return kernServHttp
+}
+
+// NewSafeBool -- возвращает новый потокобезопасный булевый признак
+func NewSafeBool() ISafeBool {
+	sb := safe_bool.NewSafeBool()
+	return sb
+}
+
+// GetKernelBusLocal -- возвращает локальную шину данных
+func GetKernelBusLocal() IKernelBus {
+	ctx := kctx.GetKernelCtx()
+	ctx.Set("monolitName", "unknown monolit", "GetKernelBusLocal()")
+	bus := kbus_local.GetKernelBusLocal()
+	return bus
+}
+
+// GetKernelBusHttp -- возвращает HTTP шину данных
+func GetKernelBusHttp() IKernelBus {
+	bus := kbus_http.GetKernelBusHttp()
+	return bus
+}
+
+// GetMonolitLocal -- возвращает монолит с локальной шиной
+func GetMonolitLocal(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
+}
+
+// GetMonolitHttp -- возвращает монолит с локальной шиной поверх HTTP
+func GetMonolitHttp(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
+}
+
+// GetModuleServHttp -- возвращает модуль для IKernelServHttp
+func GetModuleServHttp() IKernelModule {
+	modServHttp := mod_serv_http.GetModuleServHttp()
+	return modServHttp
+}
+
+// GetModuleKernelCtx -- возвращает модуль для IKernelCtx
+func GetModuleKernelCtx() IKernelModule {
+	modKernelCtx := mod_kctx.GetModuleKernelCtx()
+	return modKernelCtx
+}
+
+// GetModuleKernelKeeper -- возвращает модуль для IKernelKeeper
+func GetModuleKernelKeeper() IKernelModule {
+	modKernelKeeper := mod_keeper.GetModuleKeeper()
+	return modKernelKeeper
+}
+
+// GetModuleWui -- возвращает модуль для WUI
+func GetModuleWui() IKernelModule {
+	mod := mod_wui.GetModuleWui()
+	return mod
+}
+
+// NewLogBuf -- возвращает новый буферизованный лог
+func NewLogBuf(opts ...log_buf.OptionLogBuf) ILogBuf {
+	log := log_buf.NewLogBuf(opts...)
+	return log
+}
+
+// NewSafeBoolReact -- возвращает новую потокобезопасную реактивную булеву переменную
+func NewSafeBoolReact() ISafeBoolReact {
+	val := safe_bool_react.NewSafeBoolReact()
+	return val
+}
+
+// NewSafeInt -- возвращает новую потокобезопасную целочисленную переменную
+func NewSafeInt() ISafeInt {
+	val := safe_int.NewSafeInt()
+	return val
+}
+
+// NewLocalCtx -- возвращает новый локальный контекст
+func NewLocalCtx(ctx context.Context) ILocalCtx {
+	ctx_ := local_ctx.NewLocalCtx(ctx)
+	return ctx_
+}
+
+// NewSafeString -- возвращает новую потокобезопасную строку
+func NewSafeString() ISafeString {
+	str := safe_string.NewSafeString()
+	return str
+}
+
+// MakeOk -- возвращает новый положительный результат операции
+func MakeOk[T any](res T) Result[T] {
+	return NewOk(res)
+}
+
+// MakeErr -- возвращает новую ошибку результат операции
+func MakeErr[T any](err error) Result[T] {
+	return NewErr[T](err)
+}
+
+// MakeSome -- возвращает новый не пустой результат операции
+func MakeSome[T any](some T) Option[T] {
+	return NewSome(some)
+}
+
+// MakeNone -- возвращает новый пустой результат операции
+func MakeNone[T any]() Option[T] {
+	return NewNone[T]()
+}
+
+// GetFnHassert -- возвращает ссылку на функцию Hassert
+func GetFnHassert() helpers.FnHassert {
+	return helpers.Hassert
+}
+
+// GetFnAssert -- возвращает ссылку на функцию Hassert
+func GetFnAssert() helpers.FnAssert {
+	return helpers.Assert
+}

+ 169 - 0
v3/kern_test.go

@@ -0,0 +1,169 @@
+package kern
+
+import (
+	"fmt"
+	"os"
+	"testing"
+
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/log_buf"
+	"gitp78su.ipnodns.ru/svi/kern/v3/mock/mock_env"
+)
+
+const (
+	baseUrl   = "http://127.0.0.1:18420/"
+	pathStore = "/store/store_helpers"
+)
+
+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", pathStore)
+	_ = os.Unsetenv("LOCAL_HTTP_URL")
+	os.Setenv("LOCAL_HTTP_URL", baseUrl)
+	fnClear := func() {
+		_ = os.RemoveAll(pathStore)
+	}
+	fnClear()
+	defer 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 := GetKernelCtx()
+	{
+		if ctx == nil {
+			sf.t.Fatalf("new(): IKernelCtx==nil")
+		}
+		store := GetKernelStoreKv()
+		res := store.Delete("test_builders")
+		if res.IsErr() {
+			sf.t.Fatalf("new(): get empty key, store, err=%v", res.Err())
+		}
+		safeBool := NewSafeBool()
+		if safeBool == nil {
+			sf.t.Fatalf("new(): ISafeBool==nil")
+		}
+
+		kernBus := GetKernelBusLocal()
+		if kernBus == nil {
+			sf.t.Fatalf("new(): (local) IKernelBus==nil")
+		}
+
+		kernBusHttp := GetKernelBusHttp()
+		if kernBusHttp == nil {
+			sf.t.Fatalf("new(): (http) IKernelBus==nil")
+		}
+
+	}
+	{
+		monLocal := GetMonolitLocal("mon_local")
+		if monLocal == nil {
+			sf.t.Fatalf("new(): (local) IKernelMonolit==nil")
+		}
+
+		monHttp := GetMonolitHttp("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 := GetModuleServHttp()
+		if modServHttp == nil {
+			sf.t.Fatalf("new(): modServHttp==nil")
+		}
+
+		modKernelCtx := GetModuleKernelCtx()
+		if modKernelCtx == nil {
+			sf.t.Fatalf("new(): modKernelCtx==nil")
+		}
+		modKernelKeeper := GetModuleKernelKeeper()
+		if modKernelKeeper == nil {
+			sf.t.Fatalf("new(): modKernelKeeper==nil")
+		}
+		modWui := GetModuleWui()
+		if modWui == nil {
+			sf.t.Fatalf("new(): modWui==nil")
+		}
+
+		logBuf := NewLogBuf(log_buf.OptIsTerm(true))
+		if logBuf == nil {
+			sf.t.Fatalf("new(): ILogBuf==nil")
+		}
+		boolReact := NewSafeBoolReact()
+		if boolReact == nil {
+			sf.t.Fatalf("new(): ISafeBoolReact==nil")
+		}
+		safeInt := NewSafeInt()
+		if safeInt == nil {
+			sf.t.Fatalf("new(): ISafeInt==nil")
+		}
+		lCtx := NewLocalCtx(ctx.Ctx())
+		if lCtx == nil {
+			sf.t.Fatalf("new(): ILocalCtx==nil")
+		}
+	}
+	{
+		safeStr := NewSafeString()
+		if safeStr == nil {
+			sf.t.Fatalf("new(): ISafeStr==nil")
+		}
+
+		fnHassert := GetFnHassert()
+		if fnHassert == nil {
+			sf.t.Fatalf("new(): Hassert==nil")
+		}
+
+		fnAssert := GetFnAssert()
+		if fnAssert == nil {
+			sf.t.Fatalf("new(): Assert==nil")
+		}
+	}
+
+	_ = MakeOk("test ok")
+	_ = MakeErr[bool](fmt.Errorf("test err"))
+	_ = MakeSome("test err")
+	_ = MakeNone[int64]()
+
+	kernServHttp := GetKernelServerHttp()
+	go kernServHttp.Run()
+	ctx.Cancel()
+	ctx.Wg().Wait()
+	ctx.Cancel()
+	ctx.Wg().Wait()
+}

+ 19 - 0
v3/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
v3/krn/kalias/kalias_test.go

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

+ 57 - 0
v3/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/v3/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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
v3/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/v3/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)
+	}
+}

+ 93 - 0
v3/krn/kbus/dict_topic_serve/dict_topic_serve.go

@@ -0,0 +1,93 @@
+// package dict_topic_serve -- словарь топиков обработчиков запросов
+package dict_topic_serve
+
+import (
+	"context"
+	"fmt"
+	"sync"
+	"time"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+)
+
+// dictServe -- потокобезопасный словарь обработчиков запросов
+//
+// Допускается только один обработчик запросов на один топик
+type dictServe struct {
+	sync.RWMutex
+	ctx       IKernelCtx
+	dictServe map[ATopic]IBusHandlerServe
+}
+
+// NewDictServe -- возвращает потокобезопасный словарь обработчиков запросов
+func NewDictServe() IDictTopicServe {
+	sf := &dictServe{
+		ctx:       kctx.GetKernelCtx(),
+		dictServe: map[ATopic]IBusHandlerServe{},
+	}
+	return sf
+}
+
+// Register -- регистрирует обработчик запросов
+func (sf *dictServe) Register(handler IBusHandlerServe) {
+	sf.Lock()
+	defer sf.Unlock()
+	Hassert(handler != nil, "dictServe.Register(): IBusHandlerSubscribe==nil")
+	topic := handler.Topic()
+	Hassert(topic != "", "dictServe.Register(): empty topic of handler")
+	isTwinRegister := sf.register(handler)
+	Hassert(!isTwinRegister, "dictServe.Register(): handler of topic (%v) already register", handler.Topic())
+}
+
+// Unregister -- удаляет обработчик запросов из словаря
+func (sf *dictServe) Unregister(handler IBusHandlerServe) {
+	sf.Lock()
+	defer sf.Unlock()
+	Hassert(handler != nil, "dictServe.Unregister(): IBusHandlerSubscribe==nil")
+	delete(sf.dictServe, handler.Topic())
+}
+
+// SendRequest -- вызывает обработчик при поступлении запроса
+func (sf *dictServe) SendRequest(topic ATopic, binReq []byte) Result[[]byte] {
+	sf.RLock()
+	defer sf.RUnlock()
+	handler, isOk := sf.dictServe[topic]
+	if !isOk {
+		err := fmt.Errorf("dictServe.SendRequest(): handler for topic (%v) not exists", topic)
+		return NewErr[[]byte](err)
+	}
+	var (
+		chRes = make(chan Result[[]byte], 2)
+	)
+	ctx, fnCancel := context.WithTimeout(sf.ctx.Ctx(), time.Millisecond*time.Duration(TimeoutDefault))
+	defer fnCancel()
+	fnCall := func() {
+		defer close(chRes)
+		res := handler.FnBack(binReq)
+		chRes <- res
+	}
+	go fnCall()
+	select {
+	case <-ctx.Done():
+		err := fmt.Errorf("dictServe.SendRequest(): in call for topic (%v), err=\n\t%w", topic, ctx.Err())
+		return NewErr[[]byte](err)
+	case res := <-chRes:
+		return res
+	}
+}
+
+var TimeoutDefault = 15000
+
+// регистрирует обработчик запросов
+func (sf *dictServe) register(handler IBusHandlerServe) bool {
+	topic := handler.Topic()
+	_, isOk := sf.dictServe[topic]
+	if isOk {
+		return true
+	}
+	sf.dictServe[topic] = handler
+	return false
+}

+ 150 - 0
v3/krn/kbus/dict_topic_serve/dict_topic_serve_test.go

@@ -0,0 +1,150 @@
+package dict_topic_serve
+
+import (
+	"testing"
+
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/v3/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)
+	res := sf.dict.SendRequest(sf.hand.Topic(), []byte("test"))
+	if res.IsOk() {
+		sf.t.Fatalf("callBad3(): err==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
+	res := sf.dict.SendRequest(sf.hand.Topic(), []byte("test_good"))
+	if res.IsErr() {
+		sf.t.Fatalf("sendGood1(): err=%v", res.Err())
+	}
+	if res.Val() == nil {
+		sf.t.Fatalf("sendGood1(): binMsg==nil")
+	}
+}
+
+// Обработчик вернул ошибку
+func (sf *tester) sendBad2() {
+	sf.t.Log("sendBad2")
+	sf.hand.IsBad_.Set()
+	res := sf.dict.SendRequest(sf.hand.Topic(), []byte("test"))
+	if res.IsOk() {
+		sf.t.Fatalf("sendBad2(): err==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")
+	res := sf.dict.SendRequest("test_bad_topic", []byte("test"))
+	if res.IsOk() {
+		sf.t.Fatalf("sendBad1(): err==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
v3/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/v3/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/dict_sub_hook"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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
v3/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/v3/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/v3/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
v3/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/v3/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/local_ctx"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/safe_bool"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/dict_topic_serve"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/dict_topic_sub"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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.Ctx()),
+	}
+	Bus_.log = Bus_.ctx.Log()
+	go Bus_.close()
+	go Bus_.run()
+	Bus_.IsWork_.Set()
+	res := Bus_.Ctx_.Wg().Add(strBusBaseStream)
+	res.Hassert("GetKernelBusBase(): in add name stream(%v)", strBusBaseStream)
+	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) Result[bool] {
+	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 NewErr[bool](err)
+	}
+	sf.dictSub.Subscribe(handler)
+	return NewOk(true)
+}
+
+// SendRequest -- отправляет запрос в шину данных
+func (sf *KBusBase) SendRequest(topic ATopic, binReq []byte) Result[[]byte] {
+	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 NewErr[[]byte](err)
+	}
+	res := sf.dictServe.SendRequest(topic, binReq)
+	if res.IsErr() {
+		err := fmt.Errorf("KBusBase.SendRequest(): topic='%v', err=\n\t%w", topic, res.Err())
+		sf.log.Err(err.Error())
+		return NewErr[[]byte](err)
+	}
+	return res
+}
+
+// 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) Result[bool] {
+	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 NewErr[bool](err)
+	}
+	// Асинхронный запуск чтения
+	go sf.dictSub.Read(topic, binMsg)
+	return NewOk(true)
+}
+
+// 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")
+}

+ 188 - 0
v3/krn/kbus/kbus_base/kbus_base_test.go

@@ -0,0 +1,188 @@
+package kbus_base
+
+import (
+	"testing"
+
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/v3/mock/mock_hand_serve"
+	"gitp78su.ipnodns.ru/svi/kern/v3/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")
+	res := sf.bus.SendRequest(sf.handServ.Topic_, []byte("test_msg"))
+	if res.IsErr() {
+		sf.t.Fatalf("reqGood1(): err=%v", res.Err())
+	}
+	if res.Val() == 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")
+	res := sf.bus.SendRequest("test_topic1", []byte("test_msg"))
+	if res.IsOk() {
+		sf.t.Fatalf("reqBad1(): err==nil")
+	}
+}
+
+// Нет читателей топика
+func (sf *tester) pubGood10() {
+	sf.t.Log("pubGood10")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("pubGood10(): panic=%v", _panic)
+		}
+	}()
+	res := sf.bus.Publish("test_topic1", []byte("test_msg"))
+	if res.IsErr() {
+		sf.t.Fatalf("pubGood10(): err=%v", res.Err())
+	}
+}
+
+func (sf *tester) subGood1() {
+	sf.t.Log("subGood1")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("subGood1(): panic=%v", _panic)
+		}
+	}()
+	res := sf.bus.Subscribe(sf.handSub)
+	if res.IsErr() {
+		sf.t.Fatalf("subGood1(): err=%v", res.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")
+	}
+	res := sf.bus.Subscribe(sf.handSub)
+	if res.IsOk() {
+		sf.t.Fatalf("close(): err==nil")
+	}
+	res = sf.bus.Publish("test_topic1", []byte("test_msg"))
+	if res.IsOk() {
+		sf.t.Fatalf("close(): err==nil")
+	}
+	res1 := sf.bus.SendRequest("test_topic", []byte("test_msg"))
+	if res1.IsOk() {
+		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")
+	}
+}

+ 215 - 0
v3/krn/kbus/kbus_http/client_bus_http/client_bus_http.go

@@ -0,0 +1,215 @@
+// 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/v3/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/local_ctx"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/log_buf"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/safe_bool"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_http"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_msg/msg_pub"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_msg/msg_serve"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_msg/msg_sub"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_msg/msg_unsub"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+)
+
+// ClientBusHttp -- клиент HTTP-шины
+type ClientBusHttp struct {
+	bus       IKernelBus
+	ctx       ILocalCtx
+	log       ILogBuf
+	isWork    ISafeBool
+	urlRemote string // URL дистанционной шины
+	urlLocal  string // URL локальной шины для веб-хуков
+}
+
+// NewClientBusHttp - -возвращает новый клиент HTTP-шины
+func NewClientBusHttp(urlRemote string) IBusClient {
+	log := log_buf.NewLogBuf(log_buf.OptIsTerm(true), log_buf.OptPrefix("ClientBusHttp"))
+	log.Debug("NewClientBusHttp()")
+	Hassert(urlRemote != "", "NewClientBusHttp(): urlRemote 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.Ctx()),
+		log:       log,
+		bus:       kbus_http.GetKernelBusHttp(),
+		isWork:    safe_bool.NewSafeBool(),
+		urlRemote: strings.TrimSuffix(urlRemote, "/"),
+		urlLocal:  strings.TrimSuffix(urlLocal, "/"),
+	}
+	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, err := json.MarshalIndent(req, "", "  ")
+	Hassert(err == nil, "ClientBusHttp.Unsubscribe(): in marshal to JSON unsubscribe request, err=\n\t%v", err)
+	reader := strings.NewReader(string(binReq))
+
+	hReq, err := http.NewRequest("POST", sf.urlRemote+"/bus/unsub", reader)
+	Hassert(err == nil, "ClientBusHttp.Unsubscribe(): in new request, err=\n\t%v")
+
+	binBody, err := sf.makePost(hReq)
+	if err != nil {
+		sf.log.Err("Unsubscribe(): in make request, err=\n\t%v", err)
+		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("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) Result[bool] {
+	_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)
+		return NewErr[bool](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_)
+		return NewErr[bool](err)
+	}
+	Hassert(resp.Uuid_ == req.Uuid_, "ClientBusHttp.Subscribe(): resp uuid(%v) bad", resp.Uuid_)
+	res := sf.bus.Subscribe(handler)
+	return res
+}
+
+// SendRequest -- отправляет в дистанционную шину запрос
+func (sf *ClientBusHttp) SendRequest(topic ATopic, binReq []byte) Result[[]byte] {
+	_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)
+		return NewErr[[]byte](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_)
+		return NewErr[[]byte](err)
+	}
+	Hassert(resp.Uuid_ == req.Uuid_, "ClientBusHttp.SendRequest(): resp uuid(%v) bad", resp.Uuid_)
+	return NewOk(resp.BinResp_)
+}
+
+// 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) Result[bool] {
+	_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)
+		return NewErr[bool](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_)
+		return NewErr[bool](err)
+	}
+	Hassert(resp.Uuid_ == req.Uuid_, "ClientBusHttp.Publish(): resp uuid(%v) bad", resp.Uuid_)
+	return NewOk(true)
+}
+
+// Единый обработчик запросов
+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)
+		return nil, err
+	}
+	defer _resp.Body.Close()
+	if _resp.StatusCode != 200 {
+		return nil, fmt.Errorf("ClientBusHttp.makePost(): url=%q, status=%q", hReq.URL, _resp.Status)
+	}
+	binBody, err := io.ReadAll(_resp.Body)
+	return binBody, err
+}
+
+// Log -- возвращает локальный лог клиента
+func (sf *ClientBusHttp) Log() ILogBuf {
+	return sf.log
+}
+
+// IsWork -- возвращает признак работы
+func (sf *ClientBusHttp) IsWork() bool {
+	return sf.bus.IsWork()
+}

+ 407 - 0
v3/krn/kbus/kbus_http/client_bus_http/client_bus_http_test.go

@@ -0,0 +1,407 @@
+package client_bus_http
+
+import (
+	"net/http"
+	"os"
+	"strings"
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_base"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_http"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kserv_http"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/v3/mock/mock_env"
+	"gitp78su.ipnodns.ru/svi/kern/v3/mock/mock_hand_serve"
+	"gitp78su.ipnodns.ru/svi/kern/v3/mock/mock_hand_sub_http"
+)
+
+const (
+	baseUrl   = "http://127.0.0.1:18350/"
+	pathStore = "/store/store_client_bus_http"
+)
+
+type tester struct {
+	t        *testing.T
+	me       *mock_env.MockEnv
+	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", baseUrl).(*mock_hand_sub_http.MockHandSubHttp)
+	handServ := mock_hand_serve.NewMockHandlerServe("test_topic_serv", "local_hook")
+	sf := &tester{
+		t:        t,
+		me:       mock_env.MakeEnv(),
+		ctx:      kctx.GetKernelCtx(),
+		handSub:  handSub,
+		handServ: handServ,
+		bus:      kbus_base.GetKernelBusBase(),
+	}
+	_ = os.Unsetenv("LOCAL_STORE_PATH")
+	_ = os.Setenv("LOCAL_STORE_PATH", pathStore)
+	_ = os.Unsetenv("LOCAL_HTTP_URL")
+	os.Setenv("LOCAL_HTTP_URL", baseUrl)
+	fnClear := func() {
+		pwd := sf.me.Pwd() + pathStore
+		_ = os.RemoveAll(pwd)
+	}
+	fnClear()
+	defer fnClear()
+	sf.new()
+	sf.unsub()
+	sf.sub()
+	sf.pub()
+	sf.unsubGood1()
+	sf.reg()
+	sf.send()
+	sf.ctx.Cancel()
+	sf.ctx.Wg().Wait()
+}
+
+// Отправка запросов
+func (sf *tester) send() {
+	sf.t.Log("send")
+	sf.sendBad1()
+	sf.sendBad2()
+	sf.sendGood1()
+}
+
+func (sf *tester) sendGood1() {
+	sf.t.Log("sendGood1")
+	res := sf.cl.SendRequest("test_topic_serv", []byte("test msg 456"))
+	if res.IsErr() {
+		sf.t.Fatalf("sendGood1(): err=%v", res.Err())
+	}
+	if res.Val() == nil {
+		sf.t.Fatalf("sendGood1(): binResp==nil")
+	}
+	strResp := string(res.Val())
+	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
+	}()
+	res := sf.cl.SendRequest("test_topic_serv", []byte("test msg"))
+	if res.IsOk() {
+		sf.t.Fatalf("sendBad2(): err==nil")
+	}
+}
+
+// Нет такого топика
+func (sf *tester) sendBad1() {
+	sf.t.Log("sendBad1")
+	res := sf.cl.SendRequest("test_bad_topic", []byte("test msg"))
+	if res.IsOk() {
+		sf.t.Fatalf("sendBad1(): err==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{}
+	res := sf.cl.Publish("test_topic_sub", []byte("test_msg_456"))
+	if res.IsOk() {
+		if strings.Contains(res.Err().Error(), "topic='test_topic_sub',bus already closed") {
+			return
+		}
+		sf.t.Fatalf("pubBad2(): err=%v", res.Err())
+	}
+}
+
+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{}
+	res := sf.cl.Publish("test_topic_sub", []byte("test_msg_456"))
+	if res.IsErr() {
+		if strings.Contains(res.Err().Error(), "topic='test_topic_sub',bus already closed") {
+			return
+		}
+		sf.t.Fatalf("pubGood1(): err=%v", res.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)
+		}
+	}()
+	res := sf.cl.Publish("test_topic", []byte("test_msg"))
+	if res.IsOk() {
+		sf.t.Fatalf("pubBad1(): err==nil")
+	}
+}
+
+// Подписка на топик
+func (sf *tester) sub() {
+	sf.t.Log("sub")
+	sf.subBad1()
+	sf.subBad2()
+	sf.subBad3()
+	sf.subBad4()
+	sf.subGood1()
+}
+
+// С шиной что-то случилось
+func (sf *tester) subBad3() {
+	sf.t.Log("subBad3")
+	bus := kbus_http.Bus_
+	bus.IsWork_.Reset()
+	defer bus.IsWork_.Set()
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("subBad3(): panic=%v", _panic)
+		}
+	}()
+	res := sf.cl.Subscribe(sf.handSub)
+	if res.IsErr() {
+		if strings.Contains(res.Err().Error(), "bus already closed") {
+			return
+		}
+		sf.t.Fatalf("subBad3(): err=%v", res.Err())
+	}
+}
+
+func (sf *tester) subGood1() {
+	sf.t.Log("subGood1")
+	defer func() {
+		if _panic := recover(); _panic != nil {
+			sf.t.Fatalf("subGood1(): panic=%v", _panic)
+		}
+	}()
+	res := sf.cl.Subscribe(sf.handSub)
+	if res.IsErr() {
+		if strings.Contains(res.Err().Error(), "bus already closed") {
+			return
+		}
+		sf.t.Fatalf("subGood1(): err=%v", res.Err())
+	}
+}
+
+// Кривой запрос в сеть
+func (sf *tester) subBad4() {
+	sf.t.Log("subBad4")
+	reader := strings.NewReader("")
+	hReq, err := http.NewRequest("POST", sf.cl.urlRemote+"/bus/pub", reader)
+	if err != nil {
+		sf.t.Fatalf("subBad4(): err=%v", err)
+	}
+	binData, err := sf.cl.makePost(hReq)
+	if err == nil {
+		sf.t.Fatalf("subBad4(): err==nil")
+	}
+	if binData != nil {
+		sf.t.Fatalf("subBad4(): binData!=nil")
+	}
+}
+
+// Левый адрес
+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)
+		}
+	}()
+	res := sf.cl.Subscribe(sf.handSub)
+	if res.IsOk() {
+		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)
+		}
+	}()
+	sf.ctx.Set("monolitName", "test_client_bus_http", "comment")
+	sf.cl = NewClientBusHttp(baseUrl).(*ClientBusHttp)
+	kServHttp := kserv_http.GetKernelServHttp()
+	kServHttp.Run()
+
+	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")
+	}
+}
+
+// Пустой URL
+func (sf *tester) newBad2() {
+	sf.t.Log("newBad2")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("newBad2(): panic==nil")
+		}
+	}()
+	_ = 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")
+}

+ 215 - 0
v3/krn/kbus/kbus_http/kbus_http.go

@@ -0,0 +1,215 @@
+// package kbus_http -- шина сообщений поверх HTTP
+package kbus_http
+
+import (
+	"fmt"
+	"net/http"
+	"sync"
+
+	"github.com/gofiber/fiber/v2"
+
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/log_buf"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_base"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_msg/msg_pub"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_msg/msg_serve"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_msg/msg_sub"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_msg/msg_unsub"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kserv_http"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/v3/mock/mock_hand_sub_http"
+)
+
+// kBusHttp -- шина данных поверх HTTP
+type kBusHttp struct {
+	*kbus_base.KBusBase
+	log ILogBuf
+}
+
+var (
+	Bus_  *kBusHttp
+	block sync.Mutex
+)
+
+// GetKernelBusHttp -- возвращает шину HTTP
+func GetKernelBusHttp() IKernelBus {
+	block.Lock()
+	defer block.Unlock()
+	if Bus_ != nil {
+		return Bus_
+	}
+	log := log_buf.NewLogBuf(log_buf.OptIsTerm(true), log_buf.OptPrefix("kBusHttp"))
+	log.Debug("GetKernelBusHttp(): new")
+	ctx := kctx.GetKernelCtx()
+	sf := &kBusHttp{
+		KBusBase: kbus_base.GetKernelBusBase(),
+		log:      log,
+	}
+	servHttp := kserv_http.GetKernelServHttp()
+	fibApp := servHttp.Fiber()
+	fibApp.Post("/bus/sub", sf.postSub)             // Топик подписки, IN
+	fibApp.Post("/bus/unsub", sf.postUnsub)         // Топик отписки, IN
+	fibApp.Post("/bus/request", sf.postSendRequest) // Топик входящих запросов, IN
+	fibApp.Post("/bus/pub", sf.postPublish)         // Топик публикаций подписки, IN
+	ctx.Set("kernBus", sf, "GetKernelBusHttp(): http data bus")
+	Bus_ = sf
+	return Bus_
+}
+
+// Входящий запрос HTTP на подписку
+func (sf *kBusHttp) postSub(ctx *fiber.Ctx) error {
+	sf.log.Debug("postSub()")
+	ctx.Set("Content-type", "text/html; charset=utf8")
+	ctx.Set("Content-type", "text/json")
+	ctx.Set("Cache-Control", "no-cache")
+	sf.log.Debug("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("postSub(): in body parser, status=%q", 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(),
+	}
+	res := sf.Subscribe(handler)
+	if res.IsErr() {
+		resp.Status_ = fmt.Sprintf("kernelBusHttp.processSubscribe(): err=\n\t%v", res.Err())
+		return resp
+	}
+	return resp
+}
+
+// Входящая публикация
+func (sf *kBusHttp) postPublish(ctx *fiber.Ctx) error {
+	sf.log.Debug("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("postPublish(): in body parser, status=%v", 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()
+	res := sf.Publish(req.Topic_, req.BinMsg_)
+	resp := &msg_pub.PublishResp{
+		Status_: "ok",
+		Uuid_:   req.Uuid_,
+	}
+	if res.IsErr() {
+		resp.Status_ = fmt.Sprintf("kernelBusHttp.processPublish(): err=\n\t%v", res.Err())
+		return resp
+	}
+	return resp
+}
+
+// Входящий запрос
+func (sf *kBusHttp) postSendRequest(ctx *fiber.Ctx) error {
+	sf.log.Debug("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("postSendRequest(): in body parser, status=%v", 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()
+	res := sf.SendRequest(req.Topic_, req.BinReq_)
+	resp := &msg_serve.ServeResp{
+		Status_: "ok",
+		Uuid_:   req.Uuid_,
+	}
+	if res.IsErr() {
+		resp.Status_ = fmt.Sprintf("kernelBusHttp.processSendRequest(): err=\n\t%v", res.Err())
+		return resp
+	}
+	resp.BinResp_ = res.Val()
+	return resp
+}
+
+// Входящая отписка от топика по HTTP
+func (sf *kBusHttp) postUnsub(ctx *fiber.Ctx) error {
+	sf.log.Debug("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("postUnsub(): in body_parser, status=%q", 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
v3/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/v3/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_msg/msg_pub"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_msg/msg_serve"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_msg/msg_sub"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_msg/msg_unsub"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kserv_http"
+	"gitp78su.ipnodns.ru/svi/kern/v3/mock/mock_env"
+	"gitp78su.ipnodns.ru/svi/kern/v3/mock/mock_hand_serve"
+	"gitp78su.ipnodns.ru/svi/kern/v3/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")
+	res := Bus_.Subscribe(sf.handSub)
+	if res.IsErr() {
+		sf.t.Fatalf("unsubGood1(): err=%v", res.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")
+	res := Bus_.Subscribe(sf.handSub)
+	if res.IsErr() {
+		sf.t.Fatalf("unsubGood1(): err=%v", res.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")
+	res := Bus_.Subscribe(sf.handSub)
+	if res.IsErr() {
+		sf.t.Fatalf("pubGood1(): err=%v", res.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
v3/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/v3/krn/kbus/kbus_local"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+)
+
+// ClientBusLocal -- клиент локальной шины
+type ClientBusLocal struct {
+	IKernelBus
+}
+
+// NewClientBusLocal -- клиент локальной шины
+func NewClientBusLocal() IBusClient {
+	sf := &ClientBusLocal{
+		IKernelBus: kbus_local.GetKernelBusLocal(),
+	}
+	return sf
+}

+ 32 - 0
v3/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/v3/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")
+	}
+	res := sf.cl.Publish("local_topic", []byte("test_msg"))
+	if res.IsErr() {
+		sf.t.Fatalf("new(): err=%v", res.Err())
+	}
+}

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

@@ -0,0 +1,27 @@
+// package kbus_local -- реализация локальной шины сообщений
+package kbus_local
+
+import (
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_base"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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
v3/krn/kbus/kbus_local/kbus_local_test.go

@@ -0,0 +1,49 @@
+package kbus_local
+
+import (
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/v3/mock/mock_hand_serve"
+	"gitp78su.ipnodns.ru/svi/kern/v3/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
v3/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/v3/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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
v3/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
v3/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/v3/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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
v3/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
v3/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/v3/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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
v3/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
v3/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/v3/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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
v3/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()
+}

+ 77 - 0
v3/krn/kctx/kctx.go

@@ -0,0 +1,77 @@
+// package kctx -- контекст ядра
+package kctx
+
+import (
+	"context"
+	"sync"
+
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/local_ctx"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx/kernel_keeper"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx/kwg"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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
+}
+
+// Cancel -- отменяет контекст ядра
+func (sf *kCtx) Cancel() {
+	sf.fnCancel()
+	sf.log.Debug("kCtx.Cancel()")
+}

+ 53 - 0
v3/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.Ctx(); ctx == nil {
+		sf.t.Fatalf("new(): ctx==nil")
+	}
+	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
v3/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/v3/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/log_buf"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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(log_buf.OptIsTerm(true), log_buf.OptPrefix("kernelKeeper")),
+		chSys_:   make(chan os.Signal, 2),
+	}
+	sf.log.Debug("GetKernelKeeper(): first run")
+	res := sf.wg.Add("kernel_keeper")
+	res.Hassert("NewKernelCtx(): in add stream kernel keeper in IKernelWg")
+
+	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("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("run(): system signal, sig=%v\n", sig)
+		sf.fnCancel()
+	case <-sf.ctx.Done(): // сигнал от приложения
+		sf.log.Debug("run(): cancel app context, err=\n\t%v\n", sf.ctx.Err())
+	}
+	sf.wg.Done("kernel_keeper")
+	sf.log.Debug("run(): end")
+}

+ 77 - 0
v3/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/v3/krn/kctx/kwg"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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)
+}

+ 138 - 0
v3/krn/kctx/kwg/kwg.go

@@ -0,0 +1,138 @@
+// package kwg -- именованный ожидатель потоков ядра
+//
+// Не позволяет завершиться ядру, если есть хоть один работающий поток
+package kwg
+
+import (
+	"context"
+	"fmt"
+	"sync"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/log_buf"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/safe_bool"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/kalias"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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(log_buf.OptIsTerm(true), log_buf.OptPrefix("kernelWg")),
+	}
+	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("Done(): stream(%v) done", name)
+}
+
+// Wait -- блокирующий вызов; возвращает управление, только когда все потоки завершили работу
+func (sf *kernelWg) Wait() {
+	for {
+		SleepMs()
+		if !sf.isWork.Get() {
+			break
+		}
+	}
+	sf.log.Debug("Wait(): done")
+}
+
+// Add -- добавляет поток в ожидание
+func (sf *kernelWg) Add(name AStreamName) Result[bool] {
+	sf.Lock()
+	defer sf.Unlock()
+	sf.log.Debug("Add(): stream='%v'", name)
+	if !sf.isWork.Get() {
+		err := fmt.Errorf("Add(): stream=%v, work end", name)
+		return NewErr[bool](err)
+	}
+	Hassert(name != "", "Add(): name stream is empty")
+	_, isOk := sf.dictStream[name]
+	Hassert(!isOk, "Add(): stream '%v' already exists", name)
+	sf.dictStream[name] = true
+	return NewOk(true)
+}
+
+// Ожидает окончания работы ожидателя групп
+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("close(): end")
+}

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

@@ -0,0 +1,156 @@
+package kwg
+
+import (
+	"context"
+	"testing"
+	"time"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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")
+	res := sf.wg.Add("test_stream")
+	if res.IsOk() {
+		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")
+	res := sf.wg.Add("test_stream")
+	if res.IsErr() {
+		sf.t.Fatalf("addGood1(): err=%v", res.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)
+}

+ 124 - 0
v3/krn/kmodule/kmodule.go

@@ -0,0 +1,124 @@
+// package kmodule -- модуль на основе ядра
+package kmodule
+
+import (
+	"time"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/local_ctx"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/safe_int"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/safe_string"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kbus/kbus_local"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kmodule/mod_stat"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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.Ctx()),
+		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
+		res    Result[bool]
+	)
+	fnPhase := func() {
+		time.Sleep(time.Millisecond * time.Duration(sf.timePhase.Get()))
+		select {
+		case <-sf.kCtx.Ctx().Done():
+			return
+		default:
+			switch iPhase {
+			case 0:
+				sf.strLive.Set("|")
+				res = sf.bus.Publish(ATopic(topic), sf.strLive.Byte())
+			case 1:
+				sf.strLive.Set("/")
+				res = sf.bus.Publish(ATopic(topic), sf.strLive.Byte())
+			case 2:
+				sf.strLive.Set("-")
+				res = sf.bus.Publish(ATopic(topic), sf.strLive.Byte())
+			case 3:
+				sf.strLive.Set("\\")
+				res = sf.bus.Publish(ATopic(topic), sf.strLive.Byte())
+				iPhase = -1
+			}
+			res.Hassert("kModule.sigLive(): name=%v, in publish live", sf.Name())
+			iPhase++
+			sf.stat.Add(1)
+		}
+	}
+	for {
+		select {
+		case <-sf.kCtx.Ctx().Done():
+			return
+		default:
+			fnPhase()
+		}
+	}
+}

+ 107 - 0
v3/krn/kmodule/kmodule_test.go

@@ -0,0 +1,107 @@
+package kmodule
+
+import (
+	"os"
+	"testing"
+	"time"
+
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/v3/mock/mock_env"
+)
+
+type tester struct {
+	t   *testing.T
+	mod IKernelModule
+	me  *mock_env.MockEnv
+}
+
+func TestKernelModule(t *testing.T) {
+	sf := &tester{
+		t:  t,
+		me: mock_env.MakeEnv(),
+	}
+	_ = os.Unsetenv("LOCAL_STORE_PATH")
+	_ = os.Setenv("LOCAL_STORE_PATH", "/store/store_module")
+	_ = os.Unsetenv("LOCAL_HTTP_URL")
+	os.Setenv("LOCAL_HTTP_URL", "http://localhost:18331/")
+	fnClear := func() {
+		pwd := sf.me.Pwd() + "/store/store_module"
+		_ = os.RemoveAll(pwd)
+	}
+	fnClear()
+	defer fnClear()
+	sf.new()
+	sf.run()
+	sf.isWork()
+	sf.done()
+}
+
+// Работа после остановки локальной шины
+func (sf *tester) done() {
+	sf.t.Log("done")
+	kCtx := kctx.GetKernelCtx()
+
+	time.Sleep(time.Millisecond * 250)
+	kCtx.Cancel()
+	kCtx.Wg().Wait()
+	time.Sleep(time.Millisecond * 250)
+}
+
+// Проверить признак работы
+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")
+	}
+	_ = sf.mod.Stat()
+	_ = sf.mod.Live()
+}
+
+// Нет имени модуля
+func (sf *tester) newBad1() {
+	sf.t.Log("newBad1")
+	defer func() {
+		if _panic := recover(); _panic == nil {
+			sf.t.Fatalf("newBad1(): panic==nil")
+		}
+	}()
+	_ = NewKernelModule("")
+}

+ 80 - 0
v3/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/v3/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/safe_int"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kmodule/mod_stat/mod_stat_day"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kmodule/mod_stat/mod_stat_sec"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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
v3/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"
+)
+
+// ModStatDay -- статистика модуля за первые 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
v3/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
v3/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
v3/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
v3/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
v3/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
v3/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
v3/krn/kmonolit/kmonolit.go

@@ -0,0 +1,124 @@
+// package kmonolit -- модульный монолит на основе ядра
+package kmonolit
+
+import (
+	"fmt"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/local_ctx"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/safe_bool"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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.Ctx()),
+		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")
+}

+ 140 - 0
v3/krn/kmonolit/kmonolit_test.go

@@ -0,0 +1,140 @@
+package kmonolit
+
+import (
+	"os"
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/kc/helpers"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/kalias"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kmodule"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/v3/mock/mock_env"
+)
+
+const (
+	baseUrl   = "http://127.0.0.1:18430/"
+	pathStore = "/store/store_kmonolit"
+)
+
+type tester struct {
+	t   *testing.T
+	me  *mock_env.MockEnv
+	mon IKernelMonolit
+}
+
+func TestKernMono(t *testing.T) {
+	sf := &tester{
+		t:  t,
+		me: mock_env.MakeEnv(),
+	}
+	_ = os.Unsetenv("LOCAL_STORE_PATH")
+	_ = os.Setenv("LOCAL_STORE_PATH", pathStore)
+	_ = os.Unsetenv("LOCAL_HTTP_URL")
+	os.Setenv("LOCAL_HTTP_URL", baseUrl)
+	fnClear := func() {
+		_ = os.RemoveAll(pathStore)
+	}
+	fnClear()
+	defer fnClear()
+	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")
+}

+ 183 - 0
v3/krn/kserv_http/kserv_http.go

@@ -0,0 +1,183 @@
+// 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/v3/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/local_ctx"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/log_buf"
+	"gitp78su.ipnodns.ru/svi/kern/v3/kc/safe_bool"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/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
+	}
+	log := log_buf.NewLogBuf(log_buf.OptIsTerm(true), log_buf.OptPrefix("kServHttp"))
+	log.Debug("GetKernelServHttp(): first run")
+	ctx := kctx.GetKernelCtx()
+
+	strUrl := os.Getenv("LOCAL_HTTP_URL")
+	Hassert(strUrl != "", "GetKernelServHttp(): env LOCAL_HTTP_URL not set")
+	strMonolit := ctx.Get("monolitName").Val().(string)
+	confFiber := fiber.Config{
+		ServerHeader:      strMonolit,
+		UnescapePath:      true,
+		ReadTimeout:       time.Second * 15,
+		WriteTimeout:      time.Second * 15,
+		AppName:           strMonolit,
+		Network:           "tcp4",
+		EnablePrintRoutes: true,
+	}
+	sf := &kServHttp{
+		kCtx:     ctx,
+		log:      log,
+		ctx:      local_ctx.NewLocalCtx(ctx.Ctx()),
+		strUrl:   strUrl,
+		fiberApp: fiber.New(confFiber),
+		isWork:   safe_bool.NewSafeBool(),
+		isEnd:    safe_bool.NewSafeBool(),
+	}
+	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)}))
+	res := sf.kCtx.Wg().Add(streamName)
+	res.Hassert("GetKernelServHttp(): in add stream %v", streamName)
+	ctx.Set("fiberApp", sf.fiberApp, "GetKernelServHttp() 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
+	}
+	sf.log.Debug("Run(): url='%v'", sf.strUrl)
+	lstPort := strings.Split(sf.strUrl, ":")
+	strPort := lstPort[len(lstPort)-1]
+	strPort = strings.ReplaceAll(strPort, "/", "")
+	strPort = strings.ReplaceAll(strPort, `"`, "")
+	chErr := make(chan string, 2)
+	fnListen := func() {
+		defer close(chErr)
+		err := sf.fiberApp.Listen(":" + strPort)
+		chErr <- fmt.Sprint(err)
+	}
+	go fnListen()
+	go sf.fnChErr(chErr)
+	fnCheckServer := func() (err error) {
+		client := &http.Client{Timeout: 5 * time.Millisecond}
+		url := sf.strUrl + "monitor"
+		sf.log.Debug("url=%v", url)
+		var resp *http.Response
+		if resp, err = client.Get(url); err == nil {
+			if resp.StatusCode == http.StatusOK {
+				defer resp.Body.Close()
+			}
+		}
+		return err
+	}
+
+	for {
+		time.Sleep(time.Millisecond * 10)
+		err := fnCheckServer()
+		if err == nil {
+			break
+		}
+	}
+
+	sf.isWork.Set()
+	go sf.close()
+}
+
+// В отдельном потоке ждёт закрытия канала
+func (sf *kServHttp) fnChErr(chErr <-chan string) {
+	strErr := <-chErr
+	if strErr != "<nil>" {
+		err := fmt.Errorf("kServHttp.fnChErr(): in listen, err=\n\t%v", strErr)
+		sf.log.Err("Run(): err=\n\t%v", err.Error())
+		sf.kCtx.Cancel()
+	}
+}
+
+// Ожидает окончания работы
+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("close(): end")
+}

+ 122 - 0
v3/krn/kserv_http/kserv_http_test.go

@@ -0,0 +1,122 @@
+package kserv_http
+
+import (
+	"os"
+	"testing"
+
+	. "gitp78su.ipnodns.ru/svi/kern/v3/kc/helpers"
+	"gitp78su.ipnodns.ru/svi/kern/v3/krn/kctx"
+	. "gitp78su.ipnodns.ru/svi/kern/v3/krn/ktypes"
+	"gitp78su.ipnodns.ru/svi/kern/v3/mock/mock_env"
+)
+
+const (
+	baseUrl = "http://localhost:18400/"
+)
+
+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()
+	chErr := make(chan string, 2)
+	chErr <- "test err"
+	close(chErr)
+	kernServHttp.fnChErr(chErr)
+}
+
+// Создание сервера 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", baseUrl)
+	sf.ctx.Set("monolitName", "test_monolit", "comment")
+	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++
+	// }
+	serv.Run()
+}
+
+// Не указана 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()
+}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 4 - 0
v3/krn/kserv_http/static/css/bootstrap-grid.min.css


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
v3/krn/kserv_http/static/css/bootstrap-grid.min.css.map


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 4 - 0
v3/krn/kserv_http/static/css/bootstrap-grid.rtl.min.css


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
v3/krn/kserv_http/static/css/bootstrap-grid.rtl.min.css.map


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 4 - 0
v3/krn/kserv_http/static/css/bootstrap-reboot.min.css


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
v3/krn/kserv_http/static/css/bootstrap-reboot.min.css.map


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 4 - 0
v3/krn/kserv_http/static/css/bootstrap-reboot.rtl.min.css


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 0 - 0
v3/krn/kserv_http/static/css/bootstrap-reboot.rtl.min.css.map


La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 4 - 0
v3/krn/kserv_http/static/css/bootstrap-utilities.min.css


Algunos archivos no se mostraron porque demasiados archivos cambiaron en este cambio