diff --git a/include/appFs.hpp b/include/appFs.hpp new file mode 100644 index 0000000..bf99a2d --- /dev/null +++ b/include/appFs.hpp @@ -0,0 +1,71 @@ +/** + * @file + * @brief Definition of the AppFsFuse class for managing FUSE filesystem + * @author Pavel Siska + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#pragma once + +#include +#include +#include +#include +#include + +namespace telemetry::appFs { + +/** + * @brief The AppFsFuse class for managing FUSE filesystem. + */ +class AppFsFuse { +public: + /** + * @brief Constructor for AppFsFuse. + * + * @param rootDirectory A shared pointer to the telemetry root directory. + * + * @throws std::runtime_error if rootDirectory is nullptr. + */ + AppFsFuse(std::shared_ptr rootDirectory); + + /** + * @brief Sets up and mounts the FUSE filesystem. + * + * This method sets up the FUSE filesystem with the provided mount point directory path. + * It creates a new thread to run the FUSE event loop. + * + * @param mountPoint The mount point directory path. + * @param tryToUnmountOnStart Whether to attempt unmounting the mount point if it's already + * mounted. + * + * @throws std::runtime_error if setup and mount process fails. + */ + void setupAndMount(const std::string& mountPoint, bool tryToUnmountOnStart = true); + + /** + * @brief Unmounts the FUSE filesystem. + * + * This method unmounts the FUSE filesystem. It interrupts the FUSE event loop. + * + * @note This method is also called automatically when the AppFsFuse instance is destroyed. + */ + void unmount(); + + /** + * @brief Destructor for AppFsFuse. + * + * Terminates the thread running the loop. + * + * @note This method unmounts the FUSE filesystem. + */ + ~AppFsFuse(); + +private: + std::unique_ptr m_fuse {nullptr, &fuse_destroy}; + std::shared_ptr m_rootDirectory; + std::thread m_fuseThread; +}; + +} // namespace telemetry::appFs diff --git a/pkg/rpm/telemetry.spec.in b/pkg/rpm/telemetry.spec.in index aa82ce4..4920e90 100644 --- a/pkg/rpm/telemetry.spec.in +++ b/pkg/rpm/telemetry.spec.in @@ -33,5 +33,7 @@ telemetry data in your program. %{_libdir}/libtelemetry.so %{_includedir}/telemetry.hpp %{_includedir}/telemetry/*.hpp +%{_libdir}/libappFs.so +%{_includedir}/appFs.hpp %changelog diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index a837604..9aae901 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1 +1,2 @@ add_subdirectory(telemetry) +add_subdirectory(appFs) diff --git a/src/appFs/CMakeLists.txt b/src/appFs/CMakeLists.txt new file mode 100644 index 0000000..cc73b06 --- /dev/null +++ b/src/appFs/CMakeLists.txt @@ -0,0 +1,20 @@ +list(APPEND APPFS_SOURCE_FILES + appFs.cpp +) + +if (TELEMETRY_BUILD_SHARED) + add_library(appFs SHARED ${APPFS_SOURCE_FILES}) +else() + add_library(appFs STATIC ${APPFS_SOURCE_FILES}) +endif() + +add_library(telemetry::appFs ALIAS appFs) + +target_compile_definitions(appFs PUBLIC FUSE_USE_VERSION=30 _FILE_OFFSET_BITS=64) +target_link_libraries(appFs PUBLIC telemetry PkgConfig::fuse) +target_include_directories(appFs PUBLIC ${CMAKE_SOURCE_DIR}/include) + +if (TELEMETRY_INSTALL_TARGETS) + install(TARGETS appFs LIBRARY DESTINATION ${INSTALL_DIR_LIB}) + install(DIRECTORY ${CMAKE_SOURCE_DIR}/include/ DESTINATION ${INSTALL_DIR_INCLUDE}) +endif() diff --git a/src/appFs/appFs.cpp b/src/appFs/appFs.cpp new file mode 100644 index 0000000..58354a9 --- /dev/null +++ b/src/appFs/appFs.cpp @@ -0,0 +1,306 @@ +/** + * @file + * @brief Implementation of FUSE callback functions for filesystem operations + * @author Pavel Siska + * + * SPDX-License-Identifier: BSD-3-Clause + */ + +#include + +#include +#include +#include +#include +#include +#include + +namespace telemetry::appFs { + +static std::string fileContentToString(const std::shared_ptr& file) +{ + const Content content = file->read(); + return contentToString(content) + "\n"; +} + +static off_t getMaxFileSize(const std::shared_ptr& file) +{ + const size_t blockSize = BUFSIZ; + + if (!file->hasRead()) { + return blockSize; + } + + constexpr double requiredBlockEmptyCapacityMultiplier = 0.5; + constexpr size_t requiredCapacity = blockSize * requiredBlockEmptyCapacityMultiplier; + + const size_t contentSize = fileContentToString(file).size(); + const size_t blockSizeMultiplier = (contentSize + requiredCapacity) / blockSize + 1; + + return static_cast(blockSizeMultiplier * blockSize); +} + +static void setFileAttr(const std::shared_ptr& file, struct stat* stbuf) +{ + stbuf->st_mode = S_IFREG; + + if (file->hasRead()) { + const mode_t readMode = 0444; + stbuf->st_mode |= readMode; + } + + if (file->hasClear()) { + const mode_t writeMode = 0222; + stbuf->st_mode |= writeMode; + } + + stbuf->st_nlink = 1; + stbuf->st_size = getMaxFileSize(file); + stbuf->st_mtime = time(nullptr); +} + +static void setDirectoryAttr(struct stat* stbuf) +{ + const mode_t readExecuteMode = 0555; + stbuf->st_mode = S_IFDIR | readExecuteMode; + stbuf->st_nlink = 2; + stbuf->st_mtime = time(nullptr); +} + +std::shared_ptr getRootDirectory() +{ + return *reinterpret_cast*>(fuse_get_context()->private_data); +} + +static int getAttrCallback(const char* path, struct stat* stbuf, struct fuse_file_info* fileInfo) +{ + (void) fileInfo; + + std::memset(stbuf, 0, sizeof(struct stat)); + + const std::shared_ptr rootDirectory = getRootDirectory(); + auto node = utils::getNodeFromPath(rootDirectory, path); + + if (utils::isFile(node)) { + setFileAttr(std::dynamic_pointer_cast(node), stbuf); + return 0; + } + + if (utils::isDirectory(node)) { + setDirectoryAttr(stbuf); + return 0; + } + + return -ENOENT; +} + +static int readDirCallback( + const char* path, + void* buffer, + fuse_fill_dir_t filler, + off_t offset, + struct fuse_file_info* fileInfo, + enum fuse_readdir_flags flags) +{ + (void) offset; + (void) fileInfo; + (void) flags; + + const fuse_fill_dir_flags dirFlag = {}; + + const std::shared_ptr rootDirectory = getRootDirectory(); + auto node = utils::getNodeFromPath(rootDirectory, path); + + if (!utils::isDirectory(node)) { + return -ENOENT; + } + + filler(buffer, ".", nullptr, 0, dirFlag); + filler(buffer, "..", nullptr, 0, dirFlag); + + auto directory = std::dynamic_pointer_cast(node); + for (const auto& entry : directory->listEntries()) { + filler(buffer, entry.c_str(), nullptr, 0, dirFlag); + } + + return 0; +} + +static int readFile(const std::shared_ptr& file, char* buffer, size_t size, off_t offset) +{ + if (!file->hasRead()) { + return -ENOTSUP; + } + + const Content content = file->read(); + const std::string contentAsString = contentToString(content) + "\n"; + const size_t contentSize = contentAsString.size(); + + if (static_cast(offset) >= contentSize) { + return 0; + } + + const size_t length = std::min(size, contentSize - offset); + std::memcpy(buffer, contentAsString.data() + offset, length); + + return static_cast(length); +} + +static int readCallback( + const char* path, + char* buffer, + size_t size, + off_t offset, + struct fuse_file_info* fileInfo) +{ + (void) fileInfo; + + const std::shared_ptr rootDirectory = getRootDirectory(); + auto node = utils::getNodeFromPath(rootDirectory, path); + + if (!utils::isFile(node)) { + return -ENOENT; + } + + return readFile(std::dynamic_pointer_cast(node), buffer, size, offset); +} + +static int writeCallback( + // NOLINTBEGIN + const char* path, + const char* buffer, + size_t size, + off_t offset, + // NOLINTEND + struct fuse_file_info* fileInfo) +{ + (void) buffer; + (void) offset; + (void) fileInfo; + + const std::shared_ptr rootDirectory = getRootDirectory(); + auto node = utils::getNodeFromPath(rootDirectory, path); + + if (!utils::isFile(node)) { + return -ENOENT; + } + + auto file = std::dynamic_pointer_cast(node); + + if (!file->hasClear()) { + return -ENOTSUP; + } + + file->clear(); + + return static_cast(size); +} + +static void setFuseOperations(struct fuse_operations* fuseOps) +{ + fuseOps->getattr = getAttrCallback; + fuseOps->readdir = readDirCallback; + fuseOps->read = readCallback; + fuseOps->write = writeCallback; +} + +static void runFuseLoop(struct fuse* fuse) +{ + try { + const int ret = fuse_loop(fuse); + if (ret < 0) { + throw std::runtime_error("fuse_loop() has failed. AppFs is not running..."); + } + } catch (const std::exception& ex) { + std::cerr << ex.what() << std::endl; + } +} + +static void fillFuseArgs(struct fuse_args* fuseArgs) +{ + const std::string fuseUID = "uid=" + std::to_string(getuid()); + const std::string fuseGID = "gid=" + std::to_string(getgid()); + + fuse_opt_add_arg(fuseArgs, "appfs"); + fuse_opt_add_arg(fuseArgs, "-o"); + fuse_opt_add_arg(fuseArgs, fuseUID.c_str()); + fuse_opt_add_arg(fuseArgs, "-o"); + fuse_opt_add_arg(fuseArgs, fuseGID.c_str()); + fuse_opt_add_arg(fuseArgs, "-o"); + fuse_opt_add_arg(fuseArgs, "allow_other"); + fuse_opt_add_arg(fuseArgs, "-o"); + fuse_opt_add_arg(fuseArgs, "attr_timeout=0"); +} + +class FuseArgs { +public: + FuseArgs() + : m_fuseArgs(FUSE_ARGS_INIT(0, nullptr)) + { + } + + struct fuse_args* get() { return &m_fuseArgs; } + + ~FuseArgs() { fuse_opt_free_args(&m_fuseArgs); } + +private: + struct fuse_args m_fuseArgs; +}; + +AppFsFuse::AppFsFuse(std::shared_ptr rootDirectory) +{ + m_rootDirectory = std::move(rootDirectory); + if (m_rootDirectory == nullptr) { + throw std::runtime_error("Root directory is not set."); + } +} + +void AppFsFuse::setupAndMount(const std::string& mountPoint, bool tryToUnmountOnStart) +{ + FuseArgs fuseArgs; + fillFuseArgs(fuseArgs.get()); + + struct fuse_operations fuseOps = {}; + setFuseOperations(&fuseOps); + + m_fuse.reset(fuse_new(fuseArgs.get(), &fuseOps, sizeof(fuseOps), (void*) &m_rootDirectory)); + if (m_fuse == nullptr) { + throw std::runtime_error("fuse_new() has failed."); + } + + int ret = fuse_mount(m_fuse.get(), mountPoint.c_str()); + if (ret < 0 && tryToUnmountOnStart) { + ret = umount2(mountPoint.c_str(), MNT_FORCE | UMOUNT_NOFOLLOW); + if (ret < 0) { + throw std::runtime_error("umount of " + mountPoint + " has failed."); + } + + ret = fuse_mount(m_fuse.get(), mountPoint.c_str()); + if (ret < 0) { + throw std::runtime_error("fuse_mount() has failed again."); + } + } + + if (ret < 0) { + throw std::runtime_error("fuse_mount() has failed."); + } + + m_fuseThread = std::thread([&]() { runFuseLoop(m_fuse.get()); }); +} + +void AppFsFuse::unmount() +{ + if (m_fuse != nullptr) { + fuse_unmount(m_fuse.get()); + } +} + +AppFsFuse::~AppFsFuse() +{ + unmount(); + if (m_fuseThread.joinable()) { + m_fuseThread.join(); + } +} + +} // namespace telemetry::appFs