// SPDX-License-Identifier: Apache-2.0 // Copyright Pionix GmbH and Contributors to EVerest #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "controller/ipc.hpp" #include "system_unix.hpp" #include 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 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 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& arguments, system::SubProcess& proc_handle) { // Convert the argument list to the format required by `execv*()`. std::vector 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 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 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 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 spawn_modules(const std::vector& modules, const ManagerSettings& ms) { std::map 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 ready_token; std::shared_ptr get_config_token; }; // FIXME (aw): these are globals here, because they are used in the ready callback handlers using ModulesReadyType = std::unordered_map; 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 start_modules(ManagerConfig& config, MQTTAbstraction& mqtt_abstraction, const std::vector& ignored_modules, const std::vector& standalone_modules, const ManagerSettings& ms, StatusFifo& status_fifo, bool retain_topics) { BOOST_LOG_FUNCTION(); std::vector 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 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 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 capabilities = module_configurations.at(module_id).capabilities.value_or(std::vector{}); 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 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(); } 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(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(HandlerType::ModuleReady, std::make_shared(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& modules, ManagerConfig& config, MQTTAbstraction& mqtt_abstraction) { ModulesReadyType modules_ready_moved; { const std::lock_guard 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 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(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(); 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()); 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 config; // TODO: maybe this can stay unique when we re-work start_modules() try { config = std::make_shared(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(end_time - start_time).count() << "ms"; // dump config if requested if (vm.count("dump")) { const auto dump_path = fs::path(vm["dump"].as()); 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 standalone_modules; if (vm.count("standalone")) { standalone_modules = vm["standalone"].as>(); } 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 ignored_modules; if (vm.count("ignore")) { ignored_modules = vm["ignore"].as>(); } // create StatusFifo object auto status_fifo = StatusFifo::create_from_path(vm["status-fifo"].as()); 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(*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(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(), "Dump validated and augmented main config and all used module manifests into dir"); desc.add_options()("dumpmanifests", po::value(), "Dump manifests of all modules into dir (even modules not used in config) and exit"); desc.add_options()("prefix", po::value(), "Prefix path of everest installation"); desc.add_options()("standalone,s", po::value>()->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>()->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(), "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(), "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()->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(), "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; } }