From 03d8c62b266b5ff317278d11812c253535a4ac72 Mon Sep 17 00:00:00 2001 From: Mykhailo Lohvynenko Date: Mon, 8 Jun 2026 15:05:27 +0300 Subject: [PATCH 1/3] sm: launcher: add file copy runtime Signed-off-by: Mykhailo Lohvynenko --- src/sm/launcher/runtimes/CMakeLists.txt | 1 + .../launcher/runtimes/filecopy/CMakeLists.txt | 51 +++ src/sm/launcher/runtimes/filecopy/config.cpp | 37 ++ src/sm/launcher/runtimes/filecopy/config.hpp | 37 ++ .../launcher/runtimes/filecopy/filecopy.cpp | 400 ++++++++++++++++++ .../launcher/runtimes/filecopy/filecopy.hpp | 143 +++++++ .../runtimes/filecopy/tests/CMakeLists.txt | 33 ++ .../runtimes/filecopy/tests/config.cpp | 60 +++ .../runtimes/filecopy/tests/filecopy.cpp | 393 +++++++++++++++++ 9 files changed, 1155 insertions(+) create mode 100644 src/sm/launcher/runtimes/filecopy/CMakeLists.txt create mode 100644 src/sm/launcher/runtimes/filecopy/config.cpp create mode 100644 src/sm/launcher/runtimes/filecopy/config.hpp create mode 100644 src/sm/launcher/runtimes/filecopy/filecopy.cpp create mode 100644 src/sm/launcher/runtimes/filecopy/filecopy.hpp create mode 100644 src/sm/launcher/runtimes/filecopy/tests/CMakeLists.txt create mode 100644 src/sm/launcher/runtimes/filecopy/tests/config.cpp create mode 100644 src/sm/launcher/runtimes/filecopy/tests/filecopy.cpp diff --git a/src/sm/launcher/runtimes/CMakeLists.txt b/src/sm/launcher/runtimes/CMakeLists.txt index 5bde7a158..c027a0183 100644 --- a/src/sm/launcher/runtimes/CMakeLists.txt +++ b/src/sm/launcher/runtimes/CMakeLists.txt @@ -17,5 +17,6 @@ set(TARGET_PREFIX ${TARGET_PREFIX}_runtimes) add_subdirectory(boot) add_subdirectory(container) +add_subdirectory(filecopy) add_subdirectory(rootfs) add_subdirectory(utils) diff --git a/src/sm/launcher/runtimes/filecopy/CMakeLists.txt b/src/sm/launcher/runtimes/filecopy/CMakeLists.txt new file mode 100644 index 000000000..0852d7ab2 --- /dev/null +++ b/src/sm/launcher/runtimes/filecopy/CMakeLists.txt @@ -0,0 +1,51 @@ +# +# Copyright (C) 2026 EPAM Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +# ###################################################################################################################### +# Target name +# ###################################################################################################################### + +set(TARGET_NAME filecopy) + +# ###################################################################################################################### +# Sources +# ###################################################################################################################### + +set(SOURCES config.cpp filecopy.cpp) + +# ###################################################################################################################### +# Libraries +# ###################################################################################################################### + +set(LIBRARIES Poco::JSON aos::common::utils aos::core::common::tools aos::sm::config aos::sm::utils + aos::sm::runtimes::utils +) + +# ###################################################################################################################### +# Target +# ###################################################################################################################### + +add_module( + TARGET_NAME + ${TARGET_NAME} + LOG_MODULE + STACK_USAGE + ${AOS_STACK_USAGE} + SOURCES + ${SOURCES} + HEADERS + ${HEADERS} + LIBRARIES + ${LIBRARIES} +) + +# ###################################################################################################################### +# Tests +# ###################################################################################################################### + +if(WITH_TEST) + add_subdirectory(tests) +endif() diff --git a/src/sm/launcher/runtimes/filecopy/config.cpp b/src/sm/launcher/runtimes/filecopy/config.cpp new file mode 100644 index 000000000..784ff065e --- /dev/null +++ b/src/sm/launcher/runtimes/filecopy/config.cpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include "config.hpp" + +namespace aos::sm::launcher { + +namespace { + +constexpr auto cDefaultTargetBaseDir = "/var/aos/components"; +constexpr auto cDefaultRuntimeSubDir = "runtimes"; + +} // namespace + +Error ParseConfig(const RuntimeConfig& config, FileCopyConfig& fileCopyConfig) +{ + try { + const auto object = common::utils::CaseInsensitiveObjectWrapper(config.mConfig); + + fileCopyConfig.mTargetPath + = object.GetValue("targetPath", common::utils::JoinPath(cDefaultTargetBaseDir, config.mType)); + fileCopyConfig.mRuntimeDir = object.GetValue( + "runtimeDir", common::utils::JoinPath(config.mWorkingDir, cDefaultRuntimeSubDir, config.mType)); + } catch (const std::exception& e) { + return common::utils::ToAosError(e); + } + + return ErrorEnum::eNone; +} + +} // namespace aos::sm::launcher diff --git a/src/sm/launcher/runtimes/filecopy/config.hpp b/src/sm/launcher/runtimes/filecopy/config.hpp new file mode 100644 index 000000000..a9b028947 --- /dev/null +++ b/src/sm/launcher/runtimes/filecopy/config.hpp @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_SM_LAUNCHER_RUNTIMES_FILECOPY_CONFIG_HPP_ +#define AOS_SM_LAUNCHER_RUNTIMES_FILECOPY_CONFIG_HPP_ + +#include + +#include + +#include + +namespace aos::sm::launcher { + +/** + * File copy runtime config. + */ +struct FileCopyConfig { + std::string mTargetPath; + std::string mRuntimeDir; +}; + +/** + * Parses file copy runtime config. + * + * @param config runtime config. + * @param[out] fileCopyConfig file copy runtime config. + * @return Error. + */ +Error ParseConfig(const RuntimeConfig& config, FileCopyConfig& fileCopyConfig); + +} // namespace aos::sm::launcher + +#endif diff --git a/src/sm/launcher/runtimes/filecopy/filecopy.cpp b/src/sm/launcher/runtimes/filecopy/filecopy.cpp new file mode 100644 index 000000000..3efd7b1b9 --- /dev/null +++ b/src/sm/launcher/runtimes/filecopy/filecopy.cpp @@ -0,0 +1,400 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include +#include + +#include +#include +#include + +#include + +#include "config.hpp" +#include "filecopy.hpp" + +namespace aos::sm::launcher { + +/*********************************************************************************************************************** + * Public + **********************************************************************************************************************/ + +Error FileCopyRuntime::Init(const RuntimeConfig& config, iamclient::CurrentNodeInfoProviderItf& currentNodeInfoProvider, + imagemanager::ItemInfoProviderItf& itemInfoProvider, oci::OCISpecItf& ociSpec, + InstanceStatusReceiverItf& statusReceiver, sm::utils::SystemdConnItf& systemdConn) +{ + LOG_DBG() << "Init runtime" << Log::Field("type", config.mType.c_str()); + + mRuntimeConfig = config; + mCurrentNodeInfoProvider = ¤tNodeInfoProvider; + mItemInfoProvider = &itemInfoProvider; + mOCISpec = &ociSpec; + mStatusReceiver = &statusReceiver; + + if (auto err = ParseConfig(mRuntimeConfig, mComponentConfig); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = fs::MakeDirAll(mComponentConfig.mRuntimeDir.c_str()); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = CreateRuntimeInfo(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = mRebooter.Init(systemdConn); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error FileCopyRuntime::Start() +{ + std::lock_guard lock {mMutex}; + + LOG_DBG() << "Start runtime"; + + mCurrentInstance.reset(); + + if (auto err = InitInstalledData(); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (!mCurrentInstance.has_value()) { + return ErrorEnum::eNone; + } + + auto status = std::make_unique(); + + FillInstanceStatus(*mCurrentInstance, InstanceStateEnum::eActive, *status); + + mStatusReceiver->OnInstancesStatusesReceived(Array {status.get(), 1}); + + return ErrorEnum::eNone; +} + +Error FileCopyRuntime::Stop() +{ + LOG_DBG() << "Stop runtime"; + + return ErrorEnum::eNone; +} + +Error FileCopyRuntime::GetRuntimeInfo(RuntimeInfo& runtimeInfo) const +{ + std::lock_guard lock {mMutex}; + + LOG_DBG() << "Get runtime info"; + + runtimeInfo = mRuntimeInfo; + + return ErrorEnum::eNone; +} + +Error FileCopyRuntime::StartInstance(const InstanceInfo& instance, InstanceStatus& status) +{ + std::lock_guard lock {mMutex}; + + LOG_DBG() << "Start instance" << Log::Field("ident", static_cast(instance)) + << Log::Field("version", instance.mVersion) << Log::Field("manifestDigest", instance.mManifestDigest); + + auto notify = DeferRelease(&status, [&](const InstanceStatus*) { + mStatusReceiver->OnInstancesStatusesReceived(Array {&status, 1}); + }); + + if (mCurrentInstance.has_value() && mCurrentInstance->mManifestDigest == instance.mManifestDigest) { + FillInstanceStatus(*mCurrentInstance, InstanceStateEnum::eActive, status); + + return ErrorEnum::eNone; + } + + FillInstanceStatus(instance, InstanceStateEnum::eActivating, status); + + mStatusReceiver->OnInstancesStatusesReceived(Array {&status, 1}); + + Error err = ErrorEnum::eNone; + + auto cleanup = DeferRelease(&err, [&](const Error* e) { + if (!e->IsNone()) { + status.mState = InstanceStateEnum::eFailed; + status.mError = *e; + } + }); + + auto imageManifest = std::make_unique(); + + err = GetImageManifest(instance.mManifestDigest, *imageManifest); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + err = CopyImage(*imageManifest); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + err = SaveInstanceInfo(instance); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + mCurrentInstance = instance; + + err = mStatusReceiver->RebootRequired(mRuntimeInfo.mRuntimeID); + if (!err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error FileCopyRuntime::StopInstance(const InstanceIdent& instance, InstanceStatus& status) +{ + LOG_DBG() << "Stop instance" << Log::Field("ident", instance); + + static_cast(status) = instance; + status.mState = InstanceStateEnum::eInactive; + status.mError = ErrorEnum::eNone; + + mStatusReceiver->OnInstancesStatusesReceived(Array {&status, 1}); + + return ErrorEnum::eNone; +} + +Error FileCopyRuntime::Reboot() +{ + LOG_DBG() << "Reboot runtime"; + + return mRebooter.Reboot(); +} + +Error FileCopyRuntime::GetInstanceMonitoringData( + const InstanceIdent& instanceIdent, monitoring::InstanceMonitoringData& monitoringData) +{ + (void)monitoringData; + + LOG_DBG() << "Get instance monitoring data" << Log::Field("instance", instanceIdent); + + return ErrorEnum::eNotSupported; +} + +/*********************************************************************************************************************** + * Private + **********************************************************************************************************************/ + +Error FileCopyRuntime::InitInstalledData() +{ + const auto path = std::filesystem::path(mComponentConfig.mRuntimeDir) / cInstalledInstanceFileName; + + if (!std::filesystem::exists(path)) { + mCurrentInstance.emplace(); + static_cast(*mCurrentInstance) = mDefaultInstanceIdent; + + if (auto err = mCurrentInstance->mVersion.Assign(cDefaultVersion); !err.IsNone()) { + mCurrentInstance.reset(); + + return AOS_ERROR_WRAP(err); + } + + if (auto err = SaveInstanceInfo(*mCurrentInstance); !err.IsNone()) { + mCurrentInstance.reset(); + + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; + } + + mCurrentInstance.emplace(); + + if (auto err = LoadInstanceInfo(*mCurrentInstance); !err.IsNone()) { + mCurrentInstance.reset(); + + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error FileCopyRuntime::CreateRuntimeInfo() +{ + auto nodeInfo = std::make_unique(); + + if (auto err = mCurrentNodeInfoProvider->GetCurrentNodeInfo(*nodeInfo); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = utils::CreateRuntimeInfo(mRuntimeConfig.mType, *nodeInfo, cMaxNumInstances, mRuntimeInfo); + !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + mDefaultInstanceIdent.mType = UpdateItemTypeEnum::eComponent; + mDefaultInstanceIdent.mInstance = 0; + mDefaultInstanceIdent.mItemID = mRuntimeInfo.mRuntimeType; + mDefaultInstanceIdent.mSubjectID = nodeInfo->mNodeType; + mDefaultInstanceIdent.mPreinstalled = true; + + LOG_INF() << "Runtime info" << Log::Field("runtimeID", mRuntimeInfo.mRuntimeID) + << Log::Field("runtimeType", mRuntimeInfo.mRuntimeType) + << Log::Field("maxInstances", mRuntimeInfo.mMaxInstances); + + return ErrorEnum::eNone; +} + +void FileCopyRuntime::FillInstanceStatus( + const InstanceInfo& instanceInfo, InstanceStateEnum state, InstanceStatus& status) const +{ + static_cast(status) = static_cast(instanceInfo); + status.mState = state; + status.mVersion = instanceInfo.mVersion; + status.mRuntimeID = mRuntimeInfo.mRuntimeID; + status.mManifestDigest = instanceInfo.mManifestDigest; + status.mType = UpdateItemTypeEnum::eComponent; + status.mPreinstalled = instanceInfo.mPreinstalled; +} + +Error FileCopyRuntime::SaveInstanceInfo(const InstanceInfo& instance) const +{ + const auto path = std::filesystem::path(mComponentConfig.mRuntimeDir) / cInstalledInstanceFileName; + + LOG_DBG() << "Save instance info" << Log::Field("ident", static_cast(instance)) + << Log::Field("path", path.c_str()); + + std::ofstream file(path); + if (!file.is_open()) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eFailed, "can't store instance info")); + } + + auto json = Poco::makeShared(Poco::JSON_PRESERVE_KEY_ORDER); + + try { + json->set("itemId", instance.mItemID.CStr()); + json->set("subjectId", instance.mSubjectID.CStr()); + json->set("instance", instance.mInstance); + json->set("manifestDigest", instance.mManifestDigest.CStr()); + json->set("version", instance.mVersion.CStr()); + json->set("preinstalled", instance.mPreinstalled); + + json->stringify(file); + } catch (const std::exception& e) { + return AOS_ERROR_WRAP(common::utils::ToAosError(e)); + } + + return ErrorEnum::eNone; +} + +Error FileCopyRuntime::LoadInstanceInfo(InstanceInfo& instance) +{ + const auto path = std::filesystem::path(mComponentConfig.mRuntimeDir) / cInstalledInstanceFileName; + + LOG_DBG() << "Load instance info" << Log::Field("path", path.c_str()); + + instance.mType = UpdateItemTypeEnum::eComponent; + + std::ifstream file(path); + + if (!file.is_open()) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eNotFound, "can't open instance info file")); + } + + try { + auto parseResult = common::utils::ParseJson(file); + AOS_ERROR_CHECK_AND_THROW(parseResult.mError); + + auto jsonObject = common::utils::CaseInsensitiveObjectWrapper(parseResult.mValue); + + auto err = instance.mItemID.Assign(jsonObject.GetValue("itemId").c_str()); + AOS_ERROR_CHECK_AND_THROW(err); + + err = instance.mSubjectID.Assign(jsonObject.GetValue("subjectId").c_str()); + AOS_ERROR_CHECK_AND_THROW(err); + + instance.mInstance = jsonObject.GetValue("instance"); + + err = instance.mManifestDigest.Assign(jsonObject.GetValue("manifestDigest").c_str()); + AOS_ERROR_CHECK_AND_THROW(err); + + err = instance.mVersion.Assign(jsonObject.GetValue("version").c_str()); + AOS_ERROR_CHECK_AND_THROW(err); + + instance.mPreinstalled = jsonObject.GetValue("preinstalled"); + } catch (const std::exception& e) { + return AOS_ERROR_WRAP(common::utils::ToAosError(e)); + } + + return ErrorEnum::eNone; +} + +Error FileCopyRuntime::GetImageManifest(const String& digest, oci::ImageManifest& manifest) const +{ + StaticString blobPath; + + if (auto err = mItemInfoProvider->GetBlobPath(digest, blobPath); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto err = mOCISpec->LoadImageManifest(blobPath, manifest); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + return ErrorEnum::eNone; +} + +Error FileCopyRuntime::CopyImage(const oci::ImageManifest& manifest) const +{ + if (manifest.mLayers.Size() == 0) { + return AOS_ERROR_WRAP(Error(ErrorEnum::eInvalidArgument, "image manifest has no layers")); + } + + const auto& layer = manifest.mLayers[0]; + + StaticString imageArchivePath; + + if (auto err = mItemInfoProvider->GetBlobPath(layer.mDigest, imageArchivePath); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + LOG_DBG() << "Install component image" << Log::Field("digest", layer.mDigest) + << Log::Field("mediaType", layer.mMediaType) << Log::Field("src", imageArchivePath.CStr()); + + if (auto err = fs::MakeDirAll(mComponentConfig.mTargetPath.c_str()); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (layer.mMediaType == oci::cMediaTypeComponentFullTarGZip) { + if (auto err = fs::MakeDirAll(mComponentConfig.mTargetPath.c_str()); !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + + if (auto res + = common::utils::ExecCommand({"tar", "-xzf", imageArchivePath.CStr(), "-C", mComponentConfig.mTargetPath}); + !res.mError.IsNone()) { + return AOS_ERROR_WRAP(res.mError); + } + + return ErrorEnum::eNone; + } + + std::error_code ec; + + std::filesystem::copy_file(imageArchivePath.CStr(), mComponentConfig.mTargetPath.c_str(), + std::filesystem::copy_options::overwrite_existing, ec); + + if (ec.value() != 0) { + return AOS_ERROR_WRAP(Error(ec.value(), ec.message().c_str())); + } + + return ErrorEnum::eNone; +} + +} // namespace aos::sm::launcher diff --git a/src/sm/launcher/runtimes/filecopy/filecopy.hpp b/src/sm/launcher/runtimes/filecopy/filecopy.hpp new file mode 100644 index 000000000..92045b60f --- /dev/null +++ b/src/sm/launcher/runtimes/filecopy/filecopy.hpp @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#ifndef AOS_SM_LAUNCHER_RUNTIMES_FILECOPY_FILECOPY_HPP_ +#define AOS_SM_LAUNCHER_RUNTIMES_FILECOPY_FILECOPY_HPP_ + +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "config.hpp" + +namespace aos::sm::launcher { + +/** + * File copy runtime name. + */ +constexpr auto cRuntimeFileCopy = "filecopy"; + +/** + * File copy runtime implementation. + * + * Accepts an artifact (squashfs image or tar.gz archive) and places it into the component directory. + * Requests a reboot after installation so that initrd scripts can mount the image. + */ +class FileCopyRuntime : public RuntimeItf { +public: + /** + * Initializes file copy runtime. + * + * @param config runtime config. + * @param currentNodeInfoProvider current node info provider. + * @param itemInfoProvider item info provider. + * @param ociSpec OCI spec interface. + * @param statusReceiver instance status receiver. + * @param systemdConn systemd connection. + * @return Error. + */ + Error Init(const RuntimeConfig& config, iamclient::CurrentNodeInfoProviderItf& currentNodeInfoProvider, + imagemanager::ItemInfoProviderItf& itemInfoProvider, oci::OCISpecItf& ociSpec, + InstanceStatusReceiverItf& statusReceiver, sm::utils::SystemdConnItf& systemdConn); + + /** + * Starts runtime. + * + * @return Error. + */ + Error Start() override; + + /** + * Stops runtime. + * + * @return Error. + */ + Error Stop() override; + + /** + * Returns runtime info. + * + * @param[out] runtimeInfo runtime info. + * @return Error. + */ + Error GetRuntimeInfo(RuntimeInfo& runtimeInfo) const override; + + /** + * Start instance. + * + * @param instance instance to start. + * @param[out] status instance status. + * @return Error. + */ + Error StartInstance(const InstanceInfo& instance, InstanceStatus& status) override; + + /** + * Stop instance. + * + * @param instance instance to stop. + * @param[out] status instance status. + * @return Error. + */ + Error StopInstance(const InstanceIdent& instance, InstanceStatus& status) override; + + /** + * Reboots runtime. + * + * @return Error. + */ + Error Reboot() override; + + /** + * Returns instance monitoring data. + * + * @param instanceIdent instance ident. + * @param[out] monitoringData instance monitoring data. + * @return Error. + */ + Error GetInstanceMonitoringData( + const InstanceIdent& instanceIdent, monitoring::InstanceMonitoringData& monitoringData) override; + +private: + static constexpr auto cInstalledInstanceFileName = "installed_instance.json"; + static constexpr auto cImageFileName = "image.squashfs"; + static constexpr auto cDefaultVersion = "0.0.0"; + static constexpr auto cMaxNumInstances = 1; + + Error InitInstalledData(); + Error CreateRuntimeInfo(); + void FillInstanceStatus(const InstanceInfo& instanceInfo, InstanceStateEnum state, InstanceStatus& status) const; + Error SaveInstanceInfo(const InstanceInfo& instance) const; + Error LoadInstanceInfo(InstanceInfo& instance); + Error GetImageManifest(const String& digest, oci::ImageManifest& manifest) const; + Error CopyImage(const oci::ImageManifest& manifest) const; + + RuntimeConfig mRuntimeConfig; + FileCopyConfig mComponentConfig; + iamclient::CurrentNodeInfoProviderItf* mCurrentNodeInfoProvider {}; + imagemanager::ItemInfoProviderItf* mItemInfoProvider {}; + oci::OCISpecItf* mOCISpec {}; + InstanceStatusReceiverItf* mStatusReceiver {}; + utils::SystemdRebooter mRebooter; + InstanceIdent mDefaultInstanceIdent; + + mutable std::mutex mMutex; + std::optional mCurrentInstance; + RuntimeInfo mRuntimeInfo; +}; + +} // namespace aos::sm::launcher + +#endif diff --git a/src/sm/launcher/runtimes/filecopy/tests/CMakeLists.txt b/src/sm/launcher/runtimes/filecopy/tests/CMakeLists.txt new file mode 100644 index 000000000..460946dab --- /dev/null +++ b/src/sm/launcher/runtimes/filecopy/tests/CMakeLists.txt @@ -0,0 +1,33 @@ +# +# Copyright (C) 2026 EPAM Systems, Inc. +# +# SPDX-License-Identifier: Apache-2.0 +# + +set(TARGET_NAME filecopy_test) + +# ###################################################################################################################### +# Sources +# ###################################################################################################################### + +set(SOURCES config.cpp filecopy.cpp) + +# ###################################################################################################################### +# Libraries +# ###################################################################################################################### + +set(LIBRARIES aos::core::common::tests::utils aos::core::sm::tests::mocks aos::sm::runtimes::filecopy GTest::gmock_main) + +# ###################################################################################################################### +# Target +# ###################################################################################################################### + +add_test( + TARGET_NAME + ${TARGET_NAME} + LOG_MODULE + SOURCES + ${SOURCES} + LIBRARIES + ${LIBRARIES} +) diff --git a/src/sm/launcher/runtimes/filecopy/tests/config.cpp b/src/sm/launcher/runtimes/filecopy/tests/config.cpp new file mode 100644 index 000000000..7608d365f --- /dev/null +++ b/src/sm/launcher/runtimes/filecopy/tests/config.cpp @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include + +#include +#include + +#include + +using namespace testing; + +namespace aos::sm::launcher { + +class FileCopyConfigTest : public Test { +protected: + static void SetUpTestSuite() { tests::utils::InitLog(); } + + void SetUp() override + { + mConfig.isComponent = true; + mConfig.mPlugin = "filecopy"; + mConfig.mType = "mycomponent"; + mConfig.mWorkingDir = "/tmp"; + mConfig.mConfig = Poco::makeShared(); + } + + RuntimeConfig mConfig; + FileCopyConfig mComponentConfig; +}; + +/*********************************************************************************************************************** + * Tests + **********************************************************************************************************************/ + +TEST_F(FileCopyConfigTest, EmptyConfig) +{ + auto err = ParseConfig(mConfig, mComponentConfig); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mComponentConfig.mTargetPath, "/var/aos/components/mycomponent"); + EXPECT_EQ(mComponentConfig.mRuntimeDir, "/tmp/runtimes/mycomponent"); +} + +TEST_F(FileCopyConfigTest, ExplicitConfig) +{ + mConfig.mConfig->set("targetPath", "/opt/components/mycomponent"); + mConfig.mConfig->set("runtimeDir", "/var/lib/aos/sm/mycomponent"); + + auto err = ParseConfig(mConfig, mComponentConfig); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(mComponentConfig.mTargetPath, "/opt/components/mycomponent"); + EXPECT_EQ(mComponentConfig.mRuntimeDir, "/var/lib/aos/sm/mycomponent"); +} + +} // namespace aos::sm::launcher diff --git a/src/sm/launcher/runtimes/filecopy/tests/filecopy.cpp b/src/sm/launcher/runtimes/filecopy/tests/filecopy.cpp new file mode 100644 index 000000000..2a2186c4f --- /dev/null +++ b/src/sm/launcher/runtimes/filecopy/tests/filecopy.cpp @@ -0,0 +1,393 @@ +/* + * Copyright (C) 2026 EPAM Systems, Inc. + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include + +using namespace testing; + +namespace aos { + +std::ostream& operator<<(std::ostream& os, const String& str) +{ + return os << str.CStr(); +} + +std::ostream& operator<<(std::ostream& os, const InstanceStatus& info) +{ + return os << info.mItemID << ":" << info.mSubjectID << ":" << info.mInstance << ":" << info.mNodeID << ":" + << info.mRuntimeID << ":" << info.mManifestDigest << ":" << info.mVersion; +} + +} // namespace aos + +namespace aos::sm::launcher { + +namespace { + +const auto cTestDir = std::filesystem::path("testFileCopy"); +const auto cInstallDir = cTestDir / "install"; +const auto cMetadataDir = cTestDir / "metadata"; +const auto cComponentImage = cInstallDir / "image.squashfs"; +const auto cInstanceFile = cMetadataDir / "installed_instance.json"; +const auto cSourceImage = cTestDir / "source.squashfs"; +const auto cSourceTarGzip = cTestDir / "source.tar.gz"; +const auto cTarContentFile = cTestDir / "content.txt"; + +void CreateTarGzip(const std::filesystem::path& archive, const std::filesystem::path& contentFile) +{ + if (std::ofstream f(contentFile); f.is_open()) { + f << "component content"; + } else { + throw std::runtime_error("can't create content file for archive"); + } + + Poco::Process::Args args + = {"-czf", archive.string(), "-C", contentFile.parent_path().string(), contentFile.filename().string()}; + + if (int rc = Poco::Process::launch("tar", args).wait(); rc != 0) { + throw std::runtime_error("failed to create tar archive"); + } +} + +} // namespace + +class FileCopyRuntimeTest : public Test { +protected: + static void SetUpTestSuite() { tests::utils::InitLog(); } + + void SetUp() override + { + std::filesystem::remove_all(cTestDir); + std::filesystem::create_directories(cInstallDir); + std::filesystem::create_directories(cMetadataDir); + + mConfig.isComponent = true; + mConfig.mPlugin = "filecopy"; + mConfig.mType = "mycomponent"; + + { + auto json = Poco::makeShared(Poco::JSON_PRESERVE_KEY_ORDER); + + json->set("targetPath", cInstallDir.string()); + json->set("runtimeDir", cMetadataDir.string()); + + mConfig.mConfig = json; + } + + EXPECT_CALL(mCurrentNodeInfoProvider, GetCurrentNodeInfo(_)).WillRepeatedly(Invoke([](NodeInfo& nodeInfo) { + nodeInfo.mNodeID = "nodeId"; + nodeInfo.mNodeType = "nodeType"; + + nodeInfo.mCPUs.EmplaceBack(); + nodeInfo.mCPUs[0].mArchInfo.mArchitecture = "amd64"; + + nodeInfo.mOSInfo.mOS = "linux"; + + return ErrorEnum::eNone; + })); + + if (std::ofstream f(cSourceImage); f.is_open()) { + f << "fake squashfs content"; + } else { + throw std::runtime_error("can't create source image file"); + } + } + + std::string GetExpectedRuntimeID() const + { + return Poco::UUIDGenerator::defaultGenerator() + .createFromName(Poco::UUID::oid(), "mycomponent-nodeId") + .toString(); + } + + void WriteInstalledInstance(const std::string& itemId = "itemId", const std::string& subjectId = "subjectId", + const std::string& version = "1.0.0", const std::string& digest = "manifestDigest", bool preinstalled = false) + { + if (std::ofstream file(cInstanceFile); file.is_open()) { + file << R"({"itemId": ")" << itemId << R"(", "subjectId": ")" << subjectId + << R"(", "instance": 0, "manifestDigest": ")" << digest << R"(", "version": ")" << version + << R"(", "preinstalled": )" << (preinstalled ? "true" : "false") << "}"; + } else { + throw std::runtime_error("can't create instance file"); + } + } + + RuntimeConfig mConfig; + iamclient::CurrentNodeInfoProviderMock mCurrentNodeInfoProvider; + imagemanager::ItemInfoProviderMock mItemInfoProvider; + oci::OCISpecMock mOCISpec; + InstanceStatusReceiverStub mStatusReceiver; + sm::utils::SystemdConnMock mSystemdConn; + FileCopyRuntime mRuntime; +}; + +/*********************************************************************************************************************** + * Tests + **********************************************************************************************************************/ + +TEST_F(FileCopyRuntimeTest, GetRuntimeInfo) +{ + auto err + = mRuntime.Init(mConfig, mCurrentNodeInfoProvider, mItemInfoProvider, mOCISpec, mStatusReceiver, mSystemdConn); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mRuntime.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + auto info = std::make_unique(); + + err = mRuntime.GetRuntimeInfo(*info); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_STREQ(info->mRuntimeType.CStr(), "mycomponent"); + EXPECT_EQ(info->mMaxInstances, 1u); + EXPECT_STREQ(info->mRuntimeID.CStr(), GetExpectedRuntimeID().c_str()); + EXPECT_STREQ(info->mArchInfo.mArchitecture.CStr(), "amd64"); + EXPECT_STREQ(info->mOSInfo.mOS.CStr(), "linux"); + + err = mRuntime.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(FileCopyRuntimeTest, StartFreshPreinstalled) +{ + auto err + = mRuntime.Init(mConfig, mCurrentNodeInfoProvider, mItemInfoProvider, mOCISpec, mStatusReceiver, mSystemdConn); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mRuntime.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + std::vector statuses; + + err = mStatusReceiver.GetStatuses(statuses, std::chrono::seconds(1)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(statuses.size(), 1u); + EXPECT_EQ(statuses[0].mState, InstanceStateEnum::eActive); + EXPECT_STREQ(statuses[0].mItemID.CStr(), "mycomponent"); + EXPECT_STREQ(statuses[0].mSubjectID.CStr(), "nodeType"); + EXPECT_TRUE(statuses[0].mPreinstalled); + EXPECT_EQ(statuses[0].mInstance, 0u); + EXPECT_STREQ(statuses[0].mVersion.CStr(), "0.0.0"); + + EXPECT_TRUE(std::filesystem::exists(cInstanceFile)); + + err = mRuntime.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(FileCopyRuntimeTest, StartWithInstalledInstance) +{ + WriteInstalledInstance("itemId", "subjectId", "1.0.0", "digest1"); + + auto err + = mRuntime.Init(mConfig, mCurrentNodeInfoProvider, mItemInfoProvider, mOCISpec, mStatusReceiver, mSystemdConn); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mRuntime.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + std::vector statuses; + + err = mStatusReceiver.GetStatuses(statuses, std::chrono::seconds(1)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(statuses.size(), 1u); + EXPECT_EQ(statuses[0].mState, InstanceStateEnum::eActive); + EXPECT_STREQ(statuses[0].mItemID.CStr(), "itemId"); + EXPECT_STREQ(statuses[0].mSubjectID.CStr(), "subjectId"); + EXPECT_STREQ(statuses[0].mVersion.CStr(), "1.0.0"); + EXPECT_STREQ(statuses[0].mManifestDigest.CStr(), "digest1"); + + err = mRuntime.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(FileCopyRuntimeTest, StartInstance) +{ + const String cManifestDigest = "manifestDigest"; + const String cLayerDigest = "layerDigest"; + + auto err + = mRuntime.Init(mConfig, mCurrentNodeInfoProvider, mItemInfoProvider, mOCISpec, mStatusReceiver, mSystemdConn); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mRuntime.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(mOCISpec, LoadImageManifest(_, _)) + .WillOnce(Invoke([cLayerDigest](const String&, oci::ImageManifest& manifest) { + manifest.mLayers.Resize(1); + manifest.mLayers[0].mDigest = cLayerDigest; + + return ErrorEnum::eNone; + })); + + EXPECT_CALL(mItemInfoProvider, GetBlobPath(cManifestDigest, _)) + .WillOnce(DoAll(SetArgReferee<1>(String("manifestBlobPath")), Return(ErrorEnum::eNone))); + + EXPECT_CALL(mItemInfoProvider, GetBlobPath(cLayerDigest, _)) + .WillOnce(DoAll(SetArgReferee<1>(String(cSourceImage.c_str())), Return(ErrorEnum::eNone))); + + auto instanceInfo = std::make_unique(); + instanceInfo->mManifestDigest = cManifestDigest; + instanceInfo->mItemID = "itemId"; + instanceInfo->mSubjectID = "subjectId"; + instanceInfo->mVersion = "1.0.0"; + + auto status = std::make_unique(); + + err = mRuntime.StartInstance(*instanceInfo, *status); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(status->mState, InstanceStateEnum::eActivating); + EXPECT_TRUE(std::filesystem::exists(cComponentImage)); + EXPECT_TRUE(std::filesystem::exists(cInstanceFile)); + + std::vector> rebootRuntimes; + + err = mStatusReceiver.GetRuntimesToReboot(rebootRuntimes, std::chrono::seconds(1)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + ASSERT_EQ(rebootRuntimes.size(), 1u); + EXPECT_STREQ(rebootRuntimes[0].CStr(), GetExpectedRuntimeID().c_str()); + + err = mRuntime.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(FileCopyRuntimeTest, StartInstanceTarGzip) +{ + const String cManifestDigest = "manifestDigest"; + const String cLayerDigest = "layerDigest"; + + CreateTarGzip(cSourceTarGzip, cTarContentFile); + + auto err + = mRuntime.Init(mConfig, mCurrentNodeInfoProvider, mItemInfoProvider, mOCISpec, mStatusReceiver, mSystemdConn); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mRuntime.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(mOCISpec, LoadImageManifest(_, _)) + .WillOnce(Invoke([cLayerDigest](const String&, oci::ImageManifest& manifest) { + manifest.mLayers.Resize(1); + manifest.mLayers[0].mDigest = cLayerDigest; + manifest.mLayers[0].mMediaType = oci::cMediaTypeComponentFullTarGZip; + + return ErrorEnum::eNone; + })); + + EXPECT_CALL(mItemInfoProvider, GetBlobPath(cManifestDigest, _)) + .WillOnce(DoAll(SetArgReferee<1>(String("manifestBlobPath")), Return(ErrorEnum::eNone))); + + EXPECT_CALL(mItemInfoProvider, GetBlobPath(cLayerDigest, _)) + .WillOnce(DoAll(SetArgReferee<1>(String(cSourceTarGzip.c_str())), Return(ErrorEnum::eNone))); + + auto instanceInfo = std::make_unique(); + instanceInfo->mManifestDigest = cManifestDigest; + instanceInfo->mItemID = "itemId"; + instanceInfo->mSubjectID = "subjectId"; + instanceInfo->mVersion = "1.0.0"; + + auto status = std::make_unique(); + + err = mRuntime.StartInstance(*instanceInfo, *status); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(status->mState, InstanceStateEnum::eActivating); + EXPECT_TRUE(std::filesystem::exists(cInstallDir / cTarContentFile.filename())); + EXPECT_FALSE(std::filesystem::exists(cComponentImage)); + + err = mRuntime.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(FileCopyRuntimeTest, StartInstanceSameDigest) +{ + WriteInstalledInstance("itemId", "subjectId", "1.0.0", "digest1"); + + auto err + = mRuntime.Init(mConfig, mCurrentNodeInfoProvider, mItemInfoProvider, mOCISpec, mStatusReceiver, mSystemdConn); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mRuntime.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + std::vector startStatuses; + err = mStatusReceiver.GetStatuses(startStatuses, std::chrono::seconds(1)); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + auto instanceInfo = std::make_unique(); + instanceInfo->mItemID = "itemId"; + instanceInfo->mSubjectID = "subjectId"; + instanceInfo->mManifestDigest = "digest1"; + instanceInfo->mVersion = "1.0.0"; + + auto status = std::make_unique(); + + err = mRuntime.StartInstance(*instanceInfo, *status); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(status->mState, InstanceStateEnum::eActive); + + err = mRuntime.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +TEST_F(FileCopyRuntimeTest, StartInstanceLoadManifestFailed) +{ + auto err + = mRuntime.Init(mConfig, mCurrentNodeInfoProvider, mItemInfoProvider, mOCISpec, mStatusReceiver, mSystemdConn); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + err = mRuntime.Start(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); + + EXPECT_CALL(mItemInfoProvider, GetBlobPath(_, _)) + .WillOnce(DoAll(SetArgReferee<1>(String("manifestBlobPath")), Return(ErrorEnum::eNone))); + + EXPECT_CALL(mOCISpec, LoadImageManifest(_, _)).WillOnce(Return(ErrorEnum::eInvalidChecksum)); + + auto instanceInfo = std::make_unique(); + instanceInfo->mManifestDigest = "newDigest"; + instanceInfo->mItemID = "itemId"; + instanceInfo->mSubjectID = "subjectId"; + + auto status = std::make_unique(); + + err = mRuntime.StartInstance(*instanceInfo, *status); + ASSERT_TRUE(err.Is(ErrorEnum::eInvalidChecksum)) << tests::utils::ErrorToStr(err); + + EXPECT_EQ(status->mState, InstanceStateEnum::eFailed); + EXPECT_FALSE(std::filesystem::exists(cComponentImage)); + + err = mRuntime.Stop(); + ASSERT_TRUE(err.IsNone()) << tests::utils::ErrorToStr(err); +} + +} // namespace aos::sm::launcher From 7f0c27dacfd1dca3cd8a28ba7b67591823419f8a Mon Sep 17 00:00:00 2001 From: Mykhailo Lohvynenko Date: Mon, 8 Jun 2026 15:05:36 +0300 Subject: [PATCH 2/3] sm: launcher: initialize file copy runtime Signed-off-by: Mykhailo Lohvynenko --- src/sm/launcher/CMakeLists.txt | 10 ++++++++-- src/sm/launcher/runtimes.cpp | 11 +++++++++++ src/sm/launcher/tests/launcher.cpp | 4 ++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/sm/launcher/CMakeLists.txt b/src/sm/launcher/CMakeLists.txt index e3aa20d15..7e8a89843 100644 --- a/src/sm/launcher/CMakeLists.txt +++ b/src/sm/launcher/CMakeLists.txt @@ -16,8 +16,14 @@ set(SOURCES instanceidprovider.cpp runtimes.cpp) # Libraries # ###################################################################################################################### -set(LIBRARIES Poco::JSON aos::core::common::tools aos::sm::runtimes::container aos::sm::runtimes::boot - aos::sm::runtimes::rootfs aos::sm::utils +set(LIBRARIES + Poco::JSON + aos::core::common::tools + aos::sm::runtimes::container + aos::sm::runtimes::filecopy + aos::sm::runtimes::boot + aos::sm::runtimes::rootfs + aos::sm::utils ) # ###################################################################################################################### diff --git a/src/sm/launcher/runtimes.cpp b/src/sm/launcher/runtimes.cpp index 4de938c25..c350b653a 100644 --- a/src/sm/launcher/runtimes.cpp +++ b/src/sm/launcher/runtimes.cpp @@ -8,6 +8,7 @@ #include "runtimes.hpp" #include "runtimes/boot/boot.hpp" +#include "runtimes/filecopy/filecopy.hpp" #include "runtimes/rootfs/rootfs.hpp" namespace aos::sm::launcher { @@ -58,6 +59,16 @@ Error Runtimes::Init(const Config& config, iamclient::CurrentNodeInfoProviderItf return AOS_ERROR_WRAP(err); } + mRuntimes.emplace_back(std::move(runtime)); + } else if (runtimeConfig.mPlugin == cRuntimeFileCopy) { + auto runtime = std::make_unique(); + + if (auto err = runtime->Init( + runtimeConfig, currentNodeInfoProvider, itemInfoProvider, ociSpec, statusReceiver, systemdConn); + !err.IsNone()) { + return AOS_ERROR_WRAP(err); + } + mRuntimes.emplace_back(std::move(runtime)); } else { return AOS_ERROR_WRAP(Error(ErrorEnum::eNotSupported, "runtime is not supported")); diff --git a/src/sm/launcher/tests/launcher.cpp b/src/sm/launcher/tests/launcher.cpp index f2d6cbc25..961a86eb9 100644 --- a/src/sm/launcher/tests/launcher.cpp +++ b/src/sm/launcher/tests/launcher.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include @@ -107,6 +108,9 @@ TEST_F(RuntimesTest, InitRuntimes) RuntimeConfig {cRuntimeBoot, "aos-vm-boot", true, "", Poco::makeShared()}); config.mRuntimes.emplace_back( RuntimeConfig {cRuntimeRootfs, "aos-vm-rootfs", true, "", Poco::makeShared()}); + config.mRuntimes.emplace_back( + RuntimeConfig {cRuntimeFileCopy, "aos-vm-filecopy-test1", true, "", Poco::makeShared()}); + auto nodeInfo = std::make_unique(); CreateNodeInfo(*nodeInfo); From a6422581e66638b1081a7eb22cb4d5665ab30ec5 Mon Sep 17 00:00:00 2001 From: Mykhailo Lohvynenko Date: Tue, 9 Jun 2026 14:07:35 +0300 Subject: [PATCH 3/3] sm: utils: systemdconn: add retry logic for bus calls This patch resets dbus connection and retries the call if it fails with -ENOTCONN error, which indicates that the connection is lost. Signed-off-by: Mykhailo Lohvynenko --- src/sm/utils/systemdconn.cpp | 70 ++++++++++++++++++++++++++++++++---- src/sm/utils/systemdconn.hpp | 3 ++ 2 files changed, 67 insertions(+), 6 deletions(-) diff --git a/src/sm/utils/systemdconn.cpp b/src/sm/utils/systemdconn.cpp index 25283a3f1..3e431975b 100644 --- a/src/sm/utils/systemdconn.cpp +++ b/src/sm/utils/systemdconn.cpp @@ -64,7 +64,13 @@ RetWithError> SystemdConn::ListUnits() sd_bus_message* reply = nullptr; [[maybe_unused]] auto freeErr = DeferRelease(&error, sd_bus_error_free); - auto rv = sd_bus_call_method(mBus, cDestination, cPath, cInterface, "ListUnits", &error, &reply, nullptr); + auto [rv, connErr] = BusCallWithRetry([&] { + return sd_bus_call_method(mBus, cDestination, cPath, cInterface, "ListUnits", &error, &reply, nullptr); + }); + if (!connErr.IsNone()) { + return {{}, connErr}; + } + if (rv < 0) { return {{}, AOS_ERROR_WRAP(-rv)}; } @@ -131,7 +137,13 @@ RetWithError SystemdConn::GetUnitStatus(const std::string& name) sd_bus_message* reply = nullptr; - auto rv = sd_bus_call_method(mBus, cDestination, cPath, cInterface, "GetUnit", nullptr, &reply, "s", name.c_str()); + auto [rv, connErr] = BusCallWithRetry([&] { + return sd_bus_call_method(mBus, cDestination, cPath, cInterface, "GetUnit", nullptr, &reply, "s", name.c_str()); + }); + if (!connErr.IsNone()) { + return {{}, connErr}; + } + if (rv < 0) { return {{}, AOS_ERROR_WRAP(-rv)}; } @@ -182,13 +194,48 @@ RetWithError SystemdConn::GetUnitStatus(const std::string& name) return {status, ErrorEnum::eNone}; } +RetWithError SystemdConn::BusCallWithRetry(std::function func) +{ + auto rv = func(); + if (rv != -ENOTCONN) { + return {rv, ErrorEnum::eNone}; + } + + if (auto err = Reconnect(); !err.IsNone()) { + return {rv, err}; + } + + rv = func(); + return {rv, ErrorEnum::eNone}; +} + +Error SystemdConn::Reconnect() +{ + LOG_WRN() << "D-Bus connection lost, reconnecting"; + + sd_bus_unref(mBus); + mBus = nullptr; + + auto rv = sd_bus_open_system(&mBus); + if (rv < 0) { + return AOS_ERROR_WRAP(-rv); + } + + return ErrorEnum::eNone; +} + Error SystemdConn::StartUnit(const std::string& name, const std::string& mode, const Duration& timeout) { std::lock_guard lock {mMutex}; sd_bus_slot* slot = nullptr; - auto rv = sd_bus_match_signal(mBus, &slot, nullptr, cPath, cInterface, "JobRemoved", nullptr, nullptr); + auto [rv, connErr] = BusCallWithRetry( + [&] { return sd_bus_match_signal(mBus, &slot, nullptr, cPath, cInterface, "JobRemoved", nullptr, nullptr); }); + if (!connErr.IsNone()) { + return connErr; + } + if (rv < 0) { return AOS_ERROR_WRAP(-rv); } @@ -219,7 +266,12 @@ Error SystemdConn::StopUnit(const std::string& name, const std::string& mode, co sd_bus_slot* slot = nullptr; - auto rv = sd_bus_match_signal(mBus, &slot, nullptr, cPath, cInterface, "JobRemoved", nullptr, nullptr); + auto [rv, connErr] = BusCallWithRetry( + [&] { return sd_bus_match_signal(mBus, &slot, nullptr, cPath, cInterface, "JobRemoved", nullptr, nullptr); }); + if (!connErr.IsNone()) { + return connErr; + } + if (rv < 0) { return AOS_ERROR_WRAP(-rv); } @@ -258,8 +310,14 @@ Error SystemdConn::ResetFailedUnit(const std::string& name) sd_bus_message* reply = nullptr; [[maybe_unused]] auto freeErr = DeferRelease(&error, sd_bus_error_free); - auto rv = sd_bus_call_method( - mBus, cDestination, cPath, cInterface, "ResetFailedUnit", &error, &reply, "s", name.c_str()); + auto [rv, connErr] = BusCallWithRetry([&] { + return sd_bus_call_method( + mBus, cDestination, cPath, cInterface, "ResetFailedUnit", &error, &reply, "s", name.c_str()); + }); + if (!connErr.IsNone()) { + return connErr; + } + if (rv < 0) { if (sd_bus_error_has_name(&error, cNoSuchUnitErr)) { return ErrorEnum::eNotFound; diff --git a/src/sm/utils/systemdconn.hpp b/src/sm/utils/systemdconn.hpp index b89e97c7e..71ba82133 100644 --- a/src/sm/utils/systemdconn.hpp +++ b/src/sm/utils/systemdconn.hpp @@ -7,6 +7,7 @@ #ifndef AOS_SM_UTILS_SYSTEMDCONN_HPP_ #define AOS_SM_UTILS_SYSTEMDCONN_HPP_ +#include #include #include #include @@ -79,6 +80,8 @@ class SystemdConn : public SystemdConnItf { static constexpr auto cInterface = "org.freedesktop.systemd1.Manager"; static constexpr auto cNoSuchUnitErr = "org.freedesktop.systemd1.NoSuchUnit"; + RetWithError BusCallWithRetry(std::function func); + Error Reconnect(); Error WaitForJobCompletion(const char* jobPath, const Duration& timeout); std::pair HandleJobRemove(sd_bus_message* m, const char* jobPath); Optional GetExitCode(const char* serviceName);