From d113e4284674d112aff0744fe734581bd3fc4abf Mon Sep 17 00:00:00 2001 From: Krzysztof Grobelny Date: Fri, 3 Jul 2020 12:35:09 +0200 Subject: Fixing multiple problems with state machine in virtual media - Previously machine did not handle AnyEvent correctly, implementation in BaseState was always run - Changing from ActiveState to ReadyState was bugged, previously only one of event SubprocessStopped or UdevNotification caused state change when it is required to wait for both - Introduced longer timer when waiting for ReadyState during Eject and ActiveState during Inject, because ndbkit can timeout during Eject and it is required to complete before next inject can success. - Added event notification when process is terminated - Added resourcess classes to handle deletion and notifications Signed-off-by: Krzysztof Grobelny Signed-off-by: Karol Wachowski Change-Id: Ie914e650c2f15bd73cdc87582ea77a94997a3472 Signed-off-by: Karol Wachowski --- CMakeLists.txt | 3 +- src/configuration.hpp | 5 +- src/events.hpp | 87 ++ src/interfaces/mount_point_state_machine.hpp | 40 + src/resources.cpp | 48 ++ src/resources.hpp | 162 ++++ src/smb.hpp | 36 +- src/state/activating_state.cpp | 336 ++++++++ src/state/activating_state.hpp | 59 ++ src/state/active_state.hpp | 100 +++ src/state/basic_state.hpp | 67 ++ src/state/deactivating_state.hpp | 83 ++ src/state/initial_state.hpp | 313 ++++++++ src/state/ready_state.hpp | 58 ++ src/state_machine.hpp | 1091 ++------------------------ src/system.hpp | 107 +-- src/utils.hpp | 1 + 17 files changed, 1466 insertions(+), 1130 deletions(-) create mode 100644 src/events.hpp create mode 100644 src/interfaces/mount_point_state_machine.hpp create mode 100644 src/resources.cpp create mode 100644 src/resources.hpp create mode 100644 src/state/activating_state.cpp create mode 100644 src/state/activating_state.hpp create mode 100644 src/state/active_state.hpp create mode 100644 src/state/basic_state.hpp create mode 100644 src/state/deactivating_state.hpp create mode 100644 src/state/initial_state.hpp create mode 100644 src/state/ready_state.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d5e59f2..71e82ac 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,7 +117,8 @@ add_definitions(-DBOOST_NO_TYPEID) add_definitions(-DBOOST_ASIO_DISABLE_THREADS) # Define source files -set(SRC_FILES src/main.cpp) +include_directories(src) +set(SRC_FILES src/main.cpp src/state/activating_state.cpp src/resources.cpp) # Executables add_executable(virtual-media ${SRC_FILES} ${HEADER_FILES}) diff --git a/src/configuration.hpp b/src/configuration.hpp index 68606cc..25f9855 100644 --- a/src/configuration.hpp +++ b/src/configuration.hpp @@ -29,6 +29,8 @@ class Configuration struct MountPoint { + static constexpr int defaultTimeout = 30; + NBDDevice nbdDevice; std::string unixSocket; std::string endPointId; @@ -39,7 +41,8 @@ class Configuration static std::vector toArgs(const MountPoint& mp) { - const auto timeout = std::to_string(mp.timeout.value_or(30)); + const auto timeout = + std::to_string(mp.timeout.value_or(defaultTimeout)); std::vector args = { "-t", timeout, "-u", mp.unixSocket, mp.nbdDevice.to_path(), "-n"}; diff --git a/src/events.hpp b/src/events.hpp new file mode 100644 index 0000000..7a6adde --- /dev/null +++ b/src/events.hpp @@ -0,0 +1,87 @@ +#pragma once + +#include "interfaces/mount_point_state_machine.hpp" + +#include +#include + +struct BasicEvent +{ + BasicEvent(const char* eventName) : eventName(eventName) + { + } + + virtual ~BasicEvent() = default; + + const char* eventName; +}; + +struct RegisterDbusEvent : public BasicEvent +{ + RegisterDbusEvent( + std::shared_ptr bus, + std::shared_ptr objServer) : + BasicEvent(__FUNCTION__), + bus(bus), objServer(objServer), + emitMountEvent(std::move(emitMountEvent)) + { + } + + std::shared_ptr bus; + std::shared_ptr objServer; + std::function emitMountEvent; +}; + +struct MountEvent : public BasicEvent +{ + explicit MountEvent( + std::optional target) : + BasicEvent(__FUNCTION__), + target(std::move(target)) + { + } + + MountEvent(const MountEvent&) = delete; + MountEvent(MountEvent&& other) : + BasicEvent(__FUNCTION__), target(std::move(other.target)) + { + other.target = std::nullopt; + } + + MountEvent& operator=(const MountEvent&) = delete; + MountEvent& operator=(MountEvent&& other) + { + target = std::nullopt; + std::swap(target, other.target); + return *this; + } + + std::optional target; +}; + +struct UnmountEvent : public BasicEvent +{ + UnmountEvent() : BasicEvent(__FUNCTION__) + { + } +}; + +struct SubprocessStoppedEvent : public BasicEvent +{ + SubprocessStoppedEvent() : BasicEvent(__FUNCTION__) + { + } +}; + +struct UdevStateChangeEvent : public BasicEvent +{ + explicit UdevStateChangeEvent(const StateChange& devState) : + BasicEvent(__FUNCTION__), devState{devState} + { + } + + StateChange devState; +}; + +using Event = std::variant; diff --git a/src/interfaces/mount_point_state_machine.hpp b/src/interfaces/mount_point_state_machine.hpp new file mode 100644 index 0000000..db521fb --- /dev/null +++ b/src/interfaces/mount_point_state_machine.hpp @@ -0,0 +1,40 @@ +#pragma once + +#include "configuration.hpp" +#include "resources.hpp" + +struct BasicState; + +namespace interfaces +{ + +struct MountPointStateMachine +{ + struct Target + { + std::string imgUrl; + bool rw; + std::unique_ptr mountPoint; + std::unique_ptr credentials; + }; + + virtual ~MountPointStateMachine() = default; + + virtual std::string_view getName() const = 0; + virtual Configuration::MountPoint& getConfig() = 0; + virtual std::optional& getTarget() = 0; + virtual BasicState& getState() = 0; + virtual int& getExitCode() = 0; + virtual boost::asio::io_context& getIoc() = 0; + + virtual void emitRegisterDBusEvent( + std::shared_ptr bus, + std::shared_ptr objServer) = 0; + virtual void emitMountEvent(std::optional) = 0; + virtual void emitUnmountEvent() = 0; + virtual void emitSubprocessStoppedEvent() = 0; + virtual void emitUdevStateChangeEvent(const NBDDevice& dev, + StateChange devState) = 0; +}; + +} // namespace interfaces diff --git a/src/resources.cpp b/src/resources.cpp new file mode 100644 index 0000000..a501bba --- /dev/null +++ b/src/resources.cpp @@ -0,0 +1,48 @@ +#include "resources.hpp" + +#include "interfaces/mount_point_state_machine.hpp" + +namespace resource +{ + +Process::~Process() +{ + if (spawned) + { + process->stop([& machine = *machine] { + boost::asio::post(machine.getIoc(), [&machine]() { + machine.emitSubprocessStoppedEvent(); + }); + }); + } +} + +Gadget::Gadget(interfaces::MountPointStateMachine& machine, + StateChange devState) : + machine(&machine) +{ + status = UsbGadget::configure( + std::string(machine.getName()), machine.getConfig().nbdDevice, devState, + machine.getTarget() ? machine.getTarget()->rw : false); +} + +Gadget::~Gadget() +{ + int32_t ret = UsbGadget::configure(std::string(machine->getName()), + machine->getConfig().nbdDevice, + StateChange::removed); + if (ret != 0) + { + // This shouldn't ever happen, perhaps best is to restart + // app + LogMsg(Logger::Critical, machine->getName(), + " Some serious failure happened!"); + + boost::asio::post(machine->getIoc(), [& machine = *machine]() { + machine.emitUdevStateChangeEvent(machine.getConfig().nbdDevice, + StateChange::unknown); + }); + } +} + +} // namespace resource diff --git a/src/resources.hpp b/src/resources.hpp new file mode 100644 index 0000000..b211d55 --- /dev/null +++ b/src/resources.hpp @@ -0,0 +1,162 @@ +#pragma once + +#include "smb.hpp" +#include "system.hpp" + +namespace interfaces +{ +struct MountPointStateMachine; +} + +namespace resource +{ + +class Error : public std::runtime_error +{ + public: + Error(std::errc errorCode, std::string message) : + std::runtime_error(message), errorCode(errorCode) + { + } + + const std::errc errorCode; +}; + +class Directory +{ + public: + Directory() = delete; + Directory(const Directory&) = delete; + Directory(Directory&& other) = delete; + Directory& operator=(const Directory&) = delete; + Directory& operator=(Directory&& other) = delete; + + explicit Directory(std::filesystem::path name) : + path(std::filesystem::temp_directory_path() / name) + { + std::error_code ec; + + if (!std::filesystem::create_directory(path, ec)) + { + LogMsg(Logger::Error, ec, + " : Unable to create mount directory: ", path); + throw Error(std::errc::io_error, + "Failed to create mount directory"); + } + } + + ~Directory() + { + std::error_code ec; + + if (!std::filesystem::remove(path, ec)) + { + LogMsg(Logger::Error, ec, " : Unable to remove directory ", path); + } + } + + std::filesystem::path getPath() const + { + return path; + } + + private: + std::filesystem::path path; +}; + +class Mount +{ + public: + Mount() = delete; + Mount(const Mount&) = delete; + Mount(Mount&& other) = delete; + Mount& operator=(const Mount&) = delete; + Mount& operator=(Mount&& other) = delete; + + explicit Mount( + std::unique_ptr directory, SmbShare& smb, + const std::filesystem::path& remote, bool rw, + const std::unique_ptr& credentials) : + directory(std::move(directory)) + { + if (!smb.mount(remote, rw, credentials)) + { + throw Error(std::errc::invalid_argument, + "Failed to mount CIFS share"); + } + } + + ~Mount() + { + if (int result = ::umount(directory->getPath().string().c_str())) + { + LogMsg(Logger::Error, result, " : Unable to unmout directory ", + directory->getPath()); + } + } + + std::filesystem::path getPath() const + { + return directory->getPath(); + } + + private: + std::unique_ptr directory; +}; + +class Process +{ + public: + Process() = delete; + Process(const Process&) = delete; + Process(Process&& other) = delete; + Process& operator=(const Process&) = delete; + Process& operator=(Process&& other) = delete; + Process(interfaces::MountPointStateMachine& machine, + std::shared_ptr<::Process> process) : + machine(&machine), + process(std::move(process)) + { + if (!this->process) + { + throw Error(std::errc::io_error, "Failed to create process"); + } + } + + ~Process(); + + template + auto spawn(Args&&... args) + { + if (process->spawn(std::forward(args)...)) + { + spawned = true; + return true; + } + return false; + } + + private: + interfaces::MountPointStateMachine* machine; + std::shared_ptr<::Process> process = nullptr; + bool spawned = false; +}; + +class Gadget +{ + public: + Gadget() = delete; + Gadget& operator=(const Gadget&) = delete; + Gadget& operator=(Gadget&& other) = delete; + Gadget(const Gadget&) = delete; + Gadget(Gadget&& other) = delete; + + Gadget(interfaces::MountPointStateMachine& machine, StateChange devState); + ~Gadget(); + + private: + interfaces::MountPointStateMachine* machine; + int32_t status; +}; + +} // namespace resource diff --git a/src/smb.hpp b/src/smb.hpp index 62c3a44..4860d37 100644 --- a/src/smb.hpp +++ b/src/smb.hpp @@ -58,40 +58,6 @@ class SmbShare return true; } - static std::optional createMountDir(const fs::path& name) - { - auto destPath = fs::temp_directory_path() / name; - std::error_code ec; - - if (fs::create_directory(destPath, ec)) - { - return destPath; - } - - LogMsg(Logger::Error, ec, - " : Unable to create mount directory: ", destPath); - return {}; - } - - static void unmount(const fs::path& mountDir) - { - int result; - std::error_code ec; - - result = ::umount(mountDir.string().c_str()); - if (result) - { - LogMsg(Logger::Error, result, " : Unable to unmout directory ", - mountDir); - } - - if (!fs::remove_all(mountDir, ec)) - { - LogMsg(Logger::Error, ec, " : Unable to remove mount directory ", - mountDir); - } - } - private: std::string mountDir; -}; \ No newline at end of file +}; diff --git a/src/state/activating_state.cpp b/src/state/activating_state.cpp new file mode 100644 index 0000000..6192711 --- /dev/null +++ b/src/state/activating_state.cpp @@ -0,0 +1,336 @@ +#include "activating_state.hpp" + +#include "active_state.hpp" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +ActivatingState::ActivatingState(interfaces::MountPointStateMachine& machine) : + BasicStateT(machine){}; + +std::unique_ptr ActivatingState::onEnter() +{ + // Reset previous exit code + machine.getExitCode() = -1; + + if (machine.getConfig().mode == Configuration::Mode::proxy) + { + return activateProxyMode(); + } + return activateLegacyMode(); +} + +std::unique_ptr + ActivatingState::handleEvent(UdevStateChangeEvent event) +{ + if (event.devState == StateChange::inserted) + { + gadget = std::make_unique(machine, event.devState); + + if (gadget) + { + return std::make_unique(machine, std::move(process), + std::move(gadget)); + } + + return std::make_unique(machine, + std::errc::device_or_resource_busy, + "Unable to configure gadget"); + } + + return std::make_unique( + machine, std::errc::operation_not_supported, + "Unexpected udev event: " + static_cast(event.devState)); +} + +std::unique_ptr + ActivatingState::handleEvent(SubprocessStoppedEvent event) +{ + LogMsg(Logger::Error, "Process ended prematurely"); + return std::make_unique(machine); +} + +std::unique_ptr ActivatingState::activateProxyMode() +{ + process = std::make_unique( + machine, std::make_shared<::Process>( + machine.getIoc(), machine.getName(), + "/usr/sbin/nbd-client", machine.getConfig().nbdDevice)); + + if (!process->spawn(Configuration::MountPoint::toArgs(machine.getConfig()), + [& machine = machine](int exitCode, bool isReady) { + LogMsg(Logger::Info, machine.getName(), + " process ended."); + machine.getExitCode() = exitCode; + machine.emitSubprocessStoppedEvent(); + })) + { + return std::make_unique( + machine, std::errc::operation_canceled, "Failed to spawn process"); + } + + return nullptr; +} + +std::unique_ptr ActivatingState::activateLegacyMode() +{ + LogMsg(Logger::Debug, machine.getName(), + " Mount requested on address: ", machine.getTarget()->imgUrl, + " ; RW: ", machine.getTarget()->rw); + + if (isCifsUrl(machine.getTarget()->imgUrl)) + { + return mountSmbShare(); + } + else if (isHttpsUrl(machine.getTarget()->imgUrl)) + { + return mountHttpsShare(); + } + + return std::make_unique(machine, std::errc::invalid_argument, + "URL not recognized"); +} + +std::unique_ptr ActivatingState::mountSmbShare() +{ + try + { + auto mountDir = + std::make_unique(machine.getName()); + + SmbShare smb(mountDir->getPath()); + fs::path remote = getImagePath(machine.getTarget()->imgUrl); + auto remoteParent = "/" + remote.parent_path().string(); + auto localFile = mountDir->getPath() / remote.filename(); + + LogMsg(Logger::Debug, machine.getName(), " Remote name: ", remote, + "\n Remote parent: ", remoteParent, + "\n Local file: ", localFile); + + machine.getTarget()->mountPoint = std::make_unique( + std::move(mountDir), smb, remoteParent, machine.getTarget()->rw, + machine.getTarget()->credentials); + + process = spawnNbdKit(machine, localFile); + if (!process) + { + return std::make_unique(machine, + std::errc::operation_canceled, + "Unable to setup NbdKit"); + } + + return nullptr; + } + catch (const resource::Error& e) + { + return std::make_unique(machine, e.errorCode, e.what()); + } +} + +std::unique_ptr ActivatingState::mountHttpsShare() +{ + process = spawnNbdKit(machine, machine.getTarget()->imgUrl); + if (!process) + { + return std::make_unique(machine, + std::errc::invalid_argument, + "Failed to mount HTTPS share"); + } + + return nullptr; +} + +std::unique_ptr + ActivatingState::spawnNbdKit(interfaces::MountPointStateMachine& machine, + std::unique_ptr&& secret, + const std::vector& params) +{ + // Investigate + auto process = std::make_unique( + machine, std::make_shared<::Process>( + machine.getIoc(), std::string(machine.getName()), + "/usr/sbin/nbdkit", machine.getConfig().nbdDevice)); + + // Cleanup of previous socket + if (fs::exists(machine.getConfig().unixSocket)) + { + LogMsg(Logger::Debug, machine.getName(), + " Removing previously mounted socket: ", + machine.getConfig().unixSocket); + if (!fs::remove(machine.getConfig().unixSocket)) + { + LogMsg(Logger::Error, machine.getName(), + " Unable to remove pre-existing socket :", + machine.getConfig().unixSocket); + return {}; + } + } + + std::string nbd_client = + "/usr/sbin/nbd-client " + + boost::algorithm::join( + Configuration::MountPoint::toArgs(machine.getConfig()), " "); + + std::vector args = { + // Listen for client on this unix socket... + "--unix", + machine.getConfig().unixSocket, + + // ... then connect nbd-client to served image + "--run", + nbd_client, + +#if VM_VERBOSE_NBDKIT_LOGS + "--verbose", // swarm of debug logs - only for brave souls +#endif + }; + + if (!machine.getTarget()->rw) + { + args.push_back("--readonly"); + } + + // Insert extra params + args.insert(args.end(), params.begin(), params.end()); + + if (!process->spawn(args, [& machine = machine, secret = std::move(secret)]( + int exitCode, bool isReady) { + LogMsg(Logger::Info, machine.getName(), " process ended."); + machine.getExitCode() = exitCode; + machine.emitSubprocessStoppedEvent(); + })) + { + LogMsg(Logger::Error, machine.getName(), + " Failed to spawn Process for: ", machine.getName()); + return {}; + } + + return process; +} + +std::unique_ptr + ActivatingState::spawnNbdKit(interfaces::MountPointStateMachine& machine, + const fs::path& file) +{ + return spawnNbdKit(machine, {}, + {// Use file plugin ... + "file", + // ... to mount file at this location + "file=" + file.string()}); +} + +std::unique_ptr + ActivatingState::spawnNbdKit(interfaces::MountPointStateMachine& machine, + const std::string& url) +{ + std::unique_ptr secret; + std::vector params = {// Use curl plugin ... + "curl", + // ... to mount http resource at url + "url=" + url}; + + // Authenticate if needed + if (machine.getTarget()->credentials) + { + // Pack password into buffer + utils::CredentialsProvider::SecureBuffer buff = + machine.getTarget()->credentials->pack([](const std::string& user, + const std::string& pass, + std::vector& buff) { + std::copy(pass.begin(), pass.end(), std::back_inserter(buff)); + }); + + // Prepare file to provide the password with + secret = std::make_unique(std::move(buff)); + + params.push_back("user=" + machine.getTarget()->credentials->user()); + params.push_back("password=+" + secret->path()); + } + + return spawnNbdKit(machine, std::move(secret), params); +} + +bool ActivatingState::checkUrl(const std::string& urlScheme, + const std::string& imageUrl) +{ + return (urlScheme.compare(imageUrl.substr(0, urlScheme.size())) == 0); +} + +bool ActivatingState::getImagePathFromUrl(const std::string& urlScheme, + const std::string& imageUrl, + std::string* imagePath) +{ + if (checkUrl(urlScheme, imageUrl)) + { + if (imagePath != nullptr) + { + *imagePath = imageUrl.substr(urlScheme.size() - 1); + return true; + } + else + { + LogMsg(Logger::Error, "Invalid parameter provied"); + return false; + } + } + else + { + LogMsg(Logger::Error, "Provied url does not match scheme"); + return false; + } +} + +bool ActivatingState::isHttpsUrl(const std::string& imageUrl) +{ + return checkUrl("https://", imageUrl); +} + +bool ActivatingState::getImagePathFromHttpsUrl(const std::string& imageUrl, + std::string* imagePath) +{ + return getImagePathFromUrl("https://", imageUrl, imagePath); +} + +bool ActivatingState::isCifsUrl(const std::string& imageUrl) +{ + return checkUrl("smb://", imageUrl); +} + +bool ActivatingState::getImagePathFromCifsUrl(const std::string& imageUrl, + std::string* imagePath) +{ + return getImagePathFromUrl("smb://", imageUrl, imagePath); +} + +fs::path ActivatingState::getImagePath(const std::string& imageUrl) +{ + std::string imagePath; + + if (isHttpsUrl(imageUrl) && getImagePathFromHttpsUrl(imageUrl, &imagePath)) + { + return fs::path(imagePath); + } + else if (isCifsUrl(imageUrl) && + getImagePathFromCifsUrl(imageUrl, &imagePath)) + { + return fs::path(imagePath); + } + else + { + LogMsg(Logger::Error, "Unrecognized url's scheme encountered"); + return fs::path(""); + } +} diff --git a/src/state/activating_state.hpp b/src/state/activating_state.hpp new file mode 100644 index 0000000..bd1688f --- /dev/null +++ b/src/state/activating_state.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "basic_state.hpp" + +struct ActivatingState : public BasicStateT +{ + static std::string_view stateName() + { + return "ActivatingState"; + } + + ActivatingState(interfaces::MountPointStateMachine& machine); + + std::unique_ptr onEnter() override; + + std::unique_ptr handleEvent(UdevStateChangeEvent event); + std::unique_ptr handleEvent(SubprocessStoppedEvent event); + + template + std::unique_ptr handleEvent(AnyEvent event) + { + LogMsg(Logger::Error, "Invalid event: ", event.eventName); + return nullptr; + } + + private: + std::unique_ptr activateProxyMode(); + std::unique_ptr activateLegacyMode(); + std::unique_ptr mountSmbShare(); + std::unique_ptr mountHttpsShare(); + + static std::unique_ptr + spawnNbdKit(interfaces::MountPointStateMachine& machine, + std::unique_ptr&& secret, + const std::vector& params); + static std::unique_ptr + spawnNbdKit(interfaces::MountPointStateMachine& machine, + const fs::path& file); + static std::unique_ptr + spawnNbdKit(interfaces::MountPointStateMachine& machine, + const std::string& url); + + static bool checkUrl(const std::string& urlScheme, + const std::string& imageUrl); + static bool getImagePathFromUrl(const std::string& urlScheme, + const std::string& imageUrl, + std::string* imagePath); + static bool isHttpsUrl(const std::string& imageUrl); + static bool getImagePathFromHttpsUrl(const std::string& imageUrl, + std::string* imagePath); + + static bool isCifsUrl(const std::string& imageUrl); + static bool getImagePathFromCifsUrl(const std::string& imageUrl, + std::string* imagePath); + static fs::path getImagePath(const std::string& imageUrl); + + std::unique_ptr process; + std::unique_ptr gadget; +}; diff --git a/src/state/active_state.hpp b/src/state/active_state.hpp new file mode 100644 index 0000000..541e27e --- /dev/null +++ b/src/state/active_state.hpp @@ -0,0 +1,100 @@ +#pragma once + +#include "basic_state.hpp" +#include "deactivating_state.hpp" + +struct ActiveState : public BasicStateT +{ + static std::string_view stateName() + { + return "ActiveState"; + } + + ActiveState(interfaces::MountPointStateMachine& machine, + std::unique_ptr process, + std::unique_ptr gadget) : + BasicStateT(machine), + process(std::move(process)), gadget(std::move(gadget)){}; + + virtual std::unique_ptr onEnter() + { + handler = [this](const boost::system::error_code& ec) { + if (ec) + { + return; + } + + auto now = std::chrono::steady_clock::now(); + + auto stats = UsbGadget::getStats(std::string(machine.getName())); + if (stats && (*stats != lastStats)) + { + lastStats = std::move(*stats); + lastAccess = now; + } + + auto timeSinceLastAccess = + std::chrono::duration_cast(now - + lastAccess); + if (timeSinceLastAccess >= Configuration::inactivityTimeout) + { + LogMsg(Logger::Info, machine.getName(), + " Inactivity timer expired (", + Configuration::inactivityTimeout.count(), + "s) - Unmounting"); + // unmount media & stop retriggering timer + boost::asio::spawn( + machine.getIoc(), + [& machine = machine](boost::asio::yield_context yield) { + machine.emitUnmountEvent(); + }); + return; + } + else + { + machine.getConfig().remainingInactivityTimeout = + Configuration::inactivityTimeout - timeSinceLastAccess; + } + + timer.expires_from_now(std::chrono::seconds(1)); + timer.async_wait(handler); + }; + timer.expires_from_now(std::chrono::seconds(1)); + timer.async_wait(handler); + + return nullptr; + } + + std::unique_ptr handleEvent(UdevStateChangeEvent event) + { + return std::make_unique( + machine, std::move(process), std::move(gadget), std::move(event)); + } + + std::unique_ptr handleEvent(SubprocessStoppedEvent event) + { + return std::make_unique( + machine, std::move(process), std::move(gadget), std::move(event)); + } + + std::unique_ptr handleEvent(UnmountEvent event) + { + return std::make_unique(machine, std::move(process), + std::move(gadget)); + } + + template + std::unique_ptr handleEvent(AnyEvent event) + { + LogMsg(Logger::Error, "Invalid event: ", event.eventName); + return nullptr; + } + + private: + boost::asio::steady_timer timer{machine.getIoc()}; + std::unique_ptr process; + std::unique_ptr gadget; + std::function handler; + std::chrono::time_point lastAccess; + std::string lastStats; +}; diff --git a/src/state/basic_state.hpp b/src/state/basic_state.hpp new file mode 100644 index 0000000..4529338 --- /dev/null +++ b/src/state/basic_state.hpp @@ -0,0 +1,67 @@ +#pragma once + +#include "events.hpp" +#include "interfaces/mount_point_state_machine.hpp" + +struct BasicState +{ + BasicState(interfaces::MountPointStateMachine& machine) : machine{machine} + { + } + virtual ~BasicState() = default; + + BasicState(const BasicState& state) = delete; + BasicState(BasicState&&) = delete; + + BasicState& operator=(const BasicState&) = delete; + BasicState& operator=(BasicState&& state) = delete; + + virtual std::unique_ptr handleEvent(Event event) = 0; + virtual std::unique_ptr onEnter() = 0; + virtual std::string_view getStateName() const = 0; + + template + T* get_if() + { + if (getStateName() == T::stateName()) + { + return static_cast(this); + } + return nullptr; + } + + interfaces::MountPointStateMachine& machine; +}; + +template +struct BasicStateT : public BasicState +{ + BasicStateT(interfaces::MountPointStateMachine& machine) : + BasicState(machine) + { + } + + ~BasicStateT() + { + LogMsg(Logger::Debug, "cleaning state: ", T::stateName()); + } + + std::unique_ptr onEnter() override + { + return nullptr; + } + + std::unique_ptr handleEvent(Event event) override final + { + return std::visit( + [this](auto e) { + return static_cast(this)->handleEvent(std::move(e)); + }, + std::move(event)); + } + + std::string_view getStateName() const override final + { + return T::stateName(); + } +}; diff --git a/src/state/deactivating_state.hpp b/src/state/deactivating_state.hpp new file mode 100644 index 0000000..7f3010a --- /dev/null +++ b/src/state/deactivating_state.hpp @@ -0,0 +1,83 @@ +#pragma once + +#include "basic_state.hpp" +#include "ready_state.hpp" + +struct DeactivatingState : public BasicStateT +{ + static std::string_view stateName() + { + return "DeactivatingState"; + } + + template + DeactivatingState(interfaces::MountPointStateMachine& machine, + std::unique_ptr process, + std::unique_ptr gadget, EventT event) : + BasicStateT(machine), + process(std::move(process)), gadget(std::move(gadget)) + { + handleEvent(std::move(event)); + } + + DeactivatingState(interfaces::MountPointStateMachine& machine, + std::unique_ptr process, + std::unique_ptr gadget) : + BasicStateT(machine), + process(std::move(process)), gadget(std::move(gadget)) + { + } + + std::unique_ptr onEnter() override + { + gadget = nullptr; + process = nullptr; + + return nullptr; + } + + std::unique_ptr handleEvent(UdevStateChangeEvent event) + { + udevStateChangeEvent = std::move(event); + return evaluate(); + } + + std::unique_ptr handleEvent(SubprocessStoppedEvent event) + { + subprocessStoppedEvent = std::move(event); + return evaluate(); + } + + template + std::unique_ptr handleEvent(AnyEvent event) + { + LogMsg(Logger::Error, "Invalid event: ", event.eventName); + return nullptr; + } + + private: + std::unique_ptr evaluate() + { + if (udevStateChangeEvent && subprocessStoppedEvent) + { + if (udevStateChangeEvent->devState == StateChange::removed) + { + LogMsg(Logger::Debug, machine.getName(), + " udev StateChange::removed"); + } + else + { + LogMsg(Logger::Error, machine.getName(), " udev StateChange::", + static_cast>( + udevStateChangeEvent->devState)); + } + return std::make_unique(machine); + } + return nullptr; + } + + std::unique_ptr process; + std::unique_ptr gadget; + std::optional udevStateChangeEvent; + std::optional subprocessStoppedEvent; +}; diff --git a/src/state/initial_state.hpp b/src/state/initial_state.hpp new file mode 100644 index 0000000..f624d7e --- /dev/null +++ b/src/state/initial_state.hpp @@ -0,0 +1,313 @@ +#include "active_state.hpp" +#include "basic_state.hpp" +#include "ready_state.hpp" + +struct InitialState : public BasicStateT +{ + static std::string_view stateName() + { + return "InitialState"; + } + + InitialState(interfaces::MountPointStateMachine& machine) : + BasicStateT(machine){}; + + std::unique_ptr handleEvent(RegisterDbusEvent event) + { + const bool isLegacy = + (machine.getConfig().mode == Configuration::Mode::legacy); + +#if !LEGACY_MODE_ENABLED + if (isLegacy) + { + return std::make_unique(machine, + std::errc::invalid_argument, + "Legacy mode is not supported"); + } +#endif + addMountPointInterface(event); + addProcessInterface(event); + addServiceInterface(event, isLegacy); + + return std::make_unique(machine); + } + + template + std::unique_ptr handleEvent(AnyEvent event) + { + LogMsg(Logger::Error, "Invalid event: ", event.eventName); + return nullptr; + } + + private: + static std::string + getObjectPath(interfaces::MountPointStateMachine& machine) + { + LogMsg(Logger::Debug, "getObjectPath entry()"); + std::string objPath; + if (machine.getConfig().mode == Configuration::Mode::proxy) + { + objPath = "/xyz/openbmc_project/VirtualMedia/Proxy/"; + } + else + { + objPath = "/xyz/openbmc_project/VirtualMedia/Legacy/"; + } + return objPath; + } + + void addProcessInterface(const RegisterDbusEvent& event) + { + std::string objPath = getObjectPath(machine); + + auto processIface = event.objServer->add_interface( + objPath + std::string(machine.getName()), + "xyz.openbmc_project.VirtualMedia.Process"); + + processIface->register_property( + "Active", bool(false), + [](const bool& req, bool& property) { return 0; }, + [& machine = machine](const bool& property) -> bool { + return machine.getState().get_if(); + }); + processIface->register_property( + "ExitCode", int32_t(0), + [](const int32_t& req, int32_t& property) { return 0; }, + [& machine = machine](const int32_t& property) { + return machine.getExitCode(); + }); + processIface->initialize(); + } + + void addMountPointInterface(const RegisterDbusEvent& event) + { + std::string objPath = getObjectPath(machine); + + auto iface = event.objServer->add_interface( + objPath + std::string(machine.getName()), + "xyz.openbmc_project.VirtualMedia.MountPoint"); + iface->register_property("Device", + machine.getConfig().nbdDevice.to_string()); + iface->register_property("EndpointId", machine.getConfig().endPointId); + iface->register_property("Socket", machine.getConfig().unixSocket); + iface->register_property( + "RemainingInactivityTimeout", 0, + [](const int& req, int& property) { + throw sdbusplus::exception::SdBusError( + EPERM, "Setting RemainingInactivityTimeout property is " + "not allowed"); + return -1; + }, + [& config = machine.getConfig()](const int& property) -> int { + return config.remainingInactivityTimeout.count(); + }); + + iface->initialize(); + } + + void addServiceInterface(const RegisterDbusEvent& event, + const bool isLegacy) + { + const std::string name = "xyz.openbmc_project.VirtualMedia." + + std::string(isLegacy ? "Legacy" : "Proxy"); + + const std::string path = + getObjectPath(machine) + std::string(machine.getName()); + + auto iface = event.objServer->add_interface(path, name); + + const auto timerPeriod = std::chrono::milliseconds(100); + const auto duration = std::chrono::seconds( + machine.getConfig().timeout.value_or( + Configuration::MountPoint::defaultTimeout) + + 5); + const auto waitCnt = + std::chrono::duration_cast(duration) / + timerPeriod; + LogMsg(Logger::Debug, "[App] waitCnt == ", waitCnt); + + // Common unmount + iface->register_method( + "Unmount", [& machine = machine, waitCnt, + timerPeriod](boost::asio::yield_context yield) { + LogMsg(Logger::Info, "[App]: Unmount called on ", + machine.getName()); + try + { + machine.emitUnmountEvent(); + } + catch (const std::exception& e) + { + LogMsg(Logger::Error, e.what()); + throw sdbusplus::exception::SdBusError(EPERM, e.what()); + return false; + } + + auto repeats = waitCnt; + boost::asio::steady_timer timer(machine.getIoc()); + while (repeats > 0) + { + if (machine.getState().get_if()) + { + LogMsg(Logger::Debug, "[App] Unmount ok"); + return true; + } + boost::system::error_code ignored_ec; + timer.expires_from_now(timerPeriod); + timer.async_wait(yield[ignored_ec]); + repeats--; + } + LogMsg(Logger::Error, + "[App] timedout when waiting for ReadyState"); + return false; + }); + + // Common mount + const auto handleMount = + [waitCnt, timerPeriod]( + boost::asio::yield_context yield, + interfaces::MountPointStateMachine& machine, + std::optional + target) { + try + { + machine.emitMountEvent(std::move(target)); + } + catch (const std::exception& e) + { + LogMsg(Logger::Error, e.what()); + throw sdbusplus::exception::SdBusError(EPERM, e.what()); + return false; + } + + auto repeats = waitCnt; + boost::asio::steady_timer timer(machine.getIoc()); + while (repeats > 0) + { + if (auto s = machine.getState().get_if()) + { + if (s->error) + { + LogMsg(Logger::Error, s->error->message.c_str()); + throw sdbusplus::exception::SdBusError( + static_cast(s->error->code), + s->error->message.c_str()); + } + LogMsg(Logger::Error, "[App] Mount failed"); + return false; + } + if (machine.getState().get_if()) + { + LogMsg(Logger::Debug, "[App] Mount ok"); + return true; + } + boost::system::error_code ignored_ec; + timer.expires_from_now(timerPeriod); + timer.async_wait(yield[ignored_ec]); + repeats--; + } + LogMsg(Logger::Error, + "[App] timedout when waiting for ActiveState"); + return false; + }; + + // Mount specialization + if (isLegacy) + { + using sdbusplus::message::unix_fd; + using optional_fd = std::variant; + + iface->register_method( + "Mount", [& machine = machine, handleMount]( + boost::asio::yield_context yield, + std::string imgUrl, bool rw, optional_fd fd) { + LogMsg(Logger::Info, "[App]: Mount called on ", + getObjectPath(machine), machine.getName()); + + interfaces::MountPointStateMachine::Target target = {imgUrl, + rw}; + + if (std::holds_alternative(fd)) + { + LogMsg(Logger::Debug, "[App] Extra data available"); + + // Open pipe and prepare output buffer + boost::asio::posix::stream_descriptor secretPipe( + machine.getIoc(), dup(std::get(fd).fd)); + std::array buf; + + // Read data + auto size = secretPipe.async_read_some( + boost::asio::buffer(buf), yield); + + // Validate number of NULL delimiters, ensures + // further operations are safe + auto nullCount = + std::count(buf.begin(), buf.begin() + size, '\0'); + if (nullCount != 2) + { + throw sdbusplus::exception::SdBusError( + EINVAL, "Malformed extra data"); + } + + // First 'part' of payload + std::string user(buf.begin()); + // Second 'part', after NULL delimiter + std::string pass(buf.begin() + user.length() + 1); + + // Encapsulate credentials into safe buffer + target.credentials = + std::make_unique( + std::move(user), std::move(pass)); + + // Cover the tracks + utils::secureCleanup(buf); + } + + try + { + auto ret = + handleMount(yield, machine, std::move(target)); + if (machine.getTarget()) + { + machine.getTarget()->credentials.reset(); + } + LogMsg(Logger::Debug, "[App]: mount completed ", ret); + return ret; + } + catch (const std::exception& e) + { + LogMsg(Logger::Error, e.what()); + if (machine.getTarget()) + { + machine.getTarget()->credentials.reset(); + } + throw; + return false; + } + catch (...) + { + if (machine.getTarget()) + { + machine.getTarget()->credentials.reset(); + } + throw; + return false; + } + }); + } + else + { + iface->register_method( + "Mount", [& machine = machine, + handleMount](boost::asio::yield_context yield) { + LogMsg(Logger::Info, "[App]: Mount called on ", + getObjectPath(machine), machine.getName()); + + return handleMount(yield, machine, std::nullopt); + }); + } + + iface->initialize(); + } +}; diff --git a/src/state/ready_state.hpp b/src/state/ready_state.hpp new file mode 100644 index 0000000..5edfa06 --- /dev/null +++ b/src/state/ready_state.hpp @@ -0,0 +1,58 @@ +#pragma once + +#include "activating_state.hpp" +#include "basic_state.hpp" + +struct ReadyState : public BasicStateT +{ + static std::string_view stateName() + { + return "ReadyState"; + } + + struct Error + { + std::errc code; + std::string message; + }; + + ReadyState(interfaces::MountPointStateMachine& machine) : + BasicStateT(machine){}; + + ReadyState(interfaces::MountPointStateMachine& machine, const std::errc& ec, + const std::string& message) : + BasicStateT(machine), + error{{ec, message}} + { + LogMsg(Logger::Error, machine.getName(), + " Errno = ", static_cast(ec), " : ", message); + } + + std::unique_ptr onEnter() override + { + // Cleanup after previously mounted device + LogMsg(Logger::Debug, "exitCode: ", machine.getExitCode()); + machine.getTarget() = std::nullopt; + machine.getConfig().remainingInactivityTimeout = + std::chrono::seconds(0); + return nullptr; + } + + std::unique_ptr handleEvent(MountEvent event) + { + if (event.target) + { + machine.getTarget() = std::move(event.target); + } + return std::make_unique(machine); + } + + template + std::unique_ptr handleEvent(AnyEvent event) + { + LogMsg(Logger::Error, "Invalid event: ", event.eventName); + return nullptr; + } + + std::optional error; +}; diff --git a/src/state_machine.hpp b/src/state_machine.hpp index e442631..f9772fe 100644 --- a/src/state_machine.hpp +++ b/src/state_machine.hpp @@ -1,1084 +1,101 @@ #pragma once -#include "configuration.hpp" -#include "logger.hpp" -#include "smb.hpp" -#include "system.hpp" -#include "utils.hpp" +#include "interfaces/mount_point_state_machine.hpp" +#include "state/initial_state.hpp" -#include - -#include -#include #include -#include #include -#include -#include -#include -struct MountPointStateMachine +struct MountPointStateMachine : public interfaces::MountPointStateMachine { - struct InvalidStateError : std::runtime_error - { - InvalidStateError(const char* what) : std::runtime_error(what) - { - } - }; - - struct Error - { - std::errc code; - std::string message; - }; - - struct BasicState - { - BasicState(MountPointStateMachine& machine, - const char* stateName = nullptr) : - machine{machine}, - stateName{stateName} - { - if (stateName != nullptr) - { - LogMsg(Logger::Debug, machine.name, " State changed to ", - stateName); - } - } - - BasicState(const BasicState& state) : - machine{state.machine}, stateName{state.stateName} - { - } - - BasicState(const BasicState& state, const char* stateName) : - machine{state.machine}, stateName{stateName} - { - LogMsg(Logger::Debug, machine.name, " State changed to ", - stateName); - } - - BasicState& operator=(BasicState&& state) - { - machine = std::move(state.machine); - stateName = std::move(state.stateName); - return *this; - } - - virtual void onEnter(){}; - - MountPointStateMachine& machine; - const char* stateName = nullptr; - }; - - struct InitialState : public BasicState - { - InitialState(const BasicState& state) : - BasicState(state, __FUNCTION__){}; - InitialState(MountPointStateMachine& machine) : - BasicState(machine, __FUNCTION__){}; - }; - - struct ReadyState : public BasicState - { - ReadyState(const BasicState& state) : BasicState(state, __FUNCTION__){}; - - ReadyState(const BasicState& state, const std::errc& ec, - const std::string& message) : - BasicState(state, __FUNCTION__), - error{{ec, message}} - { - LogMsg(Logger::Error, state.machine.name, - " Errno = ", static_cast(ec), " : ", message); - }; - - virtual void onEnter() - { - if (machine.target) - { - // Cleanup after previously mounted device - if (machine.target->mountDir) - { - SmbShare::unmount(*machine.target->mountDir); - } - - machine.target.reset(); - } - - machine.config.remainingInactivityTimeout = std::chrono::seconds(0); - } - - std::optional error; - }; - - struct ActivatingState : public BasicState - { - ActivatingState(const BasicState& state) : - BasicState(state, __FUNCTION__) - { - } - - virtual void onEnter() - { - // Reset previous exit code - machine.exitCode = -1; - - machine.emitActivationStartedEvent(); - } - }; - - struct WaitingForGadgetState : public BasicState - { - WaitingForGadgetState(const BasicState& state) : - BasicState(state, __FUNCTION__) - { - } - - std::weak_ptr process; - }; - - struct ActiveState : public BasicState - { - ActiveState(const BasicState& state) : BasicState(state, __FUNCTION__) - { - } - ActiveState(const WaitingForGadgetState& state) : - BasicState(state, __FUNCTION__), process{state.process} {}; - - std::weak_ptr process; - - virtual void onEnter() - { - timer = - std::make_shared(machine.ioc.get()); - handler = [this](const boost::system::error_code& ec) { - if (ec) - { - return; - } - - auto now = std::chrono::steady_clock::now(); - - auto stats = UsbGadget::getStats(machine.name); - if (stats && (*stats != lastStats)) - { - lastStats = std::move(*stats); - lastAccess = now; - } - - auto timeSinceLastAccess = - std::chrono::duration_cast( - now - lastAccess); - if (timeSinceLastAccess >= Configuration::inactivityTimeout) - { - LogMsg(Logger::Info, machine.name, - " Inactivity timer expired (", - Configuration::inactivityTimeout.count(), - "s) - Unmounting"); - // unmount media & stop retriggering timer - machine.emitUnmountEvent(); - return; - } - else - { - machine.config.remainingInactivityTimeout = - Configuration::inactivityTimeout - timeSinceLastAccess; - } - - timer->expires_from_now(std::chrono::seconds(1)); - timer->async_wait(handler); - }; - timer->expires_from_now(std::chrono::seconds(1)); - timer->async_wait(handler); - } - - private: - // timer wrapped in shared_ptr to allow making state copies - std::shared_ptr timer; - std::function handler; - std::chrono::time_point lastAccess; - std::string lastStats; - }; - - struct WaitingForProcessEndState : public BasicState - { - WaitingForProcessEndState(const BasicState& state) : - BasicState(state, __FUNCTION__) - { - } - WaitingForProcessEndState(const ActiveState& state) : - BasicState(state, __FUNCTION__), process{state.process} - { - } - WaitingForProcessEndState(const WaitingForGadgetState& state) : - BasicState(state, __FUNCTION__), process{state.process} - { - } - - std::weak_ptr process; - }; - - using State = std::variant; - - struct BasicEvent - { - BasicEvent(const char* eventName) : eventName(eventName) - { - } - - inline void transitionError(const char* en, const BasicState& state) - { - LogMsg(Logger::Critical, state.machine.name, " Unexpected event ", - eventName, " received in ", state.stateName, - "state. Review and correct state transisions."); - } - virtual State operator()(const InitialState& state) - { - transitionError(eventName, state); - return state; - } - virtual State operator()(const ReadyState& state) - { - transitionError(eventName, state); - return state; - } - virtual State operator()(const ActivatingState& state) - { - transitionError(eventName, state); - return state; - } - virtual State operator()(const WaitingForGadgetState& state) - { - transitionError(eventName, state); - return state; - } - virtual State operator()(const ActiveState& state) - { - transitionError(eventName, state); - return state; - } - virtual State operator()(const WaitingForProcessEndState& state) - { - transitionError(eventName, state); - return state; - } - const char* eventName; - }; - - struct RegisterDbusEvent : public BasicEvent - { - RegisterDbusEvent( - std::shared_ptr bus, - std::shared_ptr objServer) : - BasicEvent(__FUNCTION__), - bus(bus), objServer(objServer), - emitMountEvent(std::move(emitMountEvent)) - { - } - - State operator()(const InitialState& state) - { - const bool isLegacy = - (state.machine.config.mode == Configuration::Mode::legacy); - -#if !LEGACY_MODE_ENABLED - if (isLegacy) - { - return ReadyState(state, std::errc::invalid_argument, - "Legacy mode is not supported"); - } -#endif - - addMountPointInterface(state); - addProcessInterface(state); - addServiceInterface(state, isLegacy); - return ReadyState(state); - } - - template - State operator()(const AnyState& state) - { - LogMsg(Logger::Critical, state.machine.name, - " If you receiving this error, this means " - "your FSM is broken. Rethink!"); - return InitialState(state); - } - - private: - std::string getObjectPath(const MountPointStateMachine& machine) - { - LogMsg(Logger::Debug, "getObjectPath entry()"); - std::string objPath; - if (machine.config.mode == Configuration::Mode::proxy) - { - objPath = "/xyz/openbmc_project/VirtualMedia/Proxy/"; - } - else - { - objPath = "/xyz/openbmc_project/VirtualMedia/Legacy/"; - } - return objPath; - } - - std::string getObjectPath(const InitialState& state) - { - return getObjectPath(state.machine); - } - - void addProcessInterface(const InitialState& state) - { - std::string objPath = getObjectPath(state); - - auto processIface = objServer->add_interface( - objPath + state.machine.name, - "xyz.openbmc_project.VirtualMedia.Process"); - - processIface->register_property( - "Active", bool(false), - [](const bool& req, bool& property) { return 0; }, - [& machine = state.machine](const bool& property) { - if (std::get_if(&machine.state)) - { - return true; - } - else - { - return false; - } - }); - processIface->register_property( - "ExitCode", int32_t(0), - [](const int32_t& req, int32_t& property) { return 0; }, - [& machine = state.machine](const int32_t& property) { - return machine.exitCode; - }); - processIface->initialize(); - } - - void addMountPointInterface(const InitialState& state) - { - std::string objPath = getObjectPath(state); - - auto iface = objServer->add_interface( - objPath + state.machine.name, - "xyz.openbmc_project.VirtualMedia.MountPoint"); - iface->register_property( - "Device", state.machine.config.nbdDevice.to_string()); - iface->register_property("EndpointId", - state.machine.config.endPointId); - iface->register_property("Socket", state.machine.config.unixSocket); - iface->register_property( - "RemainingInactivityTimeout", 0, - [](const int& req, int& property) { - throw sdbusplus::exception::SdBusError( - EPERM, "Setting RemainingInactivityTimeout property is " - "not allowed"); - return -1; - }, - [& config = state.machine.config](const int& property) -> int { - return config.remainingInactivityTimeout.count(); - }); - - iface->initialize(); - } - - void addServiceInterface(const InitialState& state, const bool isLegacy) - { - const std::string name = "xyz.openbmc_project.VirtualMedia." + - std::string(isLegacy ? "Legacy" : "Proxy"); - - const std::string path = getObjectPath(state) + state.machine.name; - - auto iface = objServer->add_interface(path, name); - - // Common unmount - iface->register_method( - "Unmount", - [& machine = state.machine](boost::asio::yield_context yield) { - LogMsg(Logger::Info, "[App]: Unmount called on ", - machine.name); - try - { - machine.emitUnmountEvent(); - } - catch (InvalidStateError& e) - { - throw sdbusplus::exception::SdBusError(EPERM, e.what()); - return false; - } - - boost::asio::steady_timer timer(machine.ioc.get()); - int waitCnt = 120; - while (waitCnt > 0) - { - if (std::get_if(&machine.state)) - { - break; - } - boost::system::error_code ignored_ec; - timer.expires_from_now(std::chrono::milliseconds(100)); - timer.async_wait(yield[ignored_ec]); - waitCnt--; - } - return true; - }); - - // Common mount - const auto handleMount = [](boost::asio::yield_context yield, - MountPointStateMachine& machine) { - try - { - machine.emitMountEvent(); - } - catch (InvalidStateError& e) - { - throw sdbusplus::exception::SdBusError(EPERM, e.what()); - return false; - } - - boost::asio::steady_timer timer(machine.ioc.get()); - int waitCnt = 120; - while (waitCnt > 0) - { - if (auto s = std::get_if(&machine.state)) - { - if (s->error) - { - throw sdbusplus::exception::SdBusError( - static_cast(s->error->code), - s->error->message.c_str()); - } - return false; - } - if (std::get_if(&machine.state)) - { - return true; - } - boost::system::error_code ignored_ec; - timer.expires_from_now(std::chrono::milliseconds(100)); - timer.async_wait(yield[ignored_ec]); - waitCnt--; - } - return false; - }; - - // Mount specialization - if (isLegacy) - { - using sdbusplus::message::unix_fd; - using optional_fd = std::variant; - - iface->register_method( - "Mount", [& machine = state.machine, this, handleMount]( - boost::asio::yield_context yield, - std::string imgUrl, bool rw, optional_fd fd) { - LogMsg(Logger::Info, "[App]: Mount called on ", - getObjectPath(machine), machine.name); - - machine.target = {imgUrl, rw}; - - if (std::holds_alternative(fd)) - { - LogMsg(Logger::Debug, "[App] Extra data available"); - - // Open pipe and prepare output buffer - boost::asio::posix::stream_descriptor secretPipe( - machine.ioc.get(), - dup(std::get(fd).fd)); - std::array buf; - - // Read data - auto size = secretPipe.async_read_some( - boost::asio::buffer(buf), yield); - - // Validate number of NULL delimiters, ensures - // further operations are safe - auto nullCount = std::count( - buf.begin(), buf.begin() + size, '\0'); - if (nullCount != 2) - { - throw sdbusplus::exception::SdBusError( - EINVAL, "Malformed extra data"); - } - - // First 'part' of payload - std::string user(buf.begin()); - // Second 'part', after NULL delimiter - std::string pass(buf.begin() + user.length() + 1); - - // Encapsulate credentials into safe buffer - machine.target->credentials = - std::make_unique( - std::move(user), std::move(pass)); - - // Cover the tracks - utils::secureCleanup(buf); - } - - try - { - auto ret = handleMount(yield, machine); - if (machine.target) - { - machine.target->credentials.reset(); - } - return ret; - } - catch (...) - { - if (machine.target) - { - machine.target->credentials.reset(); - } - throw; - return false; - } - }); - } - else - { - iface->register_method( - "Mount", [& machine = state.machine, this, - handleMount](boost::asio::yield_context yield) { - LogMsg(Logger::Info, "[App]: Mount called on ", - getObjectPath(machine), machine.name); - - return handleMount(yield, machine); - }); - } - - iface->initialize(); - } - - std::shared_ptr bus; - std::shared_ptr objServer; - std::function emitMountEvent; - }; - - struct MountEvent : public BasicEvent + MountPointStateMachine(boost::asio::io_context& ioc, + DeviceMonitor& devMonitor, const std::string& name, + const Configuration::MountPoint& config) : + ioc{ioc}, + name{name}, config{config} { - MountEvent() : BasicEvent(__FUNCTION__) - { - } - State operator()(const ReadyState& state) - { - return ActivatingState(state); - } + devMonitor.addDevice(config.nbdDevice); + } - template - State operator()(const AnyState& state) - { - throw InvalidStateError("Could not mount on not empty slot"); - } - }; + MountPointStateMachine& operator=(MountPointStateMachine&&) = delete; - struct UnmountEvent : public BasicEvent + std::string_view getName() const override { - UnmountEvent() : BasicEvent(__FUNCTION__) - { - } - State operator()(const ActivatingState& state) - { - return ReadyState(state); - } - State operator()(const WaitingForGadgetState& state) - { - state.machine.stopProcess(state.process); - return WaitingForProcessEndState(state); - } - State operator()(const ActiveState& state) - { - if (!state.machine.removeUsbGadget(state)) - { - return ReadyState(state, std::errc::device_or_resource_busy, - "Unable to unmount gadget"); - } - state.machine.stopProcess(state.process); - return WaitingForProcessEndState(state); - } - State operator()(const WaitingForProcessEndState& state) - { - throw InvalidStateError("Could not unmount on empty slot"); - } - State operator()(const ReadyState& state) - { - throw InvalidStateError("Could not unmount on empty slot"); - } - }; - - struct SubprocessStoppedEvent : public BasicEvent - { - SubprocessStoppedEvent() : BasicEvent(__FUNCTION__) - { - } - State operator()(const ActivatingState& state) - { - return ReadyState(state); - } - State operator()(const WaitingForGadgetState& state) - { - state.machine.stopProcess(state.process); - return ReadyState(state, std::errc::io_error, - "Process ended prematurely"); - } - State operator()(const ActiveState& state) - { - if (!state.machine.removeUsbGadget(state)) - { - return ReadyState(state, std::errc::device_or_resource_busy, - "Unable to unmount gadget"); - } - return ReadyState(state); - } - State operator()(const WaitingForProcessEndState& state) - { - return ReadyState(state); - } - }; + return name; + } - struct ActivationStartedEvent : public BasicEvent + Configuration::MountPoint& getConfig() override { - ActivationStartedEvent() : BasicEvent(__FUNCTION__) - { - } - State operator()(const ActivatingState& state) - { - if (state.machine.config.mode == Configuration::Mode::proxy) - { - return activateProxyMode(state); - } - return activateLegacyMode(state); - } - - State activateProxyMode(const ActivatingState& state) - { - auto process = std::make_shared( - state.machine.ioc.get(), state.machine.name, - "/usr/sbin/nbd-client", state.machine.config.nbdDevice); - if (!process) - { - return ReadyState(state, std::errc::operation_canceled, - "Failed to allocate process"); - } - if (!process->spawn( - Configuration::MountPoint::toArgs(state.machine.config), - [& machine = state.machine](int exitCode, bool isReady) { - LogMsg(Logger::Info, machine.name, " process ended."); - machine.exitCode = exitCode; - machine.emitSubprocessStoppedEvent(); - })) - { - return ReadyState(state, std::errc::operation_canceled, - "Failed to spawn process"); - } - auto newState = WaitingForGadgetState(state); - newState.process = process; - return newState; - } - - State activateLegacyMode(const ActivatingState& state) - { - LogMsg( - Logger::Debug, state.machine.name, - " Mount requested on address: ", state.machine.target->imgUrl, - " ; RW: ", state.machine.target->rw); - - if (isCifsUrl(state.machine.target->imgUrl)) - { - return mountSmbShare(state); - } - else if (isHttpsUrl(state.machine.target->imgUrl)) - { - return mountHttpsShare(state); - } - - return ReadyState(state, std::errc::invalid_argument, - "URL not recognized"); - } - - State mountSmbShare(const ActivatingState& state) - { - auto mountDir = SmbShare::createMountDir(state.machine.name); - if (!mountDir) - { - return ReadyState(state, std::errc::io_error, - "Failed to create mount directory"); - } - - SmbShare smb(*mountDir); - fs::path remote = getImagePath(state.machine.target->imgUrl); - auto remoteParent = "/" + remote.parent_path().string(); - auto localFile = *mountDir / remote.filename(); - - LogMsg(Logger::Debug, state.machine.name, " Remote name: ", remote, - "\n Remote parent: ", remoteParent, - "\n Local file: ", localFile); - - if (!smb.mount(remoteParent, state.machine.target->rw, - state.machine.target->credentials)) - { - fs::remove_all(*mountDir); - return ReadyState(state, std::errc::invalid_argument, - "Failed to mount CIFS share"); - } - - auto process = spawnNbdKit(state.machine, localFile); - if (!process) - { - SmbShare::unmount(*mountDir); - return ReadyState(state, std::errc::operation_canceled, - "Unable to setup NbdKit"); - } - - auto newState = WaitingForGadgetState(state); - newState.process = process; - newState.machine.target->mountDir = *mountDir; - - return newState; - } - - State mountHttpsShare(const ActivatingState& state) - { - auto& machine = state.machine; - - auto process = spawnNbdKit(machine, machine.target->imgUrl); - if (!process) - { - return ReadyState(state, std::errc::invalid_argument, - "Failed to mount HTTPS share"); - } - - auto newState = WaitingForGadgetState(state); - newState.process = process; - return newState; - } - - static std::shared_ptr - spawnNbdKit(MountPointStateMachine& machine, - std::unique_ptr&& secret, - const std::vector& params) - { - // Investigate - auto process = std::make_shared( - machine.ioc.get(), machine.name, "/usr/sbin/nbdkit", - machine.config.nbdDevice); - if (!process) - { - LogMsg(Logger::Error, machine.name, - " Failed to create Process for: ", machine.name); - return {}; - } - - // Cleanup of previous socket - if (fs::exists(machine.config.unixSocket)) - { - LogMsg(Logger::Debug, machine.name, - " Removing previously mounted socket: ", - machine.config.unixSocket); - if (!fs::remove(machine.config.unixSocket)) - { - LogMsg(Logger::Error, machine.name, - " Unable to remove pre-existing socket :", - machine.config.unixSocket); - return {}; - } - } - - std::string nbd_client = - "/usr/sbin/nbd-client " + - boost::algorithm::join( - Configuration::MountPoint::toArgs(machine.config), " "); - - std::vector args = { - // Listen for client on this unix socket... - "--unix", - machine.config.unixSocket, - - // ... then connect nbd-client to served image - "--run", - nbd_client, - -#if VM_VERBOSE_NBDKIT_LOGS - "--verbose", // swarm of debug logs - only for brave souls -#endif - }; - - if (!machine.target->rw) - { - args.push_back("--readonly"); - } - - // Insert extra params - args.insert(args.end(), params.begin(), params.end()); - - if (!process->spawn( - args, [& machine = machine, secret = std::move(secret)]( - int exitCode, bool isReady) { - LogMsg(Logger::Info, machine.name, " process ended."); - machine.exitCode = exitCode; - machine.emitSubprocessStoppedEvent(); - })) - { - LogMsg(Logger::Error, machine.name, - " Failed to spawn Process for: ", machine.name); - return {}; - } - - return process; - } - - static std::shared_ptr - spawnNbdKit(MountPointStateMachine& machine, const fs::path& file) - { - return spawnNbdKit(machine, {}, - {// Use file plugin ... - "file", - // ... to mount file at this location - "file=" + file.string()}); - } - - static std::shared_ptr - spawnNbdKit(MountPointStateMachine& machine, const std::string& url) - { - std::unique_ptr secret; - std::vector params = { - // Use curl plugin ... - "curl", - // ... to mount http resource at url - "url=" + url}; - - // Authenticate if needed - if (machine.target->credentials) - { - // Pack password into buffer - utils::CredentialsProvider::SecureBuffer buff = - machine.target->credentials->pack( - [](const std::string& user, const std::string& pass, - std::vector& buff) { - std::copy(pass.begin(), pass.end(), - std::back_inserter(buff)); - }); - - // Prepare file to provide the password with - secret = std::make_unique(std::move(buff)); - - params.push_back("user=" + machine.target->credentials->user()); - params.push_back("password=+" + secret->path()); - } - - return spawnNbdKit(machine, std::move(secret), params); - } - - bool checkUrl(const std::string& urlScheme, const std::string& imageUrl) - { - return (urlScheme.compare(imageUrl.substr(0, urlScheme.size())) == - 0); - } - - bool getImagePathFromUrl(const std::string& urlScheme, - const std::string& imageUrl, - std::string* imagePath) - { - if (checkUrl(urlScheme, imageUrl)) - { - if (imagePath != nullptr) - { - *imagePath = imageUrl.substr(urlScheme.size() - 1); - return true; - } - else - { - LogMsg(Logger::Error, "Invalid parameter provied"); - return false; - } - } - else - { - LogMsg(Logger::Error, "Provied url does not match scheme"); - return false; - } - } - - bool isHttpsUrl(const std::string& imageUrl) - { - return checkUrl("https://", imageUrl); - } - - bool getImagePathFromHttpsUrl(const std::string& imageUrl, - std::string* imagePath) - { - return getImagePathFromUrl("https://", imageUrl, imagePath); - } - - bool isCifsUrl(const std::string& imageUrl) - { - return checkUrl("smb://", imageUrl); - } - - bool getImagePathFromCifsUrl(const std::string& imageUrl, - std::string* imagePath) - { - return getImagePathFromUrl("smb://", imageUrl, imagePath); - } - - fs::path getImagePath(const std::string& imageUrl) - { - std::string imagePath; - - if (getImagePathFromHttpsUrl(imageUrl, &imagePath)) - { - return fs::path(imagePath); - } - else if (getImagePathFromCifsUrl(imageUrl, &imagePath)) - { - return fs::path(imagePath); - } - else - { - LogMsg(Logger::Error, "Unrecognized url's scheme encountered"); - return fs::path(""); - } - } - }; + return config; + } - struct UdevStateChangeEvent : public BasicEvent + std::optional& getTarget() override { - UdevStateChangeEvent(const StateChange& devState) : - BasicEvent(__FUNCTION__), devState{devState} - { - } - State operator()(const WaitingForGadgetState& state) - { - if (devState == StateChange::inserted) - { - int32_t ret = UsbGadget::configure( - state.machine.name, state.machine.config.nbdDevice, - devState, - state.machine.target ? state.machine.target->rw : false); - if (ret == 0) - { - return ActiveState(state); - } - return ReadyState(state, std::errc::device_or_resource_busy, - "Unable to configure gadget"); - } - return ReadyState(state, std::errc::operation_not_supported, - "Unexpected udev event: " + - static_cast(devState)); - } - - State operator()(const ReadyState& state) - { - if (devState == StateChange::removed) - { - LogMsg(Logger::Debug, state.machine.name, - " This is acceptable since udev notification is often " - "after process is being killed"); - } - return state; - } - - template - State operator()(const AnyState& state) - { - LogMsg(Logger::Info, name, - " Udev State: ", static_cast(devState)); - LogMsg(Logger::Critical, name, - " If you receiving this error, this means " - "your FSM is broken. Rethink!"); - return state; - } - StateChange devState; - }; + return target; + } - // Helper functions - bool removeUsbGadget(const BasicState& state) + BasicState& getState() override { - int32_t ret = UsbGadget::configure(state.machine.name, - state.machine.config.nbdDevice, - StateChange::removed); - if (ret != 0) - { - // This shouldn't ever happen, perhaps best is to restart app - LogMsg(Logger::Critical, name, " Some serious failure happened!"); - return false; - } - return true; + return *state; } - void stopProcess(std::weak_ptr process) + + int& getExitCode() override { - if (auto ptr = process.lock()) - { - ptr->stop(); - return; - } - LogMsg(Logger::Info, name, " No process to stop"); + return exitCode; } - MountPointStateMachine(boost::asio::io_context& ioc, - DeviceMonitor& devMonitor, const std::string& name, - const Configuration::MountPoint& config) : - ioc{ioc}, - name{name}, config{config}, state{InitialState(*this)}, exitCode{-1} + boost::asio::io_context& getIoc() override { - devMonitor.addDevice(config.nbdDevice); + return ioc; } - MountPointStateMachine& operator=(MountPointStateMachine&& machine) + void changeState(std::unique_ptr newState) { - if (this != &machine) + state = std::move(newState); + LogMsg(Logger::Debug, name, " state changed to ", + state->getStateName()); + if (newState = state->onEnter()) { - state = std::move(machine.state); - name = std::move(machine.name); - ioc = machine.ioc; - config = std::move(machine.config); - target = std::move(machine.target); + changeState(std::move(newState)); } - return *this; } - void emitEvent(BasicEvent&& event) + template + void emitEvent(EventT&& event) { - std::string stateName = std::visit( - [](const BasicState& state) { return state.stateName; }, state); - LogMsg(Logger::Debug, name, " received ", event.eventName, " while in ", - stateName); + state->getStateName()); - state = std::visit(event, state); - std::visit([](BasicState& state) { state.onEnter(); }, state); + if (auto newState = state->handleEvent(std::move(event))) + { + changeState(std::move(newState)); + } } void emitRegisterDBusEvent( std::shared_ptr bus, - std::shared_ptr objServer) + std::shared_ptr objServer) override { emitEvent(RegisterDbusEvent(bus, objServer)); } - void emitMountEvent() + void emitMountEvent(std::optional newTarget) override { - emitEvent(MountEvent()); + emitEvent(MountEvent(std::move(newTarget))); } - void emitUnmountEvent() + void emitUnmountEvent() override { emitEvent(UnmountEvent()); } - void emitActivationStartedEvent() - { - emitEvent(ActivationStartedEvent()); - } - - void emitSubprocessStoppedEvent() + void emitSubprocessStoppedEvent() override { emitEvent(SubprocessStoppedEvent()); } - void emitUdevStateChangeEvent(const NBDDevice& dev, StateChange devState) + void emitUdevStateChangeEvent(const NBDDevice& dev, + StateChange devState) override { if (config.nbdDevice == dev) { @@ -1090,19 +107,11 @@ struct MountPointStateMachine } } - struct Target - { - std::string imgUrl; - bool rw; - std::optional mountDir; - std::unique_ptr credentials; - }; - - std::reference_wrapper ioc; + boost::asio::io_context& ioc; std::string name; Configuration::MountPoint config; std::optional target; - State state; - int exitCode; + std::unique_ptr state = std::make_unique(*this); + int exitCode = -1; }; diff --git a/src/system.hpp b/src/system.hpp index a6dba84..a29b640 100644 --- a/src/system.hpp +++ b/src/system.hpp @@ -348,7 +348,7 @@ class DeviceMonitor class Process : public std::enable_shared_from_this { public: - Process(boost::asio::io_context& ioc, const std::string& name, + Process(boost::asio::io_context& ioc, std::string_view name, const std::string& app, const NBDDevice& dev) : ioc(ioc), pipe(ioc), name(name), app(app), dev(dev) @@ -372,70 +372,72 @@ class Process : public std::enable_shared_from_this return false; } - boost::asio::spawn( - ioc, [this, self = shared_from_this(), - onExit{std::move(onExit)}](boost::asio::yield_context yield) { - boost::system::error_code bec; - std::string line; - boost::asio::dynamic_string_buffer buffer{line}; - LogMsg(Logger::Info, - "[Process]: Start reading console from nbd-client"); - while (1) + boost::asio::spawn(ioc, [this, self = shared_from_this(), + onExit = std::move(onExit)]( + boost::asio::yield_context yield) { + boost::system::error_code bec; + std::string line; + boost::asio::dynamic_string_buffer buffer{line}; + LogMsg(Logger::Info, + "[Process]: Start reading console from nbd-client"); + while (1) + { + auto x = boost::asio::async_read_until(pipe, std::move(buffer), + '\n', yield[bec]); + auto lineBegin = line.begin(); + while (lineBegin != line.end()) { - auto x = boost::asio::async_read_until( - pipe, std::move(buffer), '\n', yield[bec]); - auto lineBegin = line.begin(); - while (lineBegin != line.end()) - { - auto lineEnd = find(lineBegin, line.end(), '\n'); - LogMsg(Logger::Debug, "[Process]: (", name, ") ", - std::string(lineBegin, lineEnd)); - if (lineEnd == line.end()) - { - break; - } - lineBegin = lineEnd + 1; - } - - buffer.consume(x); - if (bec) + auto lineEnd = find(lineBegin, line.end(), '\n'); + LogMsg(Logger::Debug, "[Process]: (", name, ") ", + std::string(lineBegin, lineEnd)); + if (lineEnd == line.end()) { - LogMsg(Logger::Debug, "[Process]: (", name, - ") Loop Error: ", bec); break; } + lineBegin = lineEnd + 1; } - LogMsg(Logger::Info, "[Process]: Exiting from COUT Loop"); - // The process shall be dead, or almost here, give it a chance - LogMsg(Logger::Debug, - "[Process]: Waiting process to finish normally"); - boost::asio::steady_timer timer(ioc); - int32_t waitCnt = 20; - while (child.running() && waitCnt > 0) - { - boost::system::error_code ignored_ec; - timer.expires_from_now(std::chrono::milliseconds(100)); - timer.async_wait(yield[ignored_ec]); - waitCnt--; - } - if (child.running()) + + buffer.consume(x); + if (bec) { - child.terminate(); + LogMsg(Logger::Debug, "[Process]: (", name, + ") Loop Error: ", bec); + break; } + } + LogMsg(Logger::Info, "[Process]: Exiting from COUT Loop"); + // The process shall be dead, or almost here, give it a chance + LogMsg(Logger::Debug, + "[Process]: Waiting process to finish normally"); + boost::asio::steady_timer timer(ioc); + int32_t waitCnt = 20; + while (child.running() && waitCnt > 0) + { + boost::system::error_code ignored_ec; + timer.expires_from_now(std::chrono::milliseconds(100)); + timer.async_wait(yield[ignored_ec]); + waitCnt--; + } + if (child.running()) + { + child.terminate(); + } - child.wait(); - LogMsg(Logger::Info, "[Process]: running: ", child.running(), - " EC: ", child.exit_code(), - " Native: ", child.native_exit_code()); + child.wait(); + LogMsg(Logger::Info, "[Process]: running: ", child.running(), + " EC: ", child.exit_code(), + " Native: ", child.native_exit_code()); - onExit(child.exit_code(), dev.isReady()); - }); + onExit(child.exit_code(), dev.isReady()); + }); return true; } - void stop() + template + void stop(OnTerminateCb&& onTerminate) { - boost::asio::spawn(ioc, [this, self = shared_from_this()]( + boost::asio::spawn(ioc, [this, self = shared_from_this(), + onTerminate = std::move(onTerminate)]( boost::asio::yield_context yield) { // The Good dev.disconnect(); @@ -455,6 +457,7 @@ class Process : public std::enable_shared_from_this LogMsg(Logger::Info, "[Process] Terminate if process doesnt " "want to exit nicely"); child.terminate(); + onTerminate(); } }); } diff --git a/src/utils.hpp b/src/utils.hpp index 961e1e5..f0d71d6 100644 --- a/src/utils.hpp +++ b/src/utils.hpp @@ -6,6 +6,7 @@ #include #include #include +#include #include namespace fs = std::filesystem; -- cgit v1.2.3