Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter
- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
This commit is contained in:
138
tools/EVerest-main/lib/everest/framework/.clang-format
Normal file
138
tools/EVerest-main/lib/everest/framework/.clang-format
Normal file
@@ -0,0 +1,138 @@
|
||||
---
|
||||
Language: Cpp
|
||||
# BasedOnStyle: LLVM
|
||||
AccessModifierOffset: -4
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignConsecutiveMacros: true
|
||||
AlignConsecutiveAssignments: false
|
||||
AlignConsecutiveDeclarations: false
|
||||
AlignEscapedNewlines: Right
|
||||
AlignOperands: true
|
||||
AlignTrailingComments: true
|
||||
AllowAllArgumentsOnNextLine: true
|
||||
AllowAllConstructorInitializersOnNextLine: true
|
||||
AllowAllParametersOfDeclarationOnNextLine: true
|
||||
AllowShortBlocksOnASingleLine: Never
|
||||
AllowShortCaseLabelsOnASingleLine: false
|
||||
AllowShortEnumsOnASingleLine: false
|
||||
AllowShortFunctionsOnASingleLine: None
|
||||
AllowShortLambdasOnASingleLine: All
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
AllowShortLoopsOnASingleLine: false
|
||||
AlwaysBreakAfterDefinitionReturnType: None
|
||||
AlwaysBreakAfterReturnType: None
|
||||
AlwaysBreakBeforeMultilineStrings: false
|
||||
AlwaysBreakTemplateDeclarations: MultiLine
|
||||
BinPackArguments: true
|
||||
BinPackParameters: true
|
||||
BraceWrapping:
|
||||
AfterCaseLabel: false
|
||||
AfterClass: false
|
||||
AfterControlStatement: false
|
||||
AfterEnum: false
|
||||
AfterFunction: false
|
||||
AfterNamespace: false
|
||||
AfterObjCDeclaration: false
|
||||
AfterStruct: false
|
||||
AfterUnion: false
|
||||
AfterExternBlock: false
|
||||
BeforeCatch: false
|
||||
BeforeElse: false
|
||||
IndentBraces: false
|
||||
SplitEmptyFunction: true
|
||||
SplitEmptyRecord: true
|
||||
SplitEmptyNamespace: true
|
||||
BreakBeforeBinaryOperators: None
|
||||
BreakBeforeBraces: Attach
|
||||
BreakBeforeInheritanceComma: false
|
||||
BreakInheritanceList: BeforeColon
|
||||
BreakBeforeTernaryOperators: true
|
||||
BreakConstructorInitializersBeforeComma: false
|
||||
BreakConstructorInitializers: AfterColon
|
||||
BreakAfterJavaFieldAnnotations: false
|
||||
BreakStringLiterals: true
|
||||
ColumnLimit: 120
|
||||
CommentPragmas: '^ IWYU pragma:'
|
||||
CompactNamespaces: false
|
||||
ConstructorInitializerAllOnOneLineOrOnePerLine: true
|
||||
ConstructorInitializerIndentWidth: 4
|
||||
ContinuationIndentWidth: 4
|
||||
Cpp11BracedListStyle: true
|
||||
DeriveLineEnding: true
|
||||
DerivePointerAlignment: false
|
||||
DisableFormat: false
|
||||
ExperimentalAutoDetectBinPacking: false
|
||||
FixNamespaceComments: true
|
||||
ForEachMacros:
|
||||
- foreach
|
||||
- Q_FOREACH
|
||||
- BOOST_FOREACH
|
||||
IncludeBlocks: Preserve
|
||||
IncludeCategories:
|
||||
- Regex: '^"(llvm|llvm-c|clang|clang-c)/'
|
||||
Priority: 2
|
||||
SortPriority: 0
|
||||
- Regex: '^(<|"(gtest|gmock|isl|json)/)'
|
||||
Priority: 3
|
||||
SortPriority: 0
|
||||
- Regex: '.*'
|
||||
Priority: 1
|
||||
SortPriority: 0
|
||||
IncludeIsMainRegex: '(Test)?$'
|
||||
IncludeIsMainSourceRegex: ''
|
||||
IndentCaseLabels: false
|
||||
IndentGotoLabels: true
|
||||
IndentPPDirectives: None
|
||||
IndentWidth: 4
|
||||
IndentWrappedFunctionNames: false
|
||||
JavaScriptQuotes: Leave
|
||||
JavaScriptWrapImports: true
|
||||
KeepEmptyLinesAtTheStartOfBlocks: true
|
||||
MacroBlockBegin: ''
|
||||
MacroBlockEnd: ''
|
||||
MaxEmptyLinesToKeep: 1
|
||||
NamespaceIndentation: None
|
||||
ObjCBinPackProtocolList: Auto
|
||||
ObjCBlockIndentWidth: 2
|
||||
ObjCSpaceAfterProperty: false
|
||||
ObjCSpaceBeforeProtocolList: true
|
||||
PenaltyBreakAssignment: 2
|
||||
PenaltyBreakBeforeFirstCallParameter: 19
|
||||
PenaltyBreakComment: 300
|
||||
PenaltyBreakFirstLessLess: 120
|
||||
PenaltyBreakString: 1000
|
||||
PenaltyBreakTemplateDeclaration: 10
|
||||
PenaltyExcessCharacter: 1000000
|
||||
PenaltyReturnTypeOnItsOwnLine: 60
|
||||
PointerAlignment: Left
|
||||
ReflowComments: true
|
||||
SortIncludes: true
|
||||
SortUsingDeclarations: true
|
||||
SpaceAfterCStyleCast: false
|
||||
SpaceAfterLogicalNot: false
|
||||
SpaceAfterTemplateKeyword: true
|
||||
SpaceBeforeAssignmentOperators: true
|
||||
SpaceBeforeCpp11BracedList: false
|
||||
SpaceBeforeCtorInitializerColon: true
|
||||
SpaceBeforeInheritanceColon: true
|
||||
SpaceBeforeParens: ControlStatements
|
||||
SpaceBeforeRangeBasedForLoopColon: true
|
||||
SpaceInEmptyBlock: false
|
||||
SpaceInEmptyParentheses: false
|
||||
SpacesBeforeTrailingComments: 1
|
||||
SpacesInAngles: false
|
||||
SpacesInConditionalStatement: false
|
||||
SpacesInContainerLiterals: true
|
||||
SpacesInCStyleCastParentheses: false
|
||||
SpacesInParentheses: false
|
||||
SpacesInSquareBrackets: false
|
||||
SpaceBeforeSquareBrackets: false
|
||||
Standard: Latest
|
||||
StatementMacros:
|
||||
- Q_UNUSED
|
||||
- QT_REQUIRE_VERSION
|
||||
TabWidth: 8
|
||||
UseCRLF: false
|
||||
UseTab: Never
|
||||
...
|
||||
|
||||
36
tools/EVerest-main/lib/everest/framework/.clang-tidy
Normal file
36
tools/EVerest-main/lib/everest/framework/.clang-tidy
Normal file
@@ -0,0 +1,36 @@
|
||||
Checks: >
|
||||
bugprone-*,
|
||||
cert-*,
|
||||
concurrency-*,
|
||||
cppcoreguidelines-*,
|
||||
misc-*,
|
||||
performance-*,
|
||||
-abseil-*,
|
||||
-altera-*,
|
||||
-android-*,
|
||||
-boost-*,
|
||||
-fuchsia-*,
|
||||
-google-*,
|
||||
-llvm-*,
|
||||
-llvmlibc-*,
|
||||
-zircon-*,
|
||||
-bugprone-easily-swappable-parameters,
|
||||
-cert-err60-cpp,
|
||||
-cppcoreguidelines-non-private-member-variables-in-classes,
|
||||
-misc-include-cleaner,
|
||||
-misc-non-private-member-variables-in-classes,
|
||||
-modernize-use-trailing-return-type,
|
||||
-performance-avoid-endl,
|
||||
-performance-enum-size,
|
||||
-readability-function-cognitive-complexity,
|
||||
-readability-identifier-length,
|
||||
-concurrency-mt-unsafe,
|
||||
-cppcoreguidelines-avoid-const-or-ref-data-members,
|
||||
-cppcoreguidelines-avoid-do-while,
|
||||
-cppcoreguidelines-macro-usage,
|
||||
-cppcoreguidelines-special-member-functions,
|
||||
-cppcoreguidelines-pro-type-vararg,
|
||||
# (the last -cppcoreguidelines lines are temporary)
|
||||
HeaderFilterRegex: ".*"
|
||||
CheckOptions:
|
||||
- { key: performance-unnecessary-value-param.AllowedTypes, value: ((std::shared_ptr)) }
|
||||
43
tools/EVerest-main/lib/everest/framework/.eslintrc.json
Normal file
43
tools/EVerest-main/lib/everest/framework/.eslintrc.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"commonjs": true,
|
||||
"es2021": true
|
||||
},
|
||||
"extends": [
|
||||
"airbnb-base"
|
||||
],
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 12
|
||||
},
|
||||
"rules": {
|
||||
"camelcase": "off",
|
||||
"eqeqeq": [
|
||||
"error",
|
||||
"smart"
|
||||
],
|
||||
"comma-dangle": [
|
||||
"warn",
|
||||
{
|
||||
"objects": "always-multiline",
|
||||
"arrays": "always-multiline",
|
||||
"functions": "never"
|
||||
}
|
||||
],
|
||||
"import/no-unresolved": [
|
||||
2,
|
||||
{
|
||||
"ignore": [
|
||||
"everestjs"
|
||||
]
|
||||
}
|
||||
],
|
||||
"max-len": [
|
||||
"warn",
|
||||
{
|
||||
"code": 120,
|
||||
"tabWidth": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
18
tools/EVerest-main/lib/everest/framework/.gitignore
vendored
Normal file
18
tools/EVerest-main/lib/everest/framework/.gitignore
vendored
Normal file
@@ -0,0 +1,18 @@
|
||||
*build
|
||||
*build-cross
|
||||
/build*
|
||||
!build.rs
|
||||
!everestrs-build
|
||||
target
|
||||
bazel-bin
|
||||
bazel-out
|
||||
bazel-everest-framework
|
||||
bazel-testlogs
|
||||
watcher.lua
|
||||
workspace.yaml
|
||||
|
||||
# Clang
|
||||
.cache/
|
||||
|
||||
# Bazel
|
||||
/bazel-*
|
||||
@@ -0,0 +1,3 @@
|
||||
singleQuote: true
|
||||
tabWidth: 2
|
||||
quoteProps: consistent
|
||||
24
tools/EVerest-main/lib/everest/framework/.prospector.yaml
Normal file
24
tools/EVerest-main/lib/everest/framework/.prospector.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
pep257:
|
||||
disable:
|
||||
- D203
|
||||
- D212
|
||||
- D213
|
||||
- D214
|
||||
- D215
|
||||
- D404
|
||||
- D405
|
||||
- D406
|
||||
- D407
|
||||
- D408
|
||||
- D409
|
||||
- D410
|
||||
- D411
|
||||
- D413
|
||||
- D415
|
||||
- D416
|
||||
- D417
|
||||
pylint:
|
||||
options:
|
||||
extension-pkg-allow-list: everestpy
|
||||
disable:
|
||||
- logging-fstring-interpolation
|
||||
170
tools/EVerest-main/lib/everest/framework/BUILD.bazel
Normal file
170
tools/EVerest-main/lib/everest/framework/BUILD.bazel
Normal file
@@ -0,0 +1,170 @@
|
||||
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_import", "cc_library", "cc_shared_library")
|
||||
load("@rules_python//python:py_binary.bzl", "py_binary")
|
||||
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_PYTHON_INCOMPATIBLE")
|
||||
|
||||
py_binary(
|
||||
name = "collect_migration_files",
|
||||
srcs = [
|
||||
".ci/build-kit/scripts/collect_migration_files.py",
|
||||
],
|
||||
imports = ["."],
|
||||
main = ".ci/build-kit/scripts/collect_migration_files.py",
|
||||
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE,
|
||||
)
|
||||
|
||||
genrule(
|
||||
name = "compile_time_settings",
|
||||
srcs = ["//lib/everest/framework/schemas/migrations"],
|
||||
outs = ["include/everest/compile_time_settings.hpp"],
|
||||
cmd = """
|
||||
echo "#define EVEREST_INSTALL_PREFIX \\"/usr\\"" > $@
|
||||
echo "#define EVEREST_INSTALL_LIBDIR \\"/lib\\"" >> $@
|
||||
echo "#define EVEREST_NAMESPACE (\\"everest\\")" >> $@
|
||||
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_LATENCY 1" >> $@
|
||||
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_GREEDY 2" >> $@
|
||||
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CONSERVATIVE 3" >> $@
|
||||
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_FIXED_SIZE 4" >> $@
|
||||
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM 5" >> $@
|
||||
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_LATENCY" >> $@
|
||||
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_THRESHOLD_MS 50" >> $@
|
||||
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_TICK_MS 5" >> $@
|
||||
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_FIXED_SIZE_THRESHOLD 3" >> $@
|
||||
echo "#define EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE void" >> $@
|
||||
$(location :collect_migration_files) --migration-files $(locations //lib/everest/framework/schemas/migrations) --output $@
|
||||
""",
|
||||
tools = [":collect_migration_files"],
|
||||
)
|
||||
|
||||
genrule(
|
||||
name = "version_information",
|
||||
outs = ["include/generated/version_information.hpp"],
|
||||
cmd = """
|
||||
echo "#pragma once" > $@
|
||||
echo "#define PROJECT_NAME \\"everest-framework\\"" >> $@
|
||||
echo "#define PROJECT_DESCRIPTION \\"\\"" >> $@
|
||||
echo "#define PROJECT_VERSION \\"\\"" >> $@
|
||||
echo "#define GIT_VERSION \\"\\"" >> $@
|
||||
""",
|
||||
)
|
||||
|
||||
config_setting(
|
||||
name = "_dynamic_mode_fully",
|
||||
values = {"dynamic_mode": "fully"},
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "framework_lib",
|
||||
srcs = glob(["lib/**/*.cpp"]),
|
||||
hdrs = glob(["include/**/*.hpp"]) + [
|
||||
":compile_time_settings",
|
||||
":version_information",
|
||||
],
|
||||
cxxopts = ["-std=c++17"],
|
||||
# See https://github.com/HowardHinnant/date/issues/324
|
||||
local_defines = [
|
||||
"BUILD_TZ_LIB=ON",
|
||||
"USE_SYSTEM_TZ_DB=ON",
|
||||
"USE_OS_TZDB=1",
|
||||
"USE_AUTOLOAD=0",
|
||||
"HAS_REMOTE_API=0",
|
||||
],
|
||||
strip_include_prefix = "include",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//lib/everest/io:io",
|
||||
"//lib/everest/helpers",
|
||||
"//lib/everest/log:liblog",
|
||||
"//lib/everest/sqlite:everest-sqlite",
|
||||
"//lib/everest/util",
|
||||
"//lib/everest/yaml:everest_yaml",
|
||||
"@boost.program_options",
|
||||
"@boost.uuid",
|
||||
"@com_github_HowardHinnant_date//:date",
|
||||
"@com_github_fmtlib_fmt//:fmt",
|
||||
"@com_github_nlohmann_json//:json",
|
||||
"@com_github_pboettch_json-schema-validator//:json-schema-validator",
|
||||
],
|
||||
)
|
||||
|
||||
cc_shared_library(
|
||||
name = "framework_so",
|
||||
shared_lib_name = "libframework.so",
|
||||
deps = [":framework_lib"],
|
||||
exports_filter = [":framework_lib"],
|
||||
)
|
||||
|
||||
cc_import(
|
||||
name = "framework_dynamic",
|
||||
shared_library = ":framework_so",
|
||||
deps = [":framework_lib"],
|
||||
linkopts = ["-lframework"],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "framework",
|
||||
actual = select({
|
||||
":_dynamic_mode_fully": ":framework_dynamic",
|
||||
"//conditions:default": ":framework_lib",
|
||||
}),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "controller-ipc",
|
||||
srcs = ["src/controller/ipc.cpp"],
|
||||
hdrs = ["src/controller/ipc.hpp"],
|
||||
cxxopts = ["-std=c++17"],
|
||||
strip_include_prefix = "src",
|
||||
deps = [
|
||||
"@com_github_nlohmann_json//:json",
|
||||
],
|
||||
)
|
||||
|
||||
cc_binary(
|
||||
name = "controller",
|
||||
srcs = glob(
|
||||
[
|
||||
"src/controller/*.cpp",
|
||||
"src/controller/*.hpp",
|
||||
],
|
||||
exclude = [
|
||||
"src/controller/ipc.cpp",
|
||||
"src/controller/ipc.hpp",
|
||||
],
|
||||
),
|
||||
cxxopts = ["-std=c++17"],
|
||||
deps = [
|
||||
":controller-ipc",
|
||||
":framework",
|
||||
"//lib/everest/log:liblog",
|
||||
"@com_github_fmtlib_fmt//:fmt",
|
||||
"@com_github_warmcatt_libwebsockets//:libwebsockets",
|
||||
"@libcap",
|
||||
],
|
||||
)
|
||||
|
||||
cc_binary(
|
||||
name = "manager",
|
||||
srcs = glob(
|
||||
[
|
||||
"src/*.cpp",
|
||||
"src/*.hpp",
|
||||
],
|
||||
),
|
||||
cxxopts = ["-std=c++17"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":controller-ipc",
|
||||
":framework",
|
||||
"//lib/everest/log:liblog",
|
||||
"@boost.program_options",
|
||||
"@com_github_fmtlib_fmt//:fmt",
|
||||
"@com_github_pboettch_json-schema-validator//:json-schema-validator",
|
||||
"@libcap",
|
||||
],
|
||||
)
|
||||
|
||||
exports_files([
|
||||
"dependencies.yaml",
|
||||
])
|
||||
289
tools/EVerest-main/lib/everest/framework/CMakeLists.txt
Normal file
289
tools/EVerest-main/lib/everest/framework/CMakeLists.txt
Normal file
@@ -0,0 +1,289 @@
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
|
||||
project(everest-framework
|
||||
VERSION 0.25.0
|
||||
DESCRIPTION "The open operating system for e-mobility charging stations"
|
||||
LANGUAGES CXX C
|
||||
)
|
||||
|
||||
if(DEFINED EVEREST_IO_WITH_MQTT AND NOT EVEREST_IO_WITH_MQTT)
|
||||
message(FATAL_ERROR "everest-framework requires MQTT support in everest::io. "
|
||||
"Set EVEREST_IO_WITH_MQTT=ON or exclude framework from the build "
|
||||
"(e.g. via EVEREST_EXCLUDE_LIBS=framework).")
|
||||
endif()
|
||||
|
||||
find_package(everest-cmake 0.5 REQUIRED
|
||||
PATHS ../everest-cmake
|
||||
)
|
||||
|
||||
# options
|
||||
option(${PROJECT_NAME}_BUILD_TESTING "Build unit tests, used if included as dependency" OFF)
|
||||
option(BUILD_TESTING "Build unit tests, used if standalone project" OFF)
|
||||
option(FRAMEWORK_INSTALL "Install the library (shared data might be installed anyway)" ${EVC_MAIN_PROJECT})
|
||||
option(CMAKE_RUN_CLANG_TIDY "Run clang-tidy" OFF)
|
||||
option(EVEREST_ENABLE_JS_SUPPORT "Enable everestjs for JavaScript modules" OFF)
|
||||
option(EVEREST_ENABLE_PY_SUPPORT "Enable everestpy for Python modules" ON)
|
||||
option(EVEREST_ENABLE_RS_SUPPORT "Enable everestrs for Rust modules" OFF)
|
||||
option(EVEREST_ENABLE_ADMIN_PANEL_BACKEND "Enable everest admin panel backend" OFF)
|
||||
option(EVEREST_INSTALL_ADMIN_PANEL "Download and install everest admin panel" OFF)
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY "latency" CACHE STRING
|
||||
"Thread pool scaling policy for framework message handling: latency, greedy, conservative, fixed_size, custom")
|
||||
set_property(CACHE EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY PROPERTY STRINGS
|
||||
latency greedy conservative fixed_size custom)
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_THRESHOLD_MS "50" CACHE STRING
|
||||
"Maximum queued task wait time in milliseconds before latency scaling grows the framework message handler pool")
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_TICK_MS "5" CACHE STRING
|
||||
"Supervisor tick in milliseconds for framework message handler latency scaling")
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_FIXED_SIZE_THRESHOLD "3" CACHE STRING
|
||||
"Queue size threshold for framework message handler fixed-size scaling")
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER "" CACHE STRING
|
||||
"Header to include when EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY=custom")
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE "" CACHE STRING
|
||||
"Fully-qualified scaling policy type when EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY=custom")
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_INCLUDE_DIR "" CACHE PATH
|
||||
"Include directory for the custom thread pool scaling policy header")
|
||||
|
||||
foreach(_scaling_option IN ITEMS
|
||||
EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_THRESHOLD_MS
|
||||
EVEREST_FRAMEWORK_THREAD_POOL_SCALING_LATENCY_TICK_MS
|
||||
EVEREST_FRAMEWORK_THREAD_POOL_SCALING_FIXED_SIZE_THRESHOLD)
|
||||
if(NOT ${_scaling_option} MATCHES "^[1-9][0-9]*$")
|
||||
message(FATAL_ERROR
|
||||
"${_scaling_option} must be a positive integer, got '${${_scaling_option}}'")
|
||||
endif()
|
||||
endforeach()
|
||||
|
||||
if(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY STREQUAL "latency")
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_ID 1)
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER_CONFIG "everest/util/async/thread_pool_scaling.hpp")
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE_CONFIG "void")
|
||||
elseif(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY STREQUAL "greedy")
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_ID 2)
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER_CONFIG "everest/util/async/thread_pool_scaling.hpp")
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE_CONFIG "void")
|
||||
elseif(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY STREQUAL "conservative")
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_ID 3)
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER_CONFIG "everest/util/async/thread_pool_scaling.hpp")
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE_CONFIG "void")
|
||||
elseif(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY STREQUAL "fixed_size")
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_ID 4)
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER_CONFIG "everest/util/async/thread_pool_scaling.hpp")
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE_CONFIG "void")
|
||||
elseif(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY STREQUAL "custom")
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_ID 5)
|
||||
|
||||
if(NOT EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER)
|
||||
message(FATAL_ERROR
|
||||
"EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER must be set when "
|
||||
"EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY=custom")
|
||||
endif()
|
||||
if(NOT EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE)
|
||||
message(FATAL_ERROR
|
||||
"EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE must be set when "
|
||||
"EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY=custom")
|
||||
endif()
|
||||
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER_CONFIG
|
||||
"${EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_HEADER}")
|
||||
set(EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE_CONFIG
|
||||
"${EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY_CUSTOM_TYPE}")
|
||||
else()
|
||||
message(FATAL_ERROR
|
||||
"Invalid EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY: "
|
||||
"${EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY}. "
|
||||
"Allowed values: latency, greedy, conservative, fixed_size, custom")
|
||||
endif()
|
||||
ev_setup_cmake_variables_python_wheel()
|
||||
option(${PROJECT_NAME}_USE_PYTHON_VENV "Use python venv for pip install targets" OFF)
|
||||
set(${PROJECT_NAME}_PYTHON_VENV_PATH "${CMAKE_BINARY_DIR}/venv" CACHE PATH "Path to python venv")
|
||||
ev_setup_python_executable(
|
||||
USE_PYTHON_VENV ${${PROJECT_NAME}_USE_PYTHON_VENV}
|
||||
PYTHON_VENV_PATH ${${PROJECT_NAME}_PYTHON_VENV_PATH}
|
||||
)
|
||||
|
||||
if((${CMAKE_PROJECT_NAME} STREQUAL ${PROJECT_NAME} OR ${PROJECT_NAME}_BUILD_TESTING) AND BUILD_TESTING)
|
||||
set(EVEREST_FRAMEWORK_BUILD_TESTING ON)
|
||||
# this policy allows us to link gcov to targets defined in other directories
|
||||
if(POLICY CMP0079)
|
||||
set(CMAKE_POLICY_DEFAULT_CMP0079 NEW)
|
||||
endif()
|
||||
if (NOT CMAKE_BUILD_TYPE)
|
||||
if(EVEREST_ENABLE_DEBUG_BUILD)
|
||||
set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE)
|
||||
else()
|
||||
set(CMAKE_BUILD_TYPE MinSizeRel CACHE STRING "Build type" FORCE)
|
||||
endif()
|
||||
endif()
|
||||
endif()
|
||||
|
||||
# make own cmake modules available
|
||||
list(INSERT CMAKE_MODULE_PATH 0 "${CMAKE_CURRENT_SOURCE_DIR}/cmake")
|
||||
|
||||
# this policy allows us to continue using the removed FindBoost module for now
|
||||
if(POLICY CMP0167)
|
||||
cmake_policy(SET CMP0167 OLD)
|
||||
endif()
|
||||
|
||||
# dependencies
|
||||
find_package(Boost
|
||||
COMPONENTS
|
||||
program_options
|
||||
thread
|
||||
REQUIRED
|
||||
)
|
||||
|
||||
if(Boost_VERSION_STRING VERSION_LESS "1.69.0")
|
||||
find_package(Boost
|
||||
COMPONENTS
|
||||
system
|
||||
REQUIRED
|
||||
)
|
||||
endif()
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(libcap
|
||||
REQUIRED
|
||||
IMPORTED_TARGET
|
||||
libcap
|
||||
)
|
||||
|
||||
# Make sure stdc++fs is linked for GCC < 9
|
||||
if (CMAKE_COMPILER_IS_GNUCC AND CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9)
|
||||
set(STD_FILESYSTEM_COMPAT_LIB "stdc++fs")
|
||||
else()
|
||||
set(STD_FILESYSTEM_COMPAT_LIB "")
|
||||
endif()
|
||||
|
||||
if(NOT DISABLE_EDM)
|
||||
evc_setup_edm()
|
||||
|
||||
# In EDM mode, we can't install exports (because the dependencies usually do not install their exports)
|
||||
set(FRAMEWORK_INSTALL OFF)
|
||||
|
||||
if (EVEREST_ENABLE_ADMIN_PANEL_BACKEND)
|
||||
# FIXME (aw): libwebsockets/ninja clean doesn't delete recursivly ..
|
||||
set_property(
|
||||
TARGET websockets_shared
|
||||
APPEND
|
||||
PROPERTY ADDITIONAL_CLEAN_FILES "${libwebsockets_BINARY_DIR}/include/libwebsockets"
|
||||
)
|
||||
|
||||
# FIXME (aw): libwebsockets enum-int-mismatch FIX
|
||||
# see https://github.com/warmcat/libwebsockets/pull/2824
|
||||
if (CMAKE_COMPILER_IS_GNUCC AND CMAKE_C_COMPILER_VERSION VERSION_GREATER 13.0)
|
||||
target_compile_options(websockets_shared PRIVATE -Wno-error=enum-int-mismatch)
|
||||
endif()
|
||||
endif()
|
||||
set_property(TARGET nlohmann_json_schema_validator PROPERTY POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
# FIXME (aw): add catch2's cmake folder
|
||||
if (BUILD_TESTING)
|
||||
list(APPEND CMAKE_MODULE_PATH "${Catch2_SOURCE_DIR}/contrib")
|
||||
endif()
|
||||
else()
|
||||
find_package(date REQUIRED)
|
||||
find_package(nlohmann_json REQUIRED)
|
||||
find_package(nlohmann_json_schema_validator REQUIRED)
|
||||
find_package(fmt REQUIRED)
|
||||
|
||||
if (EVEREST_ENABLE_ADMIN_PANEL_BACKEND)
|
||||
find_package(libwebsockets REQUIRED)
|
||||
endif()
|
||||
|
||||
if (BUILD_TESTING)
|
||||
find_package(Catch2 REQUIRED)
|
||||
endif()
|
||||
|
||||
include(find-mqttc)
|
||||
endif()
|
||||
|
||||
include(${everest-sqlite_SOURCE_DIR}/cmake/CollectMigrationFiles.cmake)
|
||||
|
||||
set(EVEREST_FRAMEWORK_GENERATED_INC_DIR ${PROJECT_BINARY_DIR}/generated)
|
||||
configure_file(
|
||||
include/compile_time_settings.hpp.in
|
||||
${EVEREST_FRAMEWORK_GENERATED_INC_DIR}/everest/compile_time_settings.hpp
|
||||
)
|
||||
|
||||
# library code
|
||||
add_subdirectory(lib)
|
||||
|
||||
# executable code
|
||||
add_subdirectory(src)
|
||||
|
||||
# auxillary files
|
||||
add_subdirectory(schemas)
|
||||
|
||||
# everest javascript wrapper
|
||||
if (EVEREST_ENABLE_JS_SUPPORT)
|
||||
add_subdirectory(everestjs)
|
||||
endif()
|
||||
|
||||
# everest python wrapper
|
||||
if (EVEREST_ENABLE_PY_SUPPORT)
|
||||
set(PYTHON_MODULE_EXTENSION ".so")
|
||||
add_subdirectory(everestpy)
|
||||
endif()
|
||||
|
||||
if (EVEREST_ENABLE_RS_SUPPORT)
|
||||
add_subdirectory(everestrs)
|
||||
endif()
|
||||
|
||||
# FIXME (aw): should this be installed or not? Right now it is needed for the
|
||||
# current packaging approach
|
||||
install(TARGETS framework
|
||||
EXPORT framework-targets
|
||||
LIBRARY
|
||||
)
|
||||
|
||||
# packaging
|
||||
if (FRAMEWORK_INSTALL)
|
||||
install(
|
||||
DIRECTORY include/
|
||||
DESTINATION include/everest
|
||||
)
|
||||
|
||||
install(
|
||||
FILES ${EVEREST_FRAMEWORK_GENERATED_INC_DIR}/everest/compile_time_settings.hpp
|
||||
DESTINATION include/everest
|
||||
)
|
||||
|
||||
evc_setup_package(
|
||||
NAME everest-framework
|
||||
EXPORT framework-targets
|
||||
NAMESPACE everest
|
||||
ADDITIONAL_CONTENT
|
||||
"find_dependency(everest-helpers)"
|
||||
"find_dependency(everest-util)"
|
||||
"find_dependency(nlohmann_json)"
|
||||
"find_dependency(nlohmann_json_schema_validator)"
|
||||
"find_dependency(fmt)"
|
||||
"find_dependency(date)"
|
||||
"set(EVEREST_SCHEMA_DIR \"@PACKAGE_EVEREST_SCHEMA_DIR@\")"
|
||||
PATH_VARS
|
||||
EVEREST_SCHEMA_DIR "${CMAKE_INSTALL_DATADIR}/everest/schemas"
|
||||
)
|
||||
endif ()
|
||||
|
||||
# testing
|
||||
if(EVEREST_FRAMEWORK_BUILD_TESTING)
|
||||
include(CTest)
|
||||
add_subdirectory(tests)
|
||||
else()
|
||||
message(STATUS "Not running unit tests")
|
||||
endif()
|
||||
|
||||
# configure clang-tidy if requested
|
||||
if(CMAKE_RUN_CLANG_TIDY)
|
||||
message("Enabling clang-tidy")
|
||||
set(CMAKE_CXX_CLANG_TIDY clang-tidy)
|
||||
endif()
|
||||
|
||||
# build doxygen documentation if doxygen is available
|
||||
find_package(Doxygen)
|
||||
if(DOXYGEN_FOUND)
|
||||
set( DOXYGEN_OUTPUT_DIRECTORY dist/docs )
|
||||
doxygen_add_docs(doxygen-${PROJECT_NAME} everest.js include lib src)
|
||||
else()
|
||||
message("Doxygen is needed to generate documentation")
|
||||
endif()
|
||||
201
tools/EVerest-main/lib/everest/framework/LICENSE
Normal file
201
tools/EVerest-main/lib/everest/framework/LICENSE
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
13
tools/EVerest-main/lib/everest/framework/README.md
Normal file
13
tools/EVerest-main/lib/everest/framework/README.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# EVerest Framework
|
||||
|
||||
This subproject of EVerest is providing a mechanism to manage dependencies between different modules communicating with an wrapped MQTT protocol. On startup it parses a set of configuration file, checks them agains the manifests of different modules and launches each module needed.
|
||||
|
||||
Additional documentation can be found in [docs](docs).
|
||||
|
||||
The framework message handler thread pool scaling policy is selected at CMake
|
||||
configure time with `EVEREST_FRAMEWORK_THREAD_POOL_SCALING_POLICY`. Supported
|
||||
values are `latency` (default), `greedy`, `conservative`, `fixed_size` and
|
||||
`custom`. The latency and fixed-size policies have additional CMake options for
|
||||
their thresholds. See the main EVerest documentation under
|
||||
`docs/source/explanation/dev-tools/edm.rst` for full build examples and the
|
||||
custom policy interface.
|
||||
4
tools/EVerest-main/lib/everest/framework/THIRD_PARTY.md
Normal file
4
tools/EVerest-main/lib/everest/framework/THIRD_PARTY.md
Normal file
@@ -0,0 +1,4 @@
|
||||
# Third-party dependencies used by this project
|
||||
|
||||
- [CodeCoverage.cmake](https://github.com/bilke/cmake-modules/blob/master/CodeCoverage.cmake) licensed under [The 3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause)
|
||||
- [Macros for metaprogramming](https://github.com/jspahrsummers/libextobjc/blob/master/extobjc/metamacros.h) licensed under [The MIT License](https://opensource.org/licenses/MIT)
|
||||
307
tools/EVerest-main/lib/everest/framework/bazel/everest_env.bzl
Normal file
307
tools/EVerest-main/lib/everest/framework/bazel/everest_env.bzl
Normal file
@@ -0,0 +1,307 @@
|
||||
load("@rules_python//python:defs.bzl", "PyInfo")
|
||||
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_PYTHON_INCOMPATIBLE")
|
||||
|
||||
def _everest_env(ctx):
|
||||
"""Everest Root rule
|
||||
|
||||
Rule creates a everest root from provided modules and config file.
|
||||
"""
|
||||
|
||||
# Validate the input - we make sure that the set of provided modules matches
|
||||
# the set of modules declared in config.yaml
|
||||
validation_output = ctx.actions.declare_file(ctx.attr.name + ".validation")
|
||||
ctx.actions.run(
|
||||
inputs = ctx.attr.config_file[DefaultInfo].files,
|
||||
outputs = [validation_output],
|
||||
executable = ctx.executable._validation_tool,
|
||||
arguments = [
|
||||
"--output",
|
||||
validation_output.path,
|
||||
"--config",
|
||||
ctx.attr.config_file[DefaultInfo].files.to_list()[0].path,
|
||||
"--",
|
||||
] +
|
||||
[mod.label.name for mod in ctx.attr.modules],
|
||||
)
|
||||
|
||||
symlinks = {}
|
||||
files = []
|
||||
py_toolchain = ctx.toolchains["@bazel_tools//tools/python:toolchain_type"]
|
||||
py_interpreter = py_toolchain.py3_runtime.interpreter.dirname.removeprefix("external/")
|
||||
py_imports = []
|
||||
py_transitive_sources = []
|
||||
|
||||
# Python modules get a special handling.
|
||||
for mod in ctx.attr.modules + ctx.attr.test_modules:
|
||||
if PyInfo in mod:
|
||||
py_imports.extend(mod[PyInfo].imports.to_list())
|
||||
py_transitive_sources.extend(mod[PyInfo].transitive_sources.to_list())
|
||||
|
||||
for mod in ctx.attr.modules + ctx.attr.test_modules:
|
||||
# Find the manifest in the data_runfiles and use its path as prefix.
|
||||
manifest = [
|
||||
file
|
||||
for file in mod[DefaultInfo].data_runfiles.files.to_list()
|
||||
if file.basename in ["manifest.yaml", "manifest.yml"]
|
||||
][0]
|
||||
prefix = manifest.dirname
|
||||
|
||||
symlinks.update(
|
||||
{
|
||||
"libexec/everest/modules/{0}{1}".format(
|
||||
mod.label.name,
|
||||
file.path.removeprefix(prefix),
|
||||
): file
|
||||
for file in mod[DefaultInfo].data_runfiles.files.to_list()
|
||||
if file.path.startswith(prefix)
|
||||
},
|
||||
)
|
||||
[
|
||||
files.append(file)
|
||||
for file in mod[DefaultInfo].default_runfiles.files.to_list()
|
||||
if not file.path.startswith(prefix)
|
||||
]
|
||||
|
||||
config_file = ctx.attr.config_file[DefaultInfo].files.to_list()[0]
|
||||
config_path = "etc/everest/{0}".format(config_file.basename)
|
||||
symlinks.update({"bin/manager_impl": ctx.attr.manager[DefaultInfo].files.to_list()[0]})
|
||||
symlinks.update(
|
||||
{
|
||||
config_path: config_file,
|
||||
},
|
||||
)
|
||||
symlinks.update(
|
||||
{
|
||||
"etc/everest/default_logging.cfg": ctx.attr.default_logging_file[DefaultInfo].files.to_list()[0],
|
||||
},
|
||||
)
|
||||
|
||||
# EVerest expects that there is a `share/everest/www` directory but does
|
||||
# not care about the content... We just symlink the config.yaml into it.
|
||||
symlinks.update(
|
||||
{
|
||||
"share/everest/www/config.yaml": ctx.attr.config_file[DefaultInfo].files.to_list()[0],
|
||||
},
|
||||
)
|
||||
symlinks.update(
|
||||
{
|
||||
"share/everest/schemas/{0}".format(file.basename): file
|
||||
for file in ctx.attr.schemas[DefaultInfo].files.to_list()
|
||||
},
|
||||
)
|
||||
symlinks.update(
|
||||
{
|
||||
"share/everest/interfaces/{0}".format(file.basename): file
|
||||
for interfaces in ctx.attr.interfaces
|
||||
for file in interfaces[DefaultInfo].files.to_list()
|
||||
},
|
||||
)
|
||||
symlinks.update(
|
||||
{
|
||||
"share/everest/types/{0}".format(file.basename): file
|
||||
for types in ctx.attr.types
|
||||
for file in types[DefaultInfo].files.to_list()
|
||||
},
|
||||
)
|
||||
symlinks.update(
|
||||
{
|
||||
"share/everest/errors/{0}".format(file.basename): file
|
||||
for errors in ctx.attr.errors
|
||||
for file in errors[DefaultInfo].files.to_list()
|
||||
},
|
||||
)
|
||||
|
||||
# For the executable we need to export the python specific variables by
|
||||
# hand.
|
||||
script = ctx.actions.declare_file("manager_wrapper.{}".format(ctx.label.name))
|
||||
script_content = """\
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
SCRIPT_DIR=$(cd "$(dirname "$0")/.." && pwd)
|
||||
export PATH="$SCRIPT_DIR/{py_interpreter}:$PATH"
|
||||
{pythonpath_lines}
|
||||
""".format(
|
||||
py_interpreter = py_interpreter.removeprefix("external/"),
|
||||
pythonpath_lines = "\n".join([
|
||||
'export PYTHONPATH="$SCRIPT_DIR/{0}:$PYTHONPATH"'.format(imp)
|
||||
for imp in py_imports
|
||||
]),
|
||||
)
|
||||
|
||||
if ctx.attr._is_test:
|
||||
script_content += """
|
||||
exec bin/manager_impl --prefix . --config {0} --check
|
||||
""".format(config_path)
|
||||
else:
|
||||
script_content += """
|
||||
if [ $# -gt 0 ]; then
|
||||
exec bin/manager_impl "$@"
|
||||
else
|
||||
exec bin/manager_impl --prefix . --config {0}
|
||||
fi
|
||||
""".format(config_path)
|
||||
ctx.actions.write(script, script_content, is_executable = True)
|
||||
symlinks.update({"bin/manager": script})
|
||||
|
||||
runfiles = ctx.runfiles(
|
||||
symlinks = symlinks,
|
||||
files = files + [script],
|
||||
)
|
||||
|
||||
return [
|
||||
DefaultInfo(
|
||||
executable = script,
|
||||
runfiles = runfiles,
|
||||
),
|
||||
OutputGroupInfo(_validation = depset([validation_output])),
|
||||
PyInfo(
|
||||
imports = depset(py_imports),
|
||||
transitive_sources = depset(py_transitive_sources),
|
||||
),
|
||||
]
|
||||
|
||||
ATTRS = {
|
||||
"config_file": attr.label(
|
||||
doc = """
|
||||
The EVerest configuration file. It will be linked to
|
||||
`/etc/everest/<basename>`""",
|
||||
allow_single_file = True,
|
||||
),
|
||||
"manager": attr.label(
|
||||
doc = "The EVerest manager.",
|
||||
default = Label("//lib/everest/framework:manager"),
|
||||
allow_single_file = True,
|
||||
executable = True,
|
||||
cfg = "target",
|
||||
),
|
||||
"schemas": attr.label(
|
||||
doc = "The target with the EVerest schemas.",
|
||||
default = Label("//lib/everest/framework/schemas"),
|
||||
),
|
||||
"interfaces": attr.label_list(
|
||||
doc = "A list of targets with EVerest interfaces.",
|
||||
default = [
|
||||
Label("//lib/everest/framework/everestrs/tests/interfaces"),
|
||||
],
|
||||
),
|
||||
"types": attr.label_list(
|
||||
doc = "A list of targets with EVerest types.",
|
||||
default = [
|
||||
Label("//lib/everest/framework/everestrs/tests/types"),
|
||||
],
|
||||
),
|
||||
"errors": attr.label_list(
|
||||
doc = "A list of targets with EVerest errors.",
|
||||
default = [
|
||||
Label("//lib/everest/framework/everestrs/tests/errors"),
|
||||
],
|
||||
),
|
||||
"default_logging_file": attr.label(
|
||||
doc = "The target with the EVerest logging.ini file.",
|
||||
default = Label("//lib/everest/framework/everestrs/tests:logging.ini"),
|
||||
allow_single_file = True,
|
||||
),
|
||||
"modules": attr.label_list(
|
||||
doc = """
|
||||
The list of targets with the EVerest modules under test.
|
||||
|
||||
The rule validates that the set of provided modules matches the set of modules
|
||||
defined in the given `config_file`.""",
|
||||
allow_files = False,
|
||||
),
|
||||
"test_modules": attr.label_list(
|
||||
doc = """
|
||||
The list of targets with EVerest modules which are only enabled by the
|
||||
`everest.testing` framework.
|
||||
|
||||
The rule will not enforce that these modules are defined in the given
|
||||
`config_file`.
|
||||
""",
|
||||
allow_files = False,
|
||||
),
|
||||
"_validation_tool": attr.label(
|
||||
default = Label("//lib/everest/framework/bazel/validate"),
|
||||
executable = True,
|
||||
cfg = "exec",
|
||||
),
|
||||
"_is_test": attr.bool(
|
||||
default = False,
|
||||
doc = "Indicates if target is test target to validate config",
|
||||
),
|
||||
}
|
||||
|
||||
everest_impl_env = rule(
|
||||
implementation = _everest_env,
|
||||
attrs = ATTRS,
|
||||
executable = True,
|
||||
toolchains = ["@bazel_tools//tools/python:toolchain_type"],
|
||||
)
|
||||
|
||||
_everest_impl_test = rule(
|
||||
implementation = _everest_env,
|
||||
attrs = dict(ATTRS, _is_test = attr.bool(default = True)),
|
||||
toolchains = ["@bazel_tools//tools/python:toolchain_type"],
|
||||
doc = """
|
||||
Creates an EVerest Test.
|
||||
|
||||
Example:
|
||||
|
||||
Suppose you have the EVerest modules `ModuleFoo` and `ModuleBar` and the
|
||||
EVerest config `my_config.yaml` which uses both modules. The test will launch
|
||||
the modules and return when the manager process returns.
|
||||
|
||||
Then you can create an environment by writing:
|
||||
|
||||
```
|
||||
everest_test(
|
||||
name = "my_everest_env",
|
||||
modules = [":ModuleFoo", ":ModuleBar"],
|
||||
config_file = ":my_config.yaml",
|
||||
test_script=":my_test_script",
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
You can run it with `bazel test`.
|
||||
""",
|
||||
test = True,
|
||||
)
|
||||
|
||||
def everest_test(name, target_compatible_with = [], **kwargs):
|
||||
"""Wrapper around the everest_test rule that marks the target as
|
||||
incompatible with cross-compilation platforms (no Python toolchain)."""
|
||||
_everest_impl_test(
|
||||
name = name,
|
||||
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE + target_compatible_with,
|
||||
**kwargs
|
||||
)
|
||||
|
||||
def everest_env(name, target_compatible_with = [], **kwargs):
|
||||
"""
|
||||
Creates an EVerest environment.
|
||||
|
||||
Example:
|
||||
|
||||
Suppose you have the EVerest modules `ModuleFoo` and `ModuleBar` and the
|
||||
EVerest config `my_config.yaml` which uses both modules.
|
||||
|
||||
Then you can create an environment by writing:
|
||||
|
||||
```
|
||||
everest_env(
|
||||
name = "my_everest_env",
|
||||
modules = [":ModuleFoo", ":ModuleBar"],
|
||||
config_file = ":my_config.yaml",
|
||||
)
|
||||
|
||||
```
|
||||
|
||||
You can either run this target with `bazel run` or pass it for example to a (py)
|
||||
test which will run your tests against the environment.
|
||||
"""
|
||||
everest_impl_env(
|
||||
name = name,
|
||||
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE + target_compatible_with,
|
||||
**kwargs
|
||||
)
|
||||
everest_test(name = name + "__manager_test", tags = ["exclusive"], target_compatible_with = target_compatible_with, **kwargs)
|
||||
@@ -0,0 +1,22 @@
|
||||
def rs_everest_module(
|
||||
name,
|
||||
manifest,
|
||||
binary):
|
||||
native.genrule(
|
||||
name = "copy_to_subdir",
|
||||
srcs = [binary, manifest],
|
||||
outs = [
|
||||
"{}/manifest.yaml".format(name),
|
||||
"{}/{}".format(name, name),
|
||||
],
|
||||
cmd = "mkdir -p $(RULEDIR)/{} && ".format(name) +
|
||||
"cp $(location {}) $(RULEDIR)/{}/{} && ".format(binary, name, name) +
|
||||
"cp $(location {}) $(RULEDIR)/{}/".format(manifest, name),
|
||||
)
|
||||
native.filegroup(
|
||||
name = name,
|
||||
srcs = [
|
||||
":copy_to_subdir",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
load("@everest_framework_validate_crate_index//:defs.bzl", "all_crate_deps")
|
||||
load("@rules_rust//rust:defs.bzl", "rust_binary")
|
||||
|
||||
|
||||
rust_binary(
|
||||
name = "validate",
|
||||
srcs = glob(["src/**/*.rs"]),
|
||||
deps = all_crate_deps(),
|
||||
visibility = ["//visibility:public"],
|
||||
edition = "2021",
|
||||
)
|
||||
305
tools/EVerest-main/lib/everest/framework/bazel/validate/Cargo.lock
generated
Normal file
305
tools/EVerest-main/lib/everest/framework/bazel/validate/Cargo.lock
generated
Normal file
@@ -0,0 +1,305 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
|
||||
|
||||
[[package]]
|
||||
name = "everest_validate"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"serde",
|
||||
"serde_yaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.36"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.210"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.60"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
|
||||
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "everest_validate"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.4", features = ["derive"] }
|
||||
serde = { version = "1.0.200", features = ["derive"] }
|
||||
serde_yaml = "0.9.34"
|
||||
@@ -0,0 +1,64 @@
|
||||
use clap::Parser;
|
||||
|
||||
/// Validates the EVerest config.
|
||||
#[derive(Parser, Debug)]
|
||||
struct Args {
|
||||
/// The output file to touch. Bazel will look for this one.
|
||||
#[arg(long)]
|
||||
output: String,
|
||||
|
||||
/// The input file containing the config. We will parse this file.
|
||||
#[arg(long)]
|
||||
config: String,
|
||||
|
||||
/// The list of expected modules.
|
||||
#[arg(required = true)]
|
||||
modules: Vec<String>,
|
||||
}
|
||||
|
||||
/// The relevant sub-portion of EVerest's config.
|
||||
mod config {
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct Module {
|
||||
pub module: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
pub struct EverestConfig {
|
||||
pub active_modules: std::collections::HashMap<String, Module>,
|
||||
}
|
||||
}
|
||||
|
||||
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let args = Args::parse();
|
||||
let config = std::fs::read_to_string(args.config)?;
|
||||
let config: config::EverestConfig = serde_yaml::from_str(&config)?;
|
||||
|
||||
let config_modules: std::collections::HashSet<_> = config
|
||||
.active_modules
|
||||
.into_values()
|
||||
.filter_map(|m| {
|
||||
if &m.module == "ProbeModule" {
|
||||
None
|
||||
} else {
|
||||
Some(m.module)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let given_modules: std::collections::HashSet<_> = args.modules.into_iter().collect();
|
||||
assert!(
|
||||
given_modules == config_modules,
|
||||
"given_modules != config_modules.\ngiven_modules: {:?}\nconfig_modules: {:?}",
|
||||
given_modules,
|
||||
config_modules
|
||||
);
|
||||
|
||||
std::fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.write(true)
|
||||
.open(args.output)?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
# check node API version based on https://nodejs.org/api/n-api.html#node-api-version-matrix
|
||||
|
||||
function(get_node_api_version NODE_VERSION_IN OUTPUT_NODE_API_VERSION)
|
||||
string(REPLACE "v" "" NODE_VERSION "${NODE_VERSION_IN}")
|
||||
set(${OUTPUT_NODE_API_VERSION} "" PARENT_SCOPE)
|
||||
if( ("${NODE_VERSION}" VERSION_GREATER_EQUAL "16")
|
||||
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "15.12")
|
||||
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "12.22"))
|
||||
set(${OUTPUT_NODE_API_VERSION} 8 PARENT_SCOPE)
|
||||
elseif( ("${NODE_VERSION}" VERSION_GREATER_EQUAL "15")
|
||||
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "14.12")
|
||||
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "12.19")
|
||||
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "10.23"))
|
||||
set(${OUTPUT_NODE_API_VERSION} 7 PARENT_SCOPE)
|
||||
elseif( ("${NODE_VERSION}" VERSION_GREATER_EQUAL "14")
|
||||
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "12.17")
|
||||
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "10.20"))
|
||||
set(${OUTPUT_NODE_API_VERSION} 6 PARENT_SCOPE)
|
||||
elseif( ("${NODE_VERSION}" VERSION_GREATER_EQUAL "13")
|
||||
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "12.11")
|
||||
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "10.17"))
|
||||
set(${OUTPUT_NODE_API_VERSION} 5 PARENT_SCOPE)
|
||||
elseif( ("${NODE_VERSION}" VERSION_GREATER_EQUAL "12")
|
||||
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "11.8")
|
||||
OR ("${NODE_VERSION}" VERSION_GREATER_EQUAL "10.16"))
|
||||
set(${OUTPUT_NODE_API_VERSION} 4 PARENT_SCOPE)
|
||||
elseif(("${NODE_VERSION}" VERSION_GREATER_EQUAL "10"))
|
||||
set(${OUTPUT_NODE_API_VERSION} 3 PARENT_SCOPE)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
function(require_node_api_version NODE_VERSION NODE_API_VERSION_REQUIRED)
|
||||
get_node_api_version("${NODE_VERSION}" NODE_API_VERSION)
|
||||
if ("${NODE_API_VERSION}" STREQUAL "")
|
||||
message(FATAL_ERROR "Could not determine a Node-API version from the provided nodejs version '${NODE_VERSION}'")
|
||||
endif()
|
||||
if("${NODE_API_VERSION}" LESS "${NODE_API_VERSION_REQUIRED}")
|
||||
message(FATAL_ERROR "Node-API version ${NODE_API_VERSION_REQUIRED} or higher is required. However your nodejs version '${NODE_VERSION}' can only provide Node-API version '${NODE_API_VERSION}'")
|
||||
else()
|
||||
message(STATUS "Found nodejs version '${NODE_VERSION}' that can provide Node-API version '${NODE_API_VERSION}' which satifies the requirement of Node-API version '${NODE_API_VERSION_REQUIRED}'")
|
||||
endif()
|
||||
endfunction()
|
||||
@@ -0,0 +1,14 @@
|
||||
# FIXME (aw): quite hacky, should check at least if target already exists
|
||||
add_library(mqttc STATIC IMPORTED)
|
||||
|
||||
find_library(MQTTC_LIB_FILE mqttc)
|
||||
find_path(MQTTC_INCLUDE_DIR mqtt.h)
|
||||
|
||||
if(NOT MQTTC_LIB_FILE OR NOT MQTTC_INCLUDE_DIR)
|
||||
message(FATAL_ERROR "Could not find mqttc library")
|
||||
endif()
|
||||
|
||||
set_target_properties(mqttc PROPERTIES
|
||||
IMPORTED_LOCATION ${MQTTC_LIB_FILE}
|
||||
INTERFACE_INCLUDE_DIRECTORIES ${MQTTC_INCLUDE_DIR}
|
||||
)
|
||||
17
tools/EVerest-main/lib/everest/framework/config.cmake.in
Normal file
17
tools/EVerest-main/lib/everest/framework/config.cmake.in
Normal file
@@ -0,0 +1,17 @@
|
||||
@PACKAGE_INIT@
|
||||
|
||||
include(CMakeFindDependencyMacro)
|
||||
|
||||
find_dependency(everest-tls)
|
||||
find_dependency(everest-helpers)
|
||||
find_dependency(everest-util)
|
||||
find_dependency(nlohmann_json)
|
||||
find_dependency(nlohmann_json_schema_validator)
|
||||
find_dependency(fmt)
|
||||
find_dependency(date)
|
||||
|
||||
set(EVEREST_SCHEMA_DIR "@PACKAGE_EVEREST_SCHEMA_DIR@")
|
||||
|
||||
include(${CMAKE_CURRENT_LIST_DIR}/everest-framework-targets.cmake)
|
||||
|
||||
check_required_components(everest-framework)
|
||||
81
tools/EVerest-main/lib/everest/framework/dependencies.yaml
Normal file
81
tools/EVerest-main/lib/everest/framework/dependencies.yaml
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
libwebsockets:
|
||||
git: https://github.com/warmcat/libwebsockets.git
|
||||
git_tag: v4.5.2
|
||||
cmake_condition: "EVEREST_ENABLE_ADMIN_PANEL_BACKEND"
|
||||
options:
|
||||
- CMAKE_POLICY_VERSION_MINIMUM 3.5
|
||||
- CMAKE_POLICY_DEFAULT_CMP0077 NEW
|
||||
- LWS_ROLE_RAW_FILE OFF
|
||||
- LWS_UNIX_SOCK OFF
|
||||
- LWS_IPV6 ON
|
||||
- LWS_WITH_SYS_STATE OFF
|
||||
- LWS_WITH_SYS_SMD OFF
|
||||
- LWS_WITH_UPNG OFF
|
||||
- LWS_WITH_JPEG OFF
|
||||
- LWS_WITH_DLO OFF
|
||||
- LWS_WITH_SECURE_STREAMS OFF
|
||||
- LWS_WITH_STATIC OFF
|
||||
- LWS_WITH_LHP OFF
|
||||
- LWS_WITH_LEJP_CONF OFF
|
||||
- LWS_WITH_MINIMAL_EXAMPLES OFF
|
||||
- LWS_WITH_CACHE_NSCOOKIEJAR OFF
|
||||
- LWS_WITHOUT_TESTAPPS ON
|
||||
- LWS_WITHOUT_TEST_SERVER ON
|
||||
- LWS_WITHOUT_TEST_SERVER_EXTPOLL ON
|
||||
- LWS_WITHOUT_TEST_PING ON
|
||||
- LWS_WITHOUT_TEST_CLIENT ON
|
||||
- LWS_INSTALL_LIB_DIR ${CMAKE_INSTALL_LIBDIR}
|
||||
- DISABLE_WERROR ON
|
||||
nlohmann_json:
|
||||
git: https://github.com/nlohmann/json
|
||||
git_tag: v3.12.0
|
||||
options: ["JSON_BuildTests OFF", "JSON_MultipleHeaders ON"]
|
||||
nlohmann_json_schema_validator:
|
||||
git: https://github.com/pboettch/json-schema-validator
|
||||
git_tag: 2.4.0
|
||||
options:
|
||||
[
|
||||
"JSON_VALIDATOR_INSTALL OFF",
|
||||
"JSON_VALIDATOR_BUILD_TESTS OFF",
|
||||
"JSON_VALIDATOR_BUILD_EXAMPLES OFF",
|
||||
"JSON_VALIDATOR_BUILD_SHARED_LIBS ON",
|
||||
]
|
||||
mosquitto:
|
||||
git: https://github.com/eclipse-mosquitto/mosquitto
|
||||
git_tag: v2.0.22
|
||||
options:
|
||||
- DOCUMENTATION OFF
|
||||
- WITH_BROKER OFF
|
||||
- WITH_APPS OFF
|
||||
- WITH_PLUGINS OFF
|
||||
- WITH_TESTS OFF
|
||||
libfmt:
|
||||
git: https://github.com/fmtlib/fmt.git
|
||||
git_tag: 12.1.0
|
||||
options:
|
||||
["FMT_TEST OFF", "FMT_DOC OFF", "BUILD_SHARED_LIBS ON", "FMT_INSTALL ON", "FMT_SYSTEM_HEADERS ON"]
|
||||
date:
|
||||
git: https://github.com/HowardHinnant/date.git
|
||||
git_tag: v3.0.4
|
||||
options:
|
||||
[
|
||||
"BUILD_TZ_LIB ON",
|
||||
"HAS_REMOTE_API 0",
|
||||
"USE_AUTOLOAD 0",
|
||||
"USE_SYSTEM_TZ_DB ON",
|
||||
"BUILD_SHARED_LIBS ON",
|
||||
]
|
||||
catch2:
|
||||
git: https://github.com/catchorg/Catch2.git
|
||||
git_tag: v3.9.0
|
||||
cmake_condition: "EVEREST_FRAMEWORK_BUILD_TESTING OR EVEREST_CORE_BUILD_TESTING"
|
||||
pybind11:
|
||||
git: https://github.com/pybind/pybind11.git
|
||||
git_tag: v3.0.2
|
||||
cmake_condition: "EVEREST_ENABLE_PY_SUPPORT"
|
||||
options: ["PYBIND11_TEST OFF", "PYBIND11_USE_CROSSCOMPILING ON"]
|
||||
pybind11_json:
|
||||
git: https://github.com/pybind/pybind11_json.git
|
||||
git_tag: 0.2.15
|
||||
cmake_condition: "EVEREST_ENABLE_PY_SUPPORT"
|
||||
@@ -0,0 +1,33 @@
|
||||
# EVerest Boot Mode Application Logic
|
||||
|
||||
The `manager` process retrieves the EVerest configuration, manages module dependencies and communication, and starts EVerest modules as individual processes.
|
||||
|
||||
The EVerest configuration can be provided via a YAML file, an SQLite database, or both. This section explains how EVerest starts up depending on the CLI options used:
|
||||
|
||||
- `--config`: Full path to the EVerest YAML configuration file.
|
||||
- `--db`: Full path to the EVerest SQLite configuration database.
|
||||
- `--db-init`: Indicates that the specified config should be used to initialize the database if it does not exist or does not contain a valid configuration.
|
||||
|
||||
Based on these options, there are three possible boot modes:
|
||||
|
||||
- **YAML Boot Mode**: The configuration is loaded from a YAML file. This mode is used when only the `--config` argument is provided.
|
||||
- **Database Boot Mode**: The configuration is loaded from an SQLite database. This mode is used when only the `--db` argument is provided.
|
||||
- **DatabaseInit Boot Mode**: The configuration is preferably loaded from an SQLite database. If the database does not exist or does not contain a valid EVerest configuration, the YAML file specified by `--config` is used instead, and the configuration is then written to the database. This mode requires all three options: `--config`, `--db`, and `--db-init`. In a subsequent start of the application, the database can be used to retrieve
|
||||
the configuration.
|
||||
|
||||
## Boot Mode Application Logic Flow Chart
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Start] --> B[Parse CLI arguments]
|
||||
|
||||
B --> C{--config provided}
|
||||
C -->|yes| D{--db provided}
|
||||
D -->|yes| E{--db-init provided}
|
||||
E -->|yes| F[Boot Mode: DatabaseInit]
|
||||
E -->|no| H[Invalid combination]
|
||||
D -->|no| J[Boot Mode: YAML]
|
||||
C -->|no| L{--db provided}
|
||||
L -->|yes| M[Boot Mode: Database]
|
||||
L -->|no| O[Invalid combination]
|
||||
```
|
||||
@@ -0,0 +1,134 @@
|
||||
# EVerest Framework MQTT Topic Structure
|
||||
|
||||
This document describes the MQTT topic structure used by the EVerest framework for communication between modules.
|
||||
|
||||
## Topic Prefix Structure
|
||||
|
||||
The EVerest framework uses a configurable MQTT prefixes for topics. This allows multiple instances of EVerest
|
||||
to run at the same time using the same broker. The default prefix is `everest`.
|
||||
|
||||
## 1. Variables (Vars)
|
||||
|
||||
The following structure applies for variable topics:
|
||||
|
||||
### Topic Structure
|
||||
|
||||
```bash
|
||||
{everest_prefix}modules/{module_id}/impl/{impl_id}/var/{var_name}
|
||||
```
|
||||
|
||||
### Message Payload Structure
|
||||
|
||||
The payload contains the actual variable data in the `data` field.
|
||||
|
||||
```json
|
||||
{
|
||||
"data": <variable_value>
|
||||
}
|
||||
```
|
||||
|
||||
## 2. Commands (Cmds)
|
||||
|
||||
The following structure applies for command topics. Modules that provide (implement) the command subscribe to the command topic, while modules that call the command publish to it. Each command call generates a unique UUID as the call ID. The origin field identifies the calling module. Command handlers process the request and publish responses to a separate response topic.
|
||||
|
||||
### Topic Structure
|
||||
|
||||
```bash
|
||||
{everest_prefix}modules/{module_id}/impl/{impl_id}/cmd/{cmd_name}
|
||||
```
|
||||
|
||||
### Message Payload Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "<unique_call_id>",
|
||||
"args": {
|
||||
"arg1": "value1",
|
||||
"arg2": "value2"
|
||||
},
|
||||
"origin": "<calling_module_id>"
|
||||
}
|
||||
```
|
||||
|
||||
## 3. Command Responses
|
||||
|
||||
The following structure applies for command response topics. These are used to send back results or errors from command handlers to the calling module. Response topics include the calling module ID for proper routing. The call ID matches the original command request.
|
||||
|
||||
### Topic Structure
|
||||
|
||||
```bash
|
||||
{everest_prefix}modules/{module_id}/impl/{impl_id}/cmd/{cmd_name}/response/{calling_module_id}
|
||||
```
|
||||
|
||||
### Message Payload Structure
|
||||
|
||||
#### Successful Response
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "<cmd_name>",
|
||||
"type": "result",
|
||||
"data": {
|
||||
"id": "<matching_call_id>",
|
||||
"retval": <return_value>,
|
||||
"origin": "<responding_module_id>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### Error Response
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "<cmd_name>",
|
||||
"type": "result",
|
||||
"data": {
|
||||
"id": "<matching_call_id>",
|
||||
"error": {
|
||||
"event": "<error_type>",
|
||||
"msg": "<error_message>"
|
||||
},
|
||||
"origin": "<responding_module_id>"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Error Types
|
||||
|
||||
Command responses can contain the following error events:
|
||||
|
||||
- `MessageParsingFailed`: JSON parsing error
|
||||
- `SchemaValidationFailed`: Schema validation error
|
||||
- `HandlerException`: Exception in command handler
|
||||
- `Timeout`: Command execution timeout
|
||||
- `Shutdown`: System shutdown
|
||||
- `NotReady`: Module not ready
|
||||
|
||||
## 4. Errors
|
||||
|
||||
The following structure applies for error topics. Errors are raised by modules and can be subscribed to by other modules. Each error has a unique UUID and includes information about the originating module, implementation, EVSE, and connector if applicable.
|
||||
|
||||
### Topic Structure
|
||||
|
||||
```bash
|
||||
{everest_prefix}modules/{module_id}/impl/{impl_id}/error/{error_type}
|
||||
```
|
||||
|
||||
### Message Payload Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "<error_namespace>/<error_name>",
|
||||
"message": "<error_description>",
|
||||
"severity": "<error_severity>",
|
||||
"origin": {
|
||||
"module_id": "<originating_module>",
|
||||
"implementation_id": "<originating_impl>",
|
||||
"evse": <evse_number>,
|
||||
"connector": <connector_number>
|
||||
},
|
||||
"state": "<error_state>",
|
||||
"timestamp": "<iso_timestamp>",
|
||||
"uuid": "<unique_error_id>"
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,120 @@
|
||||
# Module configuration distributed via MQTT
|
||||
|
||||
Since everest-framework 0.19.0 the module configuration is parsed once
|
||||
by the manager and then distributed to the modules via MQTT.
|
||||
This is achieved by parsing the MQTT settings from the config,
|
||||
spawning the modules and passing these MQTT settings to them.
|
||||
The modules themselves then ask for their module config via MQTT,
|
||||
which is in turn provided to them from the manager.
|
||||
After the modules have received their config, their init() function is called.
|
||||
Afterwards they signal ready to the manager.
|
||||
The manager sends out the global ready signal
|
||||
once it has received all Module ready signals.
|
||||
|
||||
The following sequence diagram illustrates this startup process
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
create participant manager
|
||||
create participant ManagerSettings
|
||||
manager-)ManagerSettings: ManagerSettings(prefix, config_path)
|
||||
ManagerSettings-->>manager: return ms
|
||||
create participant ManagerConfig
|
||||
manager-)ManagerConfig: ManagerConfig(ms)
|
||||
create participant MQTTAbstraction
|
||||
manager-)MQTTAbstraction: MQTTAbstraction(ms.mqtt_settings)
|
||||
MQTTAbstraction-->>manager: return mqtt_abstraction
|
||||
activate manager
|
||||
manager->>manager: start_modules()
|
||||
manager->>ManagerConfig: serialize()
|
||||
ManagerConfig-->>manager: serialized_config
|
||||
manager->>MQTTAbstraction: publish(interfaces, types, schemas, manifests, settings, retain=true)
|
||||
loop For every module
|
||||
manager->>manager: spawn_modules(Module)
|
||||
create participant Module
|
||||
manager->>Module: spawn Module
|
||||
Module->>MQTTAbstraction: get(Config)
|
||||
MQTTAbstraction->>manager: get(Config of Module)
|
||||
manager-->>MQTTAbstraction: publish(module configs, mappings)
|
||||
MQTTAbstraction-->>Module: publish(module configs, mappings)
|
||||
Module->>Module: init
|
||||
Module->>MQTTAbstraction: publish(ready)
|
||||
MQTTAbstraction->>manager: publish(ready of Module)
|
||||
end
|
||||
manager->>MQTTAbstraction: publish global ready
|
||||
```
|
||||
|
||||
Class diagram
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
ConfigBase <|-- ManagerConfig
|
||||
ConfigBase <|-- Config
|
||||
MQTTSettings *-- ConfigBase
|
||||
ManagerSettings *-- ManagerConfig
|
||||
|
||||
note for ConfigBase "
|
||||
Baseclass containing json config, manifests, interfaces,
|
||||
types and functions to access this information which
|
||||
needs to be available in all derived classes
|
||||
"
|
||||
|
||||
class ManagerSettings{
|
||||
+fs::path configs_dir
|
||||
+fs::path schemas_dir
|
||||
+fs::path interfaces_dir
|
||||
+fs::path types_dir
|
||||
+fs::path errors_dir
|
||||
+fs::path config_file
|
||||
+fs::path www_dir
|
||||
+int controller_port
|
||||
+int controller_rpc_timeout_ms
|
||||
+std::string run_as_user
|
||||
+std::string version_information
|
||||
+nlohmann::json config
|
||||
+MQTTSettings mqtt_settings
|
||||
+std::unique_ptr<RuntimeSettings> runtime_settings
|
||||
+ManagerSettings(const std::string& prefix, const std::string& config)
|
||||
+const RuntimeSettings& get_runtime_settings()
|
||||
}
|
||||
|
||||
class MQTTSettings{
|
||||
+std::string broker_socket_path
|
||||
+std::string broker_host
|
||||
+int broker_port
|
||||
+std::string everest_prefix
|
||||
+std::string external_prefix
|
||||
+bool uses_socket()
|
||||
}
|
||||
|
||||
class ConfigBase{
|
||||
#const MQTTSettings mqtt_settings
|
||||
+ConfigBase(const MQTTSettings& mqtt_settings)
|
||||
}
|
||||
|
||||
class ManagerConfig{
|
||||
-const ManagerSettings& ms
|
||||
+ManagerConfig(const ManagerSettings& ms)
|
||||
+nlohmann::json serialize()
|
||||
-load_and_validate_manifest(const std::string& module_id, const nlohmann::json& module_config)
|
||||
-std::tuple~nlohmann::json, int64_t~ load_and_validate_with_schema(const fs::path& file_path, const nlohmann::json& schema)
|
||||
-nlohmann::json resolve_interface(const std::string& intf_name)
|
||||
-nlohmann::json load_interface_file(const std::string& intf_name)
|
||||
-resolve_all_requirements()
|
||||
-parse(nlohmann::json config)
|
||||
}
|
||||
|
||||
class Config{
|
||||
+Config(const MQTTSettings& mqtt_settings, nlohmann::json config)
|
||||
+bool module_provides(const std::string& module_name, const std::string& impl_id);
|
||||
+nlohmann::json get_module_cmds(const std::string& module_name, const std::string& impl_id)
|
||||
+nlohmann::json resolve_requirement(const std::string& module_id, const std::string& requirement_id)
|
||||
+std::list~Requirement~ get_requirements(const std::string& module_id)
|
||||
+RequirementInitialization get_requirement_initialization(const std::string& module_id)
|
||||
+ModuleConfigs get_module_configs(const std::string& module_id)
|
||||
+nlohmann::json get_module_json_config(const std::string& module_id)
|
||||
+ModuleInfo get_module_info(const std::string& module_id)
|
||||
+std::optional~<~TelemetryConfig~ get_telemetry_config()
|
||||
+nlohmann::json get_interface_definition(const std::string& interface_name) const;
|
||||
}
|
||||
```
|
||||
3
tools/EVerest-main/lib/everest/framework/docs/README.md
Normal file
3
tools/EVerest-main/lib/everest/framework/docs/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Additional documentation about the inner workings of EVerest Framework
|
||||
|
||||
[MQTT Config distribution](MQTTConfigDistribution.md)
|
||||
2
tools/EVerest-main/lib/everest/framework/everestjs/.gitignore
vendored
Normal file
2
tools/EVerest-main/lib/everest/framework/everestjs/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
*node_modules
|
||||
package-lock.json
|
||||
@@ -0,0 +1,120 @@
|
||||
find_program(
|
||||
NODE
|
||||
node
|
||||
REQUIRED
|
||||
)
|
||||
|
||||
execute_process(
|
||||
COMMAND ${NODE} --version
|
||||
OUTPUT_VARIABLE NODE_VERSION
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
|
||||
# We need Node-API version 6 or higher
|
||||
set(NODE_API_VERSION_REQUIRED 6)
|
||||
include(NodeApiVersion)
|
||||
require_node_api_version("${NODE_VERSION}" "${NODE_API_VERSION_REQUIRED}")
|
||||
|
||||
find_program(
|
||||
NPM
|
||||
npm
|
||||
REQUIRED
|
||||
)
|
||||
|
||||
# Include Node-API wrappers
|
||||
# FIXME (aw): we want this as an requirement, not implicitely install it by ourself
|
||||
execute_process(
|
||||
COMMAND
|
||||
${NPM} list -p node-addon-api --loglevel=error | grep node-addon-api
|
||||
WORKING_DIRECTORY
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
OUTPUT_VARIABLE
|
||||
NODE_ADDON_API_PACKAGE_DIR
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
|
||||
set(NODE_ADDON_API_INSTALL_VERSION "8.1")
|
||||
message(STATUS "Adding node-addon-api@${NODE_ADDON_API_INSTALL_VERSION}")
|
||||
if (NOT NODE_ADDON_API_PACKAGE_DIR)
|
||||
execute_process(
|
||||
COMMAND
|
||||
${NPM} install node-addon-api@${NODE_ADDON_API_INSTALL_VERSION}
|
||||
WORKING_DIRECTORY
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
OUTPUT_QUIET
|
||||
RESULT_VARIABLE
|
||||
NPM_INSTALL_NODE_ADDON_API_FAILED
|
||||
)
|
||||
|
||||
if (NPM_INSTALL_NODE_ADDON_API_FAILED)
|
||||
message(FATAL_ERROR "Installation of node-addon-api failed")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
execute_process(
|
||||
COMMAND ${NODE} -p "require('node-addon-api').include"
|
||||
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
|
||||
OUTPUT_VARIABLE NODE_ADDON_API_DIR
|
||||
)
|
||||
string(REGEX REPLACE "[\r\n\"]" "" NODE_ADDON_API_DIR "${NODE_ADDON_API_DIR}")
|
||||
|
||||
find_path(NODEJS_INCLUDE_DIR "node_api.h"
|
||||
PATH_SUFFIXES
|
||||
"node"
|
||||
"node16"
|
||||
"node17"
|
||||
"node18"
|
||||
"node19"
|
||||
"node20"
|
||||
"node21"
|
||||
"node22"
|
||||
"nodejs/src"
|
||||
HINTS
|
||||
"$ENV{HOME}/.nvm/versions/node/${NODE_VERSION}/include"
|
||||
REQUIRED
|
||||
)
|
||||
|
||||
add_library(everestjs SHARED)
|
||||
target_sources(everestjs
|
||||
PRIVATE
|
||||
everestjs.cpp
|
||||
conversions.cpp
|
||||
js_exec_ctx.cpp
|
||||
)
|
||||
|
||||
target_compile_options(everestjs PRIVATE ${COMPILER_WARNING_OPTIONS})
|
||||
|
||||
# define NAPI_VERSION
|
||||
target_compile_definitions(everestjs
|
||||
PRIVATE
|
||||
-DNAPI_VERSION=${NODE_API_VERSION_REQUIRED}
|
||||
-DNAPI_CPP_EXCEPTIONS
|
||||
)
|
||||
|
||||
set_target_properties(everestjs PROPERTIES PREFIX "" SUFFIX ".node")
|
||||
|
||||
target_include_directories(everestjs
|
||||
PRIVATE
|
||||
$<BUILD_INTERFACE:${NODE_ADDON_API_DIR}>
|
||||
$<BUILD_INTERFACE:${NODEJS_INCLUDE_DIR}>
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
|
||||
)
|
||||
|
||||
target_link_libraries(everestjs
|
||||
PRIVATE
|
||||
everest::framework
|
||||
everest::log
|
||||
)
|
||||
|
||||
install(
|
||||
TARGETS everestjs
|
||||
LIBRARY
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/everest/node_modules/everestjs
|
||||
)
|
||||
|
||||
install(
|
||||
FILES
|
||||
index.js
|
||||
package.json
|
||||
DESTINATION ${CMAKE_INSTALL_LIBDIR}/everest/node_modules/everestjs
|
||||
)
|
||||
@@ -0,0 +1,213 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest
|
||||
#include "conversions.hpp"
|
||||
|
||||
#include <everest/exceptions.hpp>
|
||||
#include <everest/logging.hpp>
|
||||
|
||||
#include <utils/error/error_json.hpp>
|
||||
|
||||
namespace EverestJs {
|
||||
|
||||
Everest::json convertToJson(const Napi::Value& value) {
|
||||
BOOST_LOG_FUNCTION();
|
||||
|
||||
if (value.IsNull() || value.IsUndefined()) {
|
||||
return Everest::json(nullptr);
|
||||
} else if (value.IsString()) {
|
||||
return Everest::json(std::string(value.As<Napi::String>()));
|
||||
} else if (value.IsNumber()) {
|
||||
int64_t intNumber = value.As<Napi::Number>();
|
||||
double floatNumber = value.As<Napi::Number>();
|
||||
if (floatNumber == intNumber)
|
||||
return Everest::json(intNumber);
|
||||
return Everest::json(floatNumber);
|
||||
} else if (value.IsBoolean()) {
|
||||
return Everest::json(bool(value.As<Napi::Boolean>()));
|
||||
} else if (value.IsArray()) {
|
||||
auto j = Everest::json::array();
|
||||
Napi::Array array = value.As<Napi::Array>();
|
||||
for (uint64_t i = 0; i < array.Length(); i++) {
|
||||
Napi::Value entry = Napi::Value(array[i]);
|
||||
j[i] = convertToJson(entry);
|
||||
}
|
||||
return j;
|
||||
} else if (value.IsObject() && value.Type() == napi_object) {
|
||||
auto j = Everest::json({});
|
||||
Napi::Object obj = value.As<Napi::Object>();
|
||||
Napi::Array keys = obj.GetPropertyNames();
|
||||
for (uint64_t i = 0; i < keys.Length(); i++) {
|
||||
Napi::Value key = keys[i];
|
||||
if (key.IsString()) {
|
||||
std::string k = key.As<Napi::String>();
|
||||
Napi::Value v = Napi::Value(obj[k]);
|
||||
j[k] = convertToJson(v);
|
||||
} else {
|
||||
EVTHROW(EVEXCEPTION(Everest::EverestApiError,
|
||||
"Javascript type of object key can not be converted to Everest::json: ",
|
||||
napi_valuetype_strings[key.Type()]));
|
||||
}
|
||||
}
|
||||
return j;
|
||||
}
|
||||
EVTHROW(EVEXCEPTION(Everest::EverestApiError, "Javascript type can not be converted to Everest::json: ",
|
||||
napi_valuetype_strings[value.Type()]));
|
||||
}
|
||||
|
||||
Everest::TelemetryMap convertToTelemetryMap(const Napi::Object& obj) {
|
||||
BOOST_LOG_FUNCTION();
|
||||
Everest::TelemetryMap telemetry;
|
||||
Napi::Array keys = obj.GetPropertyNames();
|
||||
for (uint64_t i = 0; i < keys.Length(); i++) {
|
||||
Napi::Value key = keys[i];
|
||||
if (key.IsString()) {
|
||||
std::string k = key.As<Napi::String>();
|
||||
Napi::Value value = Napi::Value(obj[k]);
|
||||
if (value.IsString()) {
|
||||
telemetry[k] = std::string(value.As<Napi::String>());
|
||||
} else if (value.IsNumber()) {
|
||||
int intNumber = value.As<Napi::Number>();
|
||||
double floatNumber = value.As<Napi::Number>();
|
||||
if (floatNumber == intNumber) {
|
||||
telemetry[k] = intNumber;
|
||||
} else {
|
||||
telemetry[k] = floatNumber;
|
||||
}
|
||||
} else if (value.IsBoolean()) {
|
||||
telemetry[k] = bool(value.As<Napi::Boolean>());
|
||||
}
|
||||
}
|
||||
}
|
||||
return telemetry;
|
||||
}
|
||||
|
||||
Napi::Value convertToNapiValue(const Napi::Env& env, const json& value) {
|
||||
BOOST_LOG_FUNCTION();
|
||||
|
||||
if (value.is_null()) {
|
||||
return env.Null();
|
||||
} else if (value.is_string()) {
|
||||
return Napi::String::New(env, std::string(value));
|
||||
} else if (value.is_number_integer()) {
|
||||
return Napi::Number::New(env, int64_t(value));
|
||||
} else if (value.is_number_float()) {
|
||||
return Napi::Number::New(env, double(value));
|
||||
} else if (value.is_boolean()) {
|
||||
return Napi::Boolean::New(env, bool(value));
|
||||
} else if (value.is_array()) {
|
||||
Napi::Array v = Napi::Array::New(env);
|
||||
for (uint64_t i = 0; i < value.size(); i++) {
|
||||
v.Set(i, convertToNapiValue(env, value[i]));
|
||||
}
|
||||
return v;
|
||||
} else if (value.is_object()) {
|
||||
Napi::Object v = Napi::Object::New(env);
|
||||
for (auto& el : value.items()) {
|
||||
v.Set(el.key(), convertToNapiValue(env, el.value()));
|
||||
}
|
||||
return v;
|
||||
}
|
||||
EVTHROW(EVEXCEPTION(Everest::EverestApiError, "Javascript type can not be converted to Napi::Value: ", value));
|
||||
}
|
||||
|
||||
Everest::error::Error convertToError(const Napi::Value& value) {
|
||||
BOOST_LOG_FUNCTION();
|
||||
|
||||
Everest::json j = convertToJson(value);
|
||||
return j.get<Everest::error::Error>();
|
||||
}
|
||||
|
||||
Everest::error::ErrorType convertToErrorType(const Napi::Value& value) {
|
||||
BOOST_LOG_FUNCTION();
|
||||
|
||||
if (value.IsString()) {
|
||||
return Everest::error::ErrorType(std::string(value.As<Napi::String>()));
|
||||
}
|
||||
EVTHROW(EVEXCEPTION(Everest::EverestApiError, "Javascript type can not be converted to Everest::error::ErrorType: ",
|
||||
napi_valuetype_strings[value.Type()]));
|
||||
}
|
||||
|
||||
Everest::error::ErrorSubType convertToErrorSubType(const Napi::Value& value) {
|
||||
BOOST_LOG_FUNCTION();
|
||||
|
||||
if (value.IsString()) {
|
||||
return Everest::error::ErrorSubType(std::string(value.As<Napi::String>()));
|
||||
}
|
||||
EVTHROW(EVEXCEPTION(Everest::EverestApiError,
|
||||
"Javascript type can not be converted to Everest::error::ErrorSubType: ",
|
||||
napi_valuetype_strings[value.Type()]));
|
||||
}
|
||||
|
||||
Everest::error::Severity convertToErrorSeverity(const Napi::Value& value) {
|
||||
BOOST_LOG_FUNCTION();
|
||||
|
||||
if (value.IsString()) {
|
||||
return Everest::error::string_to_severity(std::string(value.As<Napi::String>()));
|
||||
}
|
||||
EVTHROW(EVEXCEPTION(Everest::EverestApiError, "Javascript type can not be converted to Everest::error::Severity: ",
|
||||
napi_valuetype_strings[value.Type()]));
|
||||
}
|
||||
|
||||
Everest::error::State convertToErrorState(const Napi::Value& value) {
|
||||
BOOST_LOG_FUNCTION();
|
||||
|
||||
if (value.IsString()) {
|
||||
return Everest::error::string_to_state(std::string(value.As<Napi::String>()));
|
||||
}
|
||||
EVTHROW(EVEXCEPTION(Everest::EverestApiError, "Javascript type can not be converted to Everest::error::State: ",
|
||||
napi_valuetype_strings[value.Type()]));
|
||||
}
|
||||
|
||||
Napi::Value convertToNapiValue(const Napi::Env& env, const Everest::error::Error& error) {
|
||||
BOOST_LOG_FUNCTION();
|
||||
|
||||
json j(error);
|
||||
Napi::Value res = convertToNapiValue(env, j);
|
||||
return res;
|
||||
}
|
||||
|
||||
bool isSingleErrorStateCondition(const Napi::Value& value) {
|
||||
BOOST_LOG_FUNCTION();
|
||||
|
||||
if (value.IsArray()) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Everest::error::ErrorStateMonitor::StateCondition convertToErrorStateCondition(const Napi::Value& value) {
|
||||
BOOST_LOG_FUNCTION();
|
||||
|
||||
if (value.IsObject()) {
|
||||
Napi::Object obj = value.As<Napi::Object>();
|
||||
Napi::Value type = obj.Get("type");
|
||||
Napi::Value sub_type = obj.Get("sub_type");
|
||||
Napi::Value active = obj.Get("active");
|
||||
return Everest::error::ErrorStateMonitor::StateCondition(
|
||||
convertToErrorType(type), convertToErrorSubType(sub_type), bool(active.As<Napi::Boolean>()));
|
||||
}
|
||||
EVTHROW(EVEXCEPTION(Everest::EverestApiError,
|
||||
"Javascript type can not be converted to Everest::error::ErrorStateMonitor::StateCondition: ",
|
||||
napi_valuetype_strings[value.Type()]));
|
||||
}
|
||||
|
||||
std::list<Everest::error::ErrorStateMonitor::StateCondition>
|
||||
convertToErrorStateConditionList(const Napi::Value& value) {
|
||||
BOOST_LOG_FUNCTION();
|
||||
|
||||
if (value.IsArray()) {
|
||||
std::list<Everest::error::ErrorStateMonitor::StateCondition> conditions;
|
||||
Napi::Array array = value.As<Napi::Array>();
|
||||
for (uint64_t i = 0; i < array.Length(); i++) {
|
||||
Napi::Value entry = Napi::Value(array[i]);
|
||||
conditions.push_back(convertToErrorStateCondition(entry));
|
||||
}
|
||||
return conditions;
|
||||
}
|
||||
EVTHROW(EVEXCEPTION(
|
||||
Everest::EverestApiError,
|
||||
"Javascript type can not be converted to std::list<Everest::error::ErrorStateMonitor::StateCondition>: ",
|
||||
napi_valuetype_strings[value.Type()]));
|
||||
}
|
||||
|
||||
} // namespace EverestJs
|
||||
@@ -0,0 +1,50 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef CONVERSIONS_HPP
|
||||
#define CONVERSIONS_HPP
|
||||
|
||||
#include <framework/everest.hpp>
|
||||
|
||||
#include <napi.h>
|
||||
|
||||
#include <utils/conversions.hpp>
|
||||
#include <utils/types.hpp>
|
||||
|
||||
#include <utils/error.hpp>
|
||||
#include <utils/error/error_state_monitor.hpp>
|
||||
|
||||
namespace EverestJs {
|
||||
|
||||
static const char* const napi_valuetype_strings[] = {
|
||||
"undefined", //
|
||||
"null", //
|
||||
"boolean", //
|
||||
"number", //
|
||||
"string", //
|
||||
"symbol", //
|
||||
"object", //
|
||||
"function", //
|
||||
"external", //
|
||||
"bigint", //
|
||||
};
|
||||
|
||||
Everest::json convertToJson(const Napi::Value& value);
|
||||
Everest::json convertToConfigMap(const Everest::json& json_config);
|
||||
Everest::TelemetryMap convertToTelemetryMap(const Napi::Object& obj);
|
||||
Napi::Value convertToNapiValue(const Napi::Env& env, const Everest::json& value);
|
||||
|
||||
// Error related
|
||||
Everest::error::Error convertToError(const Napi::Value& value);
|
||||
Everest::error::ErrorType convertToErrorType(const Napi::Value& value);
|
||||
Everest::error::ErrorSubType convertToErrorSubType(const Napi::Value& value);
|
||||
Everest::error::Severity convertToErrorSeverity(const Napi::Value& value);
|
||||
Everest::error::State convertToErrorState(const Napi::Value& value);
|
||||
Napi::Value convertToNapiValue(const Napi::Env& env, const Everest::error::Error& error);
|
||||
// ErrorStateCondition related
|
||||
bool isSingleErrorStateCondition(const Napi::Value& value);
|
||||
Everest::error::ErrorStateMonitor::StateCondition convertToErrorStateCondition(const Napi::Value& value);
|
||||
std::list<Everest::error::ErrorStateMonitor::StateCondition> convertToErrorStateConditionList(const Napi::Value& value);
|
||||
|
||||
} // namespace EverestJs
|
||||
|
||||
#endif // CONVERSIONS_HPP
|
||||
1009
tools/EVerest-main/lib/everest/framework/everestjs/everestjs.cpp
Normal file
1009
tools/EVerest-main/lib/everest/framework/everestjs/everestjs.cpp
Normal file
File diff suppressed because it is too large
Load Diff
118
tools/EVerest-main/lib/everest/framework/everestjs/index.js
Normal file
118
tools/EVerest-main/lib/everest/framework/everestjs/index.js
Normal file
@@ -0,0 +1,118 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
const util = require('util');
|
||||
const addon = require('./everestjs.node');
|
||||
|
||||
const helpers = {
|
||||
get_default: (obj, key, defaultValue) => ((obj[key] === undefined) ? defaultValue : obj[key]),
|
||||
to_string: (...values) => {
|
||||
let log_string = '';
|
||||
values.forEach((value) => {
|
||||
if (typeof value == 'string') {
|
||||
log_string += value;
|
||||
} else if (typeof value == 'number') {
|
||||
log_string += value;
|
||||
} else if (typeof value == 'boolean') {
|
||||
log_string += value ? 'true' : 'false';
|
||||
} else if (typeof value == 'object') {
|
||||
log_string += util.inspect(value, false, 6, true);
|
||||
} else {
|
||||
throw new Error(`The logging function cannot handle input of type ${typeof value}`);
|
||||
}
|
||||
});
|
||||
return log_string;
|
||||
},
|
||||
};
|
||||
|
||||
const EverestModule = function EverestModule(handler_setup, user_settings) {
|
||||
const env_settings = {
|
||||
module: process.env.EV_MODULE,
|
||||
prefix: process.env.EV_PREFIX,
|
||||
logging_config_file: process.env.EV_LOG_CONF_FILE,
|
||||
mqtt_everest_prefix: process.env.EV_MQTT_EVEREST_PREFIX,
|
||||
mqtt_external_prefix: process.env.EV_MQTT_EXTERNAL_PREFIX,
|
||||
mqtt_broker_socket_path: process.env.EV_MQTT_BROKER_SOCKET_PATH,
|
||||
mqtt_server_address: process.env.EV_MQTT_BROKER_HOST,
|
||||
mqtt_server_port: process.env.EV_MQTT_BROKER_PORT,
|
||||
validate_schema: process.env.EV_VALIDATE_SCHEMA,
|
||||
};
|
||||
|
||||
const settings = { ...env_settings, ...user_settings };
|
||||
|
||||
if (!settings.module) {
|
||||
throw new Error('parameter "module" is missing');
|
||||
}
|
||||
|
||||
const config = {
|
||||
module: settings.module,
|
||||
prefix: settings.prefix,
|
||||
logging_config_file: settings.logging_config_file,
|
||||
mqtt_everest_prefix: settings.mqtt_everest_prefix,
|
||||
mqtt_external_prefix: helpers.get_default(settings, 'mqtt_external_prefix', ''),
|
||||
mqtt_broker_socket_path: helpers.get_default(settings, 'mqtt_broker_socket_path', ''),
|
||||
mqtt_server_address: helpers.get_default(settings, 'mqtt_server_address', ''),
|
||||
mqtt_server_port: helpers.get_default(settings, 'mqtt_server_port', 0),
|
||||
validate_schema: helpers.get_default(settings, 'validate_schema', false),
|
||||
};
|
||||
|
||||
function callbackWrapper(on, request, ...args) {
|
||||
const result = request(...args);
|
||||
Promise.resolve(result).then(on.fulfill, on.reject);
|
||||
}
|
||||
|
||||
const available_handlers = addon.boot_module.call(this, config, callbackWrapper);
|
||||
|
||||
const module_setup = {
|
||||
setup: available_handlers,
|
||||
info: this.info,
|
||||
config: this.config,
|
||||
mqtt: this.mqtt,
|
||||
telemetry: this.telemetry,
|
||||
};
|
||||
|
||||
if (this.mqtt === undefined) {
|
||||
const missing_mqtt_getter = {
|
||||
get() { throw new Error('External mqtt not available - missing enable_external_mqtt in manifest?'); },
|
||||
};
|
||||
Object.defineProperty(module_setup, 'mqtt', missing_mqtt_getter);
|
||||
Object.defineProperty(this, 'mqtt', missing_mqtt_getter);
|
||||
}
|
||||
|
||||
if (this.telemetry === undefined) {
|
||||
const missing_telemetry_getter = {
|
||||
get() { throw new Error('Telemetry not available - missing enable_telemetry in manifest?'); },
|
||||
};
|
||||
Object.defineProperty(module_setup, 'telemetry', missing_telemetry_getter);
|
||||
Object.defineProperty(this, 'telemetry', missing_telemetry_getter);
|
||||
}
|
||||
|
||||
// check, if we need to register cmds
|
||||
if (typeof handler_setup === 'undefined') {
|
||||
if (Object.keys(available_handlers.provides).length !== 0) {
|
||||
throw new Error('handler setup callback is missing - you need to register at least one command');
|
||||
}
|
||||
return addon.signal_ready.call(this);
|
||||
}
|
||||
|
||||
if (typeof handler_setup === 'function') {
|
||||
return Promise.resolve(handler_setup.call({}, module_setup)).then(
|
||||
() => addon.signal_ready.call(this)
|
||||
);
|
||||
}
|
||||
|
||||
throw new Error('handler setup callback needs to be of type function');
|
||||
};
|
||||
|
||||
// setup log handlers
|
||||
exports.evlog = {};
|
||||
Object.keys(addon.log).forEach((key) => {
|
||||
exports.evlog[key] = (...log_args) => addon.log[key](helpers.to_string(...log_args));
|
||||
});
|
||||
|
||||
let boot_module_called = false;
|
||||
|
||||
exports.boot_module = (handler_setup, settings) => {
|
||||
if (boot_module_called) throw Error('Calling initModule more than once is not supported right now');
|
||||
boot_module_called = true;
|
||||
return new EverestModule(handler_setup, settings);
|
||||
};
|
||||
@@ -0,0 +1,60 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest
|
||||
#include "js_exec_ctx.hpp"
|
||||
#include "utils.hpp"
|
||||
|
||||
void JsExecCtx::tramp(Napi::Env env, Napi::Function callback, std::nullptr_t* context, JsExecCtx* this_) {
|
||||
(void)context; // this is unused in this callback
|
||||
|
||||
try {
|
||||
std::vector<napi_value> args{this_->result_handler_ref.Value()};
|
||||
if (this_->arg_func != nullptr) {
|
||||
// append args from our arg function via move magic ...
|
||||
auto append_args = this_->arg_func(env);
|
||||
args.reserve(args.size() + append_args.size());
|
||||
std::move(std::begin(append_args), std::end(append_args), std::back_inserter(args));
|
||||
append_args.clear();
|
||||
}
|
||||
|
||||
callback.Call(args);
|
||||
} catch (std::exception& e) {
|
||||
EVLOG_AND_RETHROW(env);
|
||||
}
|
||||
}
|
||||
|
||||
Napi::Value JsExecCtx::on_fulfill(const Napi::CallbackInfo& info) {
|
||||
JsExecCtx* this_ = reinterpret_cast<JsExecCtx*>(info.Data());
|
||||
if (this_->res_func != nullptr) {
|
||||
this_->res_func(info, false);
|
||||
}
|
||||
this_->promise.set_value();
|
||||
return info.Env().Undefined();
|
||||
}
|
||||
|
||||
Napi::Value JsExecCtx::on_reject(const Napi::CallbackInfo& info) {
|
||||
JsExecCtx* this_ = reinterpret_cast<JsExecCtx*>(info.Data());
|
||||
if (this_->res_func != nullptr) {
|
||||
this_->res_func(info, true);
|
||||
} else {
|
||||
// there is no catch handler registered, so we throw
|
||||
throw Napi::Error::New(
|
||||
info.Env(),
|
||||
"JsExecCtx call into javascript code got rejected and could not be handled (rejection handler not defined");
|
||||
}
|
||||
|
||||
this_->promise.set_value();
|
||||
return info.Env().Undefined();
|
||||
}
|
||||
|
||||
void JsExecCtx::exec(const ArgFuncType& arg_func, const ResFuncType& res_func) {
|
||||
// FIXME (aw): we're blocking all other threads trying to call this function
|
||||
// a proper solution should be found
|
||||
std::unique_lock<std::mutex> lock(exec_mutex);
|
||||
this->arg_func = arg_func;
|
||||
this->res_func = res_func;
|
||||
|
||||
promise = std::promise<void>();
|
||||
|
||||
tsfn.BlockingCall(this);
|
||||
promise.get_future().get();
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
//
|
||||
// author: aw@pionix.de
|
||||
//
|
||||
|
||||
#ifndef JS_EXEC_CTX_HPP
|
||||
#define JS_EXEC_CTX_HPP
|
||||
|
||||
#include <future>
|
||||
|
||||
#include <napi.h>
|
||||
|
||||
class JsExecCtx {
|
||||
private:
|
||||
static void tramp(Napi::Env env, Napi::Function callback, std::nullptr_t*, JsExecCtx* this_);
|
||||
static Napi::Value on_fulfill(const Napi::CallbackInfo& info);
|
||||
static Napi::Value on_reject(const Napi::CallbackInfo& info);
|
||||
|
||||
public:
|
||||
using TsfnType = Napi::TypedThreadSafeFunction<std::nullptr_t, JsExecCtx, JsExecCtx::tramp>;
|
||||
using ArgFuncType = std::function<std::vector<napi_value>(Napi::Env&)>;
|
||||
using ResFuncType = std::function<void(const Napi::CallbackInfo&, bool)>;
|
||||
|
||||
// FIXME (aw): proper module_instance handling if nullptr
|
||||
JsExecCtx(const Napi::Env& env, const Napi::Function& func, const std::string& res_name = "RequestDispatcher") :
|
||||
tsfn(TsfnType::New(env, func, res_name, 0, 1)),
|
||||
result_handler_ref(Napi::Persistent(Napi::Object::New(env))),
|
||||
func_ref(Napi::Persistent(func)) {
|
||||
|
||||
result_handler_ref.Value().DefineProperty(Napi::PropertyDescriptor::Value(
|
||||
"fulfill", Napi::Function::New(env, on_fulfill, nullptr, this), napi_enumerable));
|
||||
result_handler_ref.Value().DefineProperty(Napi::PropertyDescriptor::Value(
|
||||
"reject", Napi::Function::New(env, on_reject, nullptr, this), napi_enumerable));
|
||||
}
|
||||
|
||||
~JsExecCtx() {
|
||||
tsfn.Release();
|
||||
}
|
||||
|
||||
void exec(const ArgFuncType& arg_func, const ResFuncType& res_func);
|
||||
|
||||
private:
|
||||
ArgFuncType arg_func;
|
||||
ResFuncType res_func;
|
||||
// FIXME (aw): will the referenced object be GC'd when the references get destroyed?
|
||||
// and is okay to be destroyed in our async thread?
|
||||
TsfnType tsfn;
|
||||
Napi::ObjectReference result_handler_ref;
|
||||
Napi::FunctionReference func_ref{};
|
||||
std::mutex exec_mutex;
|
||||
std::promise<void> promise;
|
||||
};
|
||||
|
||||
#endif // JS_EXEC_CTX_HPP
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "everestjs",
|
||||
"main": "index.js",
|
||||
"version": "0.25.0",
|
||||
"description": "EVerest API for node.js",
|
||||
"dependencies": {
|
||||
"node-addon-api": "^3.2.1"
|
||||
}
|
||||
}
|
||||
56
tools/EVerest-main/lib/everest/framework/everestjs/utils.hpp
Normal file
56
tools/EVerest-main/lib/everest/framework/everestjs/utils.hpp
Normal file
@@ -0,0 +1,56 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef UTILS_HPP
|
||||
#define UTILS_HPP
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
#include <everest/metamacros.hpp>
|
||||
|
||||
namespace EverestJs {
|
||||
|
||||
// this is needed to get javascript stacktraces whenever possible while still logging boost error information
|
||||
#define EVLOG_AND_RETHROW(...) \
|
||||
do { \
|
||||
try { \
|
||||
throw; \
|
||||
} catch (Napi::Error & e) { \
|
||||
try { \
|
||||
BOOST_THROW_EXCEPTION(boost::enable_error_info(e) \
|
||||
<< boost::log::BOOST_LOG_VERSION_NAMESPACE::current_scope()); \
|
||||
} catch (boost::exception & ex) { \
|
||||
char const* const* f = boost::get_error_info<boost::throw_file>(ex); \
|
||||
int const* l = boost::get_error_info<boost::throw_line>(ex); \
|
||||
char const* const* fn = boost::get_error_info<boost::throw_function>(ex); \
|
||||
EVLOG_critical << "Catched top level Napi::Error, forwarding to javascript..." << std::endl \
|
||||
<< (f ? *f : "") << ":" << (l ? std::to_string(*l) : "") << " in Function " \
|
||||
<< (fn ? *fn : "") << std::endl \
|
||||
<< boost::diagnostic_information(e, true) << boost::diagnostic_information(ex, false) \
|
||||
<< std::endl \
|
||||
<< "==============================" << std::endl \
|
||||
<< std::endl; \
|
||||
} \
|
||||
throw; /* this will forward the exception back to javascript and enable js backtraces */ \
|
||||
} catch (std::exception & e) { \
|
||||
try { \
|
||||
BOOST_THROW_EXCEPTION(boost::enable_error_info(e) \
|
||||
<< boost::log::BOOST_LOG_VERSION_NAMESPACE::current_scope()); \
|
||||
} catch (boost::exception & ex) { \
|
||||
char const* const* f = boost::get_error_info<boost::throw_file>(ex); \
|
||||
int const* l = boost::get_error_info<boost::throw_line>(ex); \
|
||||
char const* const* fn = boost::get_error_info<boost::throw_function>(ex); \
|
||||
EVLOG_critical << "Catched top level exception, forwarding to javascript..." << std::endl \
|
||||
<< (f ? *f : "") << ":" << (l ? std::to_string(*l) : "") << " in Function " \
|
||||
<< (fn ? *fn : "") << std::endl \
|
||||
<< boost::diagnostic_information(e, true) << boost::diagnostic_information(ex, false) \
|
||||
<< std::endl \
|
||||
<< "==============================" << std::endl \
|
||||
<< std::endl; \
|
||||
/* this will forward the exception to javascript and enable js backtraces */ \
|
||||
metamacro_if_eq(0, metamacro_argcount(__VA_ARGS__))(throw;)(EVTHROW(Napi::Error::New( \
|
||||
metamacro_at(0, __VA_ARGS__), Napi::String::New(metamacro_at(0, __VA_ARGS__), e.what())));) \
|
||||
} \
|
||||
} \
|
||||
} while (0)
|
||||
} // namespace EverestJs
|
||||
|
||||
#endif // UTILS_HPP
|
||||
8
tools/EVerest-main/lib/everest/framework/everestpy/.gitignore
vendored
Normal file
8
tools/EVerest-main/lib/everest/framework/everestpy/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
__pycache__/
|
||||
|
||||
*.so
|
||||
|
||||
*.egg-info/
|
||||
|
||||
htmlcov/
|
||||
.coverage
|
||||
@@ -0,0 +1,17 @@
|
||||
ev_create_pip_install_targets(
|
||||
PACKAGE_NAME
|
||||
everestpy
|
||||
DIST_DEPENDS
|
||||
everestpy_ln_dist
|
||||
LOCAL_DEPENDS
|
||||
everestpy_ln_local
|
||||
)
|
||||
|
||||
ev_create_python_wheel_targets(
|
||||
PACKAGE_NAME
|
||||
everestpy
|
||||
DEPENDS
|
||||
everestpy_ln_dist
|
||||
)
|
||||
|
||||
add_subdirectory(src/everest)
|
||||
@@ -0,0 +1,9 @@
|
||||
[build-system]
|
||||
requires = [
|
||||
"setuptools>=42",
|
||||
"wheel"
|
||||
]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.autopep8]
|
||||
max_line_length = 120
|
||||
31
tools/EVerest-main/lib/everest/framework/everestpy/setup.cfg
Normal file
31
tools/EVerest-main/lib/everest/framework/everestpy/setup.cfg
Normal file
@@ -0,0 +1,31 @@
|
||||
[metadata]
|
||||
name = everestpy
|
||||
version = attr: everest.framework.__version__
|
||||
author = 'Kai-Uwe Hermann'
|
||||
author_email = kh@pionix.de
|
||||
description = everest framework python binding
|
||||
long_description = file: README.rst
|
||||
long_description_content_type = text/x-rst
|
||||
url = https://github.com/EVerest/everest-framework
|
||||
classifiers =
|
||||
Programming Language :: Python :: 3
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Operating System :: OS Independent
|
||||
|
||||
[options]
|
||||
package_dir =
|
||||
= src
|
||||
# packages = find:
|
||||
python_requires = >=3.7
|
||||
|
||||
[options.packages.find]
|
||||
exclude =
|
||||
tests*
|
||||
|
||||
[options.package_data]
|
||||
* = *.pyi, *.so
|
||||
|
||||
|
||||
[options.extras_require]
|
||||
test =
|
||||
coverage
|
||||
@@ -0,0 +1,25 @@
|
||||
load("@pybind11_bazel//:build_defs.bzl", "pybind_extension")
|
||||
load("@rules_python//python:py_library.bzl", "py_library")
|
||||
load("//third-party/bazel/toolchains:defs.bzl", "CROSS_PYTHON_INCOMPATIBLE")
|
||||
|
||||
pybind_extension(
|
||||
name = "everestpy",
|
||||
srcs = glob(["everest/**/*.cpp", "everest/**/*.hpp"]),
|
||||
deps = ["//lib/everest/framework:framework", "@pybind11_json//:pybind11_json"],
|
||||
includes = ["everest"],
|
||||
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE,
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
)
|
||||
|
||||
py_library(
|
||||
name = "framework",
|
||||
data = [":everestpy.so"],
|
||||
srcs = glob(["everest/framework/**/*.py",]),
|
||||
target_compatible_with = CROSS_PYTHON_INCOMPATIBLE,
|
||||
visibility = [
|
||||
"//visibility:public",
|
||||
],
|
||||
imports = ["."],
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
find_package(Python3
|
||||
REQUIRED
|
||||
COMPONENTS
|
||||
Development
|
||||
)
|
||||
|
||||
if (DISABLE_EDM)
|
||||
find_package(pybind11 REQUIRED)
|
||||
find_package(pybind11_json REQUIRED)
|
||||
endif()
|
||||
|
||||
# support overriding where the include files are to be found.
|
||||
# Used for Yocto where the pybind11 libraries may have come from a cache and the
|
||||
# full path is no longer the same
|
||||
if (PYBIND11_INTERFACE_INCLUDE_DIRECTORIES)
|
||||
set_target_properties(pybind11::pybind11 PROPERTIES
|
||||
INTERFACE_INCLUDE_DIRECTORIES ${PYBIND11_INTERFACE_INCLUDE_DIRECTORIES}
|
||||
)
|
||||
set_target_properties(pybind11_json PROPERTIES
|
||||
INTERFACE_INCLUDE_DIRECTORIES ${PYBIND11_INTERFACE_INCLUDE_DIRECTORIES}
|
||||
)
|
||||
endif()
|
||||
|
||||
pybind11_add_module(everestpy misc.cpp module.cpp everestpy.cpp)
|
||||
|
||||
target_compile_options(everestpy PRIVATE ${COMPILER_WARNING_OPTIONS})
|
||||
|
||||
target_link_libraries(everestpy
|
||||
PRIVATE
|
||||
everest::framework
|
||||
everest::log
|
||||
pybind11_json
|
||||
fmt::fmt
|
||||
)
|
||||
|
||||
set(EVERESTPY_LIB_INSTALL_DIR ${CMAKE_INSTALL_LIBDIR}/everest/everestpy/everest/framework)
|
||||
set(EVERESTPY_DIST_INSTALL_PATH ${CMAKE_INSTALL_PREFIX}/${EVERESTPY_LIB_INSTALL_DIR}/$<TARGET_FILE_NAME:everestpy>)
|
||||
|
||||
add_custom_target(everestpy_ln_dist
|
||||
COMMAND
|
||||
test -f $$DESTDIR${EVERESTPY_DIST_INSTALL_PATH} || (echo "everestpy library not found, did you run the install target?" && false)
|
||||
COMMAND
|
||||
${CMAKE_COMMAND} -E create_symlink $$DESTDIR${EVERESTPY_DIST_INSTALL_PATH} $<TARGET_FILE_NAME:everestpy>
|
||||
WORKING_DIRECTORY
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/framework
|
||||
COMMENT
|
||||
"Creating symlink to distributed everestpy"
|
||||
)
|
||||
|
||||
add_custom_target(everestpy_ln_local
|
||||
COMMAND
|
||||
${CMAKE_COMMAND} -E create_symlink $<TARGET_FILE:everestpy> $<TARGET_FILE_NAME:everestpy>
|
||||
WORKING_DIRECTORY
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/framework
|
||||
DEPENDS
|
||||
everestpy
|
||||
COMMENT
|
||||
"Creating symlink to build everestpy"
|
||||
)
|
||||
|
||||
install(
|
||||
TARGETS everestpy
|
||||
LIBRARY
|
||||
DESTINATION ${EVERESTPY_LIB_INSTALL_DIR}
|
||||
)
|
||||
|
||||
install(
|
||||
FILES
|
||||
framework/__init__.py
|
||||
DESTINATION ${EVERESTPY_LIB_INSTALL_DIR}
|
||||
)
|
||||
@@ -0,0 +1,173 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
#include <pybind11/functional.h>
|
||||
#include <pybind11/pybind11.h>
|
||||
#include <pybind11/stl.h>
|
||||
#include <pybind11/stl/filesystem.h>
|
||||
#include <pybind11_json/pybind11_json.hpp>
|
||||
|
||||
#include "misc.hpp"
|
||||
#include "module.hpp"
|
||||
|
||||
#include <utils/error.hpp>
|
||||
#include <utils/error/error_factory.hpp>
|
||||
#include <utils/error/error_state_monitor.hpp>
|
||||
|
||||
namespace py = pybind11;
|
||||
|
||||
PYBIND11_MODULE(everestpy, m) {
|
||||
|
||||
// FIXME (aw): add m.doc?
|
||||
py::class_<RuntimeSession>(m, "RuntimeSession")
|
||||
.def(py::init<>())
|
||||
.def(py::init<const std::string&, const std::string&>());
|
||||
|
||||
py::class_<ModuleInfo::Paths>(m, "ModuleInfoPaths")
|
||||
.def_readonly("etc", &ModuleInfo::Paths::etc)
|
||||
.def_readonly("libexec", &ModuleInfo::Paths::libexec)
|
||||
.def_readonly("share", &ModuleInfo::Paths::share);
|
||||
|
||||
py::class_<ModuleInfo>(m, "ModuleInfo")
|
||||
.def_readonly("name", &ModuleInfo::name)
|
||||
.def_readonly("authors", &ModuleInfo::authors)
|
||||
.def_readonly("license", &ModuleInfo::license)
|
||||
.def_readonly("id", &ModuleInfo::id)
|
||||
.def_readonly("paths", &ModuleInfo::paths)
|
||||
.def_readonly("telemetry_enabled", &ModuleInfo::telemetry_enabled);
|
||||
|
||||
auto error_submodule = m.def_submodule("error");
|
||||
|
||||
py::enum_<Everest::error::Severity>(error_submodule, "Severity")
|
||||
.value("Low", Everest::error::Severity::Low)
|
||||
.value("Medium", Everest::error::Severity::Medium)
|
||||
.value("High", Everest::error::Severity::High)
|
||||
.export_values();
|
||||
|
||||
py::class_<ImplementationIdentifier>(error_submodule, "ImplementationIdentifier")
|
||||
.def(py::init<const std::string&, const std::string&, std::optional<Mapping>>())
|
||||
.def_readwrite("module_id", &ImplementationIdentifier::module_id)
|
||||
.def_readwrite("implementation_id", &ImplementationIdentifier::implementation_id)
|
||||
.def_readwrite("mapping", &ImplementationIdentifier::mapping);
|
||||
|
||||
py::class_<Everest::error::UUID>(error_submodule, "UUID")
|
||||
.def(py::init<>())
|
||||
.def(py::init<const std::string&>())
|
||||
.def_readwrite("uuid", &Everest::error::UUID::uuid);
|
||||
|
||||
py::enum_<Everest::error::State>(error_submodule, "State")
|
||||
.value("Active", Everest::error::State::Active)
|
||||
.value("ClearedByModule", Everest::error::State::ClearedByModule)
|
||||
.value("ClearedByReboot", Everest::error::State::ClearedByReboot)
|
||||
.export_values();
|
||||
|
||||
py::class_<Everest::error::Error>(error_submodule, "Error")
|
||||
.def(py::init<>())
|
||||
.def_readwrite("type", &Everest::error::Error::type)
|
||||
.def_readwrite("sub_type", &Everest::error::Error::sub_type)
|
||||
.def_readwrite("description", &Everest::error::Error::description)
|
||||
.def_readwrite("message", &Everest::error::Error::message)
|
||||
.def_readwrite("severity", &Everest::error::Error::severity)
|
||||
.def_readwrite("origin", &Everest::error::Error::origin)
|
||||
.def_readwrite("timestamp", &Everest::error::Error::timestamp)
|
||||
.def_readwrite("uuid", &Everest::error::Error::uuid)
|
||||
.def_readwrite("state", &Everest::error::Error::state);
|
||||
|
||||
py::class_<Everest::error::ErrorStateMonitor::StateCondition>(error_submodule, "ErrorStateCondition")
|
||||
.def(py::init<std::string, std::string, bool>())
|
||||
.def_readwrite("type", &Everest::error::ErrorStateMonitor::StateCondition::type)
|
||||
.def_readwrite("sub_type", &Everest::error::ErrorStateMonitor::StateCondition::sub_type)
|
||||
.def_readwrite("active", &Everest::error::ErrorStateMonitor::StateCondition::active);
|
||||
|
||||
py::class_<Everest::error::ErrorStateMonitor, std::shared_ptr<Everest::error::ErrorStateMonitor>>(
|
||||
error_submodule, "ErrorStateMonitor")
|
||||
.def("is_error_active", &Everest::error::ErrorStateMonitor::is_error_active)
|
||||
.def("is_condition_satisfied", py::overload_cast<const Everest::error::ErrorStateMonitor::StateCondition&>(
|
||||
&Everest::error::ErrorStateMonitor::is_condition_satisfied, py::const_))
|
||||
.def("is_condition_satisfied",
|
||||
py::overload_cast<const std::list<Everest::error::ErrorStateMonitor::StateCondition>&>(
|
||||
&Everest::error::ErrorStateMonitor::is_condition_satisfied, py::const_));
|
||||
|
||||
py::class_<Everest::error::ErrorFactory, std::shared_ptr<Everest::error::ErrorFactory>>(error_submodule,
|
||||
"ErrorFactory")
|
||||
.def("create_error", py::overload_cast<>(&Everest::error::ErrorFactory::create_error, py::const_))
|
||||
.def("create_error",
|
||||
py::overload_cast<const Everest::error::ErrorType&, const Everest::error::ErrorSubType&,
|
||||
const std::string&>(&Everest::error::ErrorFactory::create_error, py::const_))
|
||||
.def("create_error", py::overload_cast<const Everest::error::ErrorType&, const Everest::error::ErrorSubType&,
|
||||
const std::string&, Everest::error::Severity>(
|
||||
&Everest::error::ErrorFactory::create_error, py::const_))
|
||||
.def("create_error", py::overload_cast<const Everest::error::ErrorType&, const Everest::error::ErrorSubType&,
|
||||
const std::string&, Everest::error::State>(
|
||||
&Everest::error::ErrorFactory::create_error, py::const_))
|
||||
.def("create_error", py::overload_cast<const Everest::error::ErrorType&, const Everest::error::ErrorSubType&,
|
||||
const std::string&, Everest::error::Severity, Everest::error::State>(
|
||||
&Everest::error::ErrorFactory::create_error, py::const_));
|
||||
|
||||
py::class_<Interface>(m, "Interface")
|
||||
.def_readonly("variables", &Interface::variables)
|
||||
.def_readonly("commands", &Interface::commands)
|
||||
.def_readonly("errors", &Interface::errors);
|
||||
|
||||
py::class_<Fulfillment>(m, "Fulfillment")
|
||||
.def_readonly("module_id", &Fulfillment::module_id)
|
||||
.def_readonly("implementation_id", &Fulfillment::implementation_id);
|
||||
|
||||
py::class_<ModuleSetup::Configurations>(m, "ModuleSetupConfigurations")
|
||||
.def_readonly("implementations", &ModuleSetup::Configurations::implementations)
|
||||
.def_readonly("module", &ModuleSetup::Configurations::module);
|
||||
|
||||
py::class_<ModuleSetup>(m, "ModuleSetup")
|
||||
.def_readonly("configs", &ModuleSetup::configs)
|
||||
.def_readonly("connections", &ModuleSetup::connections);
|
||||
|
||||
py::class_<Module>(m, "Module")
|
||||
.def(py::init<const RuntimeSession&>())
|
||||
.def(py::init<const std::string&, const RuntimeSession&>())
|
||||
.def("say_hello", &Module::say_hello)
|
||||
.def("init_done", py::overload_cast<>(&Module::init_done))
|
||||
.def("init_done", py::overload_cast<const std::function<void()>&>(&Module::init_done))
|
||||
.def("call_command", &Module::call_command)
|
||||
.def("publish_variable", &Module::publish_variable)
|
||||
.def("implement_command", &Module::implement_command)
|
||||
.def("subscribe_variable", &Module::subscribe_variable)
|
||||
.def("raise_error", &Module::raise_error)
|
||||
.def("clear_error",
|
||||
py::overload_cast<const std::string&, const Everest::error::ErrorType&>(&Module::clear_error),
|
||||
py::arg("impl_id"), py::arg("type"))
|
||||
.def("clear_error",
|
||||
py::overload_cast<const std::string&, const Everest::error::ErrorType&,
|
||||
const Everest::error::ErrorSubType&>(&Module::clear_error),
|
||||
py::arg("impl_id"), py::arg("type"), py::arg("sub_type"))
|
||||
.def("clear_all_errors_of_impl", py::overload_cast<const std::string&>(&Module::clear_all_errors_of_impl),
|
||||
py::arg("impl_id"))
|
||||
.def("clear_all_errors_of_impl",
|
||||
py::overload_cast<const std::string&, const Everest::error::ErrorType&>(&Module::clear_all_errors_of_impl),
|
||||
py::arg("impl_id"), py::arg("type"))
|
||||
.def("get_error_state_monitor_impl", &Module::get_error_state_monitor_impl)
|
||||
.def("get_error_factory", &Module::get_error_factory)
|
||||
.def("subscribe_error", &Module::subscribe_error)
|
||||
.def("subscribe_all_errors", &Module::subscribe_all_errors)
|
||||
.def("get_error_state_monitor_req", &Module::get_error_state_monitor_req)
|
||||
.def_property_readonly("fulfillments", &Module::get_fulfillments)
|
||||
.def_property_readonly("implementations", &Module::get_implementations)
|
||||
.def_property_readonly("requirements", &Module::get_requirements)
|
||||
.def_property_readonly("info", &Module::get_info);
|
||||
|
||||
auto log_submodule = m.def_submodule("log");
|
||||
log_submodule.def("update_process_name",
|
||||
[](const std::string& process_name) { Everest::Logging::update_process_name(process_name); });
|
||||
|
||||
log_submodule.def("verbose", [](const std::string& message) { EVLOG_verbose << message; });
|
||||
log_submodule.def("debug", [](const std::string& message) { EVLOG_debug << message; });
|
||||
log_submodule.def("info", [](const std::string& message) { EVLOG_info << message; });
|
||||
log_submodule.def("warning", [](const std::string& message) { EVLOG_warning << message; });
|
||||
log_submodule.def("error", [](const std::string& message) { EVLOG_error << message; });
|
||||
log_submodule.def("critical", [](const std::string& message) { EVLOG_critical << message; });
|
||||
|
||||
m.attr("__version__") = "0.25.0";
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
__version__ = '0.25.0'
|
||||
|
||||
try:
|
||||
from .everestpy import *
|
||||
except ImportError:
|
||||
from everestpy import *
|
||||
@@ -0,0 +1,127 @@
|
||||
from pathlib import Path
|
||||
from typing import Callable, overload
|
||||
from . import error
|
||||
|
||||
class ModuleInfoPaths:
|
||||
@property
|
||||
def etc(self) -> Path: ...
|
||||
|
||||
@property
|
||||
def libexec(self) -> Path: ...
|
||||
|
||||
@property
|
||||
def share(self) -> Path: ...
|
||||
|
||||
|
||||
class Interface:
|
||||
@property
|
||||
def commands(self) -> list[str]: ...
|
||||
|
||||
@property
|
||||
def variables(self) -> list[str]: ...
|
||||
|
||||
|
||||
class ModuleInfo:
|
||||
@property
|
||||
def authors(self) -> list[str]: ...
|
||||
|
||||
@property
|
||||
def license(self) -> str: ...
|
||||
|
||||
@property
|
||||
def id(self) -> str: ...
|
||||
|
||||
@property
|
||||
def name(self) -> str: ...
|
||||
|
||||
@property
|
||||
def paths(self) -> ModuleInfoPaths: ...
|
||||
|
||||
|
||||
class ModuleSetupConfigurations:
|
||||
@property
|
||||
def implementations(self) -> dict[str, dict]: ...
|
||||
|
||||
@property
|
||||
def module(self) -> dict: ...
|
||||
|
||||
|
||||
class ModuleSetup:
|
||||
@property
|
||||
def configs(self) -> ModuleSetupConfigurations: ...
|
||||
|
||||
@property
|
||||
def connections(self) -> dict[str, list[Fulfillment]]: ...
|
||||
|
||||
|
||||
class Fulfillment:
|
||||
@property
|
||||
def module_id(self) -> str: ...
|
||||
|
||||
@property
|
||||
def implementation_id(self) -> str: ...
|
||||
|
||||
|
||||
class RuntimeSession:
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(self, prefix: str, config_file_path: str) -> None: ...
|
||||
|
||||
|
||||
class Module:
|
||||
@overload
|
||||
def __init__(self, session: RuntimeSession) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(self, module_id: str, session: RuntimeSession) -> None: ...
|
||||
|
||||
def say_hello(self) -> ModuleSetup: ...
|
||||
|
||||
@overload
|
||||
def init_done(self) -> None: ...
|
||||
|
||||
@overload
|
||||
def init_done(self, on_ready_handler: Callable[[], None]) -> None: ...
|
||||
|
||||
def call_command(self, fulfillment: Fulfillment,
|
||||
command_name: str, args: dict) -> None: ...
|
||||
|
||||
def publish_variable(self, implementation_id: str,
|
||||
variable_name: str, value: dict) -> None: ...
|
||||
|
||||
def implement_command(self, implementation_id: str, command_name: str,
|
||||
handler: Callable[[dict], dict]) -> None: ...
|
||||
def subscribe_variable(self, fulfillment: Fulfillment,
|
||||
variable_name: str, callback: Callable[[dict], None]) -> None: ...
|
||||
def raise_error(self, implementation_id: str, error: error.Error) -> None: ...
|
||||
def clear_error(self, implementation_id: str, type: str) -> None: ...
|
||||
def clear_error(self, implementation_id: str, type: str, sub_type: str) -> None: ...
|
||||
def clear_all_errors_of_impl(self, implementation_id: str) -> None: ...
|
||||
def clear_all_errors_of_impl(self, implementation_id: str, type: str) -> None: ...
|
||||
def get_error_state_monitor_impl(self, implementation_id: str) -> error.ErrorStateMonitor: ...
|
||||
def get_error_factory(self, implementation_id: str) -> error.ErrorFactory: ...
|
||||
def subscribe_error(
|
||||
self,
|
||||
fulfillment: Fulfillment,
|
||||
error_type: str,
|
||||
callback: error.ErrorCallback,
|
||||
clear_callback: error.ErrorCallback
|
||||
) -> None: ...
|
||||
def subscribe_all_errors(
|
||||
self,
|
||||
fulfillment: Fulfillment,
|
||||
callback: error.ErrorCallback,
|
||||
clear_callback: error.ErrorCallback
|
||||
) -> None: ...
|
||||
def get_error_state_monitor_req(self, fulfillment: Fulfillment) -> error.ErrorStateMonitor: ...
|
||||
|
||||
@property
|
||||
def requirements(self) -> dict[str, Interface]: ...
|
||||
|
||||
@property
|
||||
def implementations(self) -> dict[str, Interface]: ...
|
||||
|
||||
@property
|
||||
def info(self) -> ModuleInfo: ...
|
||||
@@ -0,0 +1,113 @@
|
||||
from enum import Enum
|
||||
from typing import Callable, overload, Optional
|
||||
|
||||
class Severity(Enum):
|
||||
Low = "Low",
|
||||
Medium = "Medium",
|
||||
High = "High"
|
||||
|
||||
class Mapping:
|
||||
@overload
|
||||
def __init__(self, evse: int) -> None ...
|
||||
|
||||
@overload
|
||||
def __init__(self, evse: int, connector: int) -> None ...
|
||||
|
||||
@property
|
||||
def evse(self) -> int ...
|
||||
|
||||
@property
|
||||
def connector(self) -> Optional[int] ...
|
||||
|
||||
class ImplementationIdentifier:
|
||||
def __init__(self, module_id: str, implementation_id: str, mapping: Optional[Mapping]) -> None: ...
|
||||
|
||||
@property
|
||||
def module_id(self) -> str: ...
|
||||
|
||||
@property
|
||||
def implementation_id(self) -> str: ...
|
||||
|
||||
@property
|
||||
def mapping(self) -> Optional[Mapping]: ...
|
||||
|
||||
class UUID:
|
||||
@overload
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
@overload
|
||||
def __init__(self, uuid: str) -> None: ...
|
||||
|
||||
class State(Enum):
|
||||
Active = "Active",
|
||||
ClearedByModule = "ClearedByModule",
|
||||
ClearedByReboot = "ClearedByReboot"
|
||||
|
||||
class Error:
|
||||
def __init__(self) -> None: ...
|
||||
|
||||
@property
|
||||
def type(self) -> str: ...
|
||||
|
||||
@property
|
||||
def sub_type(self) -> str: ...
|
||||
|
||||
@property
|
||||
def description(self) -> str: ...
|
||||
|
||||
@property
|
||||
def message(self) -> str: ...
|
||||
|
||||
@property
|
||||
def severity(self) -> Severity: ...
|
||||
|
||||
@property
|
||||
def origin(self) -> ImplementationIdentifier: ...
|
||||
|
||||
@property
|
||||
def timestamp(self) -> int: ...
|
||||
|
||||
@property
|
||||
def uuid(self) -> UUID: ...
|
||||
|
||||
@property
|
||||
def state(self) -> State: ...
|
||||
|
||||
ErrorCallback = Callable[[Error], None]
|
||||
|
||||
class ErrorStateCondition:
|
||||
def __init__(self, type: str, sub_type: str, active: bool) -> None: ...
|
||||
|
||||
@property
|
||||
def type(self) -> str: ...
|
||||
|
||||
@property
|
||||
def sub_type(self) -> str: ...
|
||||
|
||||
@property
|
||||
def active(self) -> bool: ...
|
||||
|
||||
class ErrorStateMonitor:
|
||||
def is_error_active(self, type: str, sub_type: str) -> bool: ...
|
||||
|
||||
@overload
|
||||
def is_condition_satisfied(self, condition: ErrorStateCondition) -> bool: ...
|
||||
|
||||
@overload
|
||||
def is_condition_satisfied(self, condition: list[ErrorStateCondition]) -> bool: ...
|
||||
|
||||
class ErrorFactory:
|
||||
@overload
|
||||
def create_error(self) -> Error: ...
|
||||
|
||||
@overload
|
||||
def create_error(self, type: str, sub_type: str, message: str) -> Error: ...
|
||||
|
||||
@overload
|
||||
def create_error(self, type: str, sub_type: str, message: str, severity: Severity) -> Error: ...
|
||||
|
||||
@overload
|
||||
def create_error(self, type: str, sub_type: str, message: str, state: State) -> Error: ...
|
||||
|
||||
@overload
|
||||
def create_error(self, type: str, sub_type: str, message: str, severity: Severity, state: State) -> Error: ...
|
||||
@@ -0,0 +1,9 @@
|
||||
def verbose(message: str) -> None: ...
|
||||
def debug(message: str) -> None: ...
|
||||
def info(message: str) -> None: ...
|
||||
def warning(message: str) -> None: ...
|
||||
def error(message: str) -> None: ...
|
||||
def critical(message: str) -> None: ...
|
||||
|
||||
|
||||
def update_process_name(name: str) -> None: ...
|
||||
@@ -0,0 +1,164 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
#include "misc.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <utils/filesystem.hpp>
|
||||
|
||||
const std::string get_variable_from_env(const std::string& variable) {
|
||||
const auto value = std::getenv(variable.c_str());
|
||||
if (value == nullptr) {
|
||||
throw std::runtime_error(variable + " needed for everestpy");
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
const std::string get_variable_from_env(const std::string& variable, const std::string& default_value) {
|
||||
const auto value = std::getenv(variable.c_str());
|
||||
if (value == nullptr) {
|
||||
return default_value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
namespace {
|
||||
Everest::MQTTSettings get_mqtt_settings_from_env() {
|
||||
const auto mqtt_everest_prefix =
|
||||
get_variable_from_env(Everest::EV_MQTT_EVEREST_PREFIX, Everest::defaults::MQTT_EVEREST_PREFIX);
|
||||
const auto mqtt_external_prefix =
|
||||
get_variable_from_env(Everest::EV_MQTT_EXTERNAL_PREFIX, Everest::defaults::MQTT_EXTERNAL_PREFIX);
|
||||
const auto mqtt_broker_socket_path = std::getenv(Everest::EV_MQTT_BROKER_SOCKET_PATH);
|
||||
const auto mqtt_broker_host = std::getenv(Everest::EV_MQTT_BROKER_HOST);
|
||||
const auto mqtt_broker_port = std::getenv(Everest::EV_MQTT_BROKER_PORT);
|
||||
|
||||
if (mqtt_broker_socket_path == nullptr) {
|
||||
if (mqtt_broker_host == nullptr or mqtt_broker_port == nullptr) {
|
||||
throw std::runtime_error("If EV_MQTT_BROKER_SOCKET_PATH is not set EV_MQTT_BROKER_HOST and "
|
||||
"EV_MQTT_BROKER_PORT are needed for everestpy");
|
||||
}
|
||||
auto mqtt_broker_port_ = Everest::defaults::MQTT_BROKER_PORT;
|
||||
try {
|
||||
mqtt_broker_port_ = std::stoul(mqtt_broker_port);
|
||||
} catch (...) {
|
||||
EVLOG_warning << "Could not parse MQTT broker port, using default: " << mqtt_broker_port_;
|
||||
}
|
||||
return Everest::create_mqtt_settings(mqtt_broker_host, mqtt_broker_port_, mqtt_everest_prefix,
|
||||
mqtt_external_prefix);
|
||||
} else {
|
||||
return Everest::create_mqtt_settings(mqtt_broker_socket_path, mqtt_everest_prefix, mqtt_external_prefix);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
/// This is just kept for compatibility
|
||||
RuntimeSession::RuntimeSession(const std::string& prefix, const std::string& config_file) {
|
||||
EVLOG_warning
|
||||
<< "everestpy: Usage of the old RuntimeSession ctor detected, config should be loaded via MQTT not via "
|
||||
"the provided config_file. For this please set the appropriate environment variables and call "
|
||||
"RuntimeSession()";
|
||||
|
||||
// We extract the settings from the config file so everest-testing doesn't break
|
||||
const auto ms = Everest::ManagerSettings(prefix, config_file);
|
||||
|
||||
Everest::Logging::init(ms.runtime_settings.logging_config_file.string());
|
||||
|
||||
this->mqtt_settings = ms.mqtt_settings;
|
||||
}
|
||||
|
||||
RuntimeSession::RuntimeSession() {
|
||||
const auto module_id = get_variable_from_env("EV_MODULE");
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
const fs::path logging_config_file =
|
||||
Everest::assert_file(get_variable_from_env("EV_LOG_CONF_FILE"), "Default logging config");
|
||||
Everest::Logging::init(logging_config_file.string(), module_id);
|
||||
|
||||
this->mqtt_settings = get_mqtt_settings_from_env();
|
||||
}
|
||||
|
||||
ModuleSetup create_setup_from_config(const std::string& module_id, Everest::Config& config) {
|
||||
ModuleSetup setup;
|
||||
|
||||
const std::string& module_name = config.get_module_name(module_id);
|
||||
const auto& module_manifest = config.get_manifests().at(module_name);
|
||||
|
||||
// setup connections
|
||||
for (const auto& requirement : module_manifest.at("requires").items()) {
|
||||
const auto& requirement_id = requirement.key();
|
||||
const auto fulfillments = config.resolve_requirement(module_id, requirement_id);
|
||||
|
||||
// Make sure we store the index information in our Fulfillment structures
|
||||
std::vector<Fulfillment> indexed_fulfillments;
|
||||
indexed_fulfillments.reserve(fulfillments.size());
|
||||
for (size_t ii = 0; ii != fulfillments.size(); ++ii) {
|
||||
Fulfillment indexed_fulfillment = fulfillments.at(ii);
|
||||
indexed_fulfillment.requirement.index = ii;
|
||||
EVLOG_verbose << "Setting up " << ii << " implementation_id=" << indexed_fulfillment.implementation_id
|
||||
<< ", module_id=" << indexed_fulfillment.module_id;
|
||||
indexed_fulfillments.emplace_back(indexed_fulfillment);
|
||||
}
|
||||
|
||||
setup.connections[requirement_id] = indexed_fulfillments;
|
||||
}
|
||||
|
||||
const auto& module_config = config.get_module_config();
|
||||
|
||||
for (const auto& [impl_id, configuration_parameters] : module_config.configuration_parameters) {
|
||||
json json_config_map;
|
||||
for (const auto& config_param : configuration_parameters) {
|
||||
json_config_map[config_param.name] = config_param.value; // implicit conversion to json
|
||||
}
|
||||
if (impl_id == "!module") {
|
||||
setup.configs.module = json_config_map;
|
||||
continue;
|
||||
}
|
||||
|
||||
setup.configs.implementations.emplace(impl_id, json_config_map);
|
||||
}
|
||||
|
||||
return setup;
|
||||
}
|
||||
|
||||
Interface create_everest_interface_from_definition(const json& def) {
|
||||
Interface intf;
|
||||
if (def.contains("cmds")) {
|
||||
const auto& cmds = def.at("cmds");
|
||||
intf.commands.reserve(cmds.size());
|
||||
|
||||
for (const auto& cmd : cmds.items()) {
|
||||
intf.commands.push_back(cmd.key());
|
||||
}
|
||||
}
|
||||
|
||||
if (def.contains("vars")) {
|
||||
const auto& vars = def.at("vars");
|
||||
intf.variables.reserve(vars.size());
|
||||
|
||||
for (const auto& var : vars.items()) {
|
||||
intf.variables.push_back(var.key());
|
||||
}
|
||||
}
|
||||
|
||||
if (def.contains("errors")) {
|
||||
const auto& errors = def.at("errors");
|
||||
|
||||
std::size_t errors_size = 0;
|
||||
for (const auto& error_namespace_it : errors.items()) {
|
||||
errors_size += error_namespace_it.value().size();
|
||||
}
|
||||
intf.errors.reserve(errors_size);
|
||||
|
||||
for (const auto& error_namespace_it : errors.items()) {
|
||||
for (const auto& error_name_it : error_namespace_it.value().items()) {
|
||||
intf.errors.push_back(error_namespace_it.key() + "/" + error_name_it.key());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return intf;
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef EVERESTPY_MISC_HPP
|
||||
#define EVERESTPY_MISC_HPP
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <framework/runtime.hpp>
|
||||
#include <utils/config/mqtt_settings.hpp>
|
||||
#include <utils/config/types.hpp>
|
||||
#include <utils/types.hpp>
|
||||
|
||||
const std::string get_variable_from_env(const std::string& variable);
|
||||
const std::string get_variable_from_env(const std::string& variable, const std::string& default_value);
|
||||
|
||||
class RuntimeSession {
|
||||
public:
|
||||
RuntimeSession(const std::string& prefix, const std::string& config_file);
|
||||
|
||||
RuntimeSession();
|
||||
|
||||
const Everest::MQTTSettings& get_mqtt_settings() const {
|
||||
return mqtt_settings;
|
||||
}
|
||||
|
||||
private:
|
||||
Everest::MQTTSettings mqtt_settings;
|
||||
};
|
||||
|
||||
struct Interface {
|
||||
std::vector<std::string> variables;
|
||||
std::vector<std::string> commands;
|
||||
std::vector<std::string> errors;
|
||||
};
|
||||
|
||||
Interface create_everest_interface_from_definition(const json& def);
|
||||
|
||||
struct ModuleSetup {
|
||||
struct Configurations {
|
||||
std::map<std::string, json> implementations;
|
||||
json module;
|
||||
};
|
||||
|
||||
Configurations configs;
|
||||
|
||||
std::map<std::string, std::vector<Fulfillment>> connections;
|
||||
};
|
||||
|
||||
ModuleSetup create_setup_from_config(const std::string& module_id, Everest::Config& config);
|
||||
|
||||
#endif // EVERESTPY_MISC_HPP
|
||||
@@ -0,0 +1,145 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
#include "module.hpp"
|
||||
|
||||
#include <pybind11/pybind11.h>
|
||||
|
||||
#include <utils/error/error_factory.hpp>
|
||||
#include <utils/error/error_manager_impl.hpp>
|
||||
#include <utils/error/error_manager_req.hpp>
|
||||
#include <utils/error/error_state_monitor.hpp>
|
||||
|
||||
std::unique_ptr<Everest::Everest>
|
||||
Module::create_everest_instance(const std::string& module_id, const Everest::Config& config,
|
||||
const Everest::RuntimeSettings& rs,
|
||||
std::shared_ptr<Everest::MQTTAbstraction> mqtt_abstraction) {
|
||||
return std::make_unique<Everest::Everest>(module_id, config, rs.validate_schema, mqtt_abstraction,
|
||||
rs.telemetry_prefix, rs.telemetry_enabled, rs.forward_exceptions);
|
||||
}
|
||||
|
||||
Module::Module(const RuntimeSession& session) : Module(get_variable_from_env("EV_MODULE"), session) {
|
||||
}
|
||||
|
||||
Module::Module(const std::string& module_id_, const RuntimeSession& session_) :
|
||||
module_id(module_id_), session(session_), start_time(std::chrono::steady_clock::now()) {
|
||||
|
||||
this->mqtt_abstraction =
|
||||
std::shared_ptr<Everest::MQTTAbstraction>(Everest::make_mqtt_abstraction(session.get_mqtt_settings()));
|
||||
this->mqtt_abstraction->connect();
|
||||
this->mqtt_abstraction->spawn_main_loop_thread();
|
||||
|
||||
const auto result = Everest::get_module_config(this->mqtt_abstraction, module_id);
|
||||
|
||||
Everest::RuntimeSettings result_settings = result.at("settings");
|
||||
this->rs = std::make_unique<Everest::RuntimeSettings>(std::move(result_settings));
|
||||
|
||||
this->config_ = std::make_unique<Everest::Config>(session.get_mqtt_settings(), result);
|
||||
|
||||
const auto& config = get_config();
|
||||
|
||||
this->handle = create_everest_instance(module_id, config, *this->rs, this->mqtt_abstraction);
|
||||
|
||||
// determine the fulfillments for our requirements
|
||||
const auto& module_name = config.get_module_name(this->module_id);
|
||||
const auto module_manifest = config.get_manifests().at(module_name);
|
||||
|
||||
// setup module info
|
||||
module_info = config.get_module_info(module_id);
|
||||
populate_module_info_path_from_runtime_settings(module_info, *this->rs);
|
||||
|
||||
// setup implementations
|
||||
for (const auto& implementation : module_manifest.at("provides").items()) {
|
||||
const auto& implementation_id = implementation.key();
|
||||
const std::string interface_name = implementation.value().at("interface");
|
||||
const auto& interface_def = config.get_interface_definition(interface_name);
|
||||
implementations.emplace(implementation_id, create_everest_interface_from_definition(interface_def));
|
||||
}
|
||||
|
||||
// setup requirements
|
||||
for (const auto& requirement : module_manifest.at("requires").items()) {
|
||||
const auto& requirement_id = requirement.key();
|
||||
const std::string interface_name = requirement.value().at("interface");
|
||||
const auto& interface_def = config.get_interface_definition(interface_name);
|
||||
requirements.emplace(requirement_id, create_everest_interface_from_definition(interface_def));
|
||||
}
|
||||
}
|
||||
|
||||
ModuleSetup Module::say_hello() {
|
||||
handle->connect();
|
||||
handle->spawn_main_loop_thread();
|
||||
return create_setup_from_config(module_id, get_config());
|
||||
}
|
||||
|
||||
json Module::call_command(const Fulfillment& fulfillment, const std::string& cmd_name, json args) {
|
||||
// FIXME (aw): we're releasing the GIL here, because the mqtt thread will want to aquire it when calling the
|
||||
// callbacks
|
||||
const pybind11::gil_scoped_release release;
|
||||
const auto& result = handle->call_cmd(fulfillment.requirement, cmd_name, std::move(args));
|
||||
return result;
|
||||
}
|
||||
|
||||
void Module::publish_variable(const std::string& impl_id, const std::string& var_name, json value) {
|
||||
// NOTE (aw): publish_var just sends output directly via mqtt, so we don't need to release here as opposed to
|
||||
// call_command
|
||||
handle->publish_var(impl_id, var_name, std::move(value));
|
||||
}
|
||||
|
||||
void Module::implement_command(const std::string& impl_id, const std::string& cmd_name,
|
||||
std::function<json(json)> command_handler) {
|
||||
auto& handler = command_handlers.emplace_back(std::move(command_handler));
|
||||
|
||||
handle->provide_cmd(impl_id, cmd_name, [&handler](json args) { return handler(std::move(args)); });
|
||||
}
|
||||
|
||||
void Module::subscribe_variable(const Fulfillment& fulfillment, const std::string& var_name,
|
||||
std::function<void(json)> subscription_callback) {
|
||||
|
||||
auto& callback = subscription_callbacks.emplace_back(std::move(subscription_callback));
|
||||
handle->subscribe_var(fulfillment.requirement, var_name, [&callback](json args) { callback(std::move(args)); });
|
||||
}
|
||||
|
||||
void Module::raise_error(const std::string& impl_id, const Everest::error::Error& error) {
|
||||
handle->get_error_manager_impl(impl_id)->raise_error(error);
|
||||
}
|
||||
|
||||
void Module::clear_error(const std::string& impl_id, const Everest::error::ErrorType& type) {
|
||||
handle->get_error_manager_impl(impl_id)->clear_error(type);
|
||||
}
|
||||
|
||||
void Module::clear_error(const std::string& impl_id, const Everest::error::ErrorType& type,
|
||||
const Everest::error::ErrorSubType& sub_type) {
|
||||
handle->get_error_manager_impl(impl_id)->clear_error(type, sub_type);
|
||||
}
|
||||
|
||||
void Module::clear_all_errors_of_impl(const std::string& impl_id) {
|
||||
handle->get_error_manager_impl(impl_id)->clear_all_errors();
|
||||
}
|
||||
|
||||
void Module::clear_all_errors_of_impl(const std::string& impl_id, const Everest::error::ErrorType& type) {
|
||||
handle->get_error_manager_impl(impl_id)->clear_all_errors(type);
|
||||
}
|
||||
|
||||
std::shared_ptr<Everest::error::ErrorStateMonitor>
|
||||
Module::get_error_state_monitor_impl(const std::string& impl_id) const {
|
||||
return handle->get_error_state_monitor_impl(impl_id);
|
||||
}
|
||||
|
||||
std::shared_ptr<Everest::error::ErrorFactory> Module::get_error_factory(const std::string& impl_id) const {
|
||||
return handle->get_error_factory(impl_id);
|
||||
}
|
||||
|
||||
void Module::subscribe_error(const Fulfillment& fulfillment, const Everest::error::ErrorType& type,
|
||||
const Everest::error::ErrorCallback& callback,
|
||||
const Everest::error::ErrorCallback& clear_callback) {
|
||||
handle->get_error_manager_req(fulfillment.requirement)->subscribe_error(type, callback, clear_callback);
|
||||
}
|
||||
|
||||
void Module::subscribe_all_errors(const Fulfillment& fulfillment, const Everest::error::ErrorCallback& callback,
|
||||
const Everest::error::ErrorCallback& clear_callback) {
|
||||
handle->get_error_manager_req(fulfillment.requirement)->subscribe_all_errors(callback, clear_callback);
|
||||
}
|
||||
|
||||
std::shared_ptr<Everest::error::ErrorStateMonitor>
|
||||
Module::get_error_state_monitor_req(const Fulfillment& fulfillment) const {
|
||||
return handle->get_error_state_monitor_req(fulfillment.requirement);
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef EVERESTPY_MODULE_HPP
|
||||
#define EVERESTPY_MODULE_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <deque>
|
||||
#include <functional>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <framework/everest.hpp>
|
||||
|
||||
#include "misc.hpp"
|
||||
|
||||
class Module {
|
||||
public:
|
||||
Module(const RuntimeSession&);
|
||||
Module(const std::string&, const RuntimeSession&);
|
||||
|
||||
ModuleSetup say_hello();
|
||||
|
||||
void init_done(const std::function<void()>& on_ready_handler) {
|
||||
this->handle->check_code();
|
||||
|
||||
if (on_ready_handler) {
|
||||
handle->register_on_ready_handler(on_ready_handler);
|
||||
}
|
||||
|
||||
const auto end_time = std::chrono::steady_clock::now();
|
||||
EVLOG_info << "Module " << fmt::format(Everest::TERMINAL_STYLE_BLUE, "{}", this->module_id) << " initialized ["
|
||||
<< std::chrono::duration_cast<std::chrono::milliseconds>(end_time - this->start_time).count()
|
||||
<< "ms]";
|
||||
|
||||
handle->signal_ready();
|
||||
}
|
||||
|
||||
void init_done() {
|
||||
init_done(nullptr);
|
||||
}
|
||||
|
||||
Everest::Config& get_config() {
|
||||
return *config_;
|
||||
}
|
||||
|
||||
json call_command(const Fulfillment& fulfillment, const std::string& cmd_name, json args);
|
||||
void publish_variable(const std::string& impl_id, const std::string& var_name, json value);
|
||||
void implement_command(const std::string& impl_id, const std::string& cmd_name, std::function<json(json)> handler);
|
||||
void subscribe_variable(const Fulfillment& fulfillment, const std::string& var_name,
|
||||
std::function<void(json)> callback);
|
||||
void raise_error(const std::string& impl_id, const Everest::error::Error& error);
|
||||
void clear_error(const std::string& impl_id, const Everest::error::ErrorType& type);
|
||||
void clear_error(const std::string& impl_id, const Everest::error::ErrorType& type,
|
||||
const Everest::error::ErrorSubType& sub_type);
|
||||
void clear_all_errors_of_impl(const std::string& impl_id);
|
||||
void clear_all_errors_of_impl(const std::string& impl_id, const Everest::error::ErrorType& type);
|
||||
std::shared_ptr<Everest::error::ErrorStateMonitor> get_error_state_monitor_impl(const std::string& impl_id) const;
|
||||
std::shared_ptr<Everest::error::ErrorFactory> get_error_factory(const std::string& impl_id) const;
|
||||
void subscribe_error(const Fulfillment& fulfillment, const Everest::error::ErrorType& type,
|
||||
const Everest::error::ErrorCallback& callback,
|
||||
const Everest::error::ErrorCallback& clear_callback);
|
||||
void subscribe_all_errors(const Fulfillment& fulfillment, const Everest::error::ErrorCallback& callback,
|
||||
const Everest::error::ErrorCallback& clear_callback);
|
||||
std::shared_ptr<Everest::error::ErrorStateMonitor>
|
||||
get_error_state_monitor_req(const Fulfillment& fulfillment) const;
|
||||
|
||||
const auto& get_fulfillments() const {
|
||||
return fulfillments;
|
||||
}
|
||||
|
||||
const auto& get_info() const {
|
||||
return module_info;
|
||||
}
|
||||
|
||||
const auto& get_requirements() const {
|
||||
return requirements;
|
||||
}
|
||||
|
||||
const auto& get_implementations() const {
|
||||
return implementations;
|
||||
}
|
||||
|
||||
private:
|
||||
const std::string module_id;
|
||||
const RuntimeSession& session;
|
||||
const std::chrono::time_point<std::chrono::steady_clock> start_time;
|
||||
std::unique_ptr<Everest::RuntimeSettings> rs;
|
||||
std::shared_ptr<Everest::MQTTAbstraction> mqtt_abstraction;
|
||||
std::unique_ptr<Everest::Config> config_;
|
||||
|
||||
std::unique_ptr<Everest::Everest> handle;
|
||||
|
||||
// NOTE (aw): we're keeping the handlers local to the module instance and don't pass them by copy-construction
|
||||
// to "external" c/c++ code, so no GIL related problems should appear
|
||||
std::deque<std::function<json(json)>> command_handlers{};
|
||||
std::deque<std::function<void(json)>> subscription_callbacks{};
|
||||
std::deque<std::function<void(json)>> err_susbcription_callbacks{};
|
||||
std::deque<std::function<void(json)>> err_cleared_susbcription_callbacks{};
|
||||
|
||||
static std::unique_ptr<Everest::Everest>
|
||||
create_everest_instance(const std::string& module_id, const Everest::Config& config,
|
||||
const Everest::RuntimeSettings& rs,
|
||||
std::shared_ptr<Everest::MQTTAbstraction> mqtt_abstraction);
|
||||
|
||||
ModuleInfo module_info{};
|
||||
std::map<std::string, Interface> requirements;
|
||||
std::map<std::string, Interface> implementations;
|
||||
std::map<std::string, std::vector<Fulfillment>> fulfillments;
|
||||
};
|
||||
|
||||
#endif // EVERESTPY_MODULE_HPP
|
||||
@@ -0,0 +1,4 @@
|
||||
# The default path to the link dependencies file generated by CMake. Overwritten by CMake, this is a fallback for IDEs.
|
||||
[env.EVEREST_RS_LINK_DEPENDENCIES]
|
||||
value = "../../../build/everestrs-link-dependencies.txt"
|
||||
relative = true
|
||||
@@ -0,0 +1,134 @@
|
||||
set(CXXBRIDGE_VERSION 1.0.194) # Must be kept in sync with `everestrs/Cargo.toml`
|
||||
set(CXXBRIDGE_INSTALL_PATH ${CMAKE_CURRENT_BINARY_DIR}/cargo/bin/cxxbridge)
|
||||
|
||||
# Checks if the cxxbridge binary at CXXBRIDGE_PATH exists and matches the required version.
|
||||
# If not, sets VERSION_MATCHES to FALSE.
|
||||
function(everestrs_check_cxxbridge_version CXXBRIDGE_PATH VERSION_MATCHES)
|
||||
if(NOT CXXBRIDGE_BINARY OR NOT EXISTS ${CXXBRIDGE_BINARY})
|
||||
set(${VERSION_MATCHES} FALSE PARENT_SCOPE)
|
||||
endif()
|
||||
|
||||
execute_process(
|
||||
COMMAND
|
||||
${CXXBRIDGE_BINARY} --version
|
||||
WORKING_DIRECTORY
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
OUTPUT_VARIABLE
|
||||
CXXBRIDGE_VERSION_OUTPUT
|
||||
ERROR_QUIET
|
||||
OUTPUT_STRIP_TRAILING_WHITESPACE
|
||||
)
|
||||
|
||||
if(CXXBRIDGE_VERSION_OUTPUT MATCHES "cxxbridge ${CXXBRIDGE_VERSION}")
|
||||
set(${VERSION_MATCHES} TRUE PARENT_SCOPE)
|
||||
else()
|
||||
set(${VERSION_MATCHES} FALSE PARENT_SCOPE)
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# If cxxbridge is already installed system-wide, check if it's the correct version.
|
||||
find_program(CXXBRIDGE_BINARY cxxbridge)
|
||||
everestrs_check_cxxbridge_version(${CXXBRIDGE_BINARY} CXXBRIDGE_INSTALLED)
|
||||
|
||||
# Otherwise, check if we previously installed it in our build dir.
|
||||
if(NOT CXXBRIDGE_INSTALLED)
|
||||
set(CXXBRIDGE_BINARY ${CXXBRIDGE_INSTALL_PATH})
|
||||
everestrs_check_cxxbridge_version(${CXXBRIDGE_BINARY} CXXBRIDGE_INSTALLED)
|
||||
endif()
|
||||
|
||||
# Either we never had cxxbridge installed, or the version is incorrect. Install the correct version to our build dir.
|
||||
if(NOT CXXBRIDGE_INSTALLED)
|
||||
message(STATUS "Fetching rust cxxbridge cli tool")
|
||||
execute_process(
|
||||
COMMAND
|
||||
${CMAKE_COMMAND} -E env --unset=CARGO_BUILD_TARGET # Ensure we always build for the host
|
||||
cargo install cxxbridge-cmd --force --version ${CXXBRIDGE_VERSION} --root ${CMAKE_CURRENT_BINARY_DIR}/cargo
|
||||
WORKING_DIRECTORY
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
RESULT_VARIABLE
|
||||
CARGO_INSTALL_RESULT
|
||||
OUTPUT_QUIET
|
||||
ERROR_VARIABLE
|
||||
CARGO_INSTALL_ERROR
|
||||
)
|
||||
|
||||
if(NOT CARGO_INSTALL_RESULT)
|
||||
set(CXXBRIDGE_BINARY ${CXXBRIDGE_INSTALL_PATH})
|
||||
else()
|
||||
message(FATAL_ERROR "cargo install cxxbridge-cmd failed with:\n${CARGO_INSTALL_ERROR}")
|
||||
endif()
|
||||
endif()
|
||||
|
||||
set(CXXBRIDGE_OUTPUT_DIR ${CMAKE_CURRENT_BINARY_DIR})
|
||||
set(CXXBRIDGE_HEADER ${CXXBRIDGE_OUTPUT_DIR}/rust/cxx.h)
|
||||
set(EVERESTRS_FFI_HEADER ${CXXBRIDGE_OUTPUT_DIR}/everestrs/src/lib.rs.h)
|
||||
set(EVERESTRS_FFI_SOURCE ${CXXBRIDGE_OUTPUT_DIR}/everestrs/src/lib.rs.cc)
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
${EVERESTRS_FFI_HEADER}
|
||||
${EVERESTRS_FFI_SOURCE}
|
||||
${CXXBRIDGE_HEADER}
|
||||
COMMAND
|
||||
${CMAKE_COMMAND} -E make_directory ${CXXBRIDGE_OUTPUT_DIR}/everestrs/src ${CXXBRIDGE_OUTPUT_DIR}/rust
|
||||
COMMAND
|
||||
${CXXBRIDGE_BINARY} --header -o ${CXXBRIDGE_HEADER}
|
||||
COMMAND
|
||||
${CXXBRIDGE_BINARY} ${CMAKE_CURRENT_SOURCE_DIR}/everestrs/src/lib.rs --header -o ${EVERESTRS_FFI_HEADER}
|
||||
COMMAND
|
||||
${CXXBRIDGE_BINARY} ${CMAKE_CURRENT_SOURCE_DIR}/everestrs/src/lib.rs -o ${EVERESTRS_FFI_SOURCE}
|
||||
DEPENDS
|
||||
everestrs/src/lib.rs
|
||||
COMMENT "Generating cxxbridge glue for everestrs"
|
||||
VERBATIM
|
||||
DEPENDS
|
||||
${CXXBRIDGE_BINARY}
|
||||
)
|
||||
|
||||
add_library(everestrs_sys STATIC
|
||||
${EVERESTRS_FFI_SOURCE}
|
||||
everestrs/src/everestrs_sys.cpp
|
||||
)
|
||||
add_library(everest::everestrs_sys ALIAS everestrs_sys)
|
||||
|
||||
set_property(
|
||||
TARGET
|
||||
everestrs_sys
|
||||
PROPERTY
|
||||
EVERESTRS_DIR ${CMAKE_CURRENT_SOURCE_DIR}/everestrs
|
||||
)
|
||||
|
||||
set_property(
|
||||
TARGET
|
||||
everestrs_sys
|
||||
PROPERTY
|
||||
EVERESTRS_BUILD_DIR ${CMAKE_CURRENT_SOURCE_DIR}/everestrs-build
|
||||
)
|
||||
|
||||
target_include_directories(everestrs_sys
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_BINARY_DIR}
|
||||
)
|
||||
|
||||
# This is a requirement that linking works on systems enforcing PIE
|
||||
# FIXME (aw): investicate why this is really necessary
|
||||
set_property(TARGET everestrs_sys PROPERTY POSITION_INDEPENDENT_CODE ON)
|
||||
target_link_libraries(everestrs_sys
|
||||
PRIVATE
|
||||
everest::framework
|
||||
everest::log
|
||||
)
|
||||
|
||||
set(EVERESTRS_LINK_DEPENDENCIES $<TARGET_FILE:everest::everestrs_sys>)
|
||||
get_target_property(EVERESTRS_LINK_TARGETS everest::everestrs_sys LINK_LIBRARIES)
|
||||
foreach(link_target IN LISTS EVERESTRS_LINK_TARGETS)
|
||||
list(APPEND EVERESTRS_LINK_DEPENDENCIES $<TARGET_FILE:${link_target}>)
|
||||
endforeach()
|
||||
|
||||
set_property(
|
||||
TARGET
|
||||
everestrs_sys
|
||||
PROPERTY
|
||||
EVERESTRS_LINK_DEPENDENCIES "${EVERESTRS_LINK_DEPENDENCIES}"
|
||||
)
|
||||
742
tools/EVerest-main/lib/everest/framework/everestrs/Cargo.lock
generated
Normal file
742
tools/EVerest-main/lib/everest/framework/everestrs/Cargo.lock
generated
Normal file
@@ -0,0 +1,742 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.98"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
|
||||
[[package]]
|
||||
name = "argh"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34ff18325c8a36b82f992e533ece1ec9f9a9db446bd1c14d4f936bac88fcd240"
|
||||
dependencies = [
|
||||
"argh_derive",
|
||||
"argh_shared",
|
||||
"rust-fuzzy-search",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argh_derive"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adb7b2b83a50d329d5d8ccc620f5c7064028828538bdf5646acd60dc1f767803"
|
||||
dependencies = [
|
||||
"argh_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "argh_shared"
|
||||
version = "0.1.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a464143cc82dedcdc3928737445362466b7674b5db4e2eb8e869846d6d84f4f6"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f4ac86a9e5bc1e2b3449ab9d7d3a6a405e3d1bb28d7b9be8614f55846ae3766"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed93b9805f8ba930df42c2590f05453d5ec36cbb85d018868a5b24d31f6ac000"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "379026ff283facf611b0ea629334361c4211d1b12ee01024eec1591133b04120"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "0.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
|
||||
|
||||
[[package]]
|
||||
name = "codespan-reporting"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"termcolor",
|
||||
"unicode-width",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxx"
|
||||
version = "1.0.194"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "747d8437319e3a2f43d93b341c137927ca70c0f5dabeea7a005a73665e247c7e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cxx-build",
|
||||
"cxxbridge-cmd",
|
||||
"cxxbridge-flags",
|
||||
"cxxbridge-macro",
|
||||
"foldhash",
|
||||
"link-cplusplus",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxx-build"
|
||||
version = "1.0.194"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b0f4697d190a142477b16aef7da8a99bfdc41e7e8b1687583c0d23a79c7afc1e"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"codespan-reporting",
|
||||
"indexmap",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"scratch",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-cmd"
|
||||
version = "1.0.194"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0956799fa8678d4c50eed028f2de1c0552ae183c76e976cf7ca8c4e36a7c328"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"codespan-reporting",
|
||||
"indexmap",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-flags"
|
||||
version = "1.0.194"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23384a836ab4f0ad98ace7e3955ad2de39de42378ab487dc28d3990392cb283a"
|
||||
|
||||
[[package]]
|
||||
name = "cxxbridge-macro"
|
||||
version = "1.0.194"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6acc6b5822b9526adfb4fc377b67128fdd60aac757cc4a741a6278603f763cf"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "downcast"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "everestrs"
|
||||
version = "0.25.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"cxx",
|
||||
"everestrs-build",
|
||||
"everestrs-derive",
|
||||
"log",
|
||||
"mockall",
|
||||
"mockall_double",
|
||||
"nix",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "everestrs-build"
|
||||
version = "0.25.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"argh",
|
||||
"convert_case",
|
||||
"minijinja",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"serde_yaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "everestrs-derive"
|
||||
version = "0.25.0"
|
||||
dependencies = [
|
||||
"everestrs-build",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "fragile"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.184"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af"
|
||||
|
||||
[[package]]
|
||||
name = "link-cplusplus"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f78c730aaa7d0b9336a299029ea49f9ee53b0ed06e9202e8cb7db9bae7b8c82"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
|
||||
|
||||
[[package]]
|
||||
name = "minijinja"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3287d827e6da221ea11aa173c66b82ab69db27a1b177e8439f730b478bf33a7b"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mockall"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39a6bfcc6c8c7eed5ee98b9c3e33adc726054389233e201c95dab2d41a3839d2"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"downcast",
|
||||
"fragile",
|
||||
"mockall_derive",
|
||||
"predicates",
|
||||
"predicates-tree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mockall_derive"
|
||||
version = "0.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25ca3004c2efe9011bd4e461bd8256445052b9615405b4f7ea43fc8ca5c20898"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mockall_double"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1ca96e5ac35256ae3e13536edd39b172b88f41615e1d7b653c8ad24524113e8"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d6d0705320c1e6ba1d912b5e37cf18071b6c2e9b7fa8215a1e8a7651966f5d3"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "predicates"
|
||||
version = "3.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"predicates-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "predicates-core"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa"
|
||||
|
||||
[[package]]
|
||||
name = "predicates-tree"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c"
|
||||
dependencies = [
|
||||
"predicates-core",
|
||||
"termtree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rust-fuzzy-search"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a157657054ffe556d8858504af8a672a054a6e0bd9e8ee531059100c0fa11bb2"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
|
||||
|
||||
[[package]]
|
||||
name = "scratch"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d68f2ec51b097e4c1a75b681a8bec621909b5e91f15bb7b840c4f2f7b01148b2"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.140"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"memchr",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_yaml"
|
||||
version = "0.9.34+deprecated"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
"unsafe-libyaml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.101"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termtree"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.69"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.52.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tokio-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "2.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
|
||||
|
||||
[[package]]
|
||||
name = "unsafe-libyaml"
|
||||
version = "0.2.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.59.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
@@ -0,0 +1,11 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"everestrs",
|
||||
"everestrs-build",
|
||||
"everestrs-derive",
|
||||
]
|
||||
|
||||
[workspace.dependencies]
|
||||
everestrs-build = { path="everestrs-build" }
|
||||
everestrs-derive = { path="everestrs-derive" }
|
||||
@@ -0,0 +1,46 @@
|
||||
load("@rules_rust//rust:defs.bzl", "rust_binary", "rust_library", "rust_test")
|
||||
|
||||
filegroup(
|
||||
name = "templates",
|
||||
srcs = glob(["jinja/**/*"]),
|
||||
)
|
||||
|
||||
rust_library(
|
||||
name = "everestrs-build",
|
||||
srcs = glob(["src/**/*.rs"], exclude = ["src/bin/**"]),
|
||||
deps = [
|
||||
"@everest_framework_crate_index//:anyhow",
|
||||
"@everest_framework_crate_index//:argh",
|
||||
"@everest_framework_crate_index//:convert_case",
|
||||
"@everest_framework_crate_index//:minijinja",
|
||||
"@everest_framework_crate_index//:serde",
|
||||
"@everest_framework_crate_index//:serde_json",
|
||||
"@everest_framework_crate_index//:serde_yaml",
|
||||
],
|
||||
compile_data = [":templates"],
|
||||
visibility = ["//visibility:public"],
|
||||
edition = "2021",
|
||||
)
|
||||
|
||||
rust_binary(
|
||||
name = "codegen",
|
||||
srcs = glob(["src/bin/**/*.rs"]),
|
||||
deps = [
|
||||
"@everest_framework_crate_index//:anyhow",
|
||||
"@everest_framework_crate_index//:argh",
|
||||
"@everest_framework_crate_index//:convert_case",
|
||||
"@everest_framework_crate_index//:minijinja",
|
||||
"@everest_framework_crate_index//:serde",
|
||||
"@everest_framework_crate_index//:serde_json",
|
||||
"@everest_framework_crate_index//:serde_yaml",
|
||||
":everestrs-build",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
edition = "2021",
|
||||
)
|
||||
|
||||
rust_test(
|
||||
name = "test",
|
||||
crate = ":everestrs-build",
|
||||
edition = "2021",
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
[package]
|
||||
name = "everestrs-build"
|
||||
version = "0.25.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.75"
|
||||
argh = "0.1.12"
|
||||
convert_case = "0.6.0"
|
||||
minijinja = "1.0.8"
|
||||
serde = "1.0.188"
|
||||
serde_json = "1.0.107"
|
||||
serde_yaml = "0.9.25"
|
||||
@@ -0,0 +1,152 @@
|
||||
/// {{trait.description | replace("\n", " ")}}
|
||||
pub(crate) trait {{trait.name | title}}ClientSubscriber: Sync + Send {
|
||||
{% for var in trait.vars %}
|
||||
fn on_{{ var.name | snake }}(&self, context: &Context, value: {{ var.data_type.name }});
|
||||
{% endfor %}
|
||||
|
||||
{%- if trait.errors %}
|
||||
fn on_error_raised(&self, context: &Context, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>);
|
||||
|
||||
fn on_error_cleared(&self, context: &Context, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>);
|
||||
{%- endif %}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "mockall", feature = "trait"))]
|
||||
mockall::mock! {
|
||||
pub(crate) {{trait.name | title}}ClientSubscriber {}
|
||||
impl {{trait.name | title}}ClientSubscriber for {{trait.name | title}}ClientSubscriber {
|
||||
{% for var in trait.vars %}
|
||||
fn on_{{ var.name | snake }}<'a>(&self, context: &Context<'a>, value: {{ var.data_type.name }});
|
||||
{% endfor %}
|
||||
|
||||
{%- if trait.errors %}
|
||||
fn on_error_raised<'a>(&self, context: &Context<'a>, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>);
|
||||
|
||||
fn on_error_cleared<'a>(&self, context: &Context<'a>, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>);
|
||||
{%- endif %}
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_variable_to_{{ trait.name | snake }}(
|
||||
context: &Context,
|
||||
client_subscriber: &dyn {{trait.name | title}}ClientSubscriber,
|
||||
name: &str,
|
||||
value: __serde_json::Value,
|
||||
) -> ::everestrs::Result<()> {
|
||||
match name {
|
||||
{%- for var in trait.vars %}
|
||||
"{{ var.name }}" => {
|
||||
let v: {{ var.data_type.name }} = __serde_json::from_value(value)
|
||||
.map_err(|e| ::everestrs::Error::MessageParsingError(format!("Failed to deserialize variable `{{ var.name }}`: {e:?})")))?;
|
||||
client_subscriber.on_{{ var.name | snake }}(context, v);
|
||||
Ok(())
|
||||
},
|
||||
{%- endfor %}
|
||||
other => Err(::everestrs::Error::MessageParsingError(format!("Unknown variable {other} received.").to_string())),
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_error_to_{{ trait.name | snake }} (
|
||||
context: &Context,
|
||||
client_subscriber: &dyn {{ trait.name | title }}ClientSubscriber,
|
||||
error: ::everestrs::FfiErrorType,
|
||||
raised: bool
|
||||
) {
|
||||
{%- if trait.errors %}
|
||||
// The type is errors::{{ trait.name | snake }}::Error
|
||||
let Ok(v) = __serde_yaml::from_str(&error.error_type) else {
|
||||
everestrs::log::error!("Failed to deserialize error `{}`", error.error_type);
|
||||
return;
|
||||
};
|
||||
|
||||
let error_type = ::everestrs::ErrorType {
|
||||
error_type: v,
|
||||
description: error.description,
|
||||
message: error.message,
|
||||
severity: error.severity,
|
||||
};
|
||||
|
||||
if raised {
|
||||
client_subscriber.on_error_raised(context, error_type);
|
||||
} else {
|
||||
client_subscriber.on_error_cleared(context, error_type);
|
||||
}
|
||||
{%- endif %}
|
||||
}
|
||||
|
||||
pub(crate) mod __mockall_{{trait.name | snake }}_client {
|
||||
|
||||
use super::__serde_json;
|
||||
use super::types;
|
||||
use super::errors;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct {{trait.name | title }}ClientPublisher {
|
||||
pub(super) implementation_id: &'static str,
|
||||
pub(super) runtime: ::std::sync::Weak<::everestrs::Runtime>,
|
||||
pub(super) index: usize,
|
||||
}
|
||||
|
||||
impl {{trait.name | title }}ClientPublisher {
|
||||
{%- for cmd in trait.cmds %}
|
||||
/// {{cmd.description | replace("\n", " ")}}
|
||||
///
|
||||
{%- for arg in cmd.arguments %}
|
||||
/// `{{arg.name}}`: {{arg.description | replace("\n", " ")}}
|
||||
{%- endfor %}
|
||||
{% if cmd.result -%}
|
||||
///
|
||||
/// Returns: {{cmd.result.description | replace("\n", " ")}}
|
||||
{% endif -%}
|
||||
pub(crate) fn {{cmd.name | identifier}}(&self,
|
||||
{%- for arg in cmd.arguments %}
|
||||
{{arg.name | identifier }}: {{arg.data_type.name}},
|
||||
{%- endfor %}
|
||||
) -> ::everestrs::Result<{%- if cmd.result -%}
|
||||
{{cmd.result.data_type.name}}
|
||||
{%- else -%}
|
||||
()
|
||||
{%- endif -%}
|
||||
> {
|
||||
let args = __serde_json::json!({
|
||||
{%- for arg in cmd.arguments %}
|
||||
"{{arg.name}}": {{arg.name | identifier}},
|
||||
{%- endfor %}
|
||||
});
|
||||
let rt = self.runtime.upgrade().ok_or_else(|| {
|
||||
::everestrs::Error::HandlerException(
|
||||
"publisher used after Module was dropped".into(),
|
||||
)
|
||||
})?;
|
||||
rt.call_command(self.implementation_id, self.index, "{{ cmd.name }}", &args)
|
||||
}
|
||||
{% endfor %}
|
||||
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "mockall", not(feature = "trait")))]
|
||||
mockall::mock!{
|
||||
pub(crate) {{trait.name | title }}ClientPublisher {
|
||||
{%- for cmd in trait.cmds %}
|
||||
pub(crate) fn {{cmd.name | identifier}}(&self,
|
||||
{%- for arg in cmd.arguments %}
|
||||
{{arg.name | identifier }}: {{arg.data_type.name}},
|
||||
{%- endfor %}
|
||||
) -> ::everestrs::Result<{%- if cmd.result -%}
|
||||
{{cmd.result.data_type.name}}
|
||||
{%- else -%}
|
||||
()
|
||||
{%- endif -%}
|
||||
>;
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
impl Clone for {{trait.name | title }}ClientPublisher {
|
||||
fn clone(&self) -> Self;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#[cfg_attr(all(feature = "mockall", not(feature = "trait")), mockall_double::double)]
|
||||
pub(crate) use __mockall_{{trait.name | snake }}_client::{{trait.name | title }}ClientPublisher;
|
||||
@@ -0,0 +1,50 @@
|
||||
{% for p_config in provided_config %}
|
||||
/// The configuration for the {{ p_config.name }}.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct {{ p_config.name | title }}Config {
|
||||
{% for config in p_config.config %}
|
||||
/// {{ config.description }}
|
||||
pub(crate) {{ config.name | identifier }}: {{ config.data_type.name }},
|
||||
{% endfor %}
|
||||
}
|
||||
{% endfor %}
|
||||
|
||||
/// The configuration for the module. It also contains the config for all other
|
||||
/// interfaces.
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ModuleConfig {
|
||||
{% for config in module_config %}
|
||||
/// {{ config.description }}
|
||||
pub(crate) {{ config.name | identifier }}: {{ config.data_type.name }},
|
||||
{% endfor %}
|
||||
|
||||
{% for p_config in provided_config %}
|
||||
/// The config for the `{{ p_config.name }}` interface.
|
||||
pub(crate) {{ p_config.name }}_config: {{ p_config.name | title }}Config,
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
/// Returns the config for the whole module.
|
||||
impl Module {
|
||||
pub(crate) fn get_config(&self) -> ModuleConfig {
|
||||
|
||||
let raw_config = self.runtime.get_module_configs();
|
||||
|
||||
{% for p_config in provided_config %}
|
||||
let {{ p_config.name }}_config = {{ p_config.name | title }}Config {
|
||||
{% for config in p_config.config %}
|
||||
{{ config.name | identifier }}: raw_config.get("{{ p_config.name }}").unwrap().get("{{ config.name }}").unwrap().try_into().unwrap(),
|
||||
{% endfor %}
|
||||
};
|
||||
{% endfor %}
|
||||
ModuleConfig {
|
||||
{% for config in module_config %}
|
||||
{{ config.name | identifier }}: raw_config.get("!module").unwrap().get("{{ config.name }}").unwrap().try_into().unwrap(),
|
||||
{% endfor %}
|
||||
|
||||
{% for p_config in provided_config %}
|
||||
{{ p_config.name }}_config,
|
||||
{% endfor %}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{%- for name, errors in involved_errors | items %}
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub mod {{ name | snake }} {
|
||||
use everestrs::serde as __serde;
|
||||
{%- for error_group in errors %}
|
||||
/// The error definition of the {{ name }} interface.
|
||||
/// {{ error_group.error_list.description | replace("\n", " ") }}
|
||||
#[derive(Debug, Clone, PartialEq, __serde::Serialize, __serde::Deserialize)]
|
||||
#[serde(crate = "__serde")]
|
||||
pub enum {{ error_group.name | title }}Error {
|
||||
{%- for error_entry in error_group.error_list.errors %}
|
||||
/// {{ error_entry.description | replace("\n", " ")}}
|
||||
#[serde(rename = "{{ error_group.name | snake }}/{{ error_entry.name }}")]
|
||||
{{ error_entry.name | title}},
|
||||
{%- endfor %}
|
||||
}
|
||||
{%- endfor %}
|
||||
/// All possible errors of the {{ name }} interface.
|
||||
#[derive(Debug, Clone, PartialEq, __serde::Serialize, __serde::Deserialize)]
|
||||
#[serde(crate = "__serde")]
|
||||
#[serde(untagged)]
|
||||
pub enum Error {
|
||||
{%- for error_group in errors %}
|
||||
{{ error_group.name | title }}({{ error_group.name | title }}Error),
|
||||
{%- endfor %}
|
||||
}
|
||||
}
|
||||
{%- endfor %}
|
||||
@@ -0,0 +1,302 @@
|
||||
mod generated {
|
||||
|
||||
#![allow(
|
||||
clippy::let_unit_value,
|
||||
clippy::match_single_binding,
|
||||
clippy::upper_case_acronyms,
|
||||
clippy::useless_conversion,
|
||||
clippy::too_many_arguments,
|
||||
dead_code,
|
||||
non_camel_case_types,
|
||||
unused_mut,
|
||||
unused_variables,
|
||||
unused_imports,
|
||||
)]
|
||||
|
||||
use everestrs::serde_json as __serde_json;
|
||||
use everestrs::serde_yaml as __serde_yaml;
|
||||
|
||||
pub mod types {
|
||||
{% include "types" %}
|
||||
}
|
||||
|
||||
pub mod errors {
|
||||
{% include "errors" %}
|
||||
}
|
||||
|
||||
{% include "config" %}
|
||||
|
||||
/// Called when the module receives on_ready from EVerest.
|
||||
pub(crate) trait OnReadySubscriber: Sync + Send {
|
||||
fn on_ready(&self, pub_impl: &ModulePublisher);
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "mockall", feature = "trait"))]
|
||||
mockall::mock! {
|
||||
pub(crate) OnReadySubscriber {}
|
||||
impl OnReadySubscriber for OnReadySubscriber {
|
||||
fn on_ready(&self, pub_impl: &ModulePublisher);
|
||||
}
|
||||
}
|
||||
|
||||
{% for trait in provided_interfaces %}
|
||||
{% include "service" %}
|
||||
{% endfor %}
|
||||
|
||||
{% for trait in required_interfaces %}
|
||||
{% include "client" %}
|
||||
{% endfor %}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[cfg_attr(all(test, feature="mockall", not(feature="trait")), derive(Default))]
|
||||
pub(crate) struct ModulePublisher {
|
||||
{% for provide in provides %}
|
||||
pub(crate) {{ provide.implementation_id | identifier }}: {{provide.interface | title}}ServicePublisher,
|
||||
{% endfor %}
|
||||
{% for require in requires %}
|
||||
{% if require.min_connections == 1 and require.max_connections == 1 %}
|
||||
pub(crate) {{ require.implementation_id | identifier }}: {{require.interface | title}}ClientPublisher,
|
||||
{% elif require.min_connections == require.max_connections %}
|
||||
pub(crate) {{ require.implementation_id | identifier }}_slots: [{{ require.interface | title}}ClientPublisher; {{require.min_connections}}],
|
||||
{% else %}
|
||||
pub(crate) {{ require.implementation_id | identifier }}_slots: Vec<{{require.interface | title}}ClientPublisher>,
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
struct ModuleInner {
|
||||
on_ready: ::std::sync::Arc<dyn OnReadySubscriber>,
|
||||
{% for provide in provides %}
|
||||
{{ provide.implementation_id | identifier }}: ::std::sync::Arc<dyn {{provide.interface | title}}ServiceSubscriber>,
|
||||
{% endfor %}
|
||||
{% for require in requires %}
|
||||
{% if require.min_connections == 1 and require.max_connections == 1 %}
|
||||
{{ require.implementation_id | identifier }}: ::std::sync::Arc<dyn {{require.interface | title}}ClientSubscriber>,
|
||||
{% elif require.min_connections == require.max_connections %}
|
||||
{{ require.implementation_id | identifier }}_slots: [::std::sync::Arc<dyn {{require.interface | title}}ClientSubscriber>; {{require.max_connections}}],
|
||||
{% else %}
|
||||
{{ require.implementation_id | identifier }}_slots: Vec<::std::sync::Arc<dyn {{require.interface | title}}ClientSubscriber>>,
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
publisher: ModulePublisher,
|
||||
ready: ::std::sync::Condvar,
|
||||
ready_flag: ::std::sync::Mutex<bool>,
|
||||
}
|
||||
|
||||
pub(crate) struct Module {
|
||||
runtime: ::std::pin::Pin<::std::sync::Arc<::everestrs::Runtime>>,
|
||||
inner: ::std::sync::OnceLock<::std::sync::Arc<ModuleInner>>,
|
||||
}
|
||||
|
||||
/// The context structure.
|
||||
pub(crate) struct Context<'a> {
|
||||
pub(crate) publisher: &'a ModulePublisher,
|
||||
{# TODO(ddo) Clarify the naming. #}
|
||||
/// The name as in `implementation_id`.
|
||||
pub name: &'a str,
|
||||
/// The index of the slot.
|
||||
pub index: usize,
|
||||
}
|
||||
|
||||
impl Module {
|
||||
#[must_use]
|
||||
pub(crate) fn new() -> Self {
|
||||
let runtime = ::everestrs::Runtime::new();
|
||||
Self {
|
||||
runtime,
|
||||
inner: ::std::sync::OnceLock::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub(crate) fn new_with_args(args: ::everestrs::Args) -> Self {
|
||||
let runtime = ::everestrs::Runtime::new_with_args(args);
|
||||
Self {
|
||||
runtime,
|
||||
inner: ::std::sync::OnceLock::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn start
|
||||
{% if requires_with_generics %}
|
||||
<
|
||||
{% for require in requires %}
|
||||
{% if require.min_connections != 1 or require.max_connections != 1 %}
|
||||
{{ require.implementation_id | title }}Callback: FnMut(usize) -> ::std::sync::Arc<dyn {{require.interface | title}}ClientSubscriber>,
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
>
|
||||
{% endif %}
|
||||
(
|
||||
&self,
|
||||
on_ready: ::std::sync::Arc<dyn OnReadySubscriber>,
|
||||
{% for provide in provides %}
|
||||
{{ provide.implementation_id | identifier }}: ::std::sync::Arc<dyn {{provide.interface | title}}ServiceSubscriber>,
|
||||
{% endfor %}
|
||||
{% for require in requires %}
|
||||
{% if require.min_connections == 1 and require.max_connections == 1 %}
|
||||
{{ require.implementation_id | identifier }}: ::std::sync::Arc<dyn {{require.interface | title}}ClientSubscriber>,
|
||||
{% else %}
|
||||
{{ require.implementation_id | identifier }}_cb: {{ require.implementation_id | title }}Callback,
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
) -> &ModulePublisher {
|
||||
let runtime = &self.runtime;
|
||||
let connections = runtime.get_module_connections();
|
||||
// Publishers hold a Weak<Runtime> so ModuleInner -> publishers ->
|
||||
// Runtime -> sub_impl -> ModuleInner is not a cycle. Drop of the
|
||||
// Module deterministically tears down Runtime, ModuleInner, and the
|
||||
// mock subscribers inside it.
|
||||
let runtime_weak = ::std::sync::Arc::downgrade(&::std::pin::Pin::into_inner(runtime.clone()));
|
||||
let inner = self.inner.get_or_init(|| {
|
||||
::std::sync::Arc::new(ModuleInner {
|
||||
on_ready,
|
||||
{% for provide in provides %}
|
||||
{{ provide.implementation_id | identifier }},
|
||||
{% endfor %}
|
||||
{% for require in requires %}
|
||||
{% if require.min_connections == 1 and require.max_connections == 1 %}
|
||||
{{ require.implementation_id | identifier }},
|
||||
{% elif require.min_connections == require.max_connections %}
|
||||
{{ require.implementation_id | identifier }}_slots: ::core::array::from_fn({{ require.implementation_id | identifier }}_cb),
|
||||
{% else %}
|
||||
{{ require.implementation_id | identifier }}_slots: (0..connections.get("{{require.implementation_id}}").cloned().unwrap_or(0)).map({{ require.implementation_id | identifier }}_cb).collect(),
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
#[cfg(any(not(test), not(feature = "mockall"), feature = "trait"))]
|
||||
publisher: ModulePublisher {
|
||||
{% for provide in provides %}
|
||||
{{ provide.implementation_id | identifier }}: {{provide.interface | title}}ServicePublisher {
|
||||
implementation_id: "{{ provide.implementation_id }}",
|
||||
runtime: runtime_weak.clone(),
|
||||
},
|
||||
{% endfor %}
|
||||
{% for require in requires %}
|
||||
{% if require.min_connections == 1 and require.max_connections == 1 %}
|
||||
{{ require.implementation_id | identifier }}: {{require.interface | title}}ClientPublisher {
|
||||
implementation_id: "{{ require.implementation_id }}",
|
||||
runtime: runtime_weak.clone(),
|
||||
index: 0,
|
||||
},
|
||||
{% elif require.min_connections == require.max_connections %}
|
||||
{{ require.implementation_id | identifier }}_slots: ::core::array::from_fn(|i| {{require.interface | title}}ClientPublisher{
|
||||
implementation_id: "{{ require.implementation_id }}",
|
||||
runtime: runtime_weak.clone(),
|
||||
index: i,
|
||||
}),
|
||||
{% else %}
|
||||
{{ require.implementation_id | identifier }}_slots: (0..connections.get("{{require.implementation_id}}").cloned().unwrap_or(0)).map(|i| {{require.interface | title}}ClientPublisher{
|
||||
implementation_id: "{{ require.implementation_id }}",
|
||||
runtime: runtime_weak.clone(),
|
||||
index: i,
|
||||
}).collect(),
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
},
|
||||
#[cfg(all(test, feature = "mockall", not(feature = "trait")))]
|
||||
publisher: ModulePublisher::default(),
|
||||
ready: ::std::sync::Condvar::new(),
|
||||
ready_flag: ::std::sync::Mutex::new(false),
|
||||
})
|
||||
});
|
||||
|
||||
runtime.as_ref().set_subscriber(inner.clone());
|
||||
|
||||
// Block until on_ready has fired.
|
||||
let mut ready = inner.ready_flag.lock().unwrap();
|
||||
while !*ready {
|
||||
ready = inner.ready.wait(ready).unwrap();
|
||||
}
|
||||
|
||||
&inner.publisher
|
||||
}
|
||||
}
|
||||
|
||||
impl ::everestrs::Subscriber for ModuleInner {
|
||||
fn handle_command(
|
||||
&self,
|
||||
implementation_id: &str,
|
||||
name: &str,
|
||||
parameters: ::std::collections::HashMap<String, __serde_json::Value>,
|
||||
) -> ::everestrs::Result<__serde_json::Value> {
|
||||
let context = Context {
|
||||
publisher: &self.publisher,
|
||||
name: implementation_id,
|
||||
index: 0,
|
||||
};
|
||||
match implementation_id {
|
||||
{% for provide in provides %}
|
||||
"{{ provide.implementation_id }}" => {
|
||||
dispatch_command_to_{{ provide.interface | snake }}(&context, self.{{ provide.implementation_id | identifier }}.as_ref(), name, parameters)
|
||||
},
|
||||
{% endfor %}
|
||||
other => Err(::everestrs::Error::MessageParsingError(
|
||||
format!("Unknown implementation_id {other} called."),
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_variable(
|
||||
&self,
|
||||
implementation_id: &str,
|
||||
index: usize,
|
||||
name: &str,
|
||||
value: __serde_json::Value,
|
||||
) -> ::everestrs::Result<()> {
|
||||
let context = Context {
|
||||
publisher: &self.publisher,
|
||||
name: implementation_id,
|
||||
index,
|
||||
};
|
||||
match implementation_id {
|
||||
{% for req in requires %}
|
||||
"{{ req.implementation_id }}" => {
|
||||
{% if req.min_connections == 1 and req.max_connections == 1 %}
|
||||
dispatch_variable_to_{{ req.interface | snake }}(&context, self.{{ req.implementation_id | identifier }}.as_ref(), name, value)
|
||||
{% else %}
|
||||
dispatch_variable_to_{{ req.interface | snake }}(&context, self.{{ req.implementation_id | identifier }}_slots[index].as_ref(), name, value)
|
||||
{% endif %}
|
||||
},
|
||||
{% endfor %}
|
||||
other => Err(::everestrs::Error::MessageParsingError(
|
||||
format!("Unknown variable {other} received."),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_on_error(
|
||||
&self,
|
||||
implementation_id: &str,
|
||||
index: usize,
|
||||
error: ::everestrs::FfiErrorType,
|
||||
raised: bool
|
||||
) {
|
||||
let context = Context {
|
||||
publisher: &self.publisher,
|
||||
name: implementation_id,
|
||||
index,
|
||||
};
|
||||
match implementation_id {
|
||||
{% for req in requires %}
|
||||
"{{ req.implementation_id }}" => {
|
||||
{% if req.min_connections == 1 and req.max_connections == 1 %}
|
||||
dispatch_error_to_{{ req.interface | snake }}(&context, self.{{ req.implementation_id | identifier }}.as_ref(), error, raised)
|
||||
{% else %}
|
||||
dispatch_error_to_{{ req.interface | snake }}(&context, self.{{ req.implementation_id | identifier }}_slots[index].as_ref(), error, raised)
|
||||
{% endif %}
|
||||
},
|
||||
{% endfor %}
|
||||
_ => everestrs::log::error!("Received an unknown error from {implementation_id}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ready(&self) {
|
||||
self.on_ready.on_ready(&self.publisher);
|
||||
let mut ready = self.ready_flag.lock().unwrap();
|
||||
*ready = true;
|
||||
self.ready.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
/// {{trait.description | replace("\n", " ")}}
|
||||
pub(crate) trait {{trait.name | title}}ServiceSubscriber: Sync + Send {
|
||||
{%- for cmd in trait.cmds %}
|
||||
/// {{cmd.description | replace("\n", " ")}}
|
||||
///
|
||||
{%- for arg in cmd.arguments %}
|
||||
/// `{{arg.name}}`: {{arg.description | replace("\n", " ")}}
|
||||
{%- endfor %}
|
||||
{% if cmd.result -%}
|
||||
///
|
||||
/// Returns: {{cmd.result.description | replace("\n", " ")}}
|
||||
{% endif -%}
|
||||
fn {{cmd.name}}(&self,
|
||||
context: &Context,
|
||||
{%- for arg in cmd.arguments %}
|
||||
{{arg.name | identifier }}: {{arg.data_type.name}},
|
||||
{%- endfor %}
|
||||
) -> ::everestrs::Result<{%- if cmd.result -%}
|
||||
{{cmd.result.data_type.name}}
|
||||
{%- else -%}
|
||||
()
|
||||
{%- endif -%}>;
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "mockall", feature = "trait"))]
|
||||
mockall::mock! {
|
||||
pub(crate) {{trait.name | title}}ServiceSubscriber {}
|
||||
impl {{trait.name | title}}ServiceSubscriber for {{trait.name | title}}ServiceSubscriber {
|
||||
{%- for cmd in trait.cmds %}
|
||||
fn {{cmd.name}}<'a>(&self,
|
||||
context: &Context<'a>,
|
||||
{%- for arg in cmd.arguments %}
|
||||
{{arg.name | identifier }}: {{arg.data_type.name}},
|
||||
{%- endfor %}
|
||||
) -> ::everestrs::Result<{%- if cmd.result -%}
|
||||
{{cmd.result.data_type.name}}
|
||||
{%- else -%}
|
||||
()
|
||||
{%- endif -%}>;
|
||||
{% endfor %}
|
||||
}
|
||||
}
|
||||
|
||||
fn dispatch_command_to_{{ trait.name | snake }}(
|
||||
context: &Context,
|
||||
service: &dyn {{trait.name | title}}ServiceSubscriber,
|
||||
name: &str,
|
||||
mut parameters: ::std::collections::HashMap<String, __serde_json::Value>,
|
||||
) -> ::everestrs::Result<__serde_json::Value> {
|
||||
match name {
|
||||
{%- for cmd in trait.cmds %}
|
||||
"{{ cmd.name }}" => {
|
||||
{%- for arg in cmd.arguments %}
|
||||
let {{ arg.name | identifier }}: {{ arg.data_type.name }} = __serde_json::from_value(
|
||||
parameters.remove("{{ arg.name }}")
|
||||
.ok_or(::everestrs::Error::MessageParsingError("Argument `{{ arg.name }}` not provided".to_string()))?,
|
||||
)
|
||||
.map_err(|e| ::everestrs::Error::MessageParsingError(format!("Failed to deserialize argument `{{ arg.name }}`: {e:?}")))?;
|
||||
{%- endfor %}
|
||||
let retval = service.{{ cmd.name }}(context,
|
||||
{%- for arg in cmd.arguments %}
|
||||
{{ arg.name | identifier }},
|
||||
{%- endfor %}
|
||||
)?;
|
||||
__serde_json::to_value(retval).map_err(|e| ::everestrs::Error::MessageParsingError(format!("Failed to serialize result: {e:?}")))
|
||||
},
|
||||
{%- endfor %}
|
||||
other => Err(::everestrs::Error::MessageParsingError(format!("Unknown command `{other}` called."))),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) mod __mockall_{{trait.name | snake }}_service {
|
||||
|
||||
use super::types;
|
||||
use super::errors;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct {{trait.name | title }}ServicePublisher {
|
||||
pub(super) implementation_id: &'static str,
|
||||
pub(super) runtime: ::std::sync::Weak<::everestrs::Runtime>,
|
||||
}
|
||||
|
||||
impl {{trait.name | title }}ServicePublisher {
|
||||
{% for var in trait.vars %}
|
||||
pub(crate) fn {{ var.name | identifier }}(&self, value: {{ var.data_type.name }}) -> ::everestrs::Result<()> {
|
||||
if let Some(runtime) = self.runtime.upgrade() {
|
||||
runtime.publish_variable(self.implementation_id, "{{ var.name }}", &value)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
{% endfor %}
|
||||
|
||||
{%- if trait.errors %}
|
||||
pub(crate) fn raise_error(&self, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>) {
|
||||
if let Some(runtime) = self.runtime.upgrade() {
|
||||
runtime.raise_error(self.implementation_id, error);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear_error(&self, error: errors::{{ trait.name | snake }}::Error) {
|
||||
if let Some(runtime) = self.runtime.upgrade() {
|
||||
runtime.clear_error(self.implementation_id, error, true);
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn clear_all_errors(&self) {
|
||||
if let Some(runtime) = self.runtime.upgrade() {
|
||||
runtime.clear_error(self.implementation_id, "", true);
|
||||
}
|
||||
}
|
||||
{%- endif %}
|
||||
}
|
||||
|
||||
#[cfg(all(feature = "mockall", not(feature = "trait")))]
|
||||
mockall::mock!{
|
||||
pub(crate) {{trait.name | title }}ServicePublisher {
|
||||
{% for var in trait.vars %}
|
||||
pub(crate) fn {{ var.name | identifier }}(&self, value: {{ var.data_type.name }}) -> ::everestrs::Result<()>;
|
||||
{% endfor %}
|
||||
|
||||
{%- if trait.errors %}
|
||||
pub(crate) fn raise_error(&self, error: ::everestrs::ErrorType<errors::{{ trait.name | snake }}::Error>);
|
||||
|
||||
pub(crate) fn clear_error(&self, error: errors::{{ trait.name | snake }}::Error);
|
||||
|
||||
pub(crate) fn clear_all_errors(&self);
|
||||
{%- endif %}
|
||||
}
|
||||
|
||||
impl Clone for {{trait.name | title }}ServicePublisher {
|
||||
fn clone(&self) -> Self;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
#[cfg_attr(all(feature = "mockall", not(feature = "trait")), mockall_double::double)]
|
||||
pub(crate) use __mockall_{{trait.name | snake }}_service::{{trait.name | title }}ServicePublisher;
|
||||
@@ -0,0 +1,31 @@
|
||||
{% for name, types in types.children | items %}
|
||||
pub mod {{ name }} {
|
||||
mod types { pub use super::super::*; }
|
||||
{% include "types" %}
|
||||
}
|
||||
{% endfor %}
|
||||
|
||||
use everestrs::serde as __serde;
|
||||
|
||||
{% for object in types.objects %}
|
||||
#[derive(Debug, Clone, PartialEq, __serde::Serialize, __serde::Deserialize)]
|
||||
#[serde(crate = "__serde")]
|
||||
pub struct {{ object.name }} {
|
||||
{% for p in object.properties %}
|
||||
/// {{ p.description | replace("\n", " ") }}
|
||||
#[serde(rename="{{ p.name }}"{% if p.data_type.extra_serde_annotations %},{{ p.data_type.extra_serde_annotations | join(",") }}{% endif %})]
|
||||
pub {{ p.name | identifier }}: {{ p.data_type.name }},
|
||||
{% endfor %}
|
||||
}
|
||||
{% endfor %}
|
||||
|
||||
{% for enum in types.enums %}
|
||||
#[derive(Debug, Clone, PartialEq, __serde::Serialize, __serde::Deserialize)]
|
||||
#[serde(crate = "__serde")]
|
||||
pub enum {{ enum.name }} {
|
||||
{% for item in enum.items %}
|
||||
{{ item }},
|
||||
{% endfor %}
|
||||
}
|
||||
|
||||
{% endfor %}
|
||||
@@ -0,0 +1,30 @@
|
||||
use anyhow::Result;
|
||||
use argh::FromArgs;
|
||||
use everestrs_build::Builder;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[derive(FromArgs)]
|
||||
/// Codegen for EVerest-rs
|
||||
struct Args {
|
||||
/// path to EVerest
|
||||
#[argh(option)]
|
||||
pub everest_core: Vec<PathBuf>,
|
||||
|
||||
/// manifest to generate code for
|
||||
#[argh(option)]
|
||||
pub manifest: PathBuf,
|
||||
|
||||
/// output directory to put the generated code to.
|
||||
#[argh(option)]
|
||||
pub out_dir: PathBuf,
|
||||
}
|
||||
|
||||
pub fn main() -> Result<()> {
|
||||
let args: Args = argh::from_env();
|
||||
|
||||
Builder::new(args.manifest, args.everest_core)
|
||||
.out_dir(args.out_dir)
|
||||
.generate()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,904 @@
|
||||
use crate::schema::{
|
||||
self,
|
||||
interface::ErrorReference,
|
||||
manifest::{ConfigEntry, ConfigEnum, Ignore},
|
||||
types::{DataTypes, ObjectOptions, StringOptions, Type, TypeBase, TypeEnum},
|
||||
ErrorList, Interface, Manifest,
|
||||
};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use convert_case::{Case, Casing};
|
||||
use minijinja::{Environment, UndefinedBehavior};
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// We include the JINJA templates into the binary. This has the disadvantage
|
||||
// that every change to the templates requires a recompilation, but the
|
||||
// advantage that the codegen library/binary is truly standalone and needs
|
||||
// nothing shipped with it to work.
|
||||
const CLIENT_JINJA: &str = include_str!("../jinja/client.jinja2");
|
||||
const CONFIG_JINJA: &str = include_str!("../jinja/config.jinja2");
|
||||
const ERRORS_JINJA: &str = include_str!("../jinja/errors.jinja2");
|
||||
const MODULE_JINJA: &str = include_str!("../jinja/module.jinja2");
|
||||
const SERVICE_JINJA: &str = include_str!("../jinja/service.jinja2");
|
||||
const TYPES_JINJA: &str = include_str!("../jinja/types.jinja2");
|
||||
|
||||
fn is_reserved_keyword(s: &str) -> bool {
|
||||
// From https://doc.rust-lang.org/reference/keywords.html.
|
||||
matches!(
|
||||
s,
|
||||
"abstract"
|
||||
| "as"
|
||||
| "async"
|
||||
| "await"
|
||||
| "become"
|
||||
| "box"
|
||||
| "break"
|
||||
| "const"
|
||||
| "continue"
|
||||
| "crate"
|
||||
| "do"
|
||||
| "dyn"
|
||||
| "else"
|
||||
| "enum"
|
||||
| "extern"
|
||||
| "false"
|
||||
| "final"
|
||||
| "fn"
|
||||
| "for"
|
||||
| "if"
|
||||
| "impl"
|
||||
| "in"
|
||||
| "let"
|
||||
| "loop"
|
||||
| "macro"
|
||||
| "macro_rules"
|
||||
| "match"
|
||||
| "mod"
|
||||
| "move"
|
||||
| "mut"
|
||||
| "override"
|
||||
| "priv"
|
||||
| "pub"
|
||||
| "ref"
|
||||
| "return"
|
||||
| "self"
|
||||
| "static"
|
||||
| "struct"
|
||||
| "super"
|
||||
| "trait"
|
||||
| "true"
|
||||
| "try"
|
||||
| "type"
|
||||
| "typeof"
|
||||
| "union"
|
||||
| "unsafe"
|
||||
| "unsized"
|
||||
| "use"
|
||||
| "virtual"
|
||||
| "where"
|
||||
| "while"
|
||||
| "yield"
|
||||
)
|
||||
}
|
||||
|
||||
fn lazy_load<'a, T: DeserializeOwned>(
|
||||
storage: &'a mut HashMap<String, T>,
|
||||
everest_root: &Vec<PathBuf>,
|
||||
prefix: &str,
|
||||
postfix: &str,
|
||||
) -> Result<&'a mut T> {
|
||||
if storage.contains_key(postfix) {
|
||||
return Ok(storage.get_mut(postfix).unwrap());
|
||||
}
|
||||
|
||||
let mut matches = everest_root
|
||||
.iter()
|
||||
.filter_map(|core| {
|
||||
let p = core.join(format!("{prefix}/{postfix}.yaml"));
|
||||
// If the file is missing we ignore the error since it may be
|
||||
// present in an different root.
|
||||
let Ok(blob) = fs::read_to_string(&p) else {
|
||||
return None;
|
||||
};
|
||||
let out = serde_yaml::from_str(&blob).with_context(|| format!("Failed to parse {p:?}"));
|
||||
match out {
|
||||
Err(err) => {
|
||||
println!("{err:?}");
|
||||
None
|
||||
}
|
||||
Ok(res) => Some(res),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
assert!(
|
||||
matches.len() == 1,
|
||||
"The name `{prefix}/{postfix}` must be defined exactly once: Found {}",
|
||||
{ matches.len() }
|
||||
);
|
||||
|
||||
storage.insert(postfix.to_string(), matches.pop().unwrap());
|
||||
Ok(storage.get_mut(postfix).unwrap())
|
||||
}
|
||||
|
||||
/// A lazy loader for YAML files. If the same file is requested twice, it will
|
||||
/// not be re-parsed again.
|
||||
#[derive(Default, Debug)]
|
||||
struct YamlRepo {
|
||||
// This might be also a HashMap of "namespaces" and paths.
|
||||
everest_root: Vec<PathBuf>,
|
||||
interfaces: HashMap<String, Interface>,
|
||||
data_types: HashMap<String, DataTypes>,
|
||||
error_types: HashMap<String, ErrorList>,
|
||||
}
|
||||
|
||||
impl YamlRepo {
|
||||
pub fn new(everest_root: Vec<PathBuf>) -> Self {
|
||||
Self {
|
||||
everest_root,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_interface<'a>(&'a mut self, name: &str) -> Result<&'a mut Interface> {
|
||||
lazy_load(&mut self.interfaces, &self.everest_root, "interfaces", name)
|
||||
}
|
||||
|
||||
pub fn get_data_types<'a>(&'a mut self, name: &str) -> Result<&'a mut DataTypes> {
|
||||
lazy_load(&mut self.data_types, &self.everest_root, "types", name)
|
||||
}
|
||||
|
||||
pub fn get_errors<'a>(&'a mut self, prefix: &str, name: &str) -> Result<&'a mut ErrorList> {
|
||||
lazy_load(&mut self.error_types, &self.everest_root, prefix, name)
|
||||
}
|
||||
}
|
||||
|
||||
// We just pull out of ObjectOptions what we really need for codegen.
|
||||
#[derive(Clone, Hash, PartialOrd, Ord, PartialEq, Eq)]
|
||||
struct TypeRef {
|
||||
/// The same as the file name under EVerest/types.
|
||||
module_path: Vec<String>,
|
||||
type_name: String,
|
||||
}
|
||||
|
||||
impl TypeRef {
|
||||
fn from_object(args: &ObjectOptions) -> Result<Self> {
|
||||
assert!(args.object_reference.is_some());
|
||||
assert!(
|
||||
args.properties.is_empty(),
|
||||
"Found an object with $ref, but also with properties. Cannot handle that case."
|
||||
);
|
||||
Self::from_reference(args.object_reference.as_ref().unwrap())
|
||||
}
|
||||
|
||||
fn from_string(args: &StringOptions) -> Result<Self> {
|
||||
assert!(args.object_reference.is_some());
|
||||
Self::from_reference(args.object_reference.as_ref().unwrap())
|
||||
}
|
||||
|
||||
fn from_reference(r: &str) -> Result<Self> {
|
||||
let parts: Vec<_> = r.trim_start_matches('/').split("#/").collect();
|
||||
if parts.len() != 2 {
|
||||
bail!("Unexpected type reference: {}", r);
|
||||
}
|
||||
let module_name = parts[0].to_string();
|
||||
let module_path = module_name.split('/').map(|s| s.to_string()).collect();
|
||||
let type_name = parts[1].to_string();
|
||||
Ok(Self {
|
||||
module_path,
|
||||
type_name,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn module_name(&self) -> String {
|
||||
format!("types::{}", self.module_path.join("::"),)
|
||||
}
|
||||
|
||||
pub fn absolute_type_path(&self) -> String {
|
||||
format!("{}::{}", self.module_name(), self.type_name)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for TypeRef {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"TypeRef /{}#/{}",
|
||||
self.module_path.join("/"),
|
||||
self.type_name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn as_typename(arg: &TypeBase, type_refs: &mut BTreeSet<TypeRef>) -> Result<String> {
|
||||
use TypeBase::*;
|
||||
use TypeEnum::*;
|
||||
Ok(match arg {
|
||||
Single(Null) => "()".to_string(),
|
||||
Single(Boolean(_)) => "bool".to_string(),
|
||||
Single(String(args)) => {
|
||||
if args.object_reference.is_none() {
|
||||
"String".to_string()
|
||||
} else {
|
||||
let t = TypeRef::from_string(args)?;
|
||||
let name = t.absolute_type_path();
|
||||
type_refs.insert(t);
|
||||
name
|
||||
}
|
||||
}
|
||||
Single(Number(_)) => "f64".to_string(),
|
||||
Single(Integer(_)) => "i64".to_string(),
|
||||
Single(Object(args)) => {
|
||||
if args.object_reference.is_none() {
|
||||
"__serde_json::Value".to_string()
|
||||
} else {
|
||||
let t = TypeRef::from_object(args)?;
|
||||
let name = t.absolute_type_path();
|
||||
type_refs.insert(t);
|
||||
name
|
||||
}
|
||||
}
|
||||
Single(Array(args)) => match args.items {
|
||||
None => "Vec<__serde_json::Value>".to_string(),
|
||||
Some(ref v) => {
|
||||
let item_type = as_typename(&v.arg, type_refs)?;
|
||||
format!("Vec<{item_type}>")
|
||||
}
|
||||
},
|
||||
Multiple(_) => "__serde_json::Value".to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct DataTypeContext {
|
||||
name: String,
|
||||
extra_serde_annotations: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct ArgumentContext {
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
data_type: DataTypeContext,
|
||||
}
|
||||
|
||||
impl ArgumentContext {
|
||||
pub fn from_schema(
|
||||
name: String,
|
||||
var: &Type,
|
||||
type_refs: &mut BTreeSet<TypeRef>,
|
||||
) -> Result<Self> {
|
||||
Ok(ArgumentContext {
|
||||
name,
|
||||
description: var.description.clone(),
|
||||
data_type: DataTypeContext {
|
||||
name: as_typename(&var.arg, type_refs)?,
|
||||
extra_serde_annotations: Vec::new(),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct CommandContext {
|
||||
name: String,
|
||||
description: String,
|
||||
result: Option<ArgumentContext>,
|
||||
arguments: Vec<ArgumentContext>,
|
||||
}
|
||||
|
||||
impl CommandContext {
|
||||
pub fn from_schema(
|
||||
name: String,
|
||||
cmd: &crate::schema::interface::Command,
|
||||
type_refs: &mut BTreeSet<TypeRef>,
|
||||
) -> Result<Self> {
|
||||
let mut arguments = Vec::new();
|
||||
for (name, arg) in &cmd.arguments {
|
||||
arguments.push(ArgumentContext::from_schema(name.clone(), arg, type_refs)?);
|
||||
}
|
||||
Ok(CommandContext {
|
||||
name,
|
||||
description: cmd.description.clone(),
|
||||
result: match &cmd.result {
|
||||
None => None,
|
||||
Some(arg) => Some(ArgumentContext::from_schema(
|
||||
"return_value".to_string(),
|
||||
arg,
|
||||
type_refs,
|
||||
)?),
|
||||
},
|
||||
arguments,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// The error group maps to one error yaml file.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct ErrorGroupContext {
|
||||
/// The name is basically the yaml file in which the errors are defined.
|
||||
name: String,
|
||||
|
||||
/// The list of errors
|
||||
error_list: schema::error::ErrorList,
|
||||
}
|
||||
|
||||
mod impl_error {
|
||||
#[derive(Hash, Eq, PartialEq)]
|
||||
pub struct ErrorPath<'a> {
|
||||
/// The prefix where the error files are.
|
||||
pub prefix: &'a str,
|
||||
|
||||
/// The error file itself.
|
||||
pub file: &'a str,
|
||||
}
|
||||
|
||||
pub struct ErrorDefinition<'a> {
|
||||
/// The path of the error.
|
||||
pub path: ErrorPath<'a>,
|
||||
|
||||
/// The type which is optional. If the type is not defined we accept
|
||||
/// all errors in the path.
|
||||
pub error_type: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> ErrorDefinition<'a> {
|
||||
/// Try to construct an error definition from the string.
|
||||
pub fn try_new(value: &'a str) -> anyhow::Result<Self> {
|
||||
let mut splits = value.split("#/");
|
||||
let path = splits.next().ok_or(anyhow::anyhow!("No path defined"))?;
|
||||
|
||||
// Split the path and remove the empty parts.
|
||||
// (The first element might be empty if we have a leading `/`).
|
||||
let paths = path
|
||||
.split("/")
|
||||
.filter(|path| !path.is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
anyhow::ensure!(paths.len() == 2, "Expecting exactly two paths");
|
||||
anyhow::ensure!(
|
||||
paths.iter().all(|path| !path.is_empty()),
|
||||
"Empty paths not allowed"
|
||||
);
|
||||
|
||||
let path = ErrorPath {
|
||||
prefix: paths[0],
|
||||
file: paths[1],
|
||||
};
|
||||
|
||||
let error_type = splits.next();
|
||||
if let Some(inner) = error_type {
|
||||
anyhow::ensure!(!inner.is_empty(), "Type must not be empty");
|
||||
}
|
||||
Ok(Self { path, error_type })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ErrorGroupContext {
|
||||
/// Generates the [ErrorGroupContext] from the `error_reference`.
|
||||
///
|
||||
/// The error_reference can have two forms:
|
||||
/// - /errors/example
|
||||
/// - /errors/example#/ExampleErrorA
|
||||
///
|
||||
/// The first type is straight forward. For the second type however, we want
|
||||
/// to group them by their file name.
|
||||
fn from_yaml(yaml_repo: &mut YamlRepo, errors: &[ErrorReference]) -> Vec<Self> {
|
||||
// The errors may be defined multiple times. If we find a definition
|
||||
// which would use all, we use all. Otherwise we use the specific
|
||||
// defintions.
|
||||
enum ErrorOption {
|
||||
/// Use all errors in a file.
|
||||
All,
|
||||
|
||||
/// Use only specific errors in a file.
|
||||
Some(HashSet<String>),
|
||||
}
|
||||
|
||||
// Find all the error options defined.
|
||||
let mut error_definitions = HashMap::new();
|
||||
for error_ref in errors {
|
||||
let new_error = impl_error::ErrorDefinition::try_new(&error_ref.reference)
|
||||
.expect("Failed to parse {error_ref}");
|
||||
|
||||
let mut error_definition = error_definitions
|
||||
.entry(new_error.path)
|
||||
.or_insert(ErrorOption::Some(HashSet::new()));
|
||||
// We don't "downgrade" `All` to `Some`.
|
||||
if let ErrorOption::Some(options) = &mut error_definition {
|
||||
if let Some(new_option) = new_error.error_type {
|
||||
options.insert(new_option.to_string());
|
||||
} else {
|
||||
*error_definition = ErrorOption::All;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut output = Vec::new();
|
||||
// Load the error yaml form the disk.
|
||||
for (error_path, error_option) in error_definitions {
|
||||
let error_list = yaml_repo
|
||||
.get_errors(error_path.prefix, error_path.file)
|
||||
.unwrap();
|
||||
|
||||
let mut error_group_context = ErrorGroupContext {
|
||||
name: error_path.file.to_string(),
|
||||
error_list: error_list.clone(),
|
||||
};
|
||||
|
||||
// Remove unused options.
|
||||
if let ErrorOption::Some(options) = error_option {
|
||||
error_group_context
|
||||
.error_list
|
||||
.errors
|
||||
.retain(|e| options.contains(&e.name));
|
||||
}
|
||||
|
||||
// The yaml file might have no errors defined at all. This would
|
||||
// still comply with the EVerest schema but the user can't do
|
||||
// anything with it.
|
||||
if !error_group_context.error_list.errors.is_empty() {
|
||||
output.push(error_group_context);
|
||||
}
|
||||
}
|
||||
|
||||
output
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct InterfaceContext {
|
||||
name: String,
|
||||
description: String,
|
||||
cmds: Vec<CommandContext>,
|
||||
vars: Vec<ArgumentContext>,
|
||||
/// The errors of an interface.
|
||||
errors: Vec<ErrorGroupContext>,
|
||||
}
|
||||
|
||||
impl InterfaceContext {
|
||||
pub fn from_yaml(
|
||||
yaml_repo: &mut YamlRepo,
|
||||
name: &str,
|
||||
type_refs: &mut BTreeSet<TypeRef>,
|
||||
) -> Result<Self> {
|
||||
let interface_yaml = yaml_repo.get_interface(name)?;
|
||||
let mut vars = Vec::new();
|
||||
for (name, var) in &interface_yaml.vars {
|
||||
vars.push(ArgumentContext::from_schema(name.clone(), var, type_refs)?);
|
||||
}
|
||||
let mut cmds = Vec::new();
|
||||
for (name, cmd) in &interface_yaml.cmds {
|
||||
cmds.push(CommandContext::from_schema(name.clone(), cmd, type_refs)?);
|
||||
}
|
||||
|
||||
// We can only borrow the yaml_repo once. It's actually not necessary so
|
||||
// we should refactor this.
|
||||
let description = interface_yaml.description.clone();
|
||||
let errors = interface_yaml.errors.clone();
|
||||
let errors = ErrorGroupContext::from_yaml(yaml_repo, &errors);
|
||||
|
||||
Ok(InterfaceContext {
|
||||
name: name.to_string(),
|
||||
description,
|
||||
vars,
|
||||
cmds,
|
||||
errors,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Default)]
|
||||
struct TypeModuleContext {
|
||||
children: BTreeMap<String, TypeModuleContext>,
|
||||
objects: Vec<ObjectTypeContext>,
|
||||
enums: Vec<EnumTypeContext>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct ObjectTypeContext {
|
||||
name: String,
|
||||
properties: Vec<ArgumentContext>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct EnumTypeContext {
|
||||
name: String,
|
||||
items: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
enum TypeContext {
|
||||
Object(ObjectTypeContext),
|
||||
Enum(EnumTypeContext),
|
||||
}
|
||||
|
||||
fn type_context_from_ref(
|
||||
r: &TypeRef,
|
||||
yaml_repo: &mut YamlRepo,
|
||||
type_refs: &mut BTreeSet<TypeRef>,
|
||||
) -> Result<TypeContext> {
|
||||
use TypeBase::*;
|
||||
use TypeEnum::*;
|
||||
|
||||
let module_path = r.module_path.join("/");
|
||||
let data_types_yaml = yaml_repo.get_data_types(&module_path)?;
|
||||
|
||||
let type_descr = data_types_yaml
|
||||
.types
|
||||
.get_mut(&r.type_name)
|
||||
.ok_or_else(|| anyhow!("Unable to find data type {:?}. Is it defined?", r))?;
|
||||
|
||||
let mut new_types: BTreeMap<std::string::String, Type> = BTreeMap::new();
|
||||
|
||||
let res = match &mut type_descr.arg {
|
||||
Single(Object(args)) => {
|
||||
let mut properties = Vec::new();
|
||||
for (name, var) in &mut args.properties {
|
||||
let mut extra_serde_annotations = Vec::new();
|
||||
let data_type = {
|
||||
// This is some "trick" - if we have enums which are defined
|
||||
// inplace, we create a new entry.
|
||||
if let Single(String(enum_args)) = &mut var.arg {
|
||||
match &enum_args.enum_items {
|
||||
Some(items) => {
|
||||
let new_type = Type {
|
||||
description: Some("An inlined type".to_string()),
|
||||
arg: Single(String(StringOptions {
|
||||
pattern: None,
|
||||
format: None,
|
||||
max_length: None,
|
||||
min_length: None,
|
||||
enum_items: Some(items.clone()),
|
||||
default: None,
|
||||
object_reference: None,
|
||||
})),
|
||||
|
||||
qos: None,
|
||||
};
|
||||
let new_name = format!(
|
||||
"{}AutoGen{}",
|
||||
r.type_name.to_case(Case::Pascal),
|
||||
name.to_case(Case::Pascal)
|
||||
);
|
||||
enum_args.object_reference =
|
||||
Some(format!("/{}#/{}", module_path, new_name));
|
||||
new_types.insert(new_name, new_type);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let d = as_typename(&var.arg, type_refs)?;
|
||||
if !args.required.contains(name) {
|
||||
extra_serde_annotations
|
||||
.push("skip_serializing_if = \"Option::is_none\"".to_string());
|
||||
format!("Option<{}>", d)
|
||||
} else {
|
||||
d
|
||||
}
|
||||
};
|
||||
properties.push(ArgumentContext {
|
||||
name: name.clone(),
|
||||
description: var.description.clone(),
|
||||
data_type: DataTypeContext {
|
||||
name: data_type,
|
||||
extra_serde_annotations,
|
||||
},
|
||||
});
|
||||
}
|
||||
Ok(TypeContext::Object(ObjectTypeContext {
|
||||
name: r.type_name.clone(),
|
||||
properties,
|
||||
}))
|
||||
}
|
||||
Single(String(args)) => {
|
||||
assert!(
|
||||
args.enum_items.is_some(),
|
||||
"Expected a named string type to be an enum, but {} was not.",
|
||||
r.type_name
|
||||
);
|
||||
|
||||
Ok(TypeContext::Enum(EnumTypeContext {
|
||||
name: r.type_name.clone(),
|
||||
items: args.enum_items.clone().unwrap(),
|
||||
}))
|
||||
}
|
||||
other => unreachable!("Does not support $ref for {other:?}"),
|
||||
};
|
||||
|
||||
data_types_yaml.types.extend(new_types);
|
||||
return res;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct SlotContext {
|
||||
implementation_id: String,
|
||||
interface: String,
|
||||
min_connections: i64,
|
||||
max_connections: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct ConfigContext {
|
||||
name: String,
|
||||
config: Vec<ArgumentContext>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
struct RenderContext {
|
||||
/// The interfaces the user will need to fill in.
|
||||
provided_interfaces: Vec<InterfaceContext>,
|
||||
/// The interfaces we are requiring.
|
||||
required_interfaces: Vec<InterfaceContext>,
|
||||
/// All errors involved - those we can raise and those we can receive.
|
||||
involved_errors: HashMap<String, Vec<ErrorGroupContext>>,
|
||||
provides: Vec<SlotContext>,
|
||||
requires: Vec<SlotContext>,
|
||||
requires_with_generics: bool,
|
||||
types: TypeModuleContext,
|
||||
module_config: Vec<ArgumentContext>,
|
||||
provided_config: Vec<ConfigContext>,
|
||||
}
|
||||
|
||||
fn title_case(arg: String) -> String {
|
||||
arg.to_case(Case::Pascal)
|
||||
}
|
||||
|
||||
fn snake_case(arg: String) -> String {
|
||||
arg.to_case(Case::Snake)
|
||||
}
|
||||
|
||||
/// Like `snake_case`, but can deal with reserved names (and will then use raw identifiers).
|
||||
fn identifier_case(arg: String) -> String {
|
||||
let arg = snake_case(arg);
|
||||
if is_reserved_keyword(&arg) {
|
||||
format!("r#{arg}")
|
||||
} else {
|
||||
arg
|
||||
}
|
||||
}
|
||||
|
||||
/// Converts the config data read from yaml and generates the context for Jinja.
|
||||
///
|
||||
/// The config data contains the config name (key) and the config data (value).
|
||||
/// We use the value to derive the type and the (optional) description.
|
||||
fn emit_config(config: BTreeMap<String, ConfigEntry>) -> Vec<ArgumentContext> {
|
||||
config
|
||||
.into_iter()
|
||||
.map(|(k, v)| match v.value {
|
||||
ConfigEnum::Boolean(_) => ArgumentContext {
|
||||
name: k,
|
||||
description: v.description,
|
||||
data_type: DataTypeContext {
|
||||
name: "bool".to_string(),
|
||||
extra_serde_annotations: Vec::new(),
|
||||
},
|
||||
},
|
||||
ConfigEnum::Integer(_) => ArgumentContext {
|
||||
name: k,
|
||||
description: v.description,
|
||||
data_type: DataTypeContext {
|
||||
name: "i64".to_string(),
|
||||
extra_serde_annotations: Vec::new(),
|
||||
},
|
||||
},
|
||||
ConfigEnum::Number(_) => ArgumentContext {
|
||||
name: k,
|
||||
description: v.description,
|
||||
data_type: DataTypeContext {
|
||||
name: "f64".to_string(),
|
||||
extra_serde_annotations: Vec::new(),
|
||||
},
|
||||
},
|
||||
ConfigEnum::String(_) => ArgumentContext {
|
||||
name: k,
|
||||
description: v.description,
|
||||
data_type: DataTypeContext {
|
||||
name: "String".to_string(),
|
||||
extra_serde_annotations: Vec::new(),
|
||||
},
|
||||
},
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn emit(manifest_path: PathBuf, everest_core: Vec<PathBuf>) -> Result<String> {
|
||||
let blob = fs::read_to_string(&manifest_path).context("While reading manifest file")?;
|
||||
let manifest: Manifest = serde_yaml::from_str(&blob).context("While parsing manifest")?;
|
||||
emit_manifest(manifest, everest_core)
|
||||
}
|
||||
|
||||
pub fn emit_manifest(manifest: Manifest, everest_core: Vec<PathBuf>) -> Result<String> {
|
||||
let mut yaml_repo = YamlRepo::new(everest_core);
|
||||
|
||||
let mut env = Environment::new();
|
||||
env.set_undefined_behavior(UndefinedBehavior::Strict);
|
||||
env.add_filter("title", title_case);
|
||||
env.add_filter("snake", snake_case);
|
||||
env.add_filter("identifier", identifier_case);
|
||||
env.add_template("client", CLIENT_JINJA)?;
|
||||
env.add_template("config", CONFIG_JINJA)?;
|
||||
env.add_template("errors", ERRORS_JINJA)?;
|
||||
env.add_template("module", MODULE_JINJA)?;
|
||||
env.add_template("service", SERVICE_JINJA)?;
|
||||
env.add_template("types", TYPES_JINJA)?;
|
||||
|
||||
let provided_config = manifest
|
||||
.provides
|
||||
.iter()
|
||||
.filter(|(_, data)| !data.config.is_empty())
|
||||
.map(|(name, data)| ConfigContext {
|
||||
name: name.clone(),
|
||||
config: emit_config(data.config.clone()),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut type_refs = BTreeSet::new();
|
||||
let mut provided_interfaces = HashMap::with_capacity(manifest.provides.len());
|
||||
let mut provides = Vec::with_capacity(manifest.provides.len());
|
||||
for (implementation_id, imp) in manifest.provides {
|
||||
if !provided_interfaces.contains_key(&imp.interface) {
|
||||
let interface_context =
|
||||
InterfaceContext::from_yaml(&mut yaml_repo, &imp.interface, &mut type_refs)?;
|
||||
provided_interfaces.insert(imp.interface.clone(), interface_context);
|
||||
}
|
||||
provides.push(SlotContext {
|
||||
implementation_id,
|
||||
interface: imp.interface.clone(),
|
||||
min_connections: 1,
|
||||
max_connections: 1,
|
||||
})
|
||||
}
|
||||
|
||||
let mut required_interfaces = HashMap::with_capacity(manifest.requires.len());
|
||||
let mut requires = Vec::with_capacity(manifest.requires.len());
|
||||
// We remove the intersection off all ignored interfaces from the trait
|
||||
// signature.
|
||||
let mut ignored = HashMap::with_capacity(manifest.requires.len());
|
||||
for (implementation_id, imp) in manifest.requires {
|
||||
ignored
|
||||
.entry(imp.interface.clone())
|
||||
.and_modify(|merged_ignore: &mut Ignore| {
|
||||
merged_ignore.vars = merged_ignore
|
||||
.vars
|
||||
.intersection(&imp.ignore.vars)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
merged_ignore.errors = merged_ignore.errors & imp.ignore.errors;
|
||||
})
|
||||
.or_insert(imp.ignore);
|
||||
if !required_interfaces.contains_key(&imp.interface) {
|
||||
let interface_context =
|
||||
InterfaceContext::from_yaml(&mut yaml_repo, &imp.interface, &mut type_refs)?;
|
||||
required_interfaces.insert(imp.interface.clone(), interface_context);
|
||||
}
|
||||
|
||||
requires.push(SlotContext {
|
||||
implementation_id,
|
||||
interface: imp.interface.clone(),
|
||||
min_connections: imp.min_connections.unwrap_or(1),
|
||||
max_connections: imp.max_connections.unwrap_or(1),
|
||||
})
|
||||
}
|
||||
|
||||
for (interface, merged_ignore) in ignored.into_iter() {
|
||||
// Check if all ignored interfaces are known.
|
||||
if let Some(required_interface) = required_interfaces.get(&interface) {
|
||||
if let Some(unknown_var) = merged_ignore.vars.iter().find(|&ignored_var| {
|
||||
required_interface
|
||||
.vars
|
||||
.iter()
|
||||
.find(|&required_var| &required_var.name == ignored_var)
|
||||
.is_none()
|
||||
}) {
|
||||
panic!("The interface `{interface}` cannot ignore unkown variable `{unknown_var}`");
|
||||
}
|
||||
}
|
||||
// Remove those interfaces which were never used.
|
||||
required_interfaces
|
||||
.entry(interface)
|
||||
.and_modify(|interface| {
|
||||
interface
|
||||
.vars
|
||||
.retain(|cmd| !merged_ignore.vars.contains(&cmd.name));
|
||||
if merged_ignore.errors {
|
||||
interface.errors.clear();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let mut type_module_root = TypeModuleContext::default();
|
||||
|
||||
let mut done: BTreeSet<TypeRef> = BTreeSet::new();
|
||||
while done.len() != type_refs.len() {
|
||||
let mut new = BTreeSet::new();
|
||||
for t in &type_refs {
|
||||
if done.contains(t) {
|
||||
continue;
|
||||
}
|
||||
let mut module = &mut type_module_root;
|
||||
for p in &t.module_path {
|
||||
module = module.children.entry(p.clone()).or_default();
|
||||
}
|
||||
match type_context_from_ref(t, &mut yaml_repo, &mut new)? {
|
||||
TypeContext::Object(item) => module.objects.push(item),
|
||||
TypeContext::Enum(item) => module.enums.push(item),
|
||||
}
|
||||
done.insert(t.clone());
|
||||
}
|
||||
type_refs.extend(new.into_iter());
|
||||
}
|
||||
|
||||
let module_config = emit_config(manifest.config);
|
||||
let requires_with_generics = requires
|
||||
.iter()
|
||||
.any(|elem| elem.min_connections != 1 || elem.max_connections != 1);
|
||||
|
||||
let involved_errors = provided_interfaces
|
||||
.iter()
|
||||
.chain(required_interfaces.iter())
|
||||
.filter(|(_key, value)| !value.errors.is_empty())
|
||||
.map(|(key, value)| (key.clone(), value.errors.clone()))
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
let context = RenderContext {
|
||||
provided_interfaces: provided_interfaces.values().cloned().collect(),
|
||||
required_interfaces: required_interfaces.values().cloned().collect(),
|
||||
involved_errors,
|
||||
provides,
|
||||
requires,
|
||||
requires_with_generics,
|
||||
types: type_module_root,
|
||||
module_config,
|
||||
provided_config,
|
||||
};
|
||||
let tmpl = env.get_template("module").unwrap();
|
||||
Ok(tmpl.render(context).unwrap())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[test]
|
||||
fn test_split_paths_invalid() {
|
||||
use super::impl_error::*;
|
||||
let invalid_input = [
|
||||
"/foo/bar/baz", // too many
|
||||
"/foo", // too few,
|
||||
"/foo/", // no type
|
||||
"//foo", // no path,
|
||||
"", // just empty
|
||||
];
|
||||
|
||||
for input in invalid_input {
|
||||
assert!(ErrorDefinition::try_new(input).is_err());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_split_paths() {
|
||||
use super::impl_error::*;
|
||||
let res = ErrorDefinition::try_new("/foo/bar#/baz").unwrap();
|
||||
assert_eq!(res.path.prefix, "foo");
|
||||
assert_eq!(res.path.file, "bar");
|
||||
assert!(matches!(res.error_type, Some("baz")));
|
||||
|
||||
let res = ErrorDefinition::try_new("/foo/bar").unwrap();
|
||||
assert_eq!(res.path.prefix, "foo");
|
||||
assert_eq!(res.path.file, "bar");
|
||||
assert!(res.error_type.is_none());
|
||||
|
||||
let res = ErrorDefinition::try_new("foo/bar#/baz").unwrap();
|
||||
assert_eq!(res.path.prefix, "foo");
|
||||
assert_eq!(res.path.file, "bar");
|
||||
assert!(matches!(res.error_type, Some("baz")));
|
||||
|
||||
let res = ErrorDefinition::try_new("foo/bar").unwrap();
|
||||
assert_eq!(res.path.prefix, "foo");
|
||||
assert_eq!(res.path.file, "bar");
|
||||
assert!(res.error_type.is_none());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
pub mod codegen;
|
||||
pub mod manifest_resolver;
|
||||
pub mod schema;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
pub use manifest_resolver::build_test_manifest;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Builder {
|
||||
everest_root: Vec<PathBuf>,
|
||||
// TODO(hrapp): This is almost always the same anyways.
|
||||
manifest_path: PathBuf,
|
||||
out_dir: Option<PathBuf>,
|
||||
}
|
||||
|
||||
impl Builder {
|
||||
pub fn new(manifest_path: impl Into<PathBuf>, everest_root: Vec<impl Into<PathBuf>>) -> Self {
|
||||
Self {
|
||||
everest_root: everest_root
|
||||
.into_iter()
|
||||
.map(|element| element.into())
|
||||
.collect::<Vec<_>>(),
|
||||
manifest_path: manifest_path.into(),
|
||||
..Builder::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn out_dir(mut self, path: impl Into<PathBuf>) -> Self {
|
||||
self.out_dir = Some(path.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn generate(self) -> Result<()> {
|
||||
let path = self
|
||||
.out_dir
|
||||
.unwrap_or_else(|| PathBuf::from(std::env::var("OUT_DIR").unwrap()))
|
||||
.join("generated.rs");
|
||||
|
||||
let out = codegen::emit(self.manifest_path, self.everest_root)?;
|
||||
|
||||
let mut f = std::fs::File::create(&path).context("Could not generate the output file.")?;
|
||||
f.write_all(out.as_bytes())?;
|
||||
|
||||
if let Err(_) = Command::new("rustfmt").args(path.to_str()).output() {
|
||||
println!("Failed to format code");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
use crate::schema;
|
||||
use crate::schema::manifest::{Manifest, ProvidesEntry, RequiresEntry};
|
||||
use anyhow::{anyhow, bail, Context, Result};
|
||||
use std::collections::BTreeMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
mod inner {
|
||||
use super::*;
|
||||
/// A cache that lazily loads and stores module manifests by module type name.
|
||||
pub(super) struct ManifestCache<'a> {
|
||||
everest_core: &'a [PathBuf],
|
||||
entries: BTreeMap<String, (PathBuf, Manifest)>,
|
||||
}
|
||||
|
||||
impl<'a> ManifestCache<'a> {
|
||||
pub(super) fn new(everest_core: &'a [PathBuf]) -> Self {
|
||||
Self {
|
||||
everest_core,
|
||||
entries: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the manifest for `module_type`, loading it on first access.
|
||||
pub(super) fn get(&mut self, module_type: &str) -> Result<&Manifest> {
|
||||
if !self.entries.contains_key(module_type) {
|
||||
let (path, manifest) = find_manifest(module_type, self.everest_core)?;
|
||||
self.entries
|
||||
.insert(module_type.to_string(), (path, manifest));
|
||||
}
|
||||
Ok(&self.entries[module_type].1)
|
||||
}
|
||||
|
||||
/// Returns the paths of all manifests that were loaded.
|
||||
pub(super) fn into_paths(self) -> impl Iterator<Item = PathBuf> {
|
||||
self.entries.into_values().map(|(path, _)| path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively searches `dir` for a directory named `module_type` containing
|
||||
/// a `manifest.yaml`. Returns the path to the manifest on first match.
|
||||
/// Symlinks are skipped to avoid circular traversal.
|
||||
fn find_manifest_in(dir: &Path, module_type: &str) -> Option<PathBuf> {
|
||||
let entries = std::fs::read_dir(dir).ok()?;
|
||||
for entry in entries.flatten() {
|
||||
let ft = match entry.file_type() {
|
||||
Ok(ft) => ft,
|
||||
Err(_) => continue,
|
||||
};
|
||||
if ft.is_symlink() {
|
||||
continue;
|
||||
}
|
||||
let path = entry.path();
|
||||
if path.file_name().map_or(false, |n| n == module_type) {
|
||||
let manifest = path.join("manifest.yaml");
|
||||
if manifest.is_file() {
|
||||
return Some(manifest);
|
||||
}
|
||||
}
|
||||
if ft.is_dir() {
|
||||
if let Some(found) = find_manifest_in(&path, module_type) {
|
||||
return Some(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Finds a module manifest by searching for `{ModuleName}/manifest.yaml`
|
||||
/// anywhere under each everest_core root. Modules can be nested arbitrarily
|
||||
/// deep (e.g. `modules/Examples/RustExamples/RsExample/manifest.yaml`).
|
||||
/// Symlinks are skipped to avoid circular traversal.
|
||||
fn find_manifest(module_type: &str, everest_core: &[PathBuf]) -> Result<(PathBuf, Manifest)> {
|
||||
for root in everest_core {
|
||||
if let Some(path) = find_manifest_in(root, module_type) {
|
||||
let blob = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("Failed to read {path:?}"))?;
|
||||
let manifest: Manifest = serde_yaml::from_str(&blob)
|
||||
.with_context(|| format!("Failed to parse {path:?}"))?;
|
||||
return Ok((path, manifest));
|
||||
}
|
||||
}
|
||||
bail!("Could not find manifest for module type '{module_type}' in any everest_core root");
|
||||
}
|
||||
}
|
||||
|
||||
/// Reads config.yaml, finds the target module instance, deduces its
|
||||
/// interfaces from connected modules' manifests, returns a synthetic
|
||||
/// Manifest and the list of files read (for dependency tracking).
|
||||
pub fn build_test_manifest(
|
||||
config_path: &Path,
|
||||
module_instance: &str,
|
||||
everest_core: &[PathBuf],
|
||||
) -> Result<(Manifest, Vec<PathBuf>)> {
|
||||
let blob = std::fs::read_to_string(config_path)
|
||||
.with_context(|| format!("While reading config {config_path:?}"))?;
|
||||
let config: schema::Config =
|
||||
serde_yaml::from_str(&blob).with_context(|| format!("While parsing {config_path:?}"))?;
|
||||
|
||||
let target = config.active_modules.get(module_instance).ok_or_else(|| {
|
||||
anyhow!("Module instance '{module_instance}' not found in {config_path:?}")
|
||||
})?;
|
||||
|
||||
let mut cache = inner::ManifestCache::new(everest_core);
|
||||
|
||||
// Step 1: Resolve outgoing connections (what the target module requires).
|
||||
// For each connection slot in the target module, find what interface the
|
||||
// connected module provides at that implementation_id.
|
||||
let mut requires = BTreeMap::new();
|
||||
for (slot_name, connections) in &target.connections {
|
||||
let mut interfaces = std::collections::HashSet::new();
|
||||
for conn in connections {
|
||||
let connected_module = config.active_modules.get(&conn.module_id).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Connected module '{}' not found in {config_path:?}",
|
||||
conn.module_id
|
||||
)
|
||||
})?;
|
||||
|
||||
let connected_manifest = cache.get(&connected_module.module)?;
|
||||
|
||||
let provides_entry = connected_manifest
|
||||
.provides
|
||||
.get(&conn.implementation_id)
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Module type '{}' does not provide '{}'",
|
||||
connected_module.module,
|
||||
conn.implementation_id
|
||||
)
|
||||
})?;
|
||||
|
||||
interfaces.insert(provides_entry.interface.clone());
|
||||
}
|
||||
|
||||
if interfaces.len() != 1 {
|
||||
bail!("Slot '{slot_name}' has connections with mismatched interfaces: {interfaces:?}");
|
||||
}
|
||||
|
||||
requires.insert(
|
||||
slot_name.clone(),
|
||||
RequiresEntry {
|
||||
interface: interfaces.into_iter().next().unwrap(),
|
||||
min_connections: Some(1),
|
||||
max_connections: Some(connections.len() as i64),
|
||||
ignore: Default::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// Step 2: Resolve incoming connections (what the target module must provide).
|
||||
// Scan all other modules' connections to find ones pointing at our target.
|
||||
let mut provides = BTreeMap::new();
|
||||
for (other_id, other_module) in &config.active_modules {
|
||||
if other_id == module_instance {
|
||||
continue;
|
||||
}
|
||||
for (other_slot, connections) in &other_module.connections {
|
||||
for conn in connections {
|
||||
if conn.module_id != module_instance {
|
||||
continue;
|
||||
}
|
||||
// other_module requires interface via other_slot,
|
||||
// connected to our target's conn.implementation_id.
|
||||
let other_manifest = cache.get(&other_module.module)?;
|
||||
|
||||
let requires_entry = other_manifest.requires.get(other_slot).ok_or_else(|| {
|
||||
anyhow!(
|
||||
"Module type '{}' does not require '{}'",
|
||||
other_module.module,
|
||||
other_slot
|
||||
)
|
||||
})?;
|
||||
|
||||
provides.insert(
|
||||
conn.implementation_id.clone(),
|
||||
ProvidesEntry {
|
||||
interface: requires_entry.interface.clone(),
|
||||
description: format!(
|
||||
"Auto-generated from {}.{} connection",
|
||||
other_id, other_slot
|
||||
),
|
||||
config: BTreeMap::new(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let manifest = Manifest {
|
||||
description: format!("Synthetic test manifest for {module_instance}"),
|
||||
metadata: None,
|
||||
provides,
|
||||
requires,
|
||||
enable_telemetry: false,
|
||||
enable_external_mqtt: false,
|
||||
config: BTreeMap::new(),
|
||||
capabilities: Vec::new(),
|
||||
enable_global_errors: false,
|
||||
};
|
||||
|
||||
let mut tracked_files = vec![config_path.to_path_buf()];
|
||||
tracked_files.extend(cache.into_paths());
|
||||
|
||||
Ok((manifest, tracked_files))
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
use serde::Deserialize;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub active_modules: BTreeMap<String, ActiveModule>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ActiveModule {
|
||||
pub module: String,
|
||||
#[serde(default)]
|
||||
pub connections: BTreeMap<String, Vec<Connection>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Connection {
|
||||
pub module_id: String,
|
||||
pub implementation_id: String,
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Implements the schema defined under `error-declaration.yaml`. Every type has
|
||||
/// mandatory `name` and `description` fields.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct Error {
|
||||
/// The description of the error.
|
||||
pub description: String,
|
||||
|
||||
/// The name of the error.
|
||||
pub name: String,
|
||||
|
||||
/// The namespace of the error.
|
||||
pub namespace: Option<String>,
|
||||
}
|
||||
|
||||
/// Implements the list of errors.
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ErrorList {
|
||||
/// The description of all errors in the file.
|
||||
pub description: String,
|
||||
|
||||
/// The list of errors.
|
||||
/// We add default to allow make the `errors` field optional.
|
||||
#[serde(default)]
|
||||
pub errors: Vec<Error>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_yaml;
|
||||
|
||||
#[test]
|
||||
fn test_deserialization() {
|
||||
// Test with the list.
|
||||
let _ = serde_yaml::from_str::<ErrorList>(
|
||||
r#"
|
||||
description: this is a description
|
||||
errors:
|
||||
- name: foo
|
||||
description: bar
|
||||
- name: this
|
||||
description: that
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Test without the list
|
||||
let _ = serde_yaml::from_str::<ErrorList>(
|
||||
r#"
|
||||
description: just a description without errors
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::types::Type;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Interface {
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub cmds: BTreeMap<String, Command>,
|
||||
#[serde(default)]
|
||||
pub vars: BTreeMap<String, Type>,
|
||||
/// The error reference represents the entry in the manifest were
|
||||
/// we reference an error file.
|
||||
#[serde(default)]
|
||||
pub errors: Vec<ErrorReference>,
|
||||
}
|
||||
|
||||
/// The same as the one above but the cpp runtime returns the errors as a map
|
||||
/// contrary to the definition inside the yaml file...
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct InterfaceFromEverest {
|
||||
// Note: EVerest config over mqtt does not return descriptions even so
|
||||
// they should be necessary.
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub cmds: BTreeMap<String, Command>,
|
||||
#[serde(default)]
|
||||
pub vars: BTreeMap<String, Type>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Command {
|
||||
// Note: EVerest config over mqtt does not return descriptions even so
|
||||
// they should be necessary.
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub arguments: BTreeMap<String, Type>,
|
||||
pub result: Option<Type>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct ErrorReference {
|
||||
pub reference: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_deserialization() {
|
||||
serde_yaml::from_str::<Interface>(
|
||||
r#"
|
||||
description: >-
|
||||
This is an example interface used for the error framework example modules.
|
||||
errors:
|
||||
- reference: /errors/example#/ExampleErrorA
|
||||
- reference: /errors/example#/ExampleErrorB
|
||||
- reference: /errors/example#/ExampleErrorC
|
||||
- reference: /errors/example#/ExampleErrorD
|
||||
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
serde_yaml::from_str::<Interface>(
|
||||
r#"
|
||||
description: Nothing here.
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
use super::types::{BooleanOptions, IntegerOptions, NumberOptions, StringOptions};
|
||||
use serde::Deserialize;
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Manifest {
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub metadata: Option<Metadata>,
|
||||
pub provides: BTreeMap<String, ProvidesEntry>,
|
||||
#[serde(default)]
|
||||
pub requires: BTreeMap<String, RequiresEntry>,
|
||||
#[serde(default)]
|
||||
pub enable_telemetry: bool,
|
||||
// This is just here, so that we do not crash for deny_unknown_fields,
|
||||
// this is never used in Rust code.
|
||||
#[allow(dead_code)]
|
||||
#[serde(default)]
|
||||
pub enable_external_mqtt: bool,
|
||||
|
||||
#[serde(default)]
|
||||
pub config: BTreeMap<String, ConfigEntry>,
|
||||
|
||||
#[serde(default)]
|
||||
pub capabilities: Vec<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub enable_global_errors: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ProvidesEntry {
|
||||
pub interface: String,
|
||||
pub description: String,
|
||||
#[serde(default)]
|
||||
pub config: BTreeMap<String, ConfigEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RequiresEntry {
|
||||
pub interface: String,
|
||||
pub min_connections: Option<i64>,
|
||||
pub max_connections: Option<i64>,
|
||||
#[serde(default)]
|
||||
pub ignore: Ignore,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Ignore {
|
||||
#[serde(default)]
|
||||
pub vars: HashSet<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub errors: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct Metadata {
|
||||
pub license: String,
|
||||
pub authors: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ConfigEntry {
|
||||
pub description: Option<String>,
|
||||
#[serde(flatten)]
|
||||
pub value: ConfigEnum,
|
||||
#[serde(default = "MutabilityEnum::default")]
|
||||
pub mutability: MutabilityEnum,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "type", deny_unknown_fields)]
|
||||
pub enum ConfigEnum {
|
||||
Boolean(BooleanOptions),
|
||||
String(StringOptions),
|
||||
Integer(IntegerOptions),
|
||||
Number(NumberOptions),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub enum MutabilityEnum {
|
||||
ReadOnly,
|
||||
ReadWrite,
|
||||
WriteOnly,
|
||||
}
|
||||
|
||||
impl MutabilityEnum {
|
||||
fn default() -> Self {
|
||||
MutabilityEnum::ReadOnly
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
pub mod config;
|
||||
pub mod error;
|
||||
pub mod interface;
|
||||
pub mod manifest;
|
||||
pub mod types;
|
||||
|
||||
pub use config::Config;
|
||||
pub use error::ErrorList;
|
||||
pub use interface::{Interface, InterfaceFromEverest};
|
||||
pub use manifest::Manifest;
|
||||
pub use types::Type;
|
||||
@@ -0,0 +1,155 @@
|
||||
use serde::{Deserialize, Deserializer, Serialize};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
|
||||
/// Implements the schema defined under `type.yaml`. Every type has a `type`
|
||||
/// and a `description` field.
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct Type {
|
||||
// TODO(ddo) The schema says that this field is required, but multiple
|
||||
// type definitions do not obey this rule.
|
||||
pub description: Option<String>,
|
||||
|
||||
#[serde(flatten)]
|
||||
pub arg: TypeBase,
|
||||
|
||||
/// This is part of the Variable definition.
|
||||
pub qos: Option<i64>,
|
||||
}
|
||||
|
||||
/// The type may be either represented by a string or by an array of strings.
|
||||
/// In the case of an array of strings.
|
||||
#[derive(Debug, Serialize)]
|
||||
pub enum TypeBase {
|
||||
Single(TypeEnum),
|
||||
Multiple(Vec<TypeEnum>),
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for TypeBase {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
let serde_yaml::Value::Mapping(map) = Deserialize::deserialize(deserializer)? else {
|
||||
return Err(serde::de::Error::custom("Variable must be a mapping"));
|
||||
};
|
||||
|
||||
let arg_type = map
|
||||
.get("type")
|
||||
.ok_or("The `type` tag is missing")
|
||||
.map_err(|e| serde::de::Error::custom(e.to_string()))?;
|
||||
|
||||
let arg = match arg_type {
|
||||
serde_yaml::Value::String(_) => {
|
||||
let t: TypeEnum = serde_yaml::from_value(serde_yaml::Value::Mapping(map))
|
||||
.map_err(|e| serde::de::Error::custom(e.to_string()))?;
|
||||
TypeBase::Single(t)
|
||||
}
|
||||
serde_yaml::Value::Sequence(s) => {
|
||||
let mut types = Vec::with_capacity(s.len());
|
||||
for t in s.into_iter() {
|
||||
let mut mapping = serde_yaml::Mapping::new();
|
||||
mapping.insert(serde_yaml::Value::String("type".to_string()), t.clone());
|
||||
let t: TypeEnum = serde_yaml::from_value(serde_yaml::Value::Mapping(mapping))
|
||||
.map_err(|e| serde::de::Error::custom(e.to_string()))?;
|
||||
types.push(t);
|
||||
}
|
||||
TypeBase::Multiple(types)
|
||||
}
|
||||
_ => {
|
||||
return Err(serde::de::Error::custom(
|
||||
"'type' must be a sequence or a string.",
|
||||
))
|
||||
}
|
||||
};
|
||||
|
||||
Ok(arg)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct BooleanOptions {
|
||||
pub default: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NumberOptions {
|
||||
pub minimum: Option<f64>,
|
||||
pub maximum: Option<f64>,
|
||||
pub default: Option<f64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct IntegerOptions {
|
||||
pub minimum: Option<i64>,
|
||||
pub maximum: Option<i64>,
|
||||
pub default: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ArrayOptions {
|
||||
pub min_items: Option<usize>,
|
||||
pub max_items: Option<usize>,
|
||||
pub items: Option<Box<Type>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct ObjectOptions {
|
||||
#[serde(default)]
|
||||
pub properties: BTreeMap<String, Type>,
|
||||
|
||||
#[serde(default)]
|
||||
pub required: HashSet<String>,
|
||||
|
||||
#[serde(default)]
|
||||
pub additional_properties: bool,
|
||||
|
||||
#[serde(rename = "$ref")]
|
||||
pub object_reference: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub enum StringFormat {
|
||||
#[serde(rename = "date-time")]
|
||||
DateTime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub struct StringOptions {
|
||||
pub pattern: Option<String>,
|
||||
pub format: Option<StringFormat>,
|
||||
pub max_length: Option<usize>,
|
||||
pub min_length: Option<usize>,
|
||||
|
||||
#[serde(rename = "enum")]
|
||||
pub enum_items: Option<Vec<String>>,
|
||||
|
||||
pub default: Option<String>,
|
||||
|
||||
#[serde(rename = "$ref")]
|
||||
pub object_reference: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
#[serde(rename_all = "camelCase", tag = "type", deny_unknown_fields)]
|
||||
pub enum TypeEnum {
|
||||
Null,
|
||||
Boolean(BooleanOptions),
|
||||
String(StringOptions),
|
||||
Number(NumberOptions),
|
||||
Integer(IntegerOptions),
|
||||
Array(ArrayOptions),
|
||||
Object(ObjectOptions),
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct DataTypes {
|
||||
pub description: String,
|
||||
pub types: BTreeMap<String, Type>,
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
load("@rules_rust//rust:defs.bzl", "rust_proc_macro")
|
||||
|
||||
rust_proc_macro(
|
||||
name = "everestrs-derive",
|
||||
srcs = ["src/lib.rs"],
|
||||
edition = "2021",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//lib/everest/framework/everestrs/everestrs-build",
|
||||
"@everest_framework_crate_index//:proc-macro2",
|
||||
"@everest_framework_crate_index//:quote",
|
||||
"@everest_framework_crate_index//:syn",
|
||||
],
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "everestrs-derive"
|
||||
version = "0.25.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
syn = { version = "2", features = ["full"] }
|
||||
quote = "1"
|
||||
proc-macro2 = "1"
|
||||
everestrs-build = { workspace = true }
|
||||
@@ -0,0 +1,528 @@
|
||||
use proc_macro::TokenStream;
|
||||
use quote::quote;
|
||||
use std::path::PathBuf;
|
||||
use syn::{parse_macro_input, spanned::Spanned, ItemFn, ItemMod, Type};
|
||||
|
||||
/// Attribute macro that wraps an EVerest module's `main` function to control
|
||||
/// module lifecycle. The user function receives a borrowed `&Module`, ensuring
|
||||
/// it cannot be dropped prematurely. After the user function returns, the
|
||||
/// `Module` is dropped deterministically before process exit.
|
||||
///
|
||||
/// # Basics
|
||||
///
|
||||
/// The basic usage is demonstated below
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[everestrs::main]
|
||||
/// fn main(module: &Module) {
|
||||
/// let class = Arc::new(MyModule {});
|
||||
/// let _publishers = module.start(class.clone(), class.clone());
|
||||
/// loop { std::thread::sleep(std::time::Duration::from_secs(1)); }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Async
|
||||
///
|
||||
/// You can also use async for your code. The pattern is quite similar:
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[everestrs::main]
|
||||
/// #[tokio::main]
|
||||
/// async fn main(module: &Module) {
|
||||
/// let class = Arc::new(MyModule {});
|
||||
/// ...
|
||||
/// }
|
||||
#[proc_macro_attribute]
|
||||
pub fn main(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
if !attr.is_empty() {
|
||||
let attr2: proc_macro2::TokenStream = attr.into();
|
||||
return syn::Error::new_spanned(attr2, "#[everestrs::main] takes no arguments")
|
||||
.to_compile_error()
|
||||
.into();
|
||||
}
|
||||
|
||||
let input = parse_macro_input!(item as ItemFn);
|
||||
|
||||
if let Err(e) = main_validate(&input) {
|
||||
return e.to_compile_error().into();
|
||||
}
|
||||
|
||||
let sig = &input.sig;
|
||||
let param = &sig.inputs[0];
|
||||
let body = &input.block;
|
||||
let ret = &sig.output;
|
||||
let maybe_async = &sig.asyncness;
|
||||
let maybe_await = maybe_async.as_ref().map(|_| quote! {.await});
|
||||
let ident = &sig.ident;
|
||||
let attrs = &input.attrs;
|
||||
|
||||
// Extract the parameter name and inner type from `name: &Type`.
|
||||
let (param_name, inner_ty) = match param {
|
||||
syn::FnArg::Typed(pat_type) => {
|
||||
let name = &pat_type.pat;
|
||||
match pat_type.ty.as_ref() {
|
||||
Type::Reference(type_ref) => (name, &type_ref.elem),
|
||||
_ => unreachable!("validated above"),
|
||||
}
|
||||
}
|
||||
_ => unreachable!("validated above"),
|
||||
};
|
||||
|
||||
let expanded = quote! {
|
||||
#(#attrs)*
|
||||
#maybe_async fn #ident() #ret {
|
||||
let #param_name = #inner_ty::new();
|
||||
let __everest_result = {
|
||||
#maybe_async fn __everest_main(#param_name: &#inner_ty) #ret
|
||||
#body
|
||||
__everest_main(&#param_name) #maybe_await
|
||||
};
|
||||
__everest_result
|
||||
}
|
||||
};
|
||||
|
||||
expanded.into()
|
||||
}
|
||||
|
||||
fn main_validate(input: &ItemFn) -> Result<(), syn::Error> {
|
||||
if !input.sig.generics.params.is_empty() {
|
||||
return Err(syn::Error::new(
|
||||
input.sig.generics.span(),
|
||||
"#[everestrs::main] does not support generic functions",
|
||||
));
|
||||
}
|
||||
|
||||
if input.sig.inputs.len() != 1 {
|
||||
return Err(syn::Error::new(
|
||||
input.sig.inputs.span(),
|
||||
"#[everestrs::main] function must have exactly one parameter: `module: &Module`",
|
||||
));
|
||||
}
|
||||
|
||||
let param = &input.sig.inputs[0];
|
||||
match param {
|
||||
syn::FnArg::Receiver(_) => {
|
||||
return Err(syn::Error::new(
|
||||
param.span(),
|
||||
"#[everestrs::main] function must not take `self`",
|
||||
));
|
||||
}
|
||||
syn::FnArg::Typed(pat_type) => match pat_type.ty.as_ref() {
|
||||
Type::Reference(type_ref) => {
|
||||
if type_ref.mutability.is_some() {
|
||||
return Err(syn::Error::new(
|
||||
type_ref.mutability.span(),
|
||||
"#[everestrs::main] parameter must be a shared reference (`&Module`), not `&mut`",
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
pat_type.ty.span(),
|
||||
"#[everestrs::main] parameter must be a reference (e.g. `module: &Module`)",
|
||||
));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct TestAttr {
|
||||
config_path: syn::LitStr,
|
||||
module_name: syn::LitStr,
|
||||
harness: bool,
|
||||
}
|
||||
|
||||
impl syn::parse::Parse for TestAttr {
|
||||
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||
let mut config_path = None;
|
||||
let mut module_name = None;
|
||||
let mut harness = false;
|
||||
|
||||
while !input.is_empty() {
|
||||
let ident: syn::Ident = input.parse()?;
|
||||
let _: syn::Token![=] = input.parse()?;
|
||||
match ident.to_string().as_str() {
|
||||
"config" => {
|
||||
config_path = Some(input.parse::<syn::LitStr>()?);
|
||||
}
|
||||
"module" => {
|
||||
module_name = Some(input.parse::<syn::LitStr>()?);
|
||||
}
|
||||
"harness" => {
|
||||
harness = input.parse::<syn::LitBool>()?.value;
|
||||
}
|
||||
other => {
|
||||
return Err(syn::Error::new(
|
||||
ident.span(),
|
||||
format!(
|
||||
"unknown attribute `{other}`, expected `config`, `module`, or `harness`"
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
if !input.is_empty() {
|
||||
let _: syn::Token![,] = input.parse()?;
|
||||
}
|
||||
}
|
||||
|
||||
let config_path = config_path
|
||||
.ok_or_else(|| syn::Error::new(input.span(), "missing `config` attribute"))?;
|
||||
let module_name = module_name
|
||||
.ok_or_else(|| syn::Error::new(input.span(), "missing `module` attribute"))?;
|
||||
|
||||
Ok(Self {
|
||||
config_path,
|
||||
module_name,
|
||||
harness,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Attribute macro that creates an EVerest test. It launches the manager with
|
||||
/// the given config, spawns the module standalone, and then executes the test
|
||||
/// body. Each test gets a unique MQTT prefix so tests can run in parallel
|
||||
/// without topic collisions.
|
||||
///
|
||||
/// # Basics
|
||||
///
|
||||
/// Both `config` and `module` are required.
|
||||
///
|
||||
/// It can either be applied inside a module tagged with `everestrs::harness`.
|
||||
/// In this case all EVerest bindings generation is done by the harness and tests
|
||||
/// can share generated code.
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[everestrs::harness(config = "config.yaml", module = "example_1")]
|
||||
/// mod my_tests {
|
||||
/// #[everestrs::test(config = "config.yaml", module = "example_1")]
|
||||
/// fn test_a(module: &Module) { ... }
|
||||
///
|
||||
/// #[everestrs::test(config = "config.yaml", module = "example_1")]
|
||||
/// fn test_b(module: &Module) { ... }
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Alternatively you can use the test macro to generate the EVerest bindings.
|
||||
/// In this case the bindings are only accessible in the test itself:
|
||||
/// ```ignore
|
||||
/// #[everestrs::test(config = "config.yaml", module = "example_1", harness = true)]
|
||||
/// fn test_a(module: &Module) { ... }
|
||||
/// ```
|
||||
///
|
||||
/// # Other macros
|
||||
///
|
||||
/// The macro can be combined with other commonly used macros, for example
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[everestrs::test(config = "config.yaml", module = "example_1", harness = true)]
|
||||
/// #[should_panic]
|
||||
/// fn test_a(module: &Module) {
|
||||
/// assert!(false);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// You can also combine it with #[tokio::test]. The ordering does not matter
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[everestrs::test(config = "config.yaml", module = "example_1", harness = true)]
|
||||
/// #[tokio::test]
|
||||
/// async fn my_test(module: &Module) {
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// works same as
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[tokio::test]
|
||||
/// #[everestrs::test(config = "config.yaml", module = "example_1", harness = true)]
|
||||
/// async fn my_test(module: &Module) {
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn test(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let test_attr = parse_macro_input!(attr as TestAttr);
|
||||
let input = parse_macro_input!(item as ItemFn);
|
||||
|
||||
// If config is provided, generate harness + test in a wrapping module.
|
||||
// If not, just emit the test function (assumes harness is on an enclosing mod).
|
||||
match test_impl(&test_attr, input) {
|
||||
Ok(tokens) => tokens.into(),
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
}
|
||||
}
|
||||
|
||||
fn test_validate(input: &ItemFn) -> Result<(), syn::Error> {
|
||||
if !input.sig.generics.params.is_empty() {
|
||||
return Err(syn::Error::new(
|
||||
input.sig.generics.span(),
|
||||
"#[everestrs::test] does not support generic functions",
|
||||
));
|
||||
}
|
||||
|
||||
if input.sig.inputs.len() != 1 {
|
||||
return Err(syn::Error::new(
|
||||
input.sig.inputs.span(),
|
||||
"#[everestrs::test] function must have exactly one parameter",
|
||||
));
|
||||
}
|
||||
|
||||
let param = &input.sig.inputs[0];
|
||||
match param {
|
||||
syn::FnArg::Receiver(_) => {
|
||||
return Err(syn::Error::new(
|
||||
param.span(),
|
||||
"#[everestrs::test] function must not take `self`",
|
||||
));
|
||||
}
|
||||
syn::FnArg::Typed(pat_type) => match pat_type.ty.as_ref() {
|
||||
Type::Reference(type_ref) => {
|
||||
if type_ref.mutability.is_some() {
|
||||
return Err(syn::Error::new(
|
||||
type_ref.mutability.span(),
|
||||
"#[everestrs::test] parameter must be a shared reference, not `&mut`",
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(syn::Error::new(
|
||||
pat_type.ty.span(),
|
||||
"#[everestrs::test] parameter must be a reference (e.g. `module: &Module`)",
|
||||
));
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn test_impl(attr: &TestAttr, item_fn: ItemFn) -> Result<proc_macro2::TokenStream, syn::Error> {
|
||||
// Sanity checks.
|
||||
test_validate(&item_fn)?;
|
||||
|
||||
// Forward all attributes from the user function to the generated #[test] fn
|
||||
// (e.g. #[should_panic], #[ignore], #[allow(...)]).
|
||||
let attrs = &item_fn.attrs;
|
||||
|
||||
let sig = &item_fn.sig;
|
||||
let ident = &sig.ident;
|
||||
let param = &sig.inputs[0];
|
||||
let ret = &sig.output;
|
||||
let body = &item_fn.block;
|
||||
// Check if someone else after us might emit #[test]. `rstest` does
|
||||
// something similar to prevent reemit this. We match any attribute whose
|
||||
// last path segment is `test` — covers `#[test]`, `#[tokio::test]`,
|
||||
// `#[async_std::test]`, etc.
|
||||
// See https://github.com/la10736/rstest/blob/master/rstest_macros/src/utils.rs#L38
|
||||
// and https://github.com/la10736/rstest/blob/master/rstest_macros/src/parse/rstest/test_attr.rs#L25
|
||||
let maybe_test = if attrs.iter().any(|a| {
|
||||
a.path()
|
||||
.segments
|
||||
.last()
|
||||
.map_or(false, |s| s.ident == "test")
|
||||
}) {
|
||||
quote! {}
|
||||
} else {
|
||||
quote! { #[test] }
|
||||
};
|
||||
let maybe_async = &sig.asyncness;
|
||||
let maybe_await = maybe_async.as_ref().map(|_| quote! {.await});
|
||||
let module = &attr.module_name;
|
||||
|
||||
// Extract the parameter name and inner type from `name: &Type`.
|
||||
let (param_name, inner_ty) = match param {
|
||||
syn::FnArg::Typed(pat_type) => {
|
||||
let name = &pat_type.pat;
|
||||
match pat_type.ty.as_ref() {
|
||||
Type::Reference(type_ref) => (name, &type_ref.elem),
|
||||
_ => unreachable!("validated above"),
|
||||
}
|
||||
}
|
||||
_ => unreachable!("validated above"),
|
||||
};
|
||||
|
||||
// Derive the config filename from the attribute. The bazel rule symlinks
|
||||
// the config file to etc/everest/<basename>.
|
||||
let config_basename = std::path::Path::new(&attr.config_path.value())
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.into_owned();
|
||||
|
||||
// The function we would like to emit
|
||||
let test_fn = quote! {
|
||||
#maybe_test
|
||||
#(#attrs)*
|
||||
#maybe_async fn #ident() #ret {
|
||||
let prefix = std::env::current_dir().expect("Failed to get current directory");
|
||||
let config = prefix.join(format!("etc/everest/{}", #config_basename));
|
||||
|
||||
// Each test gets a unique MQTT prefix so tests can run in parallel
|
||||
// without topic collisions.
|
||||
let __mqtt_prefix = format!(
|
||||
"everest_test_{:?}_{:?}/",
|
||||
std::process::id(),
|
||||
std::thread::current().id(),
|
||||
);
|
||||
|
||||
// Start the manager, telling it not to spawn the module under test.
|
||||
// Blocks until the manager signals readiness via --status-fifo.
|
||||
let _manager = ::everestrs::manager::Manager::start(
|
||||
&prefix, &config, &[#module], Some(&__mqtt_prefix),
|
||||
).expect("Failed to start manager");
|
||||
let args = everestrs::Args {
|
||||
prefix: prefix.clone(),
|
||||
module: #module.to_string(),
|
||||
log_config: prefix.join("etc/everest/default_logging.cfg"),
|
||||
mqtt_broker_socket_path: None,
|
||||
mqtt_broker_host: "localhost".to_string(),
|
||||
mqtt_broker_port: 1883,
|
||||
mqtt_everest_prefix: __mqtt_prefix,
|
||||
mqtt_external_prefix: "".to_string(),
|
||||
};
|
||||
|
||||
let #param_name = #inner_ty::new_with_args(args);
|
||||
let __everest_result = {
|
||||
#maybe_async fn __everest_test(#param_name: &#inner_ty) #ret
|
||||
#body
|
||||
__everest_test(&#param_name) #maybe_await
|
||||
};
|
||||
drop(#param_name);
|
||||
__everest_result
|
||||
}
|
||||
};
|
||||
|
||||
if attr.harness {
|
||||
let generated_tokens = generate_harness_tokens(attr)?;
|
||||
let mod_ident = &item_fn.sig.ident;
|
||||
Ok(quote! {
|
||||
mod #mod_ident {
|
||||
#generated_tokens
|
||||
#[allow(unused_imports)]
|
||||
use generated::Module;
|
||||
#test_fn
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Ok(test_fn)
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_everest_core_roots() -> Vec<PathBuf> {
|
||||
if let Ok(val) = std::env::var("EVEREST_CORE_ROOT") {
|
||||
val.split(':').map(PathBuf::from).collect()
|
||||
} else {
|
||||
// Fallback: try CARGO_MANIFEST_DIR and walk up to find the repo root.
|
||||
if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
|
||||
let mut dir = PathBuf::from(&manifest_dir);
|
||||
// Walk up looking for a directory that contains an "interfaces" subdirectory.
|
||||
loop {
|
||||
if dir.join("interfaces").is_dir() {
|
||||
return vec![dir];
|
||||
}
|
||||
if !dir.pop() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
Vec::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Core logic: resolves config, builds synthetic manifest, runs codegen.
|
||||
/// Returns the generated tokens.
|
||||
fn generate_harness_tokens(attr: &TestAttr) -> Result<proc_macro2::TokenStream, syn::Error> {
|
||||
let manifest_dir = std::env::var("CARGO_MANIFEST_DIR")
|
||||
.map_err(|_| syn::Error::new(attr.config_path.span(), "CARGO_MANIFEST_DIR not set"))?;
|
||||
|
||||
let config_path = PathBuf::from(&manifest_dir).join(attr.config_path.value());
|
||||
let module_instance = attr.module_name.value();
|
||||
let everest_core_roots = resolve_everest_core_roots();
|
||||
|
||||
if everest_core_roots.is_empty() {
|
||||
return Err(syn::Error::new(
|
||||
attr.config_path.span(),
|
||||
"Could not determine everest-core root. Set EVEREST_CORE_ROOT environment variable.",
|
||||
));
|
||||
}
|
||||
|
||||
let (manifest, _tracked_files) =
|
||||
everestrs_build::build_test_manifest(&config_path, &module_instance, &everest_core_roots)
|
||||
.map_err(|e| {
|
||||
syn::Error::new(
|
||||
attr.module_name.span(),
|
||||
format!("Failed to build test manifest: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let generated_code = everestrs_build::codegen::emit_manifest(manifest, everest_core_roots)
|
||||
.map_err(|e| {
|
||||
syn::Error::new(
|
||||
attr.module_name.span(),
|
||||
format!("Code generation failed: {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
syn::parse_str(&generated_code).map_err(|e| {
|
||||
syn::Error::new(
|
||||
attr.module_name.span(),
|
||||
format!("Failed to parse generated code: {e}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Attribute macro that auto-generates a test counterpart module by reading
|
||||
/// a config.yaml and deducing interfaces from connected modules' manifests.
|
||||
///
|
||||
/// Applied to a `mod` to share generated types across multiple tests and
|
||||
/// fixtures. For single test functions, use
|
||||
/// `#[everestrs::test(config = "...", module = "...")]` instead.
|
||||
///
|
||||
/// ```ignore
|
||||
/// #[everestrs::harness(config = "config.yaml", module = "example_1")]
|
||||
/// mod my_tests {
|
||||
/// #[everestrs::test(config = "config.yaml", module = "example_1")]
|
||||
/// fn test_a(module: &Module) { ... }
|
||||
///
|
||||
/// #[everestrs::test(config = "config.yaml", module = "example_1")]
|
||||
/// fn test_b(module: &Module) { ... }
|
||||
/// }
|
||||
/// ```
|
||||
#[proc_macro_attribute]
|
||||
pub fn harness(attr: TokenStream, item: TokenStream) -> TokenStream {
|
||||
let test_attr = parse_macro_input!(attr as TestAttr);
|
||||
let item_mod = parse_macro_input!(item as ItemMod);
|
||||
|
||||
match harness_impl(&test_attr, item_mod) {
|
||||
Ok(tokens) => tokens.into(),
|
||||
Err(e) => e.to_compile_error().into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generates harness code and injects it into a module body.
|
||||
fn harness_impl(
|
||||
attr: &TestAttr,
|
||||
item_mod: syn::ItemMod,
|
||||
) -> Result<proc_macro2::TokenStream, syn::Error> {
|
||||
let generated_tokens = generate_harness_tokens(attr)?;
|
||||
if let Some((_brace, items)) = item_mod.content {
|
||||
let vis = &item_mod.vis;
|
||||
let ident = &item_mod.ident;
|
||||
let attrs = &item_mod.attrs;
|
||||
Ok(quote! {
|
||||
#(#attrs)*
|
||||
#vis mod #ident {
|
||||
#generated_tokens
|
||||
#[allow(unused_imports)]
|
||||
use generated::Module;
|
||||
#(#items)*
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Err(syn::Error::new_spanned(
|
||||
&item_mod,
|
||||
"#[everestrs::harness] requires a module with a body (not just a declaration)",
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
load("@bazel_skylib//rules:run_binary.bzl", "run_binary")
|
||||
load("@rules_cc//cc:defs.bzl", "cc_library")
|
||||
load("@rules_rust//rust:defs.bzl", "rust_library")
|
||||
|
||||
rust_library(
|
||||
name = "everestrs",
|
||||
srcs = glob(["src/**/*.rs"]),
|
||||
edition = "2021",
|
||||
proc_macro_deps = [
|
||||
"//lib/everest/framework/everestrs/everestrs-derive",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":everestrs_bridge",
|
||||
":everestrs_sys",
|
||||
"//lib/everest/framework/everestrs/everestrs-build",
|
||||
"@cxx.rs//:cxx",
|
||||
"@everest_framework_crate_index//:clap",
|
||||
"@everest_framework_crate_index//:log",
|
||||
"@everest_framework_crate_index//:nix",
|
||||
"@everest_framework_crate_index//:serde",
|
||||
"@everest_framework_crate_index//:serde_json",
|
||||
"@everest_framework_crate_index//:serde_yaml",
|
||||
"@everest_framework_crate_index//:thiserror",
|
||||
],
|
||||
)
|
||||
|
||||
run_binary(
|
||||
name = "everestrs_bridge/generated",
|
||||
srcs = ["src/lib.rs"],
|
||||
outs = [
|
||||
"src/lib.rs.cc",
|
||||
"src/lib.rs.h",
|
||||
],
|
||||
args = [
|
||||
"$(location src/lib.rs)",
|
||||
"-o",
|
||||
"$(location src/lib.rs.h)",
|
||||
"-o",
|
||||
"$(location src/lib.rs.cc)",
|
||||
],
|
||||
tool = "@cxx.rs//:codegen",
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "everestrs_bridge",
|
||||
srcs = ["src/lib.rs" + ".cc"],
|
||||
cxxopts = ["-std=c++17"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":everestrs_bridge/include",
|
||||
":everestrs_sys_include",
|
||||
"//lib/everest/framework",
|
||||
],
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "everestrs_sys",
|
||||
srcs = ["src/everestrs_sys.cpp"],
|
||||
cxxopts = ["-std=c++17"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
":everestrs_bridge/include",
|
||||
":everestrs_sys_include",
|
||||
"//lib/everest/framework",
|
||||
],
|
||||
alwayslink = True,
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "everestrs_bridge/include",
|
||||
hdrs = ["src/lib.rs" + ".h"],
|
||||
include_prefix = "everestrs",
|
||||
)
|
||||
|
||||
cc_library(
|
||||
name = "everestrs_sys_include",
|
||||
hdrs = ["src/everestrs_sys.hpp"],
|
||||
include_prefix = "everestrs",
|
||||
deps = ["@cxx.rs//:core"],
|
||||
)
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "everestrs"
|
||||
version = "0.25.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.27", features = ["derive"] }
|
||||
everestrs-build = { workspace = true }
|
||||
everestrs-derive = { workspace = true }
|
||||
log = { version = "0.4.20", features = ["std"] }
|
||||
serde = { version = "1.0.175", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
serde_yaml = "0.9.34"
|
||||
thiserror = "1.0.48"
|
||||
nix = { version = "0.31.2", features = ["fs"] }
|
||||
# Note: the version must be kept in sync with the `cxxbridge-cmd` version installed in `everestrs/CMakeLists.txt`.
|
||||
# The `=` before the version number ensures that Cargo uses exactly this version when generating new lockfiles.
|
||||
cxx = { version = "=1.0.194", features = ["c++17"] }
|
||||
|
||||
[features]
|
||||
build_bazel = []
|
||||
|
||||
# We use this for the integration tests in this project.
|
||||
[dev-dependencies]
|
||||
mockall = "0.13.0"
|
||||
mockall_double = "0.3.1"
|
||||
# For tests
|
||||
tokio = { version = "1.52.1", features = ["rt-multi-thread", "macros", "sync", "time"] }
|
||||
@@ -0,0 +1,63 @@
|
||||
# Rust support for EVerest
|
||||
|
||||
This is Rust support using cxx.rs to wrap the framework C++ library.
|
||||
|
||||
## Trying it out
|
||||
|
||||
- Install Rust as outlined on <https://rustup.rs/>, which should just be this
|
||||
one line: `curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh`
|
||||
- Built your workspace as outlined in `EVerest` README, make sure to tell
|
||||
cMake to enable `EVEREST_ENABLE_RS_SUPPORT`.
|
||||
- You can now try building the code, but it will not do anything: `cd everestrs
|
||||
&& cargo build --all`
|
||||
- You should now be able to configure the `RsExample` or `RsExampleUser` modules in your config
|
||||
YAML.
|
||||
|
||||
## Differences to other EVerest language wrappers
|
||||
|
||||
- The `enable_external_mqtt` is ignored for Rust modules. If you want to interact
|
||||
with MQTT externally, just pull an external mqtt module (for example the
|
||||
really excellent [rumqttc](https://docs.rs/rumqttc/latest/rumqttc/)) crate
|
||||
into your module and use it directly. This is a conscious decision to future
|
||||
proof, should EVerest at some point move to something different than MQTT as
|
||||
transport layer and for cleaner abstraction.
|
||||
|
||||
## Status
|
||||
|
||||
Full support for requiring and providing interfaces is implemented, missing
|
||||
currently is:
|
||||
|
||||
- Support for EVerest logging
|
||||
- Support for implementations with `max_connections != 1` or `min_connections != 1`
|
||||
|
||||
## Mocking in Unit-Tests
|
||||
|
||||
The Rust wrapper supports mocking, which allows you to unit tests your modules.
|
||||
To enable mocking in your code you need to do some steps however
|
||||
* Add [mockall](https://github.com/asomers/mockall) and
|
||||
[mockall_double](https://github.com/asomers/mockall) to your module as dependencies
|
||||
* Add a `mockall` feature to your module and enable it for your tests.
|
||||
|
||||
Then all publishers are mocked with `mockall`.
|
||||
|
||||
## Building from external repositories without CMake
|
||||
|
||||
Note that the `EVerest` (and `everest-framework` prior to migration to a monorepo layout) repositories are
|
||||
automatically configured for this use case, this section is only relevant for
|
||||
Rust modules residing in their own repositories.
|
||||
|
||||
While external Rust modules can be compiled using CMake without any setup,
|
||||
compiling with `cargo` directly requires some additional setup. This is
|
||||
required for rust-analyzer's IDE integration to work properly.
|
||||
|
||||
- First, build your EVerest workspace with Rust support enabled by
|
||||
passing `-DEVEREST_ENABLE_RS_SUPPORT=ON` to CMake.
|
||||
- Create a file at `modules/.cargo/config.toml` in your repository.
|
||||
It file should contain the following, with `<build-dir>` replaced by the
|
||||
name of your EVerest build directory.
|
||||
|
||||
```toml
|
||||
[env.EVEREST_RS_LINK_DEPENDENCIES]
|
||||
value = "../<build-dir>/everestrs-link-dependencies.txt"
|
||||
relative = true
|
||||
```
|
||||
@@ -0,0 +1,81 @@
|
||||
use std::{
|
||||
env,
|
||||
fs::File,
|
||||
io::{self, BufRead, BufReader},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
/// Takes a path to a library like `libframework.so` and returns the name for the linker, aka
|
||||
/// `framework`
|
||||
fn libname_from_path(p: &Path) -> String {
|
||||
let base = p
|
||||
.file_stem()
|
||||
.and_then(|os_str| os_str.to_str())
|
||||
.expect("'p' must be valid UTF-8 and have an extension.")
|
||||
.strip_prefix("lib")
|
||||
.expect("'p' should start with `lib`");
|
||||
|
||||
// foo.so.1.2.3 -> foo
|
||||
base.split('.').next().unwrap_or(base).to_string()
|
||||
}
|
||||
|
||||
fn print_link_options(p: &Path) {
|
||||
println!(
|
||||
"cargo:rustc-link-search=native={}",
|
||||
p.parent().unwrap().to_string_lossy()
|
||||
);
|
||||
println!("cargo:rustc-link-lib={}", libname_from_path(p));
|
||||
}
|
||||
|
||||
/// Registers the libraries specified in the `EVEREST_RS_LINK_DEPENDENCIES` environment variable.
|
||||
/// Expected to be a path to a text file that contains one object file path per line.
|
||||
fn register_everest_link_deps(link_deps_path: &str) -> io::Result<()> {
|
||||
let link_deps = File::open(link_deps_path).map_err(|e| {
|
||||
io::Error::new(
|
||||
e.kind(),
|
||||
format!("Could not open EVEREST_RS_LINK_DEPENDENCIES file '{link_deps_path}': {e}"),
|
||||
)
|
||||
})?;
|
||||
|
||||
let mut found_any = false;
|
||||
for line in BufReader::new(link_deps).lines() {
|
||||
let line = line?;
|
||||
let path = Path::new(&line);
|
||||
if !path.is_file() {
|
||||
return Err(io::Error::new(io::ErrorKind::NotFound, format!(
|
||||
"Cannot find library path '{line}' specified in EVEREST_RS_LINK_DEPENDENCIES ({link_deps_path}).",
|
||||
)));
|
||||
}
|
||||
|
||||
print_link_options(path);
|
||||
found_any = true;
|
||||
}
|
||||
|
||||
if found_any {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::new(
|
||||
io::ErrorKind::NotFound,
|
||||
format!("No library paths found in EVEREST_RS_LINK_DEPENDENCIES ({link_deps_path})."),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
// See https://doc.rust-lang.org/cargo/reference/features.html#build-scripts
|
||||
// for details.
|
||||
if env::var("CARGO_FEATURE_BUILD_BAZEL").is_ok() {
|
||||
println!("Skipping due to bazel");
|
||||
return;
|
||||
}
|
||||
|
||||
let Ok(link_deps_path) = env::var("EVEREST_RS_LINK_DEPENDENCIES") else {
|
||||
panic!("EVEREST_RS_LINK_DEPENDENCIES environment variable is not set. This variable should point to the build/everestrs-link-dependencies.txt file generated by CMake.");
|
||||
};
|
||||
|
||||
register_everest_link_deps(&link_deps_path)
|
||||
.expect("Failed to register libraries specified in EVEREST_RS_LINK_DEPENDENCIES");
|
||||
|
||||
println!("cargo:rustc-link-lib=boost_log");
|
||||
println!("cargo:rustc-link-lib=boost_log_setup");
|
||||
}
|
||||
@@ -0,0 +1,310 @@
|
||||
#include "everestrs/src/everestrs_sys.hpp"
|
||||
#include "everestrs/src/lib.rs.h"
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
#include <utils/conversions.hpp>
|
||||
#include <utils/error/error_manager_impl.hpp>
|
||||
#include <utils/error/error_manager_req.hpp>
|
||||
#include <utils/exceptions.hpp>
|
||||
#include <utils/types.hpp>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <variant>
|
||||
|
||||
namespace {
|
||||
|
||||
JsonBlob json2blob(const json& j) {
|
||||
// I did not find a way to not copy the data at least once here.
|
||||
const std::string dumped = j.dump();
|
||||
rust::Vec<uint8_t> vec;
|
||||
vec.reserve(dumped.size());
|
||||
std::copy(dumped.begin(), dumped.end(), std::back_inserter(vec));
|
||||
return JsonBlob{vec};
|
||||
}
|
||||
|
||||
// Below are overloads to be used with std::visit and our std::variant. We force
|
||||
// a compilation error if someone changes the underlying std::variant without
|
||||
// extending/adjusting the functions below.
|
||||
|
||||
template <typename T, typename... VARIANT_T> struct VariantMemberImpl : public std::false_type {};
|
||||
|
||||
template <typename T, typename... VARIANT_T>
|
||||
struct VariantMemberImpl<T, std::variant<VARIANT_T...>> : public std::disjunction<std::is_same<T, VARIANT_T>...> {};
|
||||
|
||||
/// @brief Static checker if the type T can be converted to `everest::config::ConfigEntry`.
|
||||
///
|
||||
/// We use this to detect `get_config_field` overloads which receive arguments
|
||||
/// which aren't part of our `everest::config::ConfigEntry` variant.
|
||||
template <typename T> struct ConfigEntryMember : public VariantMemberImpl<T, everest::config::ConfigEntry> {};
|
||||
|
||||
inline ConfigField get_config_field(const std::string& _name, bool _value) {
|
||||
static_assert(ConfigEntryMember<decltype(_value)>::value);
|
||||
return {_name, ConfigType::Boolean, _value, {}, 0, 0};
|
||||
}
|
||||
|
||||
inline ConfigField get_config_field(const std::string& _name, const std::string& _value) {
|
||||
static_assert(ConfigEntryMember<std::remove_cv_t<std::remove_reference_t<decltype(_value)>>>::value);
|
||||
return {_name, ConfigType::String, false, _value, 0, 0};
|
||||
}
|
||||
|
||||
inline ConfigField get_config_field(const std::string& _name, double _value) {
|
||||
static_assert(ConfigEntryMember<decltype(_value)>::value);
|
||||
return {_name, ConfigType::Number, false, {}, _value, 0};
|
||||
}
|
||||
|
||||
inline ConfigField get_config_field(const std::string& _name, int _value) {
|
||||
static_assert(ConfigEntryMember<decltype(_value)>::value);
|
||||
return {_name, ConfigType::Integer, false, {}, 0, _value};
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<Module> create_module(rust::Str module_id, rust::Str prefix, rust::Str mqtt_broker_socket_path,
|
||||
rust::Str mqtt_broker_host, const std::uint16_t& mqtt_broker_port,
|
||||
rust::Str mqtt_everest_prefix, rust::Str mqtt_external_prefix) {
|
||||
auto socket_path = std::string(mqtt_broker_socket_path);
|
||||
Everest::MQTTSettings mqtt_settings;
|
||||
if (not socket_path.empty()) {
|
||||
Everest::populate_mqtt_settings(mqtt_settings, socket_path, std::string(mqtt_everest_prefix),
|
||||
std::string(mqtt_external_prefix));
|
||||
} else {
|
||||
Everest::populate_mqtt_settings(mqtt_settings, std::string(mqtt_broker_host), mqtt_broker_port,
|
||||
std::string(mqtt_everest_prefix), std::string(mqtt_external_prefix));
|
||||
}
|
||||
return std::make_unique<Module>(std::string(module_id), std::string(prefix), mqtt_settings);
|
||||
}
|
||||
|
||||
Module::Module(const std::string& module_id, const std::string& prefix, const Everest::MQTTSettings& mqtt_settings) :
|
||||
module_id_(module_id) {
|
||||
|
||||
const auto mqtt_abstraction =
|
||||
std::shared_ptr<Everest::MQTTAbstraction>(Everest::make_mqtt_abstraction(mqtt_settings));
|
||||
// TODO(ddo) what happens when this returns false?
|
||||
mqtt_abstraction->connect();
|
||||
mqtt_abstraction->spawn_main_loop_thread();
|
||||
|
||||
const auto result = Everest::get_module_config(mqtt_abstraction, this->module_id_);
|
||||
|
||||
this->rs_ = std::make_unique<Everest::RuntimeSettings>(result.at("settings"));
|
||||
|
||||
config_ = std::make_shared<Everest::Config>(mqtt_settings, result);
|
||||
|
||||
handle_ = std::make_unique<Everest::Everest>(this->module_id_, *this->config_, this->rs_->validate_schema,
|
||||
mqtt_abstraction, this->rs_->telemetry_prefix,
|
||||
this->rs_->telemetry_enabled, this->rs_->forward_exceptions);
|
||||
|
||||
// Not needed but done to be congruent with the other bindings.
|
||||
handle_->spawn_main_loop_thread();
|
||||
}
|
||||
|
||||
Module::~Module() {
|
||||
// MQTTAbstractionImpl spawns a main loop thread whose only exit condition
|
||||
// is `disconnect_event` being notified. Without this call the `Thread`
|
||||
// member destructor joins a thread that will never exit.
|
||||
if (handle_) {
|
||||
handle_->disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
JsonBlob Module::get_interface(rust::Str interface_name) const {
|
||||
const auto& interface_def = config_->get_interface_definition(std::string(interface_name));
|
||||
return json2blob(interface_def);
|
||||
}
|
||||
|
||||
JsonBlob Module::get_manifest() const {
|
||||
const std::string& module_name = config_->get_module_name(std::string(module_id_));
|
||||
return json2blob(config_->get_manifests().at(module_name));
|
||||
}
|
||||
|
||||
void Module::signal_ready(const Runtime& rt) const {
|
||||
handle_->register_on_ready_handler([&rt]() { rt.on_ready(); });
|
||||
handle_->signal_ready();
|
||||
}
|
||||
|
||||
void Module::provide_command(const Runtime& rt, rust::String implementation_id, rust::String name) const {
|
||||
using namespace Everest;
|
||||
handle_->provide_cmd(std::string(implementation_id), std::string(name), [&rt, implementation_id, name](json args) {
|
||||
JsonBlob blob = rt.handle_command(implementation_id, name, json2blob(args));
|
||||
const auto retval = json::parse(blob.data.begin(), blob.data.end());
|
||||
|
||||
// Check if our command handler failed.
|
||||
if (retval.contains(conversions::ERROR_TYPE)) {
|
||||
const auto error_str = retval.at(conversions::ERROR_TYPE).get<std::string>();
|
||||
const auto error_msg = retval.at(conversions::ERROR_MSG).get<std::string>();
|
||||
const auto error_enm = conversions::string_to_cmd_error_type(error_str);
|
||||
switch (error_enm) {
|
||||
case CmdErrorType::MessageParsingError:
|
||||
throw MessageParsingError(error_msg);
|
||||
case CmdErrorType::SchemaValidationError:
|
||||
throw SchemaValidationError(error_msg);
|
||||
case CmdErrorType::HandlerException:
|
||||
throw HandlerException(error_msg);
|
||||
case CmdErrorType::CmdTimeout:
|
||||
throw CmdTimeout(error_msg);
|
||||
case CmdErrorType::Shutdown:
|
||||
throw Shutdown(error_msg);
|
||||
case CmdErrorType::NotReady:
|
||||
throw NotReady(error_msg);
|
||||
}
|
||||
}
|
||||
return retval;
|
||||
});
|
||||
}
|
||||
|
||||
void Module::subscribe_variable(const Runtime& rt, rust::String implementation_id, std::size_t index,
|
||||
rust::String name) const {
|
||||
const auto req = Requirement{std::string(implementation_id), index};
|
||||
// The handle_ptr is guaranteed to be alive in the callback.
|
||||
const auto handle_ptr = handle_.get();
|
||||
handle_->subscribe_var(req, std::string(name), [&rt, implementation_id, index, name, handle_ptr](json args) {
|
||||
handle_ptr->ensure_ready();
|
||||
rt.handle_variable(implementation_id, index, name, json2blob(args));
|
||||
});
|
||||
}
|
||||
|
||||
void Module::subscribe_all_errors(const Runtime& rt) const {
|
||||
for (const Requirement& req : config_->get_requirements(module_id_)) {
|
||||
std::shared_ptr<Everest::error::ErrorManagerReq> error_manager_ptr = nullptr;
|
||||
try {
|
||||
error_manager_ptr = handle_->get_error_manager_req(req);
|
||||
} catch (const std::runtime_error& ex) {
|
||||
// This is expected if the manifest has `ignore.errors=true`
|
||||
// configured.
|
||||
continue;
|
||||
}
|
||||
const auto handle_ptr = handle_.get();
|
||||
error_manager_ptr->subscribe_all_errors(
|
||||
[&rt, req, handle_ptr](Everest::error::Error error) {
|
||||
handle_ptr->ensure_ready();
|
||||
const ErrorType rust_error{rust::String(error.type), rust::String(error.description),
|
||||
rust::String(error.message), static_cast<ErrorSeverity>(error.severity)};
|
||||
rt.handle_on_error(rust::Str(req.id), req.index, rust_error, true);
|
||||
},
|
||||
[&rt, req, handle_ptr](Everest::error::Error error) {
|
||||
handle_ptr->ensure_ready();
|
||||
const ErrorType rust_error{rust::String(error.type), rust::String(error.description),
|
||||
rust::String(error.message), static_cast<ErrorSeverity>(error.severity)};
|
||||
rt.handle_on_error(rust::Str(req.id), req.index, rust_error, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
JsonBlob Module::call_command(rust::Str implementation_id, std::size_t index, rust::Str name, JsonBlob blob) const {
|
||||
using namespace Everest;
|
||||
const auto req = Requirement{std::string(implementation_id), index};
|
||||
json retval;
|
||||
try {
|
||||
retval = handle_->call_cmd(req, std::string(name), json::parse(blob.data.begin(), blob.data.end()));
|
||||
} catch (const MessageParsingError& ex) {
|
||||
to_json(retval, CmdResultError{CmdErrorType::MessageParsingError, ex.what(), nullptr});
|
||||
} catch (const SchemaValidationError& ex) {
|
||||
to_json(retval, CmdResultError{CmdErrorType::SchemaValidationError, ex.what(), nullptr});
|
||||
} catch (const HandlerException& ex) {
|
||||
to_json(retval, CmdResultError{CmdErrorType::HandlerException, ex.what(), nullptr});
|
||||
} catch (const CmdTimeout& ex) {
|
||||
to_json(retval, CmdResultError{CmdErrorType::CmdTimeout, ex.what(), nullptr});
|
||||
} catch (const Shutdown& ex) {
|
||||
to_json(retval, CmdResultError{CmdErrorType::Shutdown, ex.what(), nullptr});
|
||||
} catch (const NotReady& ex) {
|
||||
to_json(retval, CmdResultError{CmdErrorType::NotReady, ex.what(), nullptr});
|
||||
}
|
||||
return json2blob(retval);
|
||||
}
|
||||
|
||||
void Module::publish_variable(rust::Str implementation_id, rust::Str name, JsonBlob blob) const {
|
||||
handle_->publish_var(std::string(implementation_id), std::string(name),
|
||||
json::parse(blob.data.begin(), blob.data.end()));
|
||||
}
|
||||
|
||||
void Module::raise_error(rust::Str implementation_id, ErrorType error_type) const {
|
||||
// Here everything is called implementation_id: We have the Rust string type
|
||||
// then we have its c++ counterpart as std::string. And then we have the
|
||||
// ImplementationIdentifier type :S
|
||||
const std::string impl_id = std::string(implementation_id);
|
||||
const auto full_mapping = handle_->get_3_tier_model_mapping();
|
||||
std::optional<Mapping> mapping;
|
||||
if (full_mapping.has_value()) {
|
||||
const auto& inner = *full_mapping;
|
||||
mapping = inner.module;
|
||||
// We might have multiple mappings. In this case we pick the
|
||||
// implementation mapping is since this is more specific.
|
||||
const auto impl_mapping_iter = inner.implementations.find(impl_id);
|
||||
if (impl_mapping_iter != inner.implementations.end()) {
|
||||
mapping = impl_mapping_iter->second;
|
||||
}
|
||||
}
|
||||
const ImplementationIdentifier id{module_id_, impl_id, mapping};
|
||||
const Everest::error::Error error{std::string(error_type.error_type),
|
||||
std::string{},
|
||||
std::string(error_type.message),
|
||||
std::string(error_type.description),
|
||||
id,
|
||||
static_cast<Everest::error::Severity>(error_type.severity)};
|
||||
handle_->get_error_manager_impl(std::string(implementation_id))->raise_error(error);
|
||||
}
|
||||
|
||||
void Module::clear_error(rust::Str implementation_id, rust::Str error_type, bool clear_all) const {
|
||||
const auto manager = handle_->get_error_manager_impl(std::string(implementation_id));
|
||||
|
||||
if (error_type.empty()) {
|
||||
manager->clear_all_errors();
|
||||
} else {
|
||||
if (clear_all) {
|
||||
manager->clear_all_errors(std::string(error_type));
|
||||
} else {
|
||||
manager->clear_error(std::string(error_type));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rust::Vec<RsModuleConnections> Module::get_module_connections() const {
|
||||
const auto connections = config_->get_module_config().connections;
|
||||
|
||||
// Iterate over the connections block.
|
||||
rust::Vec<RsModuleConnections> out;
|
||||
out.reserve(connections.size());
|
||||
for (const auto& [req_id, fulfillment] : connections) {
|
||||
out.emplace_back(RsModuleConnections{rust::String{req_id}, fulfillment.size()});
|
||||
};
|
||||
return out;
|
||||
}
|
||||
|
||||
rust::Vec<RsModuleConfig> Module::get_module_configs(rust::Str module_id) const {
|
||||
// TODO(ddo) We call this before initializing the logger.
|
||||
const auto module_configs = config_->get_module_configs(std::string(module_id));
|
||||
|
||||
rust::Vec<RsModuleConfig> out;
|
||||
out.reserve(module_configs.size());
|
||||
|
||||
// Iterate over all modules stored in the module_config.
|
||||
for (const auto& mm : module_configs) {
|
||||
RsModuleConfig mm_out{mm.first, {}};
|
||||
mm_out.data.reserve(mm.second.size());
|
||||
|
||||
// Iterate over all configs stored in the mm (our current module).
|
||||
for (const auto& cc : mm.second) {
|
||||
mm_out.data.emplace_back(
|
||||
std::visit([&](auto&& _value) { return ::get_config_field(cc.first, _value); }, cc.second));
|
||||
}
|
||||
out.emplace_back(std::move(mm_out));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
int init_logging(rust::Str module_id, rust::Str prefix, rust::Str logging_config_file) {
|
||||
const std::string module_id_cpp{module_id};
|
||||
const std::string logging_config_file_cpp{logging_config_file};
|
||||
|
||||
// // Init the CPP logger.
|
||||
return Everest::Logging::init(logging_config_file_cpp, module_id_cpp);
|
||||
}
|
||||
|
||||
void log2cxx(int level, int line, rust::Str file, rust::Str message) {
|
||||
Everest::Logging::ffi_log(level, line, std::string(file), std::string(message));
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include <framework/everest.hpp>
|
||||
#include <framework/runtime.hpp>
|
||||
#include <utils/types.hpp>
|
||||
|
||||
#include "rust/cxx.h"
|
||||
|
||||
struct JsonBlob;
|
||||
struct Runtime;
|
||||
struct RsModuleConfig;
|
||||
struct RsModuleConnections;
|
||||
struct ConfigField;
|
||||
struct ErrorType;
|
||||
|
||||
enum class ConfigTypes : uint8_t;
|
||||
enum class ErrorState : uint8_t;
|
||||
enum class ErrorSeverity : uint8_t;
|
||||
|
||||
class Module {
|
||||
public:
|
||||
/// @brief The c'tor should not be called by the user code.
|
||||
///
|
||||
/// In order to create the Module use the `create_module` function.
|
||||
Module(const std::string& module_id, const std::string& prefix, const Everest::MQTTSettings& mqtt_settings);
|
||||
|
||||
/// Stops the MQTT main loop before member destruction so the
|
||||
/// `mqtt_mainloop_thread` join inside `~MQTTAbstractionImpl` doesn't
|
||||
/// deadlock.
|
||||
~Module();
|
||||
|
||||
JsonBlob get_manifest() const;
|
||||
JsonBlob get_interface(rust::Str interface_name) const;
|
||||
rust::Vec<RsModuleConfig> get_module_configs(rust::Str module_name) const;
|
||||
rust::Vec<RsModuleConnections> get_module_connections() const;
|
||||
|
||||
void signal_ready(const Runtime& rt) const;
|
||||
void provide_command(const Runtime& rt, rust::String implementation_id, rust::String name) const;
|
||||
JsonBlob call_command(rust::Str implementation_id, std::size_t index, rust::Str name, JsonBlob args) const;
|
||||
void subscribe_variable(const Runtime& rt, rust::String implementation_id, std::size_t index,
|
||||
rust::String name) const;
|
||||
void subscribe_all_errors(const Runtime& rt) const;
|
||||
|
||||
void publish_variable(rust::Str implementation_id, rust::Str name, JsonBlob blob) const;
|
||||
|
||||
void raise_error(rust::Str implementation_id, ErrorType error_type) const;
|
||||
|
||||
void clear_error(rust::Str implementation_id, rust::Str error_type, bool clear_all) const;
|
||||
|
||||
private:
|
||||
const std::string module_id_;
|
||||
std::unique_ptr<Everest::RuntimeSettings> rs_;
|
||||
std::shared_ptr<Everest::Config> config_;
|
||||
std::unique_ptr<Everest::Everest> handle_;
|
||||
};
|
||||
|
||||
std::unique_ptr<Module> create_module(rust::Str module_id, rust::Str prefix, rust::Str mqtt_broker_socket_path,
|
||||
rust::Str mqtt_broker_host, const std::uint16_t& mqtt_broker_port,
|
||||
rust::Str mqtt_everest_prefix, rust::Str mqtt_external_prefix);
|
||||
|
||||
int init_logging(rust::Str module_id, rust::Str prefix, rust::Str logging_config_file);
|
||||
void log2cxx(int level, int line, rust::Str file, rust::Str message);
|
||||
@@ -0,0 +1,854 @@
|
||||
pub mod manager;
|
||||
|
||||
use everestrs_build::schema;
|
||||
|
||||
use clap::Parser;
|
||||
use log::debug;
|
||||
use serde::de::DeserializeOwned;
|
||||
use std::collections::HashMap;
|
||||
use std::convert::TryFrom;
|
||||
use std::path::PathBuf;
|
||||
use std::pin::Pin;
|
||||
use std::sync::Arc;
|
||||
use std::sync::Once;
|
||||
use std::sync::OnceLock;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Prevent calling the init of loggers more than once.
|
||||
static INIT_LOGGER_ONCE: Once = Once::new();
|
||||
|
||||
// Reexport everything so the clients can use it.
|
||||
pub use everestrs_derive::{harness, main, test};
|
||||
pub use log;
|
||||
pub use serde;
|
||||
pub use serde_json;
|
||||
// TODO(ddo) Drop this again - its only there as a MVP for the enum support
|
||||
// of errors.
|
||||
pub use serde_yaml;
|
||||
|
||||
/// Errors matching the exceptions defined under `exceptions.hpp`.
|
||||
///
|
||||
/// The tags must match the tags defined under `conversions.hpp`. For client
|
||||
/// side code - always use `HandlerException`.
|
||||
#[derive(Error, Debug, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(tag = "__everest__error_type", content = "__everest__error_msg")]
|
||||
pub enum Error {
|
||||
#[error("Message Parsing Error: {0}")]
|
||||
MessageParsingError(String),
|
||||
|
||||
#[error("Schema Validation Error: {0}")]
|
||||
SchemaValidationError(String),
|
||||
|
||||
#[error("Handler Exception: {0}")]
|
||||
HandlerException(String),
|
||||
|
||||
#[error("Command Timeout: {0}")]
|
||||
CmdTimeout(String),
|
||||
|
||||
#[error("Shutdown: {0}")]
|
||||
Shutdown(String),
|
||||
|
||||
#[error("Not Ready: {0}")]
|
||||
NotReady(String),
|
||||
}
|
||||
|
||||
pub type Result<T> = ::std::result::Result<T, Error>;
|
||||
|
||||
#[cxx::bridge]
|
||||
mod ffi {
|
||||
extern "Rust" {
|
||||
type Runtime;
|
||||
fn handle_command(
|
||||
self: &Runtime,
|
||||
implementation_id: &str,
|
||||
name: &str,
|
||||
json: JsonBlob,
|
||||
) -> JsonBlob;
|
||||
fn handle_variable(
|
||||
self: &Runtime,
|
||||
implementation_id: &str,
|
||||
index: usize,
|
||||
name: &str,
|
||||
json: JsonBlob,
|
||||
);
|
||||
fn handle_on_error(
|
||||
self: &Runtime,
|
||||
implementation_id: &str,
|
||||
index: usize,
|
||||
error: ErrorType,
|
||||
raised: bool,
|
||||
);
|
||||
|
||||
fn on_ready(&self);
|
||||
}
|
||||
|
||||
struct JsonBlob {
|
||||
data: Vec<u8>,
|
||||
}
|
||||
|
||||
/// The possible types a config can have. Note: Naturally this would be am
|
||||
/// enum **with** values - however, cxx can't (for now) map Rusts enums to
|
||||
/// std::variant or union.
|
||||
#[derive(Debug)]
|
||||
enum ConfigType {
|
||||
Boolean = 0,
|
||||
String = 1,
|
||||
Number = 2,
|
||||
Integer = 3,
|
||||
}
|
||||
|
||||
/// One config entry: As said above, we can't use an enum and have to
|
||||
/// declare all values. Note: also Option is not an option...
|
||||
struct ConfigField {
|
||||
/// The name of the option, e.x. `max_voltage`.
|
||||
name: String,
|
||||
|
||||
/// Our poor-mans enum.
|
||||
config_type: ConfigType,
|
||||
|
||||
/// The value of the config field. The field has only a meaning if
|
||||
/// `conifg_type is set to [ConfigType::Boolean].
|
||||
bool_value: bool,
|
||||
|
||||
/// The value of the config field. The field has only a meaning if
|
||||
/// `config_type` is set to [ConfigType::String].
|
||||
string_value: String,
|
||||
|
||||
/// The value of the config field. The field has only a meaning if
|
||||
/// `config_type` is set to [ConfigType::Number].
|
||||
number_value: f64,
|
||||
|
||||
/// The value of the config field. The field has only a meaning if
|
||||
/// `config_type` is set to [ConfigType::Integer].
|
||||
integer_value: i64,
|
||||
}
|
||||
|
||||
/// The configs of one module. Roughly maps to the cpp's counterpart
|
||||
/// `ModuleConfig`.
|
||||
struct RsModuleConfig {
|
||||
/// The name of the group, e.x. "PowerMeter".
|
||||
module_name: String,
|
||||
|
||||
/// All `ConfigFields` in this group.
|
||||
data: Vec<ConfigField>,
|
||||
}
|
||||
|
||||
/// The information form the `connections` field of our current module.
|
||||
struct RsModuleConnections {
|
||||
/// The `implementation_id` of the connection block.
|
||||
implementation_id: String,
|
||||
|
||||
/// Number of slots under the `implementation_id`.
|
||||
slots: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ErrorSeverity {
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
}
|
||||
|
||||
/// Rust's version of the `<utils/error.hpp>`'s Error.
|
||||
#[derive(Debug)]
|
||||
pub struct ErrorType {
|
||||
/// The type of the error. We generate that in the codegen. The
|
||||
/// full error type looks like "evse_manager/PowermeterTransactionStartFailed"
|
||||
/// and may have a namespace sprinkled into it (?).
|
||||
pub error_type: String,
|
||||
|
||||
/// The description.
|
||||
pub description: String,
|
||||
|
||||
/// The message - no idea what the difference to the description
|
||||
/// actually is.
|
||||
pub message: String,
|
||||
|
||||
/// The severity of the error.
|
||||
pub severity: ErrorSeverity,
|
||||
}
|
||||
|
||||
unsafe extern "C++" {
|
||||
include!("everestrs/src/everestrs_sys.hpp");
|
||||
|
||||
type Module;
|
||||
/// Creates the module only once. The module lives then until the end
|
||||
/// of the process.
|
||||
fn create_module(
|
||||
module_id: &str,
|
||||
prefix: &str,
|
||||
mqtt_broker_socket_path: &str,
|
||||
mqtt_broker_host: &str,
|
||||
mqtt_broker_port: &u16,
|
||||
mqtt_everest_prefix: &str,
|
||||
mqtt_external_prefix: &str,
|
||||
) -> UniquePtr<Module>;
|
||||
|
||||
/// Returns the manifest.
|
||||
fn get_manifest(self: &Module) -> JsonBlob;
|
||||
|
||||
/// Returns the interface definition.
|
||||
fn get_interface(self: &Module, interface_name: &str) -> JsonBlob;
|
||||
|
||||
/// Registers the callback of the `Subscriber` to be called and calls
|
||||
/// `Everest::Module::signal_ready`.
|
||||
fn signal_ready(self: &Module, rt: Pin<&Runtime>);
|
||||
|
||||
/// Informs the runtime that we implement the command described by `implementation_id` and
|
||||
/// `name`, and registers the `handle_command` method from the `Subscriber` as the handler.
|
||||
fn provide_command(
|
||||
self: &Module,
|
||||
rt: Pin<&Runtime>,
|
||||
implementation_id: String,
|
||||
name: String,
|
||||
);
|
||||
|
||||
/// Call the command described by 'implementation_id' and `name` with the given 'args'.
|
||||
/// Returns the return value.
|
||||
fn call_command(
|
||||
self: &Module,
|
||||
implementation_id: &str,
|
||||
index: usize,
|
||||
name: &str,
|
||||
args: JsonBlob,
|
||||
) -> JsonBlob;
|
||||
|
||||
/// Informs the runtime that we want to receive the variable described by
|
||||
/// `implementation_id` and `name` and registers the `handle_variable` method from the
|
||||
/// `Subscriber` as the handler.
|
||||
fn subscribe_variable(
|
||||
self: &Module,
|
||||
rt: Pin<&Runtime>,
|
||||
implementation_id: String,
|
||||
index: usize,
|
||||
name: String,
|
||||
);
|
||||
|
||||
/// Subscribes to all errors of the required modules.
|
||||
fn subscribe_all_errors(self: &Module, rt: Pin<&Runtime>);
|
||||
|
||||
/// Returns the `connections` block defined in the `config.yaml` for
|
||||
/// the current module.
|
||||
fn get_module_connections(self: &Module) -> Vec<RsModuleConnections>;
|
||||
|
||||
/// Publishes the given `blob` under the `implementation_id` and `name`.
|
||||
fn publish_variable(self: &Module, implementation_id: &str, name: &str, blob: JsonBlob);
|
||||
|
||||
/// Raises an error
|
||||
fn raise_error(self: &Module, implementation_id: &str, error: ErrorType);
|
||||
|
||||
/// Clears an error
|
||||
/// If the error_type is empty, we will clear all errors from the module.
|
||||
fn clear_error(self: &Module, implementation_id: &str, error_type: &str, clear_all: bool);
|
||||
|
||||
/// Returns the module config from cpp.
|
||||
fn get_module_configs(self: &Module, module_id: &str) -> Vec<RsModuleConfig>;
|
||||
|
||||
/// Call this once.
|
||||
fn init_logging(module_id: &str, prefix: &str, conf: &str) -> i32;
|
||||
|
||||
/// Logging sink for the EVerest module.
|
||||
fn log2cxx(level: i32, line: i32, file: &str, message: &str);
|
||||
}
|
||||
}
|
||||
|
||||
impl ffi::JsonBlob {
|
||||
fn as_bytes(&self) -> &[u8] {
|
||||
&self.data
|
||||
}
|
||||
|
||||
fn deserialize<T: DeserializeOwned>(self) -> T {
|
||||
// TODO(hrapp): Error handling
|
||||
serde_json::from_slice(self.as_bytes()).expect(&format!(
|
||||
"Failed to deserialize {:?}",
|
||||
String::from_utf8_lossy(self.as_bytes())
|
||||
))
|
||||
}
|
||||
|
||||
fn from_vec(data: Vec<u8>) -> Self {
|
||||
Self { data }
|
||||
}
|
||||
}
|
||||
|
||||
/// Very simple logger to use by the Rust modules.
|
||||
mod logger {
|
||||
use super::ffi;
|
||||
use crate::INIT_LOGGER_ONCE;
|
||||
|
||||
pub(crate) struct Logger {
|
||||
level: log::Level,
|
||||
}
|
||||
|
||||
impl log::Log for Logger {
|
||||
fn enabled(&self, metadata: &log::Metadata) -> bool {
|
||||
// Rust gives the Error level 1 and all other severities a higher
|
||||
// value.
|
||||
metadata.level() <= self.level
|
||||
}
|
||||
|
||||
fn log(&self, record: &log::Record) {
|
||||
// The doc says `log` has to perform the filtering itself.
|
||||
if !self.enabled(record.metadata()) {
|
||||
return;
|
||||
}
|
||||
// This mapping should be kept in sync with liblog's
|
||||
// Everest::Logging::severity_level.
|
||||
let level = match record.level() {
|
||||
log::Level::Trace => 0,
|
||||
log::Level::Debug => 1,
|
||||
log::Level::Info => 2,
|
||||
log::Level::Warn => 3,
|
||||
log::Level::Error => 4,
|
||||
};
|
||||
|
||||
ffi::log2cxx(
|
||||
level,
|
||||
record.line().unwrap_or_default() as i32,
|
||||
record.file().unwrap_or_default(),
|
||||
&format!("{}", record.args()),
|
||||
)
|
||||
}
|
||||
|
||||
fn flush(&self) {}
|
||||
}
|
||||
|
||||
impl Logger {
|
||||
/// Init the logger for everest.
|
||||
///
|
||||
/// Don't do this on your own as we must also control some cxx code.
|
||||
pub(crate) fn init_logger(module_name: &str, prefix: &str, conf: &str) {
|
||||
INIT_LOGGER_ONCE.call_once(|| {
|
||||
let level = match ffi::init_logging(module_name, prefix, conf) {
|
||||
-1 => {
|
||||
return;
|
||||
}
|
||||
0 => log::Level::Trace,
|
||||
1 => log::Level::Debug,
|
||||
2 => log::Level::Info,
|
||||
3 => log::Level::Warn,
|
||||
4 => log::Level::Error,
|
||||
_ => log::Level::Info,
|
||||
};
|
||||
|
||||
let logger = Self { level };
|
||||
log::set_boxed_logger(Box::new(logger)).unwrap();
|
||||
log::set_max_level(level.to_level_filter());
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The cpp_module is for Rust an opaque type - so Rust can't tell if it is safe
|
||||
/// to be accessed from multiple threads. We know that the c++ runtime is meant
|
||||
/// to be used concurrently.
|
||||
unsafe impl Sync for ffi::Module {}
|
||||
unsafe impl Send for ffi::Module {}
|
||||
|
||||
pub use ffi::{ErrorSeverity, ErrorType as FfiErrorType};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ErrorType<T> {
|
||||
/// Serialised type from the FfiErrorType
|
||||
pub error_type: T,
|
||||
|
||||
/// Carried over directly from the FfiErrorType
|
||||
pub description: String,
|
||||
|
||||
/// Carried over directly from the FfiErrorType
|
||||
pub message: String,
|
||||
|
||||
/// The severity of the error.
|
||||
/// Carried over directly from the FfiErrorType
|
||||
pub severity: ErrorSeverity,
|
||||
}
|
||||
|
||||
impl<T> From<T> for ErrorType<T> {
|
||||
fn from(t: T) -> ErrorType<T> {
|
||||
ErrorType {
|
||||
error_type: t,
|
||||
description: String::new(),
|
||||
message: String::new(),
|
||||
severity: ErrorSeverity::High,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Arguments for an EVerest node.
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct Args {
|
||||
/// prefix of installation.
|
||||
#[arg(long)]
|
||||
pub prefix: PathBuf,
|
||||
|
||||
/// logging configuration that we are using.
|
||||
#[arg(long, long = "log_config")]
|
||||
pub log_config: PathBuf,
|
||||
|
||||
/// module name for us.
|
||||
#[arg(long)]
|
||||
pub module: String,
|
||||
|
||||
/// MQTT broker socket path
|
||||
#[arg(long = "mqtt_broker_socket_path")]
|
||||
pub mqtt_broker_socket_path: Option<PathBuf>,
|
||||
|
||||
/// MQTT broker hostname
|
||||
#[arg(long = "mqtt_broker_host")]
|
||||
pub mqtt_broker_host: String,
|
||||
|
||||
/// MQTT broker port
|
||||
#[arg(long = "mqtt_broker_port")]
|
||||
pub mqtt_broker_port: u16,
|
||||
|
||||
/// MQTT EVerest prefix
|
||||
#[arg(long = "mqtt_everest_prefix")]
|
||||
pub mqtt_everest_prefix: String,
|
||||
|
||||
/// MQTT external prefix
|
||||
#[arg(long = "mqtt_external_prefix")]
|
||||
pub mqtt_external_prefix: String,
|
||||
}
|
||||
|
||||
/// Implements the handling of commands & variables, but has no specific information about the
|
||||
/// details of the current module, i.e. it deals with JSON blobs and strings as command names. Code
|
||||
/// generation is used to build the concrete, strongly typed abstractions that are then used by
|
||||
/// final implementors.
|
||||
pub trait Subscriber: Sync + Send {
|
||||
/// Handler for the command `name` on `implementation_id` with the given `parameters`. The return value
|
||||
/// will be returned as the result of the call.
|
||||
fn handle_command(
|
||||
&self,
|
||||
implementation_id: &str,
|
||||
name: &str,
|
||||
parameters: HashMap<String, serde_json::Value>,
|
||||
) -> Result<serde_json::Value>;
|
||||
|
||||
/// Handler for the variable `name` on `implementation_id` with the given `value`.
|
||||
fn handle_variable(
|
||||
&self,
|
||||
implementation_id: &str,
|
||||
index: usize,
|
||||
name: &str,
|
||||
value: serde_json::Value,
|
||||
) -> Result<()>;
|
||||
|
||||
/// Handler for the error raised/cleared callback
|
||||
/// The `raised` flag indicates if the error is raised or cleared.
|
||||
fn handle_on_error(
|
||||
&self,
|
||||
implementation_id: &str,
|
||||
index: usize,
|
||||
error: ffi::ErrorType,
|
||||
raised: bool,
|
||||
);
|
||||
|
||||
fn on_ready(&self) {}
|
||||
}
|
||||
|
||||
enum PanicStrategy {
|
||||
/// Log the panic and abort the process. Appropriate for production modules
|
||||
/// where a panic in a callback means the module is in a broken state.
|
||||
Abort,
|
||||
/// Capture the panic and re-raise it on the main/test thread when the
|
||||
/// module is dropped. Appropriate for tests where panics (e.g. from
|
||||
/// mockall assertions) should be forwarded to the test harness.
|
||||
Capture(std::sync::Mutex<Option<Box<dyn std::any::Any + Send>>>),
|
||||
}
|
||||
|
||||
/// The [Runtime] is the central piece of the bridge between c++ and Rust. Rust
|
||||
/// owns the [ffi::Module], and the ffi::Module owns the entire mqtt stack. So
|
||||
/// when dropping, we first drop the ffi::Module and then the
|
||||
/// Subscribers/Runtime which own the callbacks.
|
||||
pub struct Runtime {
|
||||
cpp_module: cxx::UniquePtr<ffi::Module>,
|
||||
sub_impl: OnceLock<Arc<dyn Subscriber>>,
|
||||
/// The config for the client module.
|
||||
config: HashMap<String, HashMap<String, Config>>,
|
||||
panic_strategy: PanicStrategy,
|
||||
}
|
||||
|
||||
impl Runtime {
|
||||
/// Handle a panic payload according to the configured strategy.
|
||||
fn handle_panic(&self, payload: Box<dyn std::any::Any + Send>) {
|
||||
match &self.panic_strategy {
|
||||
PanicStrategy::Abort => {
|
||||
// Extract a message for the log, then abort.
|
||||
let msg = if let Some(s) = payload.downcast_ref::<&str>() {
|
||||
s.to_string()
|
||||
} else if let Some(s) = payload.downcast_ref::<String>() {
|
||||
s.clone()
|
||||
} else {
|
||||
"unknown panic".to_string()
|
||||
};
|
||||
log::error!("Panic on MQTT callback thread: {msg}");
|
||||
std::process::abort();
|
||||
}
|
||||
PanicStrategy::Capture(captured) => {
|
||||
let mut slot = captured.lock().unwrap();
|
||||
if slot.is_none() {
|
||||
*slot = Some(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// If a panic was captured on an MQTT callback thread, re-raise it on the
|
||||
/// current thread. This is intended to be called from `Drop`.
|
||||
pub fn check_panic(&self) {
|
||||
if let PanicStrategy::Capture(captured) = &self.panic_strategy {
|
||||
if let Some(payload) = captured.lock().unwrap().take() {
|
||||
std::panic::resume_unwind(payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ready(&self) {
|
||||
if let Err(payload) = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
self.sub_impl.get().unwrap().on_ready();
|
||||
})) {
|
||||
self.handle_panic(payload);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_command(&self, impl_id: &str, name: &str, json: ffi::JsonBlob) -> ffi::JsonBlob {
|
||||
debug!("handle_command: {impl_id}, {name}, '{:?}'", json.as_bytes());
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
let parameters: Option<HashMap<String, serde_json::Value>> = json.deserialize();
|
||||
let retval = self.sub_impl.get().unwrap().handle_command(
|
||||
impl_id,
|
||||
name,
|
||||
parameters.unwrap_or_default(),
|
||||
);
|
||||
|
||||
match retval {
|
||||
Ok(blob) => ffi::JsonBlob::from_vec(serde_json::to_vec(&blob).unwrap()),
|
||||
Err(err) => ffi::JsonBlob::from_vec(serde_json::to_vec(&err).unwrap()),
|
||||
}
|
||||
}));
|
||||
|
||||
match result {
|
||||
Ok(blob) => blob,
|
||||
Err(payload) => {
|
||||
self.handle_panic(payload);
|
||||
// Return an error response so the C++ side doesn't hang.
|
||||
// Only reached with PanicStrategy::Capture (Abort never returns).
|
||||
let err = Error::HandlerException("panic in command handler".into());
|
||||
ffi::JsonBlob::from_vec(serde_json::to_vec(&err).unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_variable(&self, impl_id: &str, index: usize, name: &str, json: ffi::JsonBlob) {
|
||||
debug!(
|
||||
"handle_variable: {impl_id}, {name}, '{:?}'",
|
||||
json.as_bytes()
|
||||
);
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
if let Err(err) = self.sub_impl.get().unwrap().handle_variable(
|
||||
impl_id,
|
||||
index,
|
||||
name,
|
||||
json.deserialize(),
|
||||
) {
|
||||
log::error!("`handle_variable` failed: {err:?}");
|
||||
}
|
||||
}));
|
||||
|
||||
if let Err(payload) = result {
|
||||
self.handle_panic(payload);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_on_error(&self, impl_id: &str, index: usize, error: ffi::ErrorType, raised: bool) {
|
||||
debug!("handle_on_error: {impl_id}, index {index}, raised {raised}");
|
||||
|
||||
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
||||
self.sub_impl
|
||||
.get()
|
||||
.unwrap()
|
||||
.handle_on_error(impl_id, index, error, raised);
|
||||
}));
|
||||
|
||||
if let Err(payload) = result {
|
||||
self.handle_panic(payload);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn publish_variable<T: serde::Serialize>(
|
||||
&self,
|
||||
impl_id: &str,
|
||||
var_name: &str,
|
||||
message: &T,
|
||||
) {
|
||||
let blob = ffi::JsonBlob::from_vec(
|
||||
serde_json::to_vec(&message).expect("Serialization of data cannot fail."),
|
||||
);
|
||||
(self.cpp_module).publish_variable(impl_id, var_name, blob);
|
||||
}
|
||||
|
||||
pub fn call_command<T: serde::Serialize, R: serde::de::DeserializeOwned>(
|
||||
&self,
|
||||
impl_id: &str,
|
||||
index: usize,
|
||||
name: &str,
|
||||
args: &T,
|
||||
) -> Result<R> {
|
||||
let blob = ffi::JsonBlob::from_vec(
|
||||
serde_json::to_vec(args).expect("Serialization of data cannot fail."),
|
||||
);
|
||||
let return_value = (self.cpp_module).call_command(impl_id, index, name, blob);
|
||||
match serde_json::from_slice(&return_value.data) {
|
||||
Ok(ok) => Ok(ok),
|
||||
Err(_) => match serde_json::from_slice::<Error>(&return_value.data) {
|
||||
Ok(err) => Err(err),
|
||||
Err(err) => Err(Error::MessageParsingError(format!("{err:?}"))),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Called from the generated code.
|
||||
/// The type T should be an error.
|
||||
pub fn raise_error<T: serde::Serialize + core::fmt::Debug>(
|
||||
&self,
|
||||
impl_id: &str,
|
||||
error: ErrorType<T>,
|
||||
) {
|
||||
let error_string = serde_yaml::to_string(&error.error_type).unwrap_or_default();
|
||||
// Remove the new line -> this should be gone once we stop using yaml
|
||||
// since we don't really want yaml.
|
||||
let error_string = error_string.strip_suffix("\n").unwrap_or(&error_string);
|
||||
|
||||
debug!("Raising error {error_string:?} from {error:?}");
|
||||
let error_type = ffi::ErrorType {
|
||||
error_type: error_string.to_string(),
|
||||
description: error.description,
|
||||
message: error.message,
|
||||
severity: error.severity,
|
||||
};
|
||||
self.cpp_module.raise_error(impl_id, error_type);
|
||||
}
|
||||
|
||||
/// Called from the generated code.
|
||||
/// The type T should be an error.
|
||||
pub fn clear_error<T: serde::Serialize + core::fmt::Debug>(
|
||||
&self,
|
||||
impl_id: &str,
|
||||
error: T,
|
||||
clear_all: bool,
|
||||
) {
|
||||
let error_string = serde_yaml::to_string(&error).unwrap_or_default();
|
||||
let mut error_string = error_string.strip_suffix("\n").unwrap_or(&error_string);
|
||||
|
||||
// The yaml conversion changes empty strings into a string containing two
|
||||
// single quotes which we catch and convert to an actual empty string
|
||||
if error_string == "''" {
|
||||
error_string = "";
|
||||
}
|
||||
|
||||
debug!("Clearing the {error_string} from {error:?}");
|
||||
self.cpp_module
|
||||
.clear_error(impl_id, &error_string, clear_all);
|
||||
}
|
||||
|
||||
/// Create a runtime by parsing CLI arguments. Uses [`PanicStrategy::Abort`]
|
||||
/// since this is the production entry point.
|
||||
pub fn new() -> Pin<Arc<Self>> {
|
||||
let args: Args = Args::parse();
|
||||
Self::create(args, PanicStrategy::Abort)
|
||||
}
|
||||
|
||||
/// Create a runtime with explicit args instead of parsing CLI arguments.
|
||||
/// Uses [`PanicStrategy::Capture`] since this is used by test harnesses.
|
||||
pub fn new_with_args(args: Args) -> Pin<Arc<Self>> {
|
||||
Self::create(args, PanicStrategy::Capture(std::sync::Mutex::new(None)))
|
||||
}
|
||||
|
||||
fn create(args: Args, panic_strategy: PanicStrategy) -> Pin<Arc<Self>> {
|
||||
logger::Logger::init_logger(
|
||||
&args.module,
|
||||
&args.prefix.to_string_lossy(),
|
||||
&args.log_config.to_string_lossy(),
|
||||
);
|
||||
|
||||
let cpp_module = ffi::create_module(
|
||||
&args.module,
|
||||
&args.prefix.to_string_lossy(),
|
||||
&args
|
||||
.mqtt_broker_socket_path
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy(),
|
||||
&args.mqtt_broker_host,
|
||||
&args.mqtt_broker_port,
|
||||
&args.mqtt_everest_prefix,
|
||||
&args.mqtt_external_prefix,
|
||||
);
|
||||
|
||||
let raw_config = cpp_module.get_module_configs(&args.module);
|
||||
|
||||
// Convert the nested Vec's into nested HashMaps.
|
||||
let mut config: HashMap<String, HashMap<String, Config>> = HashMap::new();
|
||||
for mm_config in raw_config {
|
||||
let cc_config = mm_config
|
||||
.data
|
||||
.into_iter()
|
||||
.map(|field| {
|
||||
let value = match field.config_type {
|
||||
ffi::ConfigType::Boolean => Config::Boolean(field.bool_value),
|
||||
ffi::ConfigType::String => Config::String(field.string_value),
|
||||
ffi::ConfigType::Number => Config::Number(field.number_value),
|
||||
ffi::ConfigType::Integer => Config::Integer(field.integer_value),
|
||||
_ => panic!("Unexpected value {:?}", field.config_type),
|
||||
};
|
||||
|
||||
(field.name, value)
|
||||
})
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
// If we have already an entry with the `module_name`, we try to extend
|
||||
// it.
|
||||
config
|
||||
.entry(mm_config.module_name)
|
||||
.or_default()
|
||||
.extend(cc_config);
|
||||
}
|
||||
Arc::pin(Self {
|
||||
cpp_module,
|
||||
sub_impl: OnceLock::new(),
|
||||
config,
|
||||
panic_strategy,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn set_subscriber(self: Pin<&Self>, sub_impl: Arc<dyn Subscriber>) {
|
||||
self.sub_impl
|
||||
.set(sub_impl)
|
||||
.unwrap_or_else(|_| panic!("set_subscriber called twice"));
|
||||
let manifest_json = self.cpp_module.get_manifest();
|
||||
let manifest: schema::Manifest = manifest_json.deserialize();
|
||||
log::debug!("Deserialiazed the manifest {manifest:?}");
|
||||
|
||||
// Implement all commands for all of our implementations, dispatch everything to the
|
||||
// Subscriber.
|
||||
for (implementation_id, provides) in manifest.provides {
|
||||
let interface_s = self.cpp_module.get_interface(&provides.interface);
|
||||
let interface: schema::InterfaceFromEverest = interface_s.deserialize();
|
||||
log::debug!("Deserialiazed the interface {interface:?}");
|
||||
|
||||
for (name, _) in interface.cmds {
|
||||
self.cpp_module
|
||||
.provide_command(self, implementation_id.clone(), name);
|
||||
}
|
||||
}
|
||||
|
||||
let connections = self.get_module_connections();
|
||||
|
||||
// Subscribe to all variables that might be of interest.
|
||||
for (implementation_id, requires) in manifest.requires {
|
||||
let connection = connections.get(&implementation_id).cloned().unwrap_or(0);
|
||||
let interface_s = self.cpp_module.get_interface(&requires.interface);
|
||||
// EVerest framework may return null if an interface is not used in
|
||||
// the config (the connection is then 0).
|
||||
if interface_s.as_bytes() == b"null" && connection == 0 {
|
||||
debug!("Skipping the interface {implementation_id}");
|
||||
continue;
|
||||
}
|
||||
let interface: schema::InterfaceFromEverest = interface_s.deserialize();
|
||||
log::debug!("Deserialiazed the interface {interface:?}");
|
||||
|
||||
for i in 0usize..connection {
|
||||
for (name, _) in interface.vars.iter() {
|
||||
if requires.ignore.vars.contains(name) {
|
||||
continue;
|
||||
}
|
||||
self.cpp_module.subscribe_variable(
|
||||
self,
|
||||
implementation_id.clone(),
|
||||
i,
|
||||
name.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.cpp_module.subscribe_all_errors(self);
|
||||
|
||||
// Since users can choose to overwrite `on_ready`, we can call signal_ready right away.
|
||||
// TODO(hrapp): There were some doubts if this strategy is too inflexible, discuss design
|
||||
// again.
|
||||
(self.cpp_module).signal_ready(self);
|
||||
}
|
||||
|
||||
/// The interface for fetching the module connections though the C++ runtime.
|
||||
pub fn get_module_connections(&self) -> HashMap<String, usize> {
|
||||
let raw_connections = self.cpp_module.get_module_connections();
|
||||
raw_connections
|
||||
.into_iter()
|
||||
.map(|connection| (connection.implementation_id, connection.slots))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Interface for fetching the configurations through the C++ runtime.
|
||||
pub fn get_module_configs(&self) -> &HashMap<String, HashMap<String, Config>> {
|
||||
&self.config
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Runtime {
|
||||
fn drop(&mut self) {
|
||||
// Re-raise any panic that was captured on an MQTT callback thread,
|
||||
// but only if we're not already unwinding from another panic.
|
||||
if !std::thread::panicking() {
|
||||
self.check_panic();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A store for our config values. The type is closely related to
|
||||
/// [ffi::ConfigField] and [ffi::ConfigType].
|
||||
#[derive(Debug)]
|
||||
pub enum Config {
|
||||
Boolean(bool),
|
||||
String(String),
|
||||
Number(f64),
|
||||
Integer(i64),
|
||||
}
|
||||
|
||||
impl TryFrom<&Config> for bool {
|
||||
type Error = Error;
|
||||
fn try_from(value: &Config) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
Config::Boolean(value) => Ok(*value),
|
||||
_ => Err(Error::MessageParsingError(format!("{:?}", value))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Config> for String {
|
||||
type Error = Error;
|
||||
fn try_from(value: &Config) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
Config::String(value) => Ok(value.clone()),
|
||||
_ => Err(Error::MessageParsingError(format!("{:?}", value))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Config> for f64 {
|
||||
type Error = Error;
|
||||
fn try_from(value: &Config) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
Config::Number(value) => Ok(*value),
|
||||
_ => Err(Error::MessageParsingError(format!("{:?}", value))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&Config> for i64 {
|
||||
type Error = Error;
|
||||
fn try_from(value: &Config) -> std::result::Result<Self, Self::Error> {
|
||||
match value {
|
||||
Config::Integer(value) => Ok(*value),
|
||||
_ => Err(Error::MessageParsingError(format!("{:?}", value))),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
use nix::fcntl::{open, OFlag};
|
||||
use nix::sys::stat;
|
||||
use nix::unistd;
|
||||
use std::io::{self};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Child, Command};
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::{Arc, Mutex};
|
||||
use std::thread;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Handle to a running EVerest manager process.
|
||||
///
|
||||
/// The manager is spawned as a subprocess using `{prefix}/bin/manager`. When
|
||||
/// dropped, the manager process is killed if it is still running.
|
||||
pub struct Manager {
|
||||
child: Arc<Mutex<Child>>,
|
||||
stopping: Arc<AtomicBool>,
|
||||
watcher: Option<thread::JoinHandle<()>>,
|
||||
}
|
||||
|
||||
impl Manager {
|
||||
/// Start the EVerest manager as a subprocess and wait until it is ready.
|
||||
///
|
||||
/// * `prefix` — the EVerest sysroot (passed as `--prefix`)
|
||||
/// * `config` — path to the config file (passed as `--config`)
|
||||
/// * `standalone` — module IDs that the manager should not spawn
|
||||
/// (passed as `--standalone`). If non-empty, this function blocks until
|
||||
/// the manager reports that all managed modules are started and it is
|
||||
/// waiting for the standalone modules.
|
||||
/// * `mqtt_everest_prefix` — optional MQTT topic prefix override. When set,
|
||||
/// passed as `--mqtt_everest_prefix` to the manager, allowing multiple
|
||||
/// test instances to run in parallel without topic collisions.
|
||||
pub fn start(
|
||||
prefix: &Path,
|
||||
config: &Path,
|
||||
standalone: &[&str],
|
||||
mqtt_everest_prefix: Option<&str>,
|
||||
) -> io::Result<Self> {
|
||||
let binary = Self::manager_binary(prefix);
|
||||
|
||||
let mut cmd = Command::new(&binary);
|
||||
cmd.arg("--prefix").arg(prefix);
|
||||
cmd.arg("--config").arg(config);
|
||||
|
||||
if let Some(mqtt_prefix) = mqtt_everest_prefix {
|
||||
cmd.arg("--mqtt_everest_prefix").arg(mqtt_prefix);
|
||||
}
|
||||
|
||||
let mut fifo = None;
|
||||
|
||||
if !standalone.is_empty() {
|
||||
let fifo_name = match mqtt_everest_prefix {
|
||||
Some(p) => format!("status-{}.fifo", p.replace('/', "_")),
|
||||
None => format!("status-{}.fifo", std::process::id()),
|
||||
};
|
||||
let fifo_path = std::env::temp_dir().join(fifo_name);
|
||||
unistd::mkfifo(&fifo_path, stat::Mode::S_IRWXU)?;
|
||||
|
||||
fifo = Some(open(
|
||||
&fifo_path,
|
||||
OFlag::O_RDONLY | OFlag::O_NONBLOCK,
|
||||
stat::Mode::empty(),
|
||||
)?);
|
||||
cmd.arg("--status-fifo").arg(&fifo_path);
|
||||
cmd.arg("--standalone");
|
||||
for module_id in standalone {
|
||||
cmd.arg(module_id);
|
||||
}
|
||||
}
|
||||
|
||||
let child = Arc::new(Mutex::new(cmd.spawn().map_err(|e| {
|
||||
io::Error::new(
|
||||
e.kind(),
|
||||
format!("Failed to start manager at {}: {e}", binary.display()),
|
||||
)
|
||||
})?));
|
||||
let stopping = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let watcher = {
|
||||
let child = child.clone();
|
||||
let stopping = stopping.clone();
|
||||
Some(std::thread::spawn(move || loop {
|
||||
let status = child.lock().unwrap().try_wait().unwrap();
|
||||
match status {
|
||||
None => thread::sleep(Duration::from_millis(100)),
|
||||
Some(status) => {
|
||||
if !stopping.load(Ordering::Relaxed) {
|
||||
panic!("Manager process exited unexpectedly with status: {status}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}))
|
||||
};
|
||||
|
||||
// Wait for the manager to signal readiness via the status fifo.
|
||||
if let Some(fd) = fifo {
|
||||
let mut buf = [0u8; 256];
|
||||
let mut accumulated = String::new();
|
||||
loop {
|
||||
match unistd::read(&fd, &mut buf) {
|
||||
Ok(n) => {
|
||||
accumulated.push_str(&String::from_utf8_lossy(&buf[..n]));
|
||||
if accumulated.contains("WAITING_FOR_STANDALONE_MODULES")
|
||||
|| accumulated.contains("ALL_MODULES_STARTED")
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
Err(nix::errno::Errno::EAGAIN) => {
|
||||
thread::sleep(Duration::from_millis(50));
|
||||
}
|
||||
Err(e) => return Err(io::Error::from_raw_os_error(e as i32)),
|
||||
}
|
||||
|
||||
// The watcher thread crashed - we won't see anything in our
|
||||
// fifo.
|
||||
if watcher.as_ref().unwrap().is_finished() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
let _ = unistd::close(fd);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
child,
|
||||
stopping,
|
||||
watcher,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the path to the manager binary for a given prefix.
|
||||
fn manager_binary(prefix: &Path) -> PathBuf {
|
||||
prefix.join("bin").join("manager")
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Manager {
|
||||
fn drop(&mut self) {
|
||||
self.stopping.store(true, Ordering::Relaxed);
|
||||
let _ = self.child.lock().unwrap().kill();
|
||||
if let Err(panic) = self.watcher.take().expect("always there").join() {
|
||||
std::panic::resume_unwind(panic);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
exports_files([
|
||||
"logging.ini",
|
||||
])
|
||||
@@ -0,0 +1,5 @@
|
||||
filegroup(
|
||||
name = "errors",
|
||||
srcs = glob(["*.yaml"]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
description: Example errors
|
||||
errors:
|
||||
- name: ExampleErrorA
|
||||
description: Example error A
|
||||
- name: ExampleErrorB
|
||||
description: Example error B
|
||||
- name: ExampleErrorC
|
||||
description: Example error C
|
||||
- name: ExampleErrorD
|
||||
description: Example error D
|
||||
@@ -0,0 +1,10 @@
|
||||
description: More errors.
|
||||
errors:
|
||||
- name: ExampleErrorA
|
||||
description: >-
|
||||
The same error as in example_errors.yaml. The error names are placed in a
|
||||
namespace
|
||||
- name: MoreError
|
||||
description: additinal error
|
||||
- name: snake_case_error
|
||||
description: We will correct snake cases
|
||||
@@ -0,0 +1,2 @@
|
||||
description: No errors.
|
||||
errors: []
|
||||
@@ -0,0 +1,5 @@
|
||||
filegroup(
|
||||
name = "interfaces",
|
||||
srcs = glob(["*.yaml"]),
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
description: An empty interface.
|
||||
@@ -0,0 +1,5 @@
|
||||
description: >-
|
||||
We make sure that we allow duplicate errors
|
||||
errors:
|
||||
- reference: /errors/example_errors#/ExampleErrorA
|
||||
- reference: /errors/example_errors
|
||||
@@ -0,0 +1,5 @@
|
||||
description: >-
|
||||
We make sure that we respect multiple error definitions
|
||||
errors:
|
||||
- reference: /errors/example_errors
|
||||
- reference: /errors/more_errors
|
||||
@@ -0,0 +1,4 @@
|
||||
description: >-
|
||||
We make sure that we can parse the error files which don't define any errors
|
||||
errors:
|
||||
- reference: /errors/no_errors
|
||||
@@ -0,0 +1,5 @@
|
||||
description: >-
|
||||
We make sure that we respect a subset of errors
|
||||
errors:
|
||||
- reference: /errors/example_errors#/ExampleErrorA
|
||||
- reference: /errors/example_errors#/ExampleErrorB
|
||||
@@ -0,0 +1,20 @@
|
||||
description: >-
|
||||
This interface defines an example interface that uses multiple framework
|
||||
features
|
||||
cmds:
|
||||
uses_something:
|
||||
description: This command checks if something is stored under a given key
|
||||
arguments:
|
||||
key:
|
||||
description: Key to check the existence for
|
||||
type: string
|
||||
pattern: ^[A-Za-z0-9_.]+$
|
||||
result:
|
||||
description: Returns 'True' if something was stored for this key
|
||||
type: boolean
|
||||
vars:
|
||||
max_current:
|
||||
description: Provides maximum current of this supply in ampere
|
||||
type: number
|
||||
errors:
|
||||
- reference: /errors/example_errors
|
||||
@@ -0,0 +1,24 @@
|
||||
# for documentation on this file format see:
|
||||
# https://www.boost.org/doc/libs/1_54_0/libs/log/doc/html/log/detailed/utilities.html#log.detailed.utilities.setup.filter_formatter
|
||||
|
||||
[Core]
|
||||
DisableLogging=false
|
||||
|
||||
# To get debug logs of only one module, add the "%Process% contains" filter, e.g.:
|
||||
#
|
||||
# "(%Process% contains OCPP201 and %Severity% >= DEBG)"
|
||||
#
|
||||
# whereas "OCPP201" is the value of the field `active_modules.NAME.module` in the respective /config/config-*.yaml.
|
||||
Filter="%Severity% >= INFO"
|
||||
|
||||
[Sinks.Console]
|
||||
Destination=Console
|
||||
# Filter="%Target% contains \"MySink1\""
|
||||
Format="%TimeStamp% [%Severity%] \033[1;32m%Process%\033[0m \033[1;36m%function%\033[0m \033[1;30m%file%:\033[0m\033[1;32m%line%\033[0m: %Message%"
|
||||
Asynchronous=false
|
||||
AutoFlush=true
|
||||
SeverityStringColorDebug="\033[1;30m"
|
||||
SeverityStringColorInfo="\033[1;37m"
|
||||
SeverityStringColorWarning="\033[1;33m"
|
||||
SeverityStringColorError="\033[1;31m"
|
||||
SeverityStringColorCritical="\033[1;35m"
|
||||
@@ -0,0 +1 @@
|
||||
exports_files(["smoke_test.sh"])
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user