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:
45
tools/EVerest-main/lib/everest/framework/src/CMakeLists.txt
Normal file
45
tools/EVerest-main/lib/everest/framework/src/CMakeLists.txt
Normal file
@@ -0,0 +1,45 @@
|
||||
add_executable(manager)
|
||||
target_sources(manager
|
||||
PRIVATE
|
||||
system_unix.cpp
|
||||
manager.cpp
|
||||
)
|
||||
# generate version information header
|
||||
evc_generate_version_information()
|
||||
target_include_directories(manager
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_BINARY_DIR}/generated/include
|
||||
)
|
||||
|
||||
target_link_libraries(manager
|
||||
PRIVATE
|
||||
everest::framework
|
||||
Boost::program_options
|
||||
PkgConfig::libcap
|
||||
)
|
||||
|
||||
if (EVEREST_ENABLE_ADMIN_PANEL_BACKEND)
|
||||
target_link_libraries(manager
|
||||
PRIVATE
|
||||
controller-ipc
|
||||
)
|
||||
|
||||
target_compile_definitions(manager PRIVATE ENABLE_ADMIN_PANEL)
|
||||
|
||||
add_subdirectory(controller)
|
||||
endif ()
|
||||
|
||||
target_compile_options(manager PRIVATE ${COMPILER_WARNING_OPTIONS})
|
||||
|
||||
target_compile_features(manager PRIVATE cxx_std_17)
|
||||
|
||||
install(
|
||||
TARGETS manager
|
||||
RUNTIME
|
||||
)
|
||||
|
||||
# FIXME (aw): the www folder currently always needs to exist, so that the manager does not complain
|
||||
install(
|
||||
DIRECTORY # intentionally left blank
|
||||
DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/everest/www"
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
add_library(controller-ipc OBJECT ipc.cpp)
|
||||
|
||||
target_link_libraries(controller-ipc
|
||||
PUBLIC
|
||||
nlohmann_json::nlohmann_json
|
||||
)
|
||||
|
||||
target_compile_options(controller-ipc PRIVATE ${COMPILER_WARNING_OPTIONS})
|
||||
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
add_executable(controller
|
||||
controller.cpp
|
||||
command_api.cpp
|
||||
rpc.cpp
|
||||
server.cpp
|
||||
transpile_config.cpp
|
||||
${PROJECT_SOURCE_DIR}/lib/formatter.cpp
|
||||
)
|
||||
|
||||
target_include_directories(controller PRIVATE
|
||||
${PROJECT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_compile_options(controller PRIVATE ${COMPILER_WARNING_OPTIONS})
|
||||
|
||||
target_link_libraries(controller
|
||||
PRIVATE
|
||||
Threads::Threads
|
||||
|
||||
controller-ipc
|
||||
|
||||
websockets_shared
|
||||
fmt::fmt
|
||||
|
||||
everest::log
|
||||
everest::yaml
|
||||
|
||||
${STD_FILESYSTEM_COMPAT_LIB}
|
||||
)
|
||||
|
||||
target_compile_features(controller PRIVATE cxx_std_17)
|
||||
|
||||
install(TARGETS controller RUNTIME)
|
||||
|
||||
if (EVEREST_INSTALL_ADMIN_PANEL)
|
||||
include(FetchContent)
|
||||
|
||||
message(STATUS "Adding admin-panel")
|
||||
|
||||
FetchContent_Declare(admin-panel
|
||||
DOWNLOAD_EXTRACT_TIMESTAMP TRUE
|
||||
URL https://github.com/EVerest/everest-admin-panel/releases/download/v0.5.1/everest-admin-panel.tar.gz
|
||||
)
|
||||
|
||||
FetchContent_MakeAvailable(admin-panel)
|
||||
install(
|
||||
DIRECTORY ${admin-panel_SOURCE_DIR}/
|
||||
DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/everest/www"
|
||||
)
|
||||
endif ()
|
||||
@@ -0,0 +1,118 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest
|
||||
#include "command_api.hpp"
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <ryml.hpp>
|
||||
#include <ryml_std.hpp>
|
||||
|
||||
#include "rpc.hpp"
|
||||
#include "transpile_config.hpp"
|
||||
|
||||
#include <utils/formatter.hpp>
|
||||
#include <utils/yaml_loader.hpp>
|
||||
|
||||
using json = nlohmann::json;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
CommandApi::CommandApi(const Config& config, RPC& rpc) : config(config), rpc(rpc) {
|
||||
}
|
||||
|
||||
nlohmann::json CommandApi::handle(const std::string& cmd, const json& params) {
|
||||
// fmt::print("Handling command {}\n", cmd);
|
||||
|
||||
if (cmd == "get_modules") {
|
||||
auto modules_list = json::object();
|
||||
|
||||
for (const auto& item : fs::directory_iterator(this->config.module_dir)) {
|
||||
if (!fs::is_directory(item)) {
|
||||
continue;
|
||||
}
|
||||
const auto& module_path = item.path();
|
||||
const auto module_name = module_path.filename().string();
|
||||
|
||||
// fetch the manifest
|
||||
const auto manifest_path = module_path / "manifest.yaml";
|
||||
if (!fs::is_regular_file(manifest_path)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
modules_list[module_name] = Everest::load_yaml(manifest_path);
|
||||
}
|
||||
|
||||
return modules_list;
|
||||
} else if (cmd == "get_configs") {
|
||||
auto config_list = json::object();
|
||||
|
||||
for (const auto& item : fs::directory_iterator(this->config.configs_dir)) {
|
||||
if (!fs::is_regular_file(item)) {
|
||||
continue;
|
||||
}
|
||||
if (item.path().extension() != std::string(".yaml")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto config_name = item.path().stem().string();
|
||||
|
||||
config_list[config_name] = Everest::load_yaml(item.path());
|
||||
}
|
||||
|
||||
return config_list;
|
||||
} else if (cmd == "get_interfaces") {
|
||||
auto interface_list = json::object();
|
||||
|
||||
for (const auto& item : fs::directory_iterator(this->config.interface_dir)) {
|
||||
|
||||
if (!fs::is_regular_file(item)) {
|
||||
continue;
|
||||
}
|
||||
if (item.path().extension() != std::string(".yaml")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto interface_name = item.path().stem().string();
|
||||
|
||||
interface_list[interface_name] = Everest::load_yaml(item.path());
|
||||
}
|
||||
|
||||
return interface_list;
|
||||
} else if (cmd == "save_config") {
|
||||
// FIXME (aw): this is quite hacky
|
||||
if (!params.contains("name") || !params.at("name").is_string()) {
|
||||
throw CommandApiParamsError("The save_config needs a 'name' parameter for the config file of type string");
|
||||
}
|
||||
|
||||
const auto name = params.at("name").get<std::string>();
|
||||
|
||||
const json config_json = params.value("config", json::object());
|
||||
auto ryml_deserialized = transpile_config(config_json);
|
||||
|
||||
const auto configs_path = fs::path(this->config.configs_dir);
|
||||
const auto check_config_file_path = configs_path / fmt::format("_{}.yaml", name);
|
||||
|
||||
std::ofstream(check_config_file_path.string()) << ryml_deserialized;
|
||||
|
||||
const auto result = this->rpc.ipc_request("check_config", check_config_file_path.string(), false);
|
||||
|
||||
if (result.is_string()) {
|
||||
fs::remove(check_config_file_path);
|
||||
throw CommandApiParamsError(result);
|
||||
}
|
||||
|
||||
fs::rename(check_config_file_path, configs_path / fmt::format("{}.yaml", name));
|
||||
|
||||
return true;
|
||||
} else if (cmd == "restart_modules") {
|
||||
this->rpc.ipc_request("restart_modules", nullptr, true);
|
||||
|
||||
return nullptr;
|
||||
} else if (cmd == "get_rpc_timeout") {
|
||||
return this->config.controller_rpc_timeout_ms;
|
||||
}
|
||||
|
||||
throw CommandApiMethodNotFound(fmt::format("Command '{}' unknown", cmd));
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2022 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef CONTROLLER_COMMAND_API_HPP
|
||||
#define CONTROLLER_COMMAND_API_HPP
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
// forward declaration
|
||||
class RPC;
|
||||
|
||||
class CommandApiParamsError : public std::runtime_error {
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
class CommandApiMethodNotFound : public std::runtime_error {
|
||||
using std::runtime_error::runtime_error;
|
||||
};
|
||||
|
||||
class CommandApi {
|
||||
public:
|
||||
struct Config {
|
||||
std::string module_dir;
|
||||
std::string interface_dir;
|
||||
std::string configs_dir;
|
||||
int controller_rpc_timeout_ms;
|
||||
};
|
||||
|
||||
CommandApi(const Config& config, RPC& rpc);
|
||||
|
||||
nlohmann::json handle(const std::string& cmd, const nlohmann::json& params);
|
||||
|
||||
private:
|
||||
Config config;
|
||||
RPC& rpc;
|
||||
};
|
||||
|
||||
#endif // CONTROLLER_COMMAND_API_HPP
|
||||
@@ -0,0 +1,73 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <cstdlib>
|
||||
#include <thread>
|
||||
|
||||
#include <fmt/format.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
|
||||
#include "command_api.hpp"
|
||||
#include "ipc.hpp"
|
||||
#include "rpc.hpp"
|
||||
#include "server.hpp"
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
namespace {
|
||||
int run_controller() {
|
||||
auto socket_fd = STDIN_FILENO;
|
||||
|
||||
const auto message = Everest::controller_ipc::receive_message(socket_fd);
|
||||
|
||||
if (message.status != Everest::controller_ipc::MESSAGE_RETURN_STATUS::OK) {
|
||||
throw std::runtime_error("Controller process could not read initial config message");
|
||||
}
|
||||
|
||||
// FIXME (aw): validation
|
||||
const auto config_params = message.json.at("params");
|
||||
|
||||
Everest::Logging::init(config_params.at("logging_config_file"), "everest_ctrl");
|
||||
|
||||
EVLOG_debug << "everest controller process started ...";
|
||||
|
||||
const CommandApi::Config config{
|
||||
config_params.at("module_dir"),
|
||||
config_params.at("interface_dir"),
|
||||
config_params.at("configs_dir"),
|
||||
config_params.at("controller_rpc_timeout_ms"),
|
||||
};
|
||||
|
||||
RPC rpc(socket_fd, config);
|
||||
Server backend;
|
||||
const int controller_port = config_params.at("controller_port").get<int>();
|
||||
|
||||
// FIXME (aw): don't use hard-coded path!
|
||||
std::thread(
|
||||
&Server::run, &backend, [&rpc](const nlohmann::json& request) { return rpc.handle_json_rpc(request); },
|
||||
config_params.at("www_dir"), controller_port)
|
||||
.detach();
|
||||
|
||||
while (true) {
|
||||
rpc.run([&backend](const nlohmann::json& notification) { backend.push(notification); });
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main([[maybe_unused]] int argc, char* argv[]) {
|
||||
const auto argv0 = *argv;
|
||||
if (strcmp(argv0, MAGIC_CONTROLLER_ARG0) != 0) {
|
||||
fmt::print(stderr, "This binary does not yet support to be started manually\n");
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
try {
|
||||
return run_controller();
|
||||
} catch (...) {
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2022 Pionix GmbH and Contributors to EVerest
|
||||
#include "ipc.hpp"
|
||||
|
||||
#include <cerrno>
|
||||
#include <iterator>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/time.h>
|
||||
#include <system_error>
|
||||
#include <unistd.h>
|
||||
|
||||
// FIXME (aw): this needs be done better!
|
||||
namespace {
|
||||
constexpr auto raw_message_buffer_size = 16 * std::size_t{1024};
|
||||
std::array<char, raw_message_buffer_size> raw_message_buffer;
|
||||
} // namespace
|
||||
|
||||
namespace Everest {
|
||||
namespace controller_ipc {
|
||||
|
||||
void set_read_timeout(int fd, int timeout_in_ms) {
|
||||
|
||||
const int seconds = timeout_in_ms / 1000;
|
||||
const int u_seconds = (timeout_in_ms - seconds * 1000) * 1000;
|
||||
|
||||
struct timeval socket_timeout {
|
||||
seconds, u_seconds
|
||||
};
|
||||
|
||||
setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, static_cast<void*>(&socket_timeout), sizeof(socket_timeout));
|
||||
}
|
||||
|
||||
void send_message(int fd, const nlohmann::json& msg) {
|
||||
auto raw = nlohmann::json::to_bson(msg);
|
||||
auto raw_it = raw.begin();
|
||||
size_t already_sent = 0;
|
||||
|
||||
while (already_sent < raw.size()) {
|
||||
const ssize_t c = write(fd, &(*raw_it), raw.size() - already_sent);
|
||||
|
||||
if (c == -1) {
|
||||
throw std::system_error(errno, std::system_category(), "Error while sending message");
|
||||
}
|
||||
|
||||
already_sent += c;
|
||||
std::advance(raw_it, c);
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME (aw): recv should be buffered and memory is not that costly here!
|
||||
Message receive_message(int fd) {
|
||||
auto retval = read(fd, raw_message_buffer.data(), raw_message_buffer.size());
|
||||
|
||||
if (retval == -1) {
|
||||
if (errno == EAGAIN || errno == EWOULDBLOCK) {
|
||||
return {MESSAGE_RETURN_STATUS::TIMEOUT, nullptr};
|
||||
}
|
||||
|
||||
return {MESSAGE_RETURN_STATUS::ERROR, {{"error", strerror(errno)}}};
|
||||
}
|
||||
|
||||
const auto& msg_size = retval;
|
||||
|
||||
return {MESSAGE_RETURN_STATUS::OK,
|
||||
nlohmann::json::from_bson(raw_message_buffer.begin(), raw_message_buffer.begin() + msg_size)};
|
||||
}
|
||||
|
||||
} // namespace controller_ipc
|
||||
} // namespace Everest
|
||||
@@ -0,0 +1,35 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2022 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef CONTROLLER_IPC_HPP
|
||||
#define CONTROLLER_IPC_HPP
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#define MAGIC_CONTROLLER_ARG0 "MT.EVEREST"
|
||||
|
||||
namespace Everest {
|
||||
namespace controller_ipc {
|
||||
|
||||
enum class MESSAGE_RETURN_STATUS {
|
||||
OK,
|
||||
ERROR,
|
||||
TIMEOUT,
|
||||
};
|
||||
|
||||
struct Message {
|
||||
Message(MESSAGE_RETURN_STATUS status, nlohmann::json json) : status(status), json(std::move(json)){};
|
||||
const MESSAGE_RETURN_STATUS status;
|
||||
const nlohmann::json json;
|
||||
};
|
||||
|
||||
// FIXME (aw): add return value for failed set_timeout
|
||||
void set_read_timeout(int fd, int timeout_in_ms);
|
||||
|
||||
// FIXME (aw): add return value for failed send
|
||||
void send_message(int fd, const nlohmann::json& msg);
|
||||
Message receive_message(int fd);
|
||||
|
||||
} // namespace controller_ipc
|
||||
} // namespace Everest
|
||||
|
||||
#endif // CONTROLLER_IPC_HPP
|
||||
207
tools/EVerest-main/lib/everest/framework/src/controller/rpc.cpp
Normal file
207
tools/EVerest-main/lib/everest/framework/src/controller/rpc.cpp
Normal file
@@ -0,0 +1,207 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2022 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "rpc.hpp"
|
||||
|
||||
#include "command_api.hpp"
|
||||
#include "ipc.hpp"
|
||||
|
||||
enum class RPCRequestErrorCode {
|
||||
ParseError = -32700,
|
||||
InvalidRequest = -32600,
|
||||
MethodNotFound = -32601,
|
||||
InvalidParams = -32602,
|
||||
InternalError = -32603
|
||||
};
|
||||
|
||||
class RPCRequestError : public std::runtime_error {
|
||||
public:
|
||||
RPCRequestError(RPCRequestErrorCode error_code, const std::string& message, const nlohmann::json& id) :
|
||||
std::runtime_error(
|
||||
"Error code: " + std::to_string(static_cast<std::underlying_type_t<RPCRequestErrorCode>>(error_code)) +
|
||||
"\n" + message),
|
||||
error_code(error_code),
|
||||
message(message),
|
||||
id(id) {
|
||||
}
|
||||
|
||||
RPCRequestErrorCode get_error_code() const {
|
||||
return error_code;
|
||||
}
|
||||
|
||||
const std::string& get_message() const {
|
||||
return this->message;
|
||||
}
|
||||
|
||||
const nlohmann::json& get_id() const {
|
||||
return this->id;
|
||||
}
|
||||
|
||||
private:
|
||||
RPCRequestErrorCode error_code;
|
||||
std::string message;
|
||||
nlohmann::json id;
|
||||
};
|
||||
|
||||
class RPCRequest {
|
||||
public:
|
||||
RPCRequest(const std::string& request_string) {
|
||||
const auto request = nlohmann::json::parse(request_string, nullptr, false, false);
|
||||
|
||||
if (request.is_discarded()) {
|
||||
throw RPCRequestError(RPCRequestErrorCode::ParseError, "", nullptr);
|
||||
}
|
||||
|
||||
this->id = request.value("id", nlohmann::json(nullptr));
|
||||
|
||||
if (!this->id.is_number() && !this->id.is_string() && !this->id.is_null()) {
|
||||
throw RPCRequestError(RPCRequestErrorCode::InvalidRequest, "Invalid ID type", nullptr);
|
||||
}
|
||||
|
||||
if (!request.contains("method") || !request.at("method").is_string()) {
|
||||
throw RPCRequestError(RPCRequestErrorCode::InvalidRequest, "Missing 'method' key or invalid type",
|
||||
this->id);
|
||||
}
|
||||
|
||||
this->method = request.at("method");
|
||||
this->params = request.value("params", nlohmann::json(nullptr));
|
||||
}
|
||||
|
||||
bool is_notification() const {
|
||||
return this->id.is_null();
|
||||
}
|
||||
|
||||
std::string get_method() const {
|
||||
return this->method;
|
||||
}
|
||||
|
||||
const nlohmann::json& get_params() const {
|
||||
return this->params;
|
||||
}
|
||||
|
||||
const nlohmann::json& get_id() const {
|
||||
return this->id;
|
||||
}
|
||||
|
||||
private:
|
||||
nlohmann::json id;
|
||||
std::string method;
|
||||
nlohmann::json params;
|
||||
};
|
||||
|
||||
RPC::RPC(int ipc_fd, const CommandApi::Config& config) :
|
||||
ipc_fd(ipc_fd), rpc_timeout(std::chrono::milliseconds(config.controller_rpc_timeout_ms)) {
|
||||
this->api = std::make_unique<CommandApi>(config, *this);
|
||||
}
|
||||
|
||||
void RPC::run(const NotificationHandler& notification_handler) {
|
||||
if (!notification_handler) {
|
||||
throw std::runtime_error("Could not run the RPC loop with a null notification handler");
|
||||
}
|
||||
|
||||
while (true) {
|
||||
// polling on command api ..
|
||||
const auto msg = Everest::controller_ipc::receive_message(this->ipc_fd);
|
||||
|
||||
if (msg.status != Everest::controller_ipc::MESSAGE_RETURN_STATUS::OK) {
|
||||
// FIXME (aw): proper error handling!
|
||||
continue;
|
||||
}
|
||||
|
||||
const auto& payload = msg.json;
|
||||
if (!payload.contains("id")) {
|
||||
// probably only a simple notification
|
||||
this->notification_handler(payload);
|
||||
}
|
||||
|
||||
// otherwise a result
|
||||
const std::mt19937::result_type id = payload.at("id");
|
||||
|
||||
const std::lock_guard<std::mutex> lock(this->ipc_mutex);
|
||||
auto call = this->ipc_calls.find(id);
|
||||
|
||||
if (call == this->ipc_calls.end()) {
|
||||
// does not exists (anymore)
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
call->second.set_value(payload.value("result", nlohmann::json(nullptr)));
|
||||
} catch (const std::future_error& e) {
|
||||
if (e.code() == std::future_errc::promise_already_satisfied) {
|
||||
// this could, but should not happen
|
||||
continue;
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nlohmann::json RPC::handle_json_rpc(const std::string& payload) {
|
||||
// FIXME (aw): these nested tries look like code smell
|
||||
try {
|
||||
const auto rpc_request = RPCRequest(payload);
|
||||
|
||||
try {
|
||||
nlohmann::json reply{
|
||||
{"id", rpc_request.get_id()},
|
||||
{"result", this->api->handle(rpc_request.get_method(), rpc_request.get_params())},
|
||||
};
|
||||
|
||||
if (rpc_request.is_notification()) {
|
||||
return nullptr;
|
||||
} else {
|
||||
return reply;
|
||||
}
|
||||
} catch (const CommandApiParamsError& e) {
|
||||
throw RPCRequestError(RPCRequestErrorCode::InvalidParams, e.what(), rpc_request.get_id());
|
||||
} catch (const CommandApiMethodNotFound& e) {
|
||||
throw RPCRequestError(RPCRequestErrorCode::MethodNotFound, e.what(), rpc_request.get_id());
|
||||
} catch (const std::exception& e) {
|
||||
throw RPCRequestError(RPCRequestErrorCode::InternalError, e.what(), rpc_request.get_id());
|
||||
}
|
||||
} catch (const RPCRequestError& e) {
|
||||
const auto error_code = e.get_error_code();
|
||||
const auto error_code_int = static_cast<std::underlying_type_t<RPCRequestErrorCode>>(error_code);
|
||||
return {
|
||||
{"error",
|
||||
{
|
||||
{"code", error_code_int},
|
||||
{"message", e.get_message()},
|
||||
}},
|
||||
{"id", e.get_id()},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
nlohmann::json RPC::ipc_request(const std::string& method, const nlohmann::json& params, bool only_notify) {
|
||||
if (only_notify) {
|
||||
Everest::controller_ipc::send_message(this->ipc_fd, {{"method", method}, {"params", params}});
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::unique_lock<std::mutex> lock(this->ipc_mutex);
|
||||
auto new_call = this->ipc_calls.emplace(this->rng(), std::promise<nlohmann::json>());
|
||||
while (!new_call.second) {
|
||||
new_call = this->ipc_calls.emplace(this->rng(), std::promise<nlohmann::json>());
|
||||
}
|
||||
|
||||
const auto id = new_call.first->first;
|
||||
auto call_result_future = new_call.first->second.get_future();
|
||||
|
||||
lock.unlock();
|
||||
|
||||
Everest::controller_ipc::send_message(this->ipc_fd, {{"method", method}, {"params", params}, {"id", id}});
|
||||
|
||||
const auto status = call_result_future.wait_for(this->rpc_timeout);
|
||||
|
||||
lock.lock();
|
||||
this->ipc_calls.erase(new_call.first);
|
||||
lock.unlock();
|
||||
|
||||
if (status == std::future_status::timeout) {
|
||||
throw std::runtime_error("Promise timeout");
|
||||
}
|
||||
|
||||
return call_result_future.get();
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2022 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef CONTROLLER_RPC_HPP
|
||||
#define CONTROLLER_RPC_HPP
|
||||
|
||||
#include <functional>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <random>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "command_api.hpp"
|
||||
|
||||
class RPC {
|
||||
public:
|
||||
using NotificationHandler = std::function<void(const nlohmann::json&)>;
|
||||
|
||||
RPC(int ipc_fd, const CommandApi::Config& config);
|
||||
|
||||
nlohmann::json handle_json_rpc(const std::string& request_string);
|
||||
nlohmann::json ipc_request(const std::string& method, const nlohmann::json& params, bool only_notify);
|
||||
|
||||
void run(const NotificationHandler& handler);
|
||||
|
||||
private:
|
||||
int ipc_fd;
|
||||
std::unique_ptr<CommandApi> api;
|
||||
NotificationHandler notification_handler;
|
||||
std::chrono::milliseconds rpc_timeout;
|
||||
|
||||
// FIXME (aw): what type of cafe?
|
||||
// NOLINTNEXTLINE(cert-msc51-cpp, cert-msc32-c): used as keys in ipc_calls, no strict randomness requirement
|
||||
std::mt19937 rng{0xcafe}; // NOLINT(cppcoreguidelines-avoid-magic-numbers): why not have a bit of magic in life?
|
||||
std::unordered_map<std::mt19937::result_type, std::promise<nlohmann::json>> ipc_calls{};
|
||||
std::mutex ipc_mutex{};
|
||||
};
|
||||
|
||||
#endif // CONTROLLER_RPC_HPP
|
||||
@@ -0,0 +1,288 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
#include "server.hpp"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
|
||||
#include <fstream>
|
||||
#include <mutex>
|
||||
#include <queue>
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <libwebsockets.h>
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
|
||||
// FIXME (aw): naming
|
||||
class WebsocketSession {
|
||||
public:
|
||||
enum class OutputState {
|
||||
EMPTY,
|
||||
LAST_DATA,
|
||||
MORE_DATA,
|
||||
};
|
||||
class Output {
|
||||
public:
|
||||
OutputState state{OutputState::EMPTY};
|
||||
unsigned char* buffer{nullptr};
|
||||
std::size_t len{0};
|
||||
|
||||
void set_data(const std::string& data) {
|
||||
len = data.size();
|
||||
d.resize(LWS_PRE + len);
|
||||
buffer = d.data() + LWS_PRE;
|
||||
memcpy(buffer, data.data(), len);
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<unsigned char> d{};
|
||||
};
|
||||
|
||||
void push_output_data(std::string data);
|
||||
Output pop_output();
|
||||
void add_input(const char*, std::size_t len);
|
||||
std::string finish_input();
|
||||
|
||||
private:
|
||||
std::string input;
|
||||
std::queue<std::string> output_queue;
|
||||
std::mutex output_mtx;
|
||||
};
|
||||
|
||||
void WebsocketSession::add_input(const char* in, std::size_t len) {
|
||||
input.append(in, len);
|
||||
}
|
||||
|
||||
std::string WebsocketSession::finish_input() {
|
||||
auto data = std::move(input);
|
||||
input.clear();
|
||||
return data;
|
||||
}
|
||||
|
||||
void WebsocketSession::push_output_data(std::string data) {
|
||||
const std::lock_guard<std::mutex> lock(output_mtx);
|
||||
output_queue.emplace(std::move(data));
|
||||
}
|
||||
|
||||
WebsocketSession::Output WebsocketSession::pop_output() {
|
||||
const std::lock_guard<std::mutex> lock(output_mtx);
|
||||
WebsocketSession::Output output;
|
||||
if (output_queue.empty()) {
|
||||
// output is empty by default
|
||||
return output;
|
||||
}
|
||||
|
||||
output.set_data(output_queue.front());
|
||||
output_queue.pop();
|
||||
|
||||
output.state = output_queue.empty() ? OutputState::LAST_DATA : OutputState::MORE_DATA;
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
class Server::Impl {
|
||||
public:
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-member-init): info initialized via memset
|
||||
Impl() {
|
||||
memset(&info, 0, sizeof(info));
|
||||
}
|
||||
|
||||
void run(const Server::IncomingMessageHandler& handler, const std::string& html_origin, int port);
|
||||
void push(const nlohmann::json& msg);
|
||||
|
||||
private:
|
||||
static const lws_protocol_vhost_options pvo_opt;
|
||||
static const lws_protocol_vhost_options pvo;
|
||||
|
||||
static const lws_protocol_vhost_options pvo_mime;
|
||||
static lws_http_mount mount;
|
||||
|
||||
lws_context_creation_info info;
|
||||
lws_context* context{nullptr};
|
||||
|
||||
std::mutex context_mtx;
|
||||
|
||||
static int callback(struct lws* wsi, lws_callback_reasons reason, void* user, void* in, std::size_t len);
|
||||
|
||||
void create_session(WebsocketSession* session);
|
||||
void destroy_session(WebsocketSession* session);
|
||||
|
||||
IncomingMessageHandler message_in_handler;
|
||||
|
||||
std::set<WebsocketSession*> sessions{};
|
||||
};
|
||||
|
||||
const lws_protocol_vhost_options Server::Impl::pvo_opt = {nullptr, nullptr, "default", "1"};
|
||||
const lws_protocol_vhost_options Server::Impl::pvo = {nullptr, &pvo_opt, "everest-controller", ""};
|
||||
|
||||
const lws_protocol_vhost_options Server::Impl::pvo_mime = {nullptr, nullptr, ".mp4", "application/x-mp4"};
|
||||
|
||||
lws_http_mount Server::Impl::mount = {
|
||||
nullptr, // lws_http_mount *mount_next;
|
||||
"/", // const char *mountpoint;
|
||||
"/", // const char *origin;
|
||||
"index.html", // const char *def;
|
||||
nullptr, // const char *protocol;
|
||||
nullptr, // const struct lws_protocol_vhost_options *cgienv;
|
||||
&pvo_mime, // const struct lws_protocol_vhost_options *extra_mimetypes;
|
||||
nullptr, // const struct lws_protocol_vhost_options *interpret;
|
||||
0, // int cgi_timeout;
|
||||
0, // int cache_max_age;
|
||||
0, // unsigned int auth_mask;
|
||||
0, // unsigned int cache_reusable:1; /**< set if client cache may reuse this */
|
||||
0, // unsigned int cache_revalidate:1; /**< set if client cache should revalidate on use */
|
||||
0, // unsigned int cache_intermediaries:1; /**< set if intermediaries are allowed to cache */
|
||||
#if defined(LWS_LIBRARY_VERSION_NUMBER) && LWS_LIBRARY_VERSION_NUMBER >= 4004000
|
||||
0, // unsigned int cache_no:1; /**< set if client should check cache always*/
|
||||
#endif
|
||||
LWSMPRO_FILE, // unsigned char origin_protocol; /**< one of enum lws_mount_protocols */
|
||||
1, // unsigned char mountpoint_len; /**< length of mountpoint string */
|
||||
nullptr, // const char *basic_auth_login_file;
|
||||
#if defined(LWS_LIBRARY_VERSION_NUMBER) && LWS_LIBRARY_VERSION_NUMBER >= 4004000
|
||||
nullptr, // const char *cgi_chroot_path;
|
||||
nullptr, // const char *cgi_wd;
|
||||
nullptr, // const struct lws_protocol_vhost_options *headers;
|
||||
0, // unsigned int keepalive_timeout; /**< 0 or seconds http stream should stay alive while idle. */
|
||||
#endif
|
||||
};
|
||||
|
||||
int Server::Impl::callback(struct lws* wsi, lws_callback_reasons reason, void* user, void* in, std::size_t len) {
|
||||
auto session = static_cast<WebsocketSession*>(user);
|
||||
|
||||
auto& instance = *static_cast<Server::Impl*>(lws_get_protocol(wsi)->user);
|
||||
|
||||
if (LWS_CALLBACK_PROTOCOL_INIT == reason) {
|
||||
} else if (LWS_CALLBACK_ESTABLISHED == reason) {
|
||||
instance.create_session(session);
|
||||
|
||||
} else if (LWS_CALLBACK_SERVER_WRITEABLE == reason) {
|
||||
using State = WebsocketSession::OutputState;
|
||||
auto output = session->pop_output();
|
||||
|
||||
if (output.state == State::EMPTY) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
lws_write(wsi, output.buffer, output.len, LWS_WRITE_TEXT);
|
||||
|
||||
if (output.state == State::MORE_DATA) {
|
||||
// more to write
|
||||
lws_callback_on_writable(wsi);
|
||||
}
|
||||
|
||||
} else if (LWS_CALLBACK_RECEIVE == reason) {
|
||||
session->add_input(static_cast<char*>(in), len);
|
||||
|
||||
if (!lws_is_final_fragment(wsi)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto input = session->finish_input();
|
||||
|
||||
const auto& retval = instance.message_in_handler(input);
|
||||
// FIXME (aw): this is blocking - do we want that?
|
||||
if (!retval.is_null()) {
|
||||
session->push_output_data(retval.dump());
|
||||
|
||||
lws_callback_on_writable(wsi);
|
||||
}
|
||||
|
||||
return 0;
|
||||
} else if (LWS_CALLBACK_CLOSED == reason) {
|
||||
instance.destroy_session(session);
|
||||
} else {
|
||||
return lws_callback_http_dummy(wsi, reason, user, in, len);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void Server::Impl::create_session(WebsocketSession* session) {
|
||||
new (session) WebsocketSession();
|
||||
const std::lock_guard<std::mutex> lock(context_mtx);
|
||||
sessions.insert(session);
|
||||
}
|
||||
|
||||
void Server::Impl::destroy_session(WebsocketSession* session) {
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(context_mtx);
|
||||
sessions.erase(session);
|
||||
}
|
||||
|
||||
session->~WebsocketSession();
|
||||
}
|
||||
|
||||
void Server::Impl::run(const Server::IncomingMessageHandler& handler, const std::string& html_origin, int port) {
|
||||
if (!handler) {
|
||||
throw std::runtime_error("Could not run the server with a null incoming message handler");
|
||||
}
|
||||
this->message_in_handler = handler;
|
||||
|
||||
static std::array<lws_protocols, 3> protocols = {{
|
||||
{"http", lws_callback_http_dummy, 0, 0, 0, NULL, 0},
|
||||
{"everest-controller", Server::Impl::callback, sizeof(WebsocketSession), 1024, 0, this, 0},
|
||||
LWS_PROTOCOL_LIST_TERM,
|
||||
}};
|
||||
|
||||
mount.origin = html_origin.c_str();
|
||||
info.port = port;
|
||||
info.mounts = &mount;
|
||||
info.protocols = protocols.data();
|
||||
info.pvo = &pvo;
|
||||
|
||||
lws_set_log_level(LLL_ERR | LLL_WARN | LLL_NOTICE, [](int level, const char* line) {
|
||||
if (level == LLL_NOTICE) {
|
||||
EVLOG_debug << line;
|
||||
} else {
|
||||
// should be LLL_ERR or LLL_WARN
|
||||
EVLOG_info << line;
|
||||
}
|
||||
});
|
||||
|
||||
EVLOG_info << fmt::format("Launching controller service on port {}\n", info.port);
|
||||
|
||||
{
|
||||
const std::lock_guard<std::mutex> lck(context_mtx);
|
||||
context = lws_create_context(&info);
|
||||
}
|
||||
|
||||
while (lws_service(context, 0) >= 0) {
|
||||
}
|
||||
|
||||
// FIXME (aw): check for errors and log them somehow ...
|
||||
{
|
||||
const std::lock_guard<std::mutex> lck(context_mtx);
|
||||
lws_context_destroy(context);
|
||||
context = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void Server::Impl::push(const nlohmann::json& msg) {
|
||||
const std::lock_guard<std::mutex> lock(context_mtx);
|
||||
if (context == nullptr) {
|
||||
// context does not exist, nothing to do
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto session : sessions) {
|
||||
session->push_output_data(msg.dump());
|
||||
}
|
||||
lws_cancel_service(context);
|
||||
}
|
||||
|
||||
Server::Server() : pimpl(std::make_unique<Impl>()) {
|
||||
}
|
||||
|
||||
Server::~Server() = default;
|
||||
|
||||
void Server::run(const IncomingMessageHandler& handler, const std::string& html_origin, int port) {
|
||||
pimpl->run(handler, html_origin, port);
|
||||
}
|
||||
|
||||
void Server::push(const nlohmann::json& msg) {
|
||||
pimpl->push(msg);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
#ifndef CONTROLLER_SERVER_HPP
|
||||
#define CONTROLLER_SERVER_HPP
|
||||
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "command_api.hpp"
|
||||
|
||||
class Server {
|
||||
public:
|
||||
using IncomingMessageHandler = std::function<nlohmann::json(const nlohmann::json&)>;
|
||||
Server();
|
||||
~Server();
|
||||
void run(const IncomingMessageHandler& handler, const std::string& html_origin, int port);
|
||||
void push(const nlohmann::json& msg);
|
||||
|
||||
private:
|
||||
class Impl; // forward declaration
|
||||
std::unique_ptr<Impl> pimpl;
|
||||
};
|
||||
|
||||
#endif // CONTROLLER_SERVER_HPP
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <ryml.hpp>
|
||||
#include <ryml_std.hpp>
|
||||
|
||||
#include "transpile_config.hpp"
|
||||
|
||||
namespace {
|
||||
// NOLINTNEXTLINE(misc-no-recursion)
|
||||
void clear_quote_flags(ryml::NodeRef& root) {
|
||||
if (root.has_key()) {
|
||||
// Remove quotes from key
|
||||
if (root.tree()->type_has_any(root.id(), ryml::KEYQUO)) {
|
||||
root.tree()->_rem_flags(root.id(), ryml::KEYQUO);
|
||||
}
|
||||
if (root.key().find('-') != ryml::csubstr::npos) {
|
||||
root.tree()->_add_flags(root.id(), ryml::KEY_SQUO);
|
||||
}
|
||||
}
|
||||
if (root.has_val()) {
|
||||
if (root.val().has_str() && root.val() != "") {
|
||||
root.tree()->_rem_flags(root.id(), ryml::VALQUO);
|
||||
}
|
||||
if (root.val().empty()) {
|
||||
root.tree()->_add_flags(root.id(), ryml::VAL_SQUO);
|
||||
}
|
||||
}
|
||||
root.tree()->_rem_flags(root.id(), ryml::NodeType_e::FLOW_SL);
|
||||
|
||||
for (auto child : root.children()) {
|
||||
clear_quote_flags(child);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
c4::yml::Tree transpile_config(const nlohmann::json& config_json) {
|
||||
const auto json_serialized = config_json.dump();
|
||||
auto ryml_deserialized = ryml::parse_json_in_arena(ryml::to_csubstr(json_serialized));
|
||||
auto root = ryml_deserialized.rootref();
|
||||
clear_quote_flags(root);
|
||||
return ryml_deserialized;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest
|
||||
#pragma once
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <ryml.hpp>
|
||||
#include <ryml_std.hpp>
|
||||
|
||||
c4::yml::Tree transpile_config(const nlohmann::json& config_json);
|
||||
985
tools/EVerest-main/lib/everest/framework/src/manager.cpp
Normal file
985
tools/EVerest-main/lib/everest/framework/src/manager.cpp
Normal file
@@ -0,0 +1,985 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <cstdlib>
|
||||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <pwd.h>
|
||||
#include <signal.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <boost/exception/diagnostic_information.hpp>
|
||||
#include <boost/program_options.hpp>
|
||||
|
||||
#include <fmt/color.h>
|
||||
#include <fmt/core.h>
|
||||
#include <fmt/ranges.h>
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
#include <framework/everest.hpp>
|
||||
#include <framework/runtime.hpp>
|
||||
#include <utils/config.hpp>
|
||||
#include <utils/mqtt_abstraction.hpp>
|
||||
#include <utils/status_fifo.hpp>
|
||||
|
||||
#include "controller/ipc.hpp"
|
||||
#include "system_unix.hpp"
|
||||
#include <generated/version_information.hpp>
|
||||
|
||||
namespace po = boost::program_options;
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
using namespace Everest;
|
||||
|
||||
const auto PARENT_DIED_SIGNAL = SIGTERM;
|
||||
const int CONTROLLER_IPC_READ_TIMEOUT_MS = 50;
|
||||
const auto complete_start_time = std::chrono::steady_clock::now();
|
||||
|
||||
#ifdef ENABLE_ADMIN_PANEL
|
||||
class ControllerHandle {
|
||||
public:
|
||||
ControllerHandle(pid_t pid, int socket_fd) : pid(pid), socket_fd(socket_fd) {
|
||||
// we do "non-blocking" read
|
||||
controller_ipc::set_read_timeout(socket_fd, CONTROLLER_IPC_READ_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
void send_message(const nlohmann::json& msg) {
|
||||
controller_ipc::send_message(socket_fd, msg);
|
||||
}
|
||||
|
||||
controller_ipc::Message receive_message() {
|
||||
return controller_ipc::receive_message(socket_fd);
|
||||
}
|
||||
|
||||
void shutdown() {
|
||||
// FIXME (aw): tbd
|
||||
}
|
||||
|
||||
const pid_t pid;
|
||||
|
||||
private:
|
||||
const int socket_fd;
|
||||
};
|
||||
#endif
|
||||
|
||||
// Helper struct keeping information on how to start module
|
||||
struct ModuleStartInfo {
|
||||
enum class Language {
|
||||
cpp,
|
||||
javascript,
|
||||
python
|
||||
};
|
||||
ModuleStartInfo(const std::string& name_, const std::string& printable_name_, Language lang_, const fs::path& path_,
|
||||
std::vector<std::string> capabilities_) :
|
||||
name(name_),
|
||||
printable_name(printable_name_),
|
||||
language(lang_),
|
||||
path(path_),
|
||||
capabilities(std::move(capabilities_)) {
|
||||
}
|
||||
std::string name;
|
||||
std::string printable_name;
|
||||
Language language;
|
||||
fs::path path;
|
||||
|
||||
// required capabilities of this module
|
||||
std::vector<std::string> capabilities;
|
||||
};
|
||||
|
||||
namespace {
|
||||
/// \brief Setup common environment variables for everestjs and everestpy
|
||||
void setup_environment(const ModuleStartInfo& module_info, const RuntimeSettings& rs,
|
||||
const MQTTSettings& mqtt_settings) {
|
||||
setenv(EV_MODULE, module_info.name.c_str(), 1);
|
||||
setenv(EV_PREFIX, rs.prefix.c_str(), 0);
|
||||
setenv(EV_LOG_CONF_FILE, rs.logging_config_file.c_str(), 0);
|
||||
setenv(EV_MQTT_EVEREST_PREFIX, mqtt_settings.everest_prefix.c_str(), 0);
|
||||
setenv(EV_MQTT_EXTERNAL_PREFIX, mqtt_settings.external_prefix.c_str(), 0);
|
||||
if (mqtt_settings.uses_socket()) {
|
||||
setenv(EV_MQTT_BROKER_SOCKET_PATH, mqtt_settings.broker_socket_path.c_str(), 0);
|
||||
} else {
|
||||
setenv(EV_MQTT_BROKER_HOST, mqtt_settings.broker_host.c_str(), 0);
|
||||
setenv(EV_MQTT_BROKER_PORT, std::to_string(mqtt_settings.broker_port).c_str(), 0);
|
||||
}
|
||||
|
||||
if (rs.validate_schema) {
|
||||
setenv(EV_VALIDATE_SCHEMA, "1", 1);
|
||||
}
|
||||
}
|
||||
|
||||
static void exec_module(const std::string& bin, std::vector<std::string>& arguments, system::SubProcess& proc_handle) {
|
||||
// Convert the argument list to the format required by `execv*()`.
|
||||
std::vector<char*> argv_list(arguments.size() + 1);
|
||||
std::transform(arguments.begin(), arguments.end(), argv_list.begin(), [](auto& value) { return value.data(); });
|
||||
argv_list.back() = nullptr; // Add a null terminator
|
||||
|
||||
// Execute the module binary, replacing the current process.
|
||||
execvp(bin.c_str(), argv_list.data());
|
||||
|
||||
// `execv()` failed, notify the parent process and exit.
|
||||
const auto msg = fmt::format("Syscall to execv() with \"{} {}\" failed ({})", bin,
|
||||
fmt::join(arguments.begin() + 1, arguments.end(), " "), strerror(errno));
|
||||
proc_handle.send_error_and_exit(msg);
|
||||
}
|
||||
|
||||
void exec_cpp_module(system::SubProcess& proc_handle, const ModuleStartInfo& module_info, const RuntimeSettings& rs,
|
||||
const MQTTSettings& mqtt_settings) {
|
||||
std::vector<std::string> arguments = {
|
||||
module_info.printable_name,
|
||||
"--prefix",
|
||||
rs.prefix.string(),
|
||||
"--module",
|
||||
module_info.name,
|
||||
"--log_config",
|
||||
rs.logging_config_file.string(),
|
||||
"--mqtt_everest_prefix",
|
||||
mqtt_settings.everest_prefix,
|
||||
"--mqtt_external_prefix",
|
||||
mqtt_settings.external_prefix}; // TODO: check if this is empty and do not append if needed?
|
||||
|
||||
if (mqtt_settings.uses_socket()) {
|
||||
arguments.insert(arguments.end(), {"--mqtt_broker_socket_path", mqtt_settings.broker_socket_path});
|
||||
} else {
|
||||
arguments.insert(arguments.end(), {"--mqtt_broker_host", mqtt_settings.broker_host, "--mqtt_broker_port",
|
||||
std::to_string(mqtt_settings.broker_port)});
|
||||
}
|
||||
|
||||
exec_module(module_info.path.string(), arguments, proc_handle);
|
||||
}
|
||||
|
||||
void exec_javascript_module(system::SubProcess& proc_handle, const ModuleStartInfo& module_info,
|
||||
const RuntimeSettings& rs, const MQTTSettings& mqtt_settings) {
|
||||
// instead of using setenv, using execvpe might be a better way for a controlled environment!
|
||||
|
||||
// FIXME (aw): everest directory layout
|
||||
const auto node_modules_path = rs.prefix / defaults::LIB_DIR / defaults::NAMESPACE / "node_modules";
|
||||
setenv("NODE_PATH", node_modules_path.c_str(), 0);
|
||||
setup_environment(module_info, rs, mqtt_settings);
|
||||
|
||||
std::vector<std::string> arguments = {
|
||||
"node",
|
||||
"--unhandled-rejections=strict",
|
||||
module_info.path.string(),
|
||||
};
|
||||
|
||||
exec_module("node", arguments, proc_handle);
|
||||
}
|
||||
|
||||
void exec_python_module(system::SubProcess& proc_handle, const ModuleStartInfo& module_info, const RuntimeSettings& rs,
|
||||
const MQTTSettings& mqtt_settings) {
|
||||
// instead of using setenv, using execvpe might be a better way for a controlled environment!
|
||||
|
||||
setup_environment(module_info, rs, mqtt_settings);
|
||||
|
||||
// Prepend the everestpy path to $PYTHONPATH. This ensures modules can always find everestpy.
|
||||
const auto everestpy_path = rs.prefix / defaults::LIB_DIR / defaults::NAMESPACE / "everestpy";
|
||||
if (const auto prev_pythonpath = std::getenv("PYTHONPATH")) {
|
||||
const auto pythonpath = fmt::format("{}:{}", everestpy_path.string(), prev_pythonpath);
|
||||
setenv("PYTHONPATH", pythonpath.c_str(), 1);
|
||||
} else {
|
||||
setenv("PYTHONPATH", everestpy_path.c_str(), 1);
|
||||
}
|
||||
|
||||
std::string python_binary = "python3";
|
||||
|
||||
// Check if a virtual environment exists in the module directory, and if so use its python runtime.
|
||||
const auto venv_dir = module_info.path.parent_path() / ".venv";
|
||||
if (fs::exists(venv_dir)) {
|
||||
const auto venv_bin_dir = venv_dir / "bin";
|
||||
const auto venv_python = venv_bin_dir / "python3";
|
||||
if (fs::exists(venv_python)) {
|
||||
// Activate the virtual environment. This approximates the behaviour of the `.venv/bin/activate` script.
|
||||
python_binary = venv_python.string();
|
||||
setenv("VIRTUAL_ENV", venv_dir.c_str(), 1);
|
||||
setenv("VIRTUAL_ENV_PROMPT", "venv", 1);
|
||||
unsetenv("PYTHONHOME");
|
||||
|
||||
if (const auto prev_path = std::getenv("PATH")) {
|
||||
const auto path = fmt::format("{}:{}", venv_bin_dir.string(), prev_path);
|
||||
setenv("PATH", path.c_str(), 1);
|
||||
} else {
|
||||
setenv("PATH", venv_bin_dir.c_str(), 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> arguments = {python_binary, module_info.path.c_str()};
|
||||
exec_module(python_binary, arguments, proc_handle);
|
||||
}
|
||||
|
||||
void exec_module(const RuntimeSettings& rs, const MQTTSettings& mqtt_settings, const ModuleStartInfo& module,
|
||||
system::SubProcess& proc_handle) {
|
||||
switch (module.language) {
|
||||
case ModuleStartInfo::Language::cpp:
|
||||
exec_cpp_module(proc_handle, module, rs, mqtt_settings);
|
||||
break;
|
||||
case ModuleStartInfo::Language::javascript:
|
||||
exec_javascript_module(proc_handle, module, rs, mqtt_settings);
|
||||
break;
|
||||
case ModuleStartInfo::Language::python:
|
||||
exec_python_module(proc_handle, module, rs, mqtt_settings);
|
||||
break;
|
||||
default:
|
||||
throw std::logic_error("Module language not in enum");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::map<pid_t, std::string> spawn_modules(const std::vector<ModuleStartInfo>& modules, const ManagerSettings& ms) {
|
||||
std::map<pid_t, std::string> started_modules;
|
||||
|
||||
const auto& rs = ms.runtime_settings;
|
||||
|
||||
for (const auto& module : modules) {
|
||||
|
||||
auto proc_handle = system::SubProcess::create(ms.run_as_user, module.capabilities);
|
||||
|
||||
if (proc_handle.is_child()) {
|
||||
// first, check if we need any capabilities
|
||||
|
||||
try {
|
||||
exec_module(rs, ms.mqtt_settings, module, proc_handle);
|
||||
} catch (const std::exception& err) {
|
||||
proc_handle.send_error_and_exit(err.what());
|
||||
}
|
||||
}
|
||||
|
||||
// we can only come here, if we're the parent!
|
||||
const auto child_pid = proc_handle.check_child_executed();
|
||||
|
||||
EVLOG_debug << fmt::format("Forked module {} with pid: {}", module.name, child_pid);
|
||||
started_modules[child_pid] = module.name;
|
||||
}
|
||||
|
||||
return started_modules;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
struct ModuleReadyInfo {
|
||||
bool ready;
|
||||
std::shared_ptr<TypedHandler> ready_token;
|
||||
std::shared_ptr<TypedHandler> get_config_token;
|
||||
};
|
||||
|
||||
// FIXME (aw): these are globals here, because they are used in the ready callback handlers
|
||||
using ModulesReadyType = std::unordered_map<std::string, ModuleReadyInfo>;
|
||||
namespace {
|
||||
ModulesReadyType modules_ready; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
// Don't hold the mutex and use any function of the `mqtt_abstraction` since the
|
||||
// mutex is also held inside the `ready` handler which can deadlock.
|
||||
std::mutex modules_ready_mutex; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
|
||||
|
||||
std::map<pid_t, std::string> start_modules(ManagerConfig& config, MQTTAbstraction& mqtt_abstraction,
|
||||
const std::vector<std::string>& ignored_modules,
|
||||
const std::vector<std::string>& standalone_modules,
|
||||
const ManagerSettings& ms, StatusFifo& status_fifo, bool retain_topics) {
|
||||
BOOST_LOG_FUNCTION();
|
||||
|
||||
std::vector<ModuleStartInfo> modules_to_spawn;
|
||||
|
||||
const auto& module_configurations = config.get_module_configurations();
|
||||
const auto& module_names = config.get_module_names();
|
||||
modules_to_spawn.reserve(module_configurations.size());
|
||||
const auto number_of_modules = module_configurations.size();
|
||||
EVLOG_info << "Starting " << number_of_modules << " modules";
|
||||
|
||||
// TODO: move this into its own functions / class? ConfigService?
|
||||
const auto interface_definitions = config.get_interface_definitions();
|
||||
std::vector<std::string> interface_names;
|
||||
for (auto& interface_definition : interface_definitions.items()) {
|
||||
interface_names.push_back(interface_definition.key());
|
||||
}
|
||||
|
||||
MqttMessagePayload payload{MqttMessageType::GetConfigResponse, interface_names};
|
||||
|
||||
mqtt_abstraction.publish(fmt::format("{}interfaces", ms.mqtt_settings.everest_prefix), payload, QOS::QOS2, true);
|
||||
|
||||
for (const auto& interface_definition : interface_definitions.items()) {
|
||||
|
||||
MqttMessagePayload interface_definition_payload{MqttMessageType::GetConfigResponse,
|
||||
interface_definition.value()};
|
||||
|
||||
mqtt_abstraction.publish(
|
||||
fmt::format("{}interface_definitions/{}", ms.mqtt_settings.everest_prefix, interface_definition.key()),
|
||||
interface_definition_payload, QOS::QOS2, true);
|
||||
}
|
||||
|
||||
const auto type_definitions = config.get_types();
|
||||
std::vector<std::string> type_names;
|
||||
for (auto& type_definition : type_definitions.items()) {
|
||||
type_names.push_back(type_definition.key());
|
||||
}
|
||||
|
||||
MqttMessagePayload type_names_payload{MqttMessageType::GetConfigResponse, type_names};
|
||||
|
||||
mqtt_abstraction.publish(fmt::format("{}types", ms.mqtt_settings.everest_prefix), type_names_payload, QOS::QOS2,
|
||||
true);
|
||||
for (const auto& type_definition : type_definitions.items()) {
|
||||
|
||||
MqttMessagePayload type_definition_payload{MqttMessageType::GetConfigResponse, type_definition.value()};
|
||||
|
||||
// type_definition keys already start with a / so omit it in the topic name
|
||||
mqtt_abstraction.publish(
|
||||
fmt::format("{}type_definitions{}", ms.mqtt_settings.everest_prefix, type_definition.key()),
|
||||
type_definition_payload, QOS::QOS2, true);
|
||||
}
|
||||
|
||||
const auto settings = config.get_settings();
|
||||
|
||||
MqttMessagePayload settings_payload{MqttMessageType::GetConfigResponse, settings};
|
||||
|
||||
mqtt_abstraction.publish(fmt::format("{}settings", ms.mqtt_settings.everest_prefix), settings_payload, QOS::QOS2,
|
||||
true);
|
||||
|
||||
if (ms.runtime_settings.validate_schema) {
|
||||
const auto schemas = config.get_schemas();
|
||||
|
||||
MqttMessagePayload schemas_payload{MqttMessageType::GetConfigResponse, schemas};
|
||||
|
||||
mqtt_abstraction.publish(fmt::format("{}schemas", ms.mqtt_settings.everest_prefix), schemas_payload, QOS::QOS2,
|
||||
true);
|
||||
}
|
||||
const auto manifests = config.get_manifests();
|
||||
|
||||
for (const auto& manifest : manifests.items()) {
|
||||
auto manifest_copy = manifest.value();
|
||||
manifest_copy.erase("config");
|
||||
|
||||
MqttMessagePayload manifest_payload{MqttMessageType::GetConfigResponse, manifest_copy};
|
||||
|
||||
mqtt_abstraction.publish(fmt::format("{}manifests/{}", ms.mqtt_settings.everest_prefix, manifest.key()),
|
||||
manifest_payload, QOS::QOS2, true);
|
||||
}
|
||||
|
||||
MqttMessagePayload module_names_payload{MqttMessageType::GetConfigResponse, module_names};
|
||||
|
||||
mqtt_abstraction.publish(fmt::format("{}module_names", ms.mqtt_settings.everest_prefix), module_names_payload,
|
||||
QOS::QOS2, true);
|
||||
|
||||
for (const auto& [module_id_, module_config] : module_configurations) {
|
||||
const auto& module_name = module_config.module_name;
|
||||
const auto& module_id = module_id_;
|
||||
if (std::any_of(ignored_modules.begin(), ignored_modules.end(),
|
||||
[module_id](const auto& element) { return element == module_id; })) {
|
||||
EVLOG_info << fmt::format("Ignoring module: {}", module_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
// FIXME (aw): implicitly adding ModuleReadyInfo and setting its ready member
|
||||
auto module_it = modules_ready.emplace(module_id, ModuleReadyInfo{false, nullptr, nullptr}).first;
|
||||
|
||||
std::vector<std::string> capabilities =
|
||||
module_configurations.at(module_id).capabilities.value_or(std::vector<std::string>{});
|
||||
|
||||
if (not capabilities.empty()) {
|
||||
EVLOG_info << fmt::format("Module {} wants to acquire the following capabilities: {}", module_name,
|
||||
fmt::join(capabilities.begin(), capabilities.end(), " "));
|
||||
}
|
||||
|
||||
const Handler module_ready_handler = [module_id, &mqtt_abstraction, &config, standalone_modules,
|
||||
mqtt_everest_prefix = ms.mqtt_settings.everest_prefix, &status_fifo,
|
||||
retain_topics](const std::string&, const nlohmann::json& json) {
|
||||
EVLOG_debug << fmt::format("received module ready signal for module: {}({})", module_id, json.dump());
|
||||
const std::unique_lock<std::mutex> lock(modules_ready_mutex);
|
||||
// FIXME (aw): here are race conditions, if the ready handler gets called while modules are shut down!
|
||||
try {
|
||||
modules_ready.at(module_id).ready = json.get<bool>();
|
||||
} catch (const std::out_of_range& ex) {
|
||||
// This can happen if we're shutting down and a module becomes
|
||||
// ready.
|
||||
EVLOG_error << "The module " << module_id << " is not in `modules_ready`: " << ex.what();
|
||||
return;
|
||||
}
|
||||
std::size_t modules_spawned = 0;
|
||||
for (const auto& mod : modules_ready) {
|
||||
const std::string text_ready =
|
||||
fmt::format((mod.second.ready) ? TERMINAL_STYLE_OK : TERMINAL_STYLE_ERROR, "ready");
|
||||
EVLOG_debug << fmt::format(" {}: {}", mod.first, text_ready);
|
||||
if (mod.second.ready) {
|
||||
modules_spawned += 1;
|
||||
}
|
||||
}
|
||||
if (!standalone_modules.empty() && std::find(standalone_modules.begin(), standalone_modules.end(),
|
||||
module_id) != standalone_modules.end()) {
|
||||
EVLOG_info << fmt::format("Standalone module {} initialized.", module_id);
|
||||
}
|
||||
if (std::all_of(modules_ready.begin(), modules_ready.end(),
|
||||
[](const auto& element) { return element.second.ready; })) {
|
||||
const auto complete_end_time = std::chrono::steady_clock::now();
|
||||
status_fifo.update(StatusFifo::ALL_MODULES_STARTED);
|
||||
if (not retain_topics) {
|
||||
EVLOG_info << "Clearing retained topics published by manager during startup";
|
||||
mqtt_abstraction.clear_retained_topics();
|
||||
} else {
|
||||
EVLOG_info << "Keeping retained topics published by manager during startup for inspection";
|
||||
}
|
||||
EVLOG_info << fmt::format(
|
||||
TERMINAL_STYLE_OK, "🚙🚙🚙 All modules are initialized. EVerest up and running [{}ms] 🚙🚙🚙",
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(complete_end_time - complete_start_time)
|
||||
.count());
|
||||
|
||||
MqttMessagePayload payload{MqttMessageType::GlobalReady, nlohmann::json(true)};
|
||||
|
||||
mqtt_abstraction.publish(fmt::format("{}ready", mqtt_everest_prefix), payload);
|
||||
} else if (!standalone_modules.empty()) {
|
||||
if (modules_spawned == modules_ready.size() - standalone_modules.size()) {
|
||||
EVLOG_info << fmt::format(fg(fmt::terminal_color::green),
|
||||
"Modules started by manager are ready, waiting for standalone modules.");
|
||||
status_fifo.update(StatusFifo::WAITING_FOR_STANDALONE_MODULES);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const std::string ready_topic = fmt::format("{}/ready", config.mqtt_module_prefix(module_id));
|
||||
module_it->second.ready_token =
|
||||
std::make_shared<TypedHandler>(HandlerType::ModuleReady, std::make_shared<Handler>(module_ready_handler));
|
||||
mqtt_abstraction.register_handler(ready_topic, module_it->second.ready_token, QOS::QOS2);
|
||||
|
||||
if (std::any_of(standalone_modules.begin(), standalone_modules.end(),
|
||||
[module_id](const auto& element) { return element == module_id; })) {
|
||||
EVLOG_info << "Not starting standalone module: " << fmt::format(TERMINAL_STYLE_BLUE, "{}", module_id);
|
||||
continue;
|
||||
}
|
||||
|
||||
const std::string binary_filename = fmt::format("{}", module_name);
|
||||
const std::string javascript_library_filename = "index.js";
|
||||
const std::string python_filename = "module.py";
|
||||
const auto module_path = ms.runtime_settings.modules_dir / module_name;
|
||||
const auto printable_module_name = config.printable_identifier(module_id);
|
||||
const auto binary_path = module_path / binary_filename;
|
||||
const auto javascript_library_path = module_path / javascript_library_filename;
|
||||
const auto python_module_path = module_path / python_filename;
|
||||
|
||||
if (fs::exists(binary_path)) {
|
||||
EVLOG_debug << fmt::format("module: {} ({}) provided as binary", module_id, module_name);
|
||||
modules_to_spawn.emplace_back(module_id, printable_module_name, ModuleStartInfo::Language::cpp, binary_path,
|
||||
capabilities);
|
||||
} else if (fs::exists(javascript_library_path)) {
|
||||
EVLOG_debug << fmt::format("module: {} ({}) provided as javascript library", module_id, module_name);
|
||||
modules_to_spawn.emplace_back(module_id, printable_module_name, ModuleStartInfo::Language::javascript,
|
||||
fs::canonical(javascript_library_path), capabilities);
|
||||
} else if (fs::exists(python_module_path)) {
|
||||
EVLOG_verbose << fmt::format("module: {} ({}) provided as python module", module_id, module_name);
|
||||
modules_to_spawn.emplace_back(module_id, printable_module_name, ModuleStartInfo::Language::python,
|
||||
fs::canonical(python_module_path), capabilities);
|
||||
} else {
|
||||
if (module_id == "probe" || module_name == "ProbeModule") {
|
||||
EVLOG_error << "You are trying to start the probe module as binary, please check "
|
||||
"your test case, did you add \"@pytest.mark.probe_module\" to your test case?";
|
||||
}
|
||||
throw std::runtime_error(
|
||||
fmt::format("module: {} ({}) cannot be loaded because no Binary, JavaScript or Python "
|
||||
"module has been found\n"
|
||||
" checked paths:\n"
|
||||
" binary: {}\n"
|
||||
" js: {}\n"
|
||||
" py: {}\n",
|
||||
module_id, module_name, binary_path.string(), javascript_library_path.string(),
|
||||
python_module_path.string()));
|
||||
}
|
||||
}
|
||||
|
||||
return spawn_modules(modules_to_spawn, ms);
|
||||
}
|
||||
|
||||
void shutdown_modules(const std::map<pid_t, std::string>& modules, ManagerConfig& config,
|
||||
MQTTAbstraction& mqtt_abstraction) {
|
||||
|
||||
ModulesReadyType modules_ready_moved;
|
||||
{
|
||||
const std::lock_guard<std::mutex> lck(modules_ready_mutex);
|
||||
modules_ready_moved = std::move(modules_ready);
|
||||
// Probably not needed after our move but lets be explicit.
|
||||
modules_ready.clear();
|
||||
}
|
||||
|
||||
for (const auto& module : modules_ready_moved) {
|
||||
const auto& ready_info = module.second;
|
||||
const auto& module_name = module.first;
|
||||
const std::string topic = fmt::format("{}/ready", config.mqtt_module_prefix(module_name));
|
||||
mqtt_abstraction.unregister_handler(topic, ready_info.ready_token);
|
||||
}
|
||||
|
||||
for (const auto& child : modules) {
|
||||
auto retval = kill(child.first, SIGTERM);
|
||||
// FIXME (aw): supply errno strings
|
||||
if (retval != 0) {
|
||||
EVLOG_critical << fmt::format("SIGTERM of child: {} (pid: {}) {}: {}. Escalating to SIGKILL", child.second,
|
||||
child.first, fmt::format(TERMINAL_STYLE_ERROR, "failed"), retval);
|
||||
retval = kill(child.first, SIGKILL);
|
||||
if (retval != 0) {
|
||||
EVLOG_critical << fmt::format("SIGKILL of child: {} (pid: {}) {}: {}.", child.second, child.first,
|
||||
fmt::format(TERMINAL_STYLE_ERROR, "failed"), retval);
|
||||
} else {
|
||||
EVLOG_info << fmt::format("SIGKILL of child: {} (pid: {}) {}.", child.second, child.first,
|
||||
fmt::format(TERMINAL_STYLE_OK, "succeeded"));
|
||||
}
|
||||
} else {
|
||||
EVLOG_info << fmt::format("SIGTERM of child: {} (pid: {}) {}.", child.second, child.first,
|
||||
fmt::format(TERMINAL_STYLE_OK, "succeeded"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ENABLE_ADMIN_PANEL
|
||||
ControllerHandle start_controller(const ManagerSettings& ms) {
|
||||
std::array<int, 2> socket_pair; // NOLINT(cppcoreguidelines-pro-type-member-init): this is always initialized in the
|
||||
// following socketpair call
|
||||
|
||||
// FIXME (aw): destroy this socketpair somewhere
|
||||
socketpair(AF_UNIX, SOCK_DGRAM, 0, socket_pair.data());
|
||||
const int manager_socket = socket_pair[0];
|
||||
const int controller_socket = socket_pair[1];
|
||||
|
||||
auto proc_handle = system::SubProcess::create(ms.run_as_user);
|
||||
|
||||
if (proc_handle.is_child()) {
|
||||
// FIXME (aw): hack to get the correct directory of the controller
|
||||
const auto bin_dir = fs::canonical("/proc/self/exe").parent_path();
|
||||
|
||||
const auto controller_binary = bin_dir / "controller";
|
||||
|
||||
close(manager_socket);
|
||||
dup2(controller_socket, STDIN_FILENO);
|
||||
close(controller_socket);
|
||||
|
||||
execl(controller_binary.c_str(), MAGIC_CONTROLLER_ARG0, NULL);
|
||||
|
||||
// exec failed
|
||||
proc_handle.send_error_and_exit(
|
||||
fmt::format("Syscall to execl() with \"{} {}\" failed ({})", controller_binary.string(), strerror(errno)));
|
||||
}
|
||||
|
||||
close(controller_socket);
|
||||
|
||||
// send initial config to controller
|
||||
controller_ipc::send_message(manager_socket,
|
||||
{
|
||||
{"method", "boot"},
|
||||
{"params",
|
||||
{
|
||||
{"module_dir", ms.runtime_settings.modules_dir.string()},
|
||||
{"interface_dir", ms.interfaces_dir.string()},
|
||||
{"www_dir", ms.www_dir.string()},
|
||||
{"configs_dir", ms.configs_dir.string()},
|
||||
{"logging_config_file", ms.runtime_settings.logging_config_file.string()},
|
||||
{"controller_port", ms.controller_port},
|
||||
{"controller_rpc_timeout_ms", ms.controller_rpc_timeout_ms},
|
||||
}},
|
||||
});
|
||||
|
||||
return {proc_handle.check_child_executed(), manager_socket};
|
||||
}
|
||||
#endif
|
||||
|
||||
ConfigBootMode parse_config_boot_mode(const std::string& config_opt, const std::string& db_opt, const bool db_init) {
|
||||
if (config_opt.empty() and db_opt.empty()) {
|
||||
// no config or db option given, use default
|
||||
return ConfigBootMode::YamlFile;
|
||||
}
|
||||
if (!config_opt.empty() && !db_opt.empty()) {
|
||||
if (db_init == false) {
|
||||
throw BootException("Both --config and --db options are set, but no --db-init option is given. "
|
||||
"This is not allowed.");
|
||||
}
|
||||
return ConfigBootMode::DatabaseInit;
|
||||
}
|
||||
if (!config_opt.empty()) {
|
||||
// only config option given, use yaml file
|
||||
return ConfigBootMode::YamlFile;
|
||||
}
|
||||
if (!db_opt.empty()) {
|
||||
// only db option given, use database
|
||||
return ConfigBootMode::Database;
|
||||
}
|
||||
throw std::logic_error("Could not parse config boot source, this should never happen.");
|
||||
}
|
||||
|
||||
int boot(const po::variables_map& vm) {
|
||||
const bool check = (vm.count("check") != 0);
|
||||
|
||||
const auto prefix_opt = parse_string_option(vm, "prefix");
|
||||
const auto config_opt = parse_string_option(vm, "config");
|
||||
const auto db_opt = parse_string_option(vm, "db");
|
||||
const auto db_init = vm.count("db-init") != 0;
|
||||
ConfigBootMode boot_mode = parse_config_boot_mode(config_opt, db_opt, db_init);
|
||||
|
||||
ManagerSettings ms;
|
||||
|
||||
switch (boot_mode) {
|
||||
case ConfigBootMode::YamlFile:
|
||||
ms = ManagerSettings(prefix_opt, config_opt);
|
||||
break;
|
||||
case ConfigBootMode::Database:
|
||||
ms = ManagerSettings(prefix_opt, db_opt, DatabaseTag{});
|
||||
break;
|
||||
case ConfigBootMode::DatabaseInit:
|
||||
ms = ManagerSettings(prefix_opt, config_opt, db_opt);
|
||||
break;
|
||||
default:
|
||||
throw BootException(fmt::format("Invalid boot source: {}", static_cast<int>(boot_mode)));
|
||||
}
|
||||
|
||||
// CLI override for mqtt_everest_prefix (e.g. for parallel test execution).
|
||||
if (vm.count("mqtt_everest_prefix") != 0) {
|
||||
auto prefix = vm["mqtt_everest_prefix"].as<std::string>();
|
||||
if (!prefix.empty() && prefix.back() != '/') {
|
||||
prefix += "/";
|
||||
}
|
||||
ms.mqtt_settings.everest_prefix = prefix;
|
||||
}
|
||||
|
||||
Logging::init(ms.runtime_settings.logging_config_file.string());
|
||||
|
||||
EVLOG_info << " \033[0;1;35;95m_\033[0;1;31;91m__\033[0;1;33;93m__\033[0;1;32;92m__\033[0;1;36;96m_\033[0m "
|
||||
"\033[0;1;31;91m_\033[0;1;33;93m_\033[0m \033[0;1;36;96m_\033[0m ";
|
||||
EVLOG_info << " \033[0;1;31;91m|\033[0m \033[0;1;33;93m_\033[0;1;32;92m__\033[0;1;36;96m_\\\033[0m "
|
||||
"\033[0;1;34;94m\\\033[0m \033[0;1;33;93m/\033[0m \033[0;1;32;92m/\033[0m "
|
||||
"\033[0;1;34;94m|\033[0m \033[0;1;35;95m|\033[0m";
|
||||
EVLOG_info
|
||||
<< " \033[0;1;33;93m|\033[0m \033[0;1;32;92m|_\033[0;1;36;96m_\033[0m \033[0;1;35;95m\\\033[0m "
|
||||
"\033[0;1;31;91m\\\033[0m \033[0;1;33;93m/\033[0m \033[0;1;32;92m/\033[0;1;36;96m__\033[0m "
|
||||
"\033[0;1;34;94m_\033[0m \033[0;1;35;95m_\033[0;1;31;91m_\033[0m \033[0;1;33;93m__\033[0;1;32;92m_\033[0m "
|
||||
"\033[0;1;36;96m_\033[0;1;34;94m__\033[0;1;35;95m|\033[0m \033[0;1;31;91m|_\033[0m";
|
||||
EVLOG_info << " \033[0;1;32;92m|\033[0m \033[0;1;36;96m_\033[0;1;34;94m_|\033[0m \033[0;1;31;91m\\\033[0m "
|
||||
"\033[0;1;33;93m\\\033[0;1;32;92m/\033[0m \033[0;1;36;96m/\033[0m \033[0;1;34;94m_\033[0m "
|
||||
"\033[0;1;35;95m\\\033[0m \033[0;1;31;91m'_\033[0;1;33;93m_/\033[0m \033[0;1;32;92m_\033[0m "
|
||||
"\033[0;1;36;96m\\\033[0;1;34;94m/\033[0m \033[0;1;35;95m__\033[0;1;31;91m|\033[0m "
|
||||
"\033[0;1;33;93m__\033[0;1;32;92m|\033[0m";
|
||||
EVLOG_info << " \033[0;1;36;96m|\033[0m \033[0;1;34;94m|_\033[0;1;35;95m__\033[0;1;31;91m_\033[0m "
|
||||
"\033[0;1;32;92m\\\033[0m \033[0;1;36;96m/\033[0m \033[0;1;35;95m__\033[0;1;31;91m/\033[0m "
|
||||
"\033[0;1;33;93m|\033[0m \033[0;1;32;92m|\033[0m "
|
||||
"\033[0;1;36;96m_\033[0;1;34;94m_/\033[0;1;35;95m\\_\033[0;1;31;91m_\033[0m \033[0;1;33;93m\\\033[0m "
|
||||
"\033[0;1;32;92m|_\033[0m";
|
||||
EVLOG_info << " \033[0;1;34;94m|_\033[0;1;35;95m__\033[0;1;31;91m__\033[0;1;33;93m_|\033[0m "
|
||||
"\033[0;1;36;96m\\\033[0;1;34;94m/\033[0m "
|
||||
"\033[0;1;35;95m\\_\033[0;1;31;91m__\033[0;1;33;93m|_\033[0;1;32;92m|\033[0m "
|
||||
"\033[0;1;36;96m\\\033[0;1;34;94m__\033[0;1;35;95m_|\033[0;1;31;91m|_\033[0;1;33;93m__\033[0;1;32;"
|
||||
"92m/\\\033[0;1;36;96m__\033[0;1;34;94m|\033[0m";
|
||||
EVLOG_info << "";
|
||||
EVLOG_info << PROJECT_NAME << " " << PROJECT_VERSION << " " << GIT_VERSION;
|
||||
EVLOG_info << ms.version_information;
|
||||
EVLOG_info << "";
|
||||
|
||||
if (not ms.mqtt_settings.uses_socket()) {
|
||||
EVLOG_info << "Using MQTT broker " << ms.mqtt_settings.broker_host << ":" << ms.mqtt_settings.broker_port;
|
||||
} else {
|
||||
EVLOG_info << "Using MQTT broker unix domain sockets:" << ms.mqtt_settings.broker_socket_path;
|
||||
}
|
||||
if (ms.runtime_settings.telemetry_enabled) {
|
||||
EVLOG_info << "Telemetry enabled";
|
||||
}
|
||||
if (not ms.run_as_user.empty()) {
|
||||
EVLOG_info << "EVerest will run as system user: " << ms.run_as_user;
|
||||
}
|
||||
if (ms.runtime_settings.forward_exceptions) {
|
||||
EVLOG_info << "Catching and forwarding command exceptions to callers";
|
||||
}
|
||||
|
||||
#ifdef ENABLE_ADMIN_PANEL
|
||||
auto controller_handle = start_controller(ms);
|
||||
#endif
|
||||
|
||||
EVLOG_verbose << fmt::format("EVerest prefix was set to {}", ms.runtime_settings.prefix.string());
|
||||
|
||||
// dump all manifests if requested and terminate afterwards
|
||||
if (vm.count("dumpmanifests")) {
|
||||
const auto dumpmanifests_path = fs::path(vm["dumpmanifests"].as<std::string>());
|
||||
EVLOG_debug << fmt::format("Dumping all known validated manifests into '{}'", dumpmanifests_path.string());
|
||||
|
||||
auto manifests = Config::load_all_manifests(ms.runtime_settings.modules_dir.string(), ms.schemas_dir.string());
|
||||
|
||||
for (const auto& module : manifests.items()) {
|
||||
const std::string filename = module.key() + ".yaml";
|
||||
const auto module_output_path = dumpmanifests_path / filename;
|
||||
// FIXME (aw): should we check if the directory exists?
|
||||
std::ofstream output_stream(module_output_path);
|
||||
|
||||
// FIXME (aw): this should be either YAML prettyfied, or better, directly copied
|
||||
output_stream << module.value().dump(DUMP_INDENT);
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
const bool retain_topics = (vm.count("retain-topics") != 0);
|
||||
|
||||
const auto start_time = std::chrono::steady_clock::now();
|
||||
std::shared_ptr<ManagerConfig> config; // TODO: maybe this can stay unique when we re-work start_modules()
|
||||
try {
|
||||
config = std::make_shared<ManagerConfig>(ms);
|
||||
} catch (EverestInternalError& e) {
|
||||
EVLOG_error << fmt::format("Failed to load and validate config!\n{}", boost::diagnostic_information(e, true));
|
||||
return EXIT_FAILURE;
|
||||
} catch (boost::exception& e) {
|
||||
EVLOG_error << "Failed to load and validate config!";
|
||||
EVLOG_critical << fmt::format("Caught top level boost::exception:\n{}", boost::diagnostic_information(e, true));
|
||||
return EXIT_FAILURE;
|
||||
} catch (std::exception& e) {
|
||||
EVLOG_error << "Failed to load and validate config!";
|
||||
EVLOG_critical << fmt::format("Caught top level std::exception:\n{}", boost::diagnostic_information(e, true));
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
const auto end_time = std::chrono::steady_clock::now();
|
||||
EVLOG_info << "Config loading completed in "
|
||||
<< std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count() << "ms";
|
||||
|
||||
// dump config if requested
|
||||
if (vm.count("dump")) {
|
||||
const auto dump_path = fs::path(vm["dump"].as<std::string>());
|
||||
EVLOG_debug << fmt::format("Dumping validated config and manifests into '{}'", dump_path.string());
|
||||
|
||||
const auto config_dump_path = dump_path / "config.json";
|
||||
|
||||
std::ofstream output_config_stream(config_dump_path);
|
||||
|
||||
output_config_stream << json(config->get_module_configurations()).dump(DUMP_INDENT);
|
||||
|
||||
const auto manifests = config->get_manifests();
|
||||
|
||||
for (const auto& module : manifests.items()) {
|
||||
const std::string filename = module.key() + ".json";
|
||||
const auto module_output_path = dump_path / filename;
|
||||
std::ofstream output_stream(module_output_path);
|
||||
|
||||
output_stream << module.value().dump(DUMP_INDENT);
|
||||
}
|
||||
}
|
||||
|
||||
// only config check (and or config dumping) was requested, log check result and exit
|
||||
if (check) {
|
||||
EVLOG_debug << "Config is valid, terminating as requested";
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
std::vector<std::string> standalone_modules;
|
||||
if (vm.count("standalone")) {
|
||||
standalone_modules = vm["standalone"].as<std::vector<std::string>>();
|
||||
}
|
||||
|
||||
const auto& module_configurations = config->get_module_configurations();
|
||||
for (const auto& [module_id, module_config] : module_configurations) {
|
||||
// check if standalone parameter is set
|
||||
if (module_config.standalone) {
|
||||
if (std::find(standalone_modules.begin(), standalone_modules.end(), module_id) ==
|
||||
standalone_modules.end()) {
|
||||
EVLOG_info << "Module " << fmt::format(TERMINAL_STYLE_BLUE, "{}", module_id)
|
||||
<< " marked as standalone in config";
|
||||
|
||||
standalone_modules.push_back(module_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> ignored_modules;
|
||||
if (vm.count("ignore")) {
|
||||
ignored_modules = vm["ignore"].as<std::vector<std::string>>();
|
||||
}
|
||||
|
||||
// create StatusFifo object
|
||||
auto status_fifo = StatusFifo::create_from_path(vm["status-fifo"].as<std::string>());
|
||||
|
||||
auto mqtt_abstraction = make_mqtt_abstraction(ms.mqtt_settings);
|
||||
|
||||
if (!mqtt_abstraction->connect()) {
|
||||
if (not ms.mqtt_settings.uses_socket()) {
|
||||
EVLOG_error << fmt::format("Cannot connect to MQTT broker at {}:{}", ms.mqtt_settings.broker_host,
|
||||
ms.mqtt_settings.broker_port);
|
||||
} else {
|
||||
EVLOG_error << fmt::format("Cannot connect to MQTT broker socket at {}",
|
||||
ms.mqtt_settings.broker_socket_path);
|
||||
}
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
|
||||
mqtt_abstraction->spawn_main_loop_thread();
|
||||
|
||||
auto config_service = std::make_unique<config::ConfigService>(*mqtt_abstraction, config);
|
||||
|
||||
auto module_handles =
|
||||
start_modules(*config, *mqtt_abstraction, ignored_modules, standalone_modules, ms, status_fifo, retain_topics);
|
||||
bool modules_started = true;
|
||||
bool restart_modules = false;
|
||||
|
||||
#ifndef ENABLE_ADMIN_PANEL
|
||||
// switch to low privilege user if configured
|
||||
if (not ms.run_as_user.empty()) {
|
||||
auto err_set_user = system::set_real_user(ms.run_as_user);
|
||||
if (not err_set_user.empty()) {
|
||||
EVLOG_error << "Error switching manager to user " << ms.run_as_user << ": " << err_set_user;
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
int wstatus; // NOLINT(cppcoreguidelines-init-variables): this is always initialized in the following waitpid call
|
||||
|
||||
while (true) {
|
||||
// check if anyone died
|
||||
#ifdef ENABLE_ADMIN_PANEL
|
||||
// non-blocking if admin panel is enabled, as this main loop also processes controller RPC
|
||||
auto pid = waitpid(-1, &wstatus, WNOHANG);
|
||||
#else
|
||||
// block if admin panel is disabled, no controller RPC is handled by main loop
|
||||
auto pid = waitpid(-1, &wstatus, 0);
|
||||
#endif
|
||||
|
||||
if (pid == 0) {
|
||||
// nothing new from our child process
|
||||
} else if (pid == -1) {
|
||||
throw std::runtime_error(fmt::format("Syscall to waitpid() failed ({})", strerror(errno)));
|
||||
} else {
|
||||
|
||||
#ifdef ENABLE_ADMIN_PANEL
|
||||
// one of our children exited (first check controller, then modules)
|
||||
if (pid == controller_handle.pid) {
|
||||
// FIXME (aw): what to do, if the controller exited? Restart it?
|
||||
throw std::runtime_error("The controller process exited.");
|
||||
}
|
||||
#endif
|
||||
|
||||
const auto module_iter = module_handles.find(pid);
|
||||
if (module_iter == module_handles.end()) {
|
||||
throw std::runtime_error(fmt::format("Unknown child width pid ({}) died.", pid));
|
||||
}
|
||||
|
||||
const auto module_name = module_iter->second;
|
||||
module_handles.erase(module_iter);
|
||||
// one of our modules died -> kill 'em all
|
||||
if (modules_started) {
|
||||
EVLOG_critical << fmt::format("Module {} (pid: {}) exited with status: {}. Terminating all modules.",
|
||||
module_name, pid, wstatus);
|
||||
shutdown_modules(module_handles, *config, *mqtt_abstraction);
|
||||
|
||||
mqtt_abstraction->clear_retained_topics();
|
||||
mqtt_abstraction->disconnect();
|
||||
|
||||
// Exit if a module died, this gives systemd a change to restart manager
|
||||
EVLOG_critical << "Exiting manager.";
|
||||
return EXIT_FAILURE;
|
||||
} else {
|
||||
EVLOG_info << fmt::format("Module {} (pid: {}) exited with status: {}.", module_name, pid, wstatus);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ENABLE_ADMIN_PANEL
|
||||
if (module_handles.size() == 0 && restart_modules) {
|
||||
module_handles = start_modules(*config, *mqtt_abstraction, ignored_modules, standalone_modules, ms,
|
||||
status_fifo, retain_topics);
|
||||
restart_modules = false;
|
||||
modules_started = true;
|
||||
}
|
||||
|
||||
// check for news from the controller
|
||||
const auto msg = controller_handle.receive_message();
|
||||
if (msg.status == controller_ipc::MESSAGE_RETURN_STATUS::OK) {
|
||||
// FIXME (aw): implement all possible messages here, for now just log them
|
||||
const auto& payload = msg.json;
|
||||
if (payload.at("method") == "restart_modules") {
|
||||
shutdown_modules(module_handles, *config, *mqtt_abstraction);
|
||||
config = std::make_unique<ManagerConfig>(ms);
|
||||
modules_started = false;
|
||||
restart_modules = true;
|
||||
} else if (payload.at("method") == "check_config") {
|
||||
const std::string check_config_file_path = payload.at("params");
|
||||
|
||||
try {
|
||||
// check the config
|
||||
auto cfg = ManagerConfig(ManagerSettings(prefix_opt, check_config_file_path));
|
||||
controller_handle.send_message({{"id", payload.at("id")}});
|
||||
} catch (const std::exception& e) {
|
||||
controller_handle.send_message({{"result", e.what()}, {"id", payload.at("id")}});
|
||||
}
|
||||
} else {
|
||||
// unknown payload
|
||||
EVLOG_error << fmt::format("Received unknown command via controller ipc:\n{}\n... ignoring",
|
||||
payload.dump(DUMP_INDENT));
|
||||
}
|
||||
} else if (msg.status == controller_ipc::MESSAGE_RETURN_STATUS::ERROR) {
|
||||
fmt::print("Error in IPC communication with controller: {}\nExiting\n", msg.json.at("error").dump(2));
|
||||
return EXIT_FAILURE;
|
||||
} else {
|
||||
// TIMEOUT fall-through
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
po::options_description desc("EVerest manager");
|
||||
desc.add_options()("version", "Print version and exit");
|
||||
desc.add_options()("help,h", "produce help message");
|
||||
desc.add_options()("check", "Check and validate all config files and exit (0=success)");
|
||||
desc.add_options()("dump", po::value<std::string>(),
|
||||
"Dump validated and augmented main config and all used module manifests into dir");
|
||||
desc.add_options()("dumpmanifests", po::value<std::string>(),
|
||||
"Dump manifests of all modules into dir (even modules not used in config) and exit");
|
||||
desc.add_options()("prefix", po::value<std::string>(), "Prefix path of everest installation");
|
||||
desc.add_options()("standalone,s", po::value<std::vector<std::string>>()->multitoken(),
|
||||
"Module ID(s) to not automatically start child processes for (those must be started manually to "
|
||||
"make the framework start!).");
|
||||
desc.add_options()("ignore", po::value<std::vector<std::string>>()->multitoken(),
|
||||
"Module ID(s) to ignore: Do not automatically start child processes and do not require that "
|
||||
"they are started.");
|
||||
desc.add_options()("dontvalidateschema", "Don't validate json schema on every message");
|
||||
desc.add_options()("config", po::value<std::string>(),
|
||||
"Full path to a config file. If the file does not exist and has no extension, it will be "
|
||||
"looked up in the default config directory");
|
||||
desc.add_options()("db", po::value<std::string>(), "Full path to the configuration database file");
|
||||
desc.add_options()("db-init", "Indicator to initialize the database if it does not contain a valid configuration. "
|
||||
"Requires --config and --db to be set.");
|
||||
desc.add_options()("status-fifo", po::value<std::string>()->default_value(""),
|
||||
"Path to a named pipe, that shall be used for status updates from the manager");
|
||||
desc.add_options()("retain-topics", "Retain configuration MQTT topics setup by manager for inspection, by default "
|
||||
"these will be cleared after startup");
|
||||
desc.add_options()("mqtt_everest_prefix", po::value<std::string>(),
|
||||
"Override the MQTT everest prefix (useful for running multiple instances in parallel)");
|
||||
|
||||
po::variables_map vm;
|
||||
|
||||
try {
|
||||
const auto default_logging_cfg =
|
||||
defaults::PREFIX / fs::path(defaults::SYSCONF_DIR) / defaults::NAMESPACE / defaults::LOGGING_CONFIG_NAME;
|
||||
if (fs::exists(default_logging_cfg)) {
|
||||
Logging::init(default_logging_cfg.string());
|
||||
}
|
||||
po::store(po::parse_command_line(argc, argv, desc), vm);
|
||||
po::notify(vm);
|
||||
|
||||
if (vm.count("help") != 0) {
|
||||
desc.print(std::cout);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
if (vm.count("version") != 0) {
|
||||
std::string argv0;
|
||||
if (argc > 0) {
|
||||
argv0 = *argv;
|
||||
}
|
||||
std::cout << argv0 << " (" << PROJECT_NAME << " " << PROJECT_VERSION << " " << GIT_VERSION << ") "
|
||||
<< std::endl;
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
return boot(vm);
|
||||
|
||||
} catch (const BootException& e) {
|
||||
EVLOG_error << "Failed to start up everest:\n" << e.what();
|
||||
return EXIT_FAILURE;
|
||||
} catch (const std::exception& e) {
|
||||
EVLOG_error << "Main manager process exits because of caught exception:\n" << e.what();
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
}
|
||||
267
tools/EVerest-main/lib/everest/framework/src/system_unix.cpp
Normal file
267
tools/EVerest-main/lib/everest/framework/src/system_unix.cpp
Normal file
@@ -0,0 +1,267 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "system_unix.hpp"
|
||||
|
||||
#include <cassert>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <grp.h>
|
||||
#include <linux/securebits.h>
|
||||
#include <pwd.h>
|
||||
#include <signal.h>
|
||||
#include <sys/capability.h>
|
||||
#include <sys/prctl.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <fmt/core.h>
|
||||
|
||||
#include <utils/helpers.hpp>
|
||||
|
||||
namespace Everest::system {
|
||||
|
||||
const auto PARENT_DIED_SIGNAL = SIGTERM;
|
||||
|
||||
struct GetPasswdEntryResult {
|
||||
explicit GetPasswdEntryResult(const std::string& error_) : error(error_) {
|
||||
}
|
||||
|
||||
GetPasswdEntryResult(uid_t uid_, gid_t gid_, const std::vector<gid_t>& groups_) :
|
||||
uid(uid_), gid(gid_), groups(groups_) {
|
||||
}
|
||||
|
||||
std::string error;
|
||||
uid_t uid{};
|
||||
gid_t gid{};
|
||||
std::vector<gid_t> groups;
|
||||
|
||||
operator bool() const {
|
||||
return this->error.empty();
|
||||
}
|
||||
};
|
||||
|
||||
namespace {
|
||||
GetPasswdEntryResult get_passwd_entry(const std::string& user_name) {
|
||||
// Assuming that a user does not have more than 50 groups
|
||||
constexpr int max_supplementary_groups = 50;
|
||||
|
||||
const auto entry = getpwnam(user_name.c_str());
|
||||
|
||||
if (not entry) {
|
||||
return GetPasswdEntryResult("Could not get passwd entry for user name: " + user_name);
|
||||
}
|
||||
|
||||
// get supplementary groups for this user
|
||||
int max_ngroups = max_supplementary_groups;
|
||||
std::array<gid_t, max_supplementary_groups> groups{};
|
||||
|
||||
const int ngroups = getgrouplist(user_name.c_str(), entry->pw_gid, groups.data(), &max_ngroups);
|
||||
if (ngroups < 0) {
|
||||
return GetPasswdEntryResult("Could not get supplementary groups for user name: " + user_name);
|
||||
}
|
||||
|
||||
// Clang-tidy recommends using `std::span` here instead, which isn't available in C++17.
|
||||
// NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
|
||||
const std::vector<gid_t> user_groups{groups.begin(), groups.begin() + ngroups};
|
||||
return GetPasswdEntryResult(entry->pw_uid, entry->pw_gid, user_groups);
|
||||
}
|
||||
} // namespace
|
||||
|
||||
bool keep_caps() {
|
||||
// cap_set_secbits was added in libcap 2.30.
|
||||
// LIBCAP_MAJOR/LIBCAP_MINOR are defined in libcap >= 2.64.
|
||||
// For older versions without these macros, fall back to prctl.
|
||||
#if defined(LIBCAP_MAJOR) && (LIBCAP_MAJOR > 2 || (LIBCAP_MAJOR == 2 && LIBCAP_MINOR >= 30))
|
||||
return (0 == cap_set_secbits(SECBIT_KEEP_CAPS));
|
||||
#else
|
||||
return (0 == prctl(PR_SET_SECUREBITS, SECBIT_KEEP_CAPS));
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string set_caps(const std::vector<std::string>& capabilities) {
|
||||
|
||||
std::vector<cap_value_t> capability_values;
|
||||
capability_values.resize(capabilities.size());
|
||||
|
||||
for (const auto& cap_name : capabilities) {
|
||||
auto& cap_value = capability_values.emplace_back();
|
||||
const auto error = cap_from_name(cap_name.c_str(), &cap_value);
|
||||
|
||||
if (error) {
|
||||
return fmt::format("Failed to get capability value for capability name {}", cap_name);
|
||||
}
|
||||
}
|
||||
|
||||
auto cap_ctx = cap_get_proc();
|
||||
if (cap_set_flag(cap_ctx, CAP_INHERITABLE, Everest::helpers::clamp_to<int>(capability_values.size()),
|
||||
capability_values.data(), CAP_SET) != 0) {
|
||||
return "Failed to add capability flags to CAP_INHERITABLE";
|
||||
}
|
||||
|
||||
if (cap_set_proc(cap_ctx) != 0) {
|
||||
return "Failed to set capabilities for process";
|
||||
};
|
||||
|
||||
if (cap_free(cap_ctx) != 0) {
|
||||
return "Failed free memory for capability flags";
|
||||
};
|
||||
|
||||
for (const auto cap_value : capability_values) {
|
||||
if (cap_set_ambient(cap_value, CAP_SET) != 0) {
|
||||
return "Failed to add capabilities to ambient set";
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string set_real_user(const std::string& user_name) {
|
||||
// Set special capabilities if required by module
|
||||
|
||||
const auto entry = get_passwd_entry(user_name);
|
||||
|
||||
if (not entry) {
|
||||
return entry.error;
|
||||
}
|
||||
|
||||
const auto set_groups_failed = setgroups(entry.groups.size(), entry.groups.data());
|
||||
if (set_groups_failed) {
|
||||
return "setgroups failed";
|
||||
}
|
||||
|
||||
const auto set_gid_failed = setgid(entry.gid);
|
||||
if (set_gid_failed) {
|
||||
return "setgid failed";
|
||||
}
|
||||
|
||||
const auto set_uid_failed = setuid(entry.uid);
|
||||
if (set_uid_failed) {
|
||||
return "setuid failed";
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
void SubProcess::send_error_and_exit(const std::string& message) {
|
||||
assert(pid == 0);
|
||||
|
||||
// There isn't much we can do if writing the error message fails, just exit
|
||||
[[maybe_unused]] auto _write = write(fd, message.c_str(), std::min(message.size(), MAX_PIPE_MESSAGE_SIZE - 1));
|
||||
close(fd);
|
||||
_exit(EXIT_FAILURE);
|
||||
}
|
||||
|
||||
pid_t SubProcess::check_child_executed() {
|
||||
assert(pid != 0);
|
||||
|
||||
if (check_child_executed_done) {
|
||||
return pid;
|
||||
}
|
||||
check_child_executed_done = true;
|
||||
|
||||
std::string message(MAX_PIPE_MESSAGE_SIZE, 0);
|
||||
|
||||
auto retval = read(fd, message.data(), MAX_PIPE_MESSAGE_SIZE);
|
||||
if (retval == -1) {
|
||||
throw std::runtime_error(fmt::format(
|
||||
"Failed to communicate via pipe with forked child process. Syscall to read() failed ({}), exiting",
|
||||
strerror(errno)));
|
||||
} else if (retval > 0) {
|
||||
throw std::runtime_error(fmt::format("Forked child process did not complete exec():\n{}", message.c_str()));
|
||||
}
|
||||
|
||||
close(fd);
|
||||
return pid;
|
||||
}
|
||||
|
||||
std::string set_user_and_capabilities(const std::string& run_as_user, const std::vector<std::string>& capabilities) {
|
||||
if (not capabilities.empty()) {
|
||||
// we need to keep caps, otherwise, we'll loose all our capabilities (except inherited)
|
||||
if (system::keep_caps() == false) {
|
||||
return "Keeping capabilities (SECBIT_KEEP_CAPS) failed";
|
||||
}
|
||||
}
|
||||
|
||||
// Set real user for child process
|
||||
std::string error;
|
||||
if (not run_as_user.empty()) {
|
||||
error = system::set_real_user(run_as_user);
|
||||
if (not error.empty()) {
|
||||
return fmt::format("Failed to set real user to: {}", run_as_user);
|
||||
}
|
||||
}
|
||||
|
||||
// Set capabilities for child process
|
||||
if (not capabilities.empty()) {
|
||||
error = system::set_caps(capabilities);
|
||||
if (not error.empty()) {
|
||||
return fmt::format("Failed to set capabilities: {}", error);
|
||||
}
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
SubProcess SubProcess::create(const std::string& run_as_user, const std::vector<std::string>& capabilities) {
|
||||
std::array<int, 2> pipefd{};
|
||||
|
||||
const auto flags = O_CLOEXEC;
|
||||
// First, try to create a pipe with O_DIRECT
|
||||
if (pipe2(pipefd.data(), flags | O_DIRECT)) {
|
||||
auto errored = true;
|
||||
|
||||
// pipe2 returns EINVAL if the kernel does not support O_DIRECT. We retry without it and see if it still fails
|
||||
if (errno == EINVAL) {
|
||||
if (pipe2(pipefd.data(), flags) == 0) {
|
||||
errored = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (errored) {
|
||||
throw std::runtime_error(fmt::format("Syscall pipe2() failed ({}), exiting", strerror(errno)));
|
||||
}
|
||||
}
|
||||
|
||||
const auto reading_end_fd = pipefd[0];
|
||||
const auto writing_end_fd = pipefd[1];
|
||||
|
||||
const auto parent_pid = getpid();
|
||||
|
||||
const auto pid = fork();
|
||||
|
||||
if (pid == -1) {
|
||||
throw std::runtime_error(fmt::format("Syscall fork() failed ({}), exiting", strerror(errno)));
|
||||
}
|
||||
|
||||
if (pid == 0) {
|
||||
// close read end in child
|
||||
close(reading_end_fd);
|
||||
|
||||
SubProcess handle{writing_end_fd, pid};
|
||||
auto error = set_user_and_capabilities(run_as_user, capabilities);
|
||||
|
||||
if (not error.empty()) {
|
||||
handle.send_error_and_exit(error);
|
||||
}
|
||||
|
||||
// FIXME (aw): how does the the forked process does cleanup when receiving PARENT_DIED_SIGNAL compared to
|
||||
// _exit() before exec() has been called?
|
||||
if (prctl(PR_SET_PDEATHSIG, PARENT_DIED_SIGNAL)) {
|
||||
handle.send_error_and_exit(fmt::format("Syscall prctl() failed ({}), exiting", strerror(errno)));
|
||||
}
|
||||
|
||||
if (getppid() != parent_pid) {
|
||||
// kill ourself, with the same handler as we would have
|
||||
// happened when the parent process died
|
||||
kill(getpid(), PARENT_DIED_SIGNAL);
|
||||
}
|
||||
|
||||
return handle;
|
||||
} else {
|
||||
close(writing_end_fd);
|
||||
return {reading_end_fd, pid};
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Everest::system
|
||||
41
tools/EVerest-main/lib/everest/framework/src/system_unix.hpp
Normal file
41
tools/EVerest-main/lib/everest/framework/src/system_unix.hpp
Normal file
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <sys/types.h>
|
||||
|
||||
namespace Everest::system {
|
||||
|
||||
class SubProcess {
|
||||
public:
|
||||
static SubProcess create(const std::string& run_as_user, const std::vector<std::string>& capabilities = {});
|
||||
bool is_child() const {
|
||||
return this->pid == 0;
|
||||
}
|
||||
|
||||
void send_error_and_exit(const std::string& message);
|
||||
|
||||
pid_t check_child_executed();
|
||||
|
||||
private:
|
||||
const std::size_t MAX_PIPE_MESSAGE_SIZE = 1024;
|
||||
SubProcess(int fd, pid_t pid) : fd(fd), pid(pid){};
|
||||
int fd{};
|
||||
pid_t pid{0};
|
||||
bool check_child_executed_done{false};
|
||||
};
|
||||
|
||||
bool keep_caps();
|
||||
|
||||
std::string set_caps(const std::vector<std::string>& capabilities);
|
||||
|
||||
std::string set_real_user(const std::string& user_name);
|
||||
|
||||
std::string set_user_and_capabilities(const std::string& run_as_user, const std::vector<std::string>& capabilities);
|
||||
|
||||
} // namespace Everest::system
|
||||
Reference in New Issue
Block a user