/******************************************************************************** * Copyright (c) 2025-2026 ZF Friedrichshafen AG * * This program and the accompanying materials are made available under the * terms of the Apache License Version 2.0 which is available at * https://www.apache.org/licenses/LICENSE-2.0 * * SPDX-License-Identifier: Apache-2.0 * * Contributors: * Erik Verhoeven - initial API and implementation ********************************************************************************/ #include "module_control.h" #include #include "../../global/exec_dir_helper.h" #include "toml_parser/parser_toml.h" #include "toml_parser//parser_node_toml.h" #include "app_settings.h" #include "app_config.h" /// @cond DOXYGEN_IGNORE #ifdef _WIN32 #include #elif defined __unix__ #include #include #else #error OS is not supported! #endif /// @endcond // GetModuleControl might be redirected for unit tests. #ifndef GetModuleControl CModuleControl& GetModuleControl() { static CModuleControl module_control; return module_control; } #endif // !defined GetModuleControl CModuleControl::~CModuleControl() { try { UnloadAll(std::vector()); } catch (const sdv::XSysExcept&) { } catch (const std::exception&) { } } bool CModuleControl::AddModuleSearchDir(const sdv::u8string& ssDir) { // Add initial paths if not done so already AddCurrentPath(); std::unique_lock lock(m_mtxModules); std::filesystem::path pathDir(ssDir.c_str()); // Relative paths are always relative to the executable if (pathDir.is_relative()) pathDir = GetExecDirectory() / ssDir.c_str(); // Remove any indirections pathDir = pathDir.lexically_normal(); // If the current path is not a directory, it cannot be added if (!std::filesystem::is_directory(pathDir)) return false; // Check whether the path is already in the list if (std::find(m_lstSearchPaths.begin(), m_lstSearchPaths.end(), pathDir) != m_lstSearchPaths.end()) return true; // This is not an error // Add the path m_lstSearchPaths.push_back(pathDir); return true; } sdv::sequence CModuleControl::GetModuleSearchDirs() const { std::unique_lock lock(m_mtxModules); sdv::sequence seqSearchDirs; for (const std::filesystem::path& rpathSearchDir : m_lstSearchPaths) seqSearchDirs.push_back(rpathSearchDir.generic_u8string()); return seqSearchDirs; } sdv::sequence CModuleControl::GetModuleList() const { std::unique_lock lock(m_mtxModules); sdv::sequence seqModuleInfos; // Build the module information sequence for (const std::shared_ptr& ptrModule : m_lstModules) { if (!ptrModule) continue; seqModuleInfos.push_back(ptrModule->GetModuleInfo()); } return seqModuleInfos; } sdv::sequence CModuleControl::GetClassList(/*in*/ sdv::core::TModuleID tModuleID) const { // Find the module instance std::unique_lock lock(m_mtxModules); sdv::sequence seqClassInfos; auto itModule = std::find_if(m_lstModules.begin(), m_lstModules.end(), [&](const std::shared_ptr& ptrModule) { return ptrModule && ptrModule->GetModuleID() == tModuleID; }); if (itModule == m_lstModules.end()) return seqClassInfos; // Cannot find the module. // Request the class infos from the module instance. std::vector vecClasses = (*itModule)->GetAvailableClasses(); for (const std::string& rssClass : vecClasses) { auto optClassInfo = (*itModule)->GetClassInfo(rssClass); if (!optClassInfo) continue; seqClassInfos.push_back(*optClassInfo); } return seqClassInfos; } sdv::core::TModuleID CModuleControl::Load(const sdv::u8string& ssModulePath) { if(ssModulePath.empty()) return 0; // Add initial paths if not done so already AddCurrentPath(); // Core services bypass. std::unique_lock lock(m_mtxModules); std::filesystem::path pathModule; if (ssModulePath == "core_services.sdv") pathModule = static_cast(ssModulePath); else { if (GetAppSettings().IsMainApplication() || GetAppSettings().IsIsolatedApplication()) { // Check the installation for the module. pathModule = GetAppConfig().FindInstalledModule(static_cast(ssModulePath)); } else { try { // Get the search paths if the module path is relative std::list lstSearchPaths; std::filesystem::path pathSupplied(static_cast(ssModulePath)); if (pathSupplied.is_relative()) lstSearchPaths = m_lstSearchPaths; // Add an empty path to allow the OS to search when our own search paths could not find the module. lstSearchPaths.push_back(std::filesystem::path()); // Run through the search paths and try to find the module. for (const std::filesystem::path& rpathDirectory : lstSearchPaths) { // Compose the path std::filesystem::path pathModuleTemp; if (rpathDirectory.is_absolute()) pathModuleTemp = (rpathDirectory / pathSupplied).lexically_normal(); else { if (rpathDirectory.empty()) pathModuleTemp = pathSupplied.lexically_normal(); else pathModuleTemp = (GetExecDirectory() / rpathDirectory / pathSupplied).lexically_normal(); } if (std::filesystem::exists(pathModuleTemp)) { pathModule = pathModuleTemp; break; } } } catch (const std::exception& re) { SDV_LOG_ERROR("Supplied path to module load is not valid \"", ssModulePath, "\": ", re.what()); } } } // Check for an exisiting module. If existing, return the existing module. for (const std::shared_ptr& rptrModule : m_lstModules) { if (rptrModule && rptrModule->GetModulePath() == pathModule) return rptrModule->GetModuleID(); } // Create a new instance (even if the module could not be found). std::shared_ptr ptrModule = std::make_shared(static_cast(ssModulePath), pathModule); m_lstModules.push_back(ptrModule); if (!ptrModule->IsValid()) { SDV_LOG_ERROR("The module was not found \"", ssModulePath, "\""); return 0; // Do not return the module ID of not loadable modules. } return ptrModule->GetModuleID(); } bool CModuleControl::Unload(sdv::core::TModuleID tModuleID) { // Cannot unload the core services if (tModuleID == sdv::core::tCoreLibModule) return true; return ContextUnload(tModuleID, false); } bool CModuleControl::HasActiveObjects(sdv::core::TModuleID tModuleID) const { // Find the module instance std::unique_lock lock(m_mtxModules); auto itModule = std::find_if(m_lstModules.begin(), m_lstModules.end(), [&](const std::shared_ptr& ptrModule) { return ptrModule && ptrModule->GetModuleID() == tModuleID; }); if (itModule == m_lstModules.end()) return false; // Cannot find the module; no active objects known. // Redirect the call to the module instance. return (*itModule)->HasActiveObjects(); } std::shared_ptr CModuleControl::FindModuleByClass(const std::string& rssClass) { // Find the module instance std::unique_lock lock(m_mtxModules); for (const std::shared_ptr& ptrModule : m_lstModules) { if (!ptrModule) continue; std::vector vecClasses = ptrModule->GetAvailableClasses(); for (const std::string& rssClassLocal : vecClasses) { if (rssClassLocal == rssClass) return ptrModule; auto optClassInfo = ptrModule->GetClassInfo(rssClass); if (!optClassInfo) continue; if (std::find(optClassInfo->seqClassAliases.begin(), optClassInfo->seqClassAliases.end(), rssClass) != optClassInfo->seqClassAliases.end()) return ptrModule; } } // For main and isolated applications, check whether the module is in one of the installation manifests. auto optManifest = GetAppConfig().FindInstalledComponent(rssClass); if (!optManifest) return nullptr; std::filesystem::path pathModule = std::filesystem::u8path(static_cast(optManifest->ssModulePath)); auto ssManifest = GetAppConfig().FindInstalledModuleManifest(pathModule); if (ssManifest.empty()) return nullptr; lock.unlock(); // Load the module return GetModule(ContextLoad(pathModule, ssManifest)); } std::shared_ptr CModuleControl::GetModule(sdv::core::TModuleID tModuleID) const { // Find the module instance std::unique_lock lock(m_mtxModules); auto itModule = std::find_if(m_lstModules.begin(), m_lstModules.end(), [&](const std::shared_ptr& ptrModule) { return ptrModule && ptrModule->GetModuleID() == tModuleID; }); if (itModule == m_lstModules.end()) return nullptr; // Cannot find the module. return *itModule; } void CModuleControl::UnloadAll(const std::vector& rvecIgnoreModules) { // Force unloading all modules in reverse order of their loading std::unique_lock lock(m_mtxModules); std::list> lstCopy = m_lstModules; lock.unlock(); while (lstCopy.size()) { std::shared_ptr ptrModule = std::move(lstCopy.back()); lstCopy.pop_back(); if (!ptrModule) continue; // On the ignore list? if (std::find(rvecIgnoreModules.begin(), rvecIgnoreModules.end(), ptrModule->GetModuleID()) != rvecIgnoreModules.end()) continue; // Unload... -> call the module control function to remove it from the list. ContextUnload(ptrModule->GetModuleID(), true); } } void CModuleControl::ResetConfigBaseline() { std::unique_lock lock(m_mtxModules); m_setConfigModules.clear(); } sdv::core::EConfigProcessResult CModuleControl::LoadModulesFromConfig(const CAppConfigFile& rconfig, bool bAllowPartialLoad) { // TODO EVE: Extract the parameters from the configuration and store them at the class info. switch (GetAppSettings().GetContextType()) { case sdv::app::EAppContext::main: case sdv::app::EAppContext::isolated: case sdv::app::EAppContext::maintenance: return sdv::core::EConfigProcessResult::successful; // Do not load anything default: break; } // First load the modules in the module list auto vecModules = rconfig.GetModuleList(); size_t nSuccess = 0, nFail = 0; for (const auto& rsModule : vecModules) { sdv::core::TModuleID tModuleID = Load(rsModule.pathModule.generic_u8string()); if (tModuleID) ++nSuccess; else ++nFail; } // Load all the modules from the class list auto vecClasses = rconfig.GetClassList(); for (const auto& rsClass : vecClasses) { if (rsClass.ssModulePath.empty()) continue; sdv::core::TModuleID tModuleID = Load(rsClass.ssModulePath); if (tModuleID) ++nSuccess; else ++nFail; } // Load all the modules from the component list auto vecComponents = rconfig.GetComponentList(); for (const auto& rsComponent : vecComponents) { if (rsComponent.pathModule.empty()) continue; sdv::core::TModuleID tModuleID = Load(rsComponent.pathModule.generic_u8string()); if (tModuleID) ++nSuccess; else ++nFail; } if (!bAllowPartialLoad && nFail) return sdv::core::EConfigProcessResult::failed; if (!nFail) return sdv::core::EConfigProcessResult::successful; if (nSuccess) return sdv::core::EConfigProcessResult::partially_successful; return sdv::core::EConfigProcessResult::failed; } std::string CModuleControl::SaveConfig(const std::set& /*rsetIgnoreModule*/) { std::stringstream sstream; //std::unique_lock lock(m_mtxModules); //// Add all the loaded modules //for (const std::shared_ptr& rptrModule : m_lstModules) //{ // if (m_setConfigModules.find(rptrModule->GetModuleID()) != m_setConfigModules.end() && // rsetIgnoreModule.find(rptrModule->GetModuleConfigPath()) != rsetIgnoreModule.end()) // { // sstream << std::endl; // sstream << "[[Module]]" << std::endl; // sstream << "Path = \"" << rptrModule->GetModuleConfigPath().generic_u8string() << "\"" << std::endl; // } //} return sstream.str(); } sdv::core::TModuleID CModuleControl::ContextLoad(const std::filesystem::path& rpathModule, const std::string& rssManifest) { if (GetAppSettings().IsMaintenanceApplication()) return 0; // Not allowed // Run through the manifest and check for complex services, applications and utilities. // TODO EVE: Temporary suppression of cppcheck warning. // cppcheck-suppress variableScope [[maybe_unused]] bool bIsolationNeeded = false; try { size_t nIndex = 0; size_t nUnsupportedObjectCount = 0; toml_parser::CParser parser(rssManifest); do { std::shared_ptr ptrComponentType = parser.Root().Direct(std::string("Component[") + std::to_string(nIndex++) + "].Type"); if (!ptrComponentType) break; std::string ssType = static_cast(ptrComponentType->GetValue()); if (ssType == "Device") continue; // Okay if (ssType == "BasicService") continue; // Okay if (ssType == "Proxy") continue; // Okay if (ssType == "Stub") continue; // Okay if (ssType == "ComplexService" || ssType == "Application" || ssType == "Utility") { bIsolationNeeded = true; continue; // Okay } // Not okay nUnsupportedObjectCount++; } while (true); if (nIndex == nUnsupportedObjectCount) return 0; // No object to load. } catch (const sdv::toml::XTOMLParseException& /*rexcept*/) { return 0; } // TODO: If the module to load needs isolation... // If this is a main application: isolate modules that should be isolated // If this is an isolated application: check for another loaded module that needs isolation already. If this do not allow // this module to load. // Load the module return Load(rpathModule.generic_u8string()); } bool CModuleControl::ContextUnload(sdv::core::TModuleID tModuleID, bool bForce) { // Find the module instance std::unique_lock lock(m_mtxModules); auto itModule = std::find_if(m_lstModules.begin(), m_lstModules.end(), [&](const std::shared_ptr& ptrModule) { return ptrModule && ptrModule->GetModuleID() == tModuleID; }); if (itModule == m_lstModules.end()) return true; // Cannot find the module to unload. This is not an error! // Check whether it is possible to unload. bool bSuccess = (*itModule)->Unload(bForce); if (bSuccess) m_lstModules.erase(itModule); return bSuccess; } std::shared_ptr CModuleControl::FindModuleByPath(const std::filesystem::path& rpathModule) const { // Find the module instance std::unique_lock lock(m_mtxModules); auto itModule = std::find_if(m_lstModules.begin(), m_lstModules.end(), [&](const std::shared_ptr& ptrModule) { return ptrModule && ptrModule->GetModulePath() == rpathModule; }); if (itModule == m_lstModules.end()) return nullptr; // Cannot find the module return *itModule; } void CModuleControl::AddCurrentPath() { std::unique_lock lock(m_mtxModules); // Do this only once. if (!m_lstSearchPaths.empty()) return; // Add the core directory std::filesystem::path pathCoreDir = GetCoreDirectory().lexically_normal(); m_lstSearchPaths.push_back(pathCoreDir); // Add the exe dir std::filesystem::path pathExeDir = GetExecDirectory().lexically_normal(); if (pathExeDir != pathCoreDir) m_lstSearchPaths.push_back(pathExeDir); } bool CModuleControlService::EnableModuleControlAccess() { return GetAppSettings().IsStandaloneApplication() || GetAppSettings().IsEssentialApplication(); }